From 7b3bb72b543d573352fa422d43ed6c9763af435e Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 12 Oct 2026 21:47:54 +0900 Subject: [PATCH] =?UTF-8?q?feat(b06):=20=EB=AC=BC=EB=84=98=EC=9D=B4=20?= =?UTF-8?q?=EB=82=AE=EC=9D=80=20=EC=AA=BD=20=EB=85=B8=EB=A9=B4=20=EB=81=9D?= =?UTF-8?q?=EC=9D=B4=20=EC=A0=88=ED=86=A0=EB=A9=B4=20=EC=BD=98=ED=81=AC?= =?UTF-8?q?=EB=A6=AC=ED=8A=B8=20=C2=B7=20=EB=85=B8=EB=A9=B4=EC=84=A0?= =?UTF-8?q?=EC=9D=84=20=EB=85=B8=EB=A9=B4=20=EA=B8=B0=EC=9A=B8=EA=B8=B0?= =?UTF-8?q?=EB=A1=9C=20=EB=8A=98=EC=97=AC=20=EC=9B=90=EC=A7=80=EB=B0=98?= =?UTF-8?q?=EA=B9=8C=EC=A7=80=20(294-3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 설계 계산(파이썬 _SectionGeometry · TS 짝 SectionGeometry) — 기울인 자리(tilted) 노면이 내려가는 쪽 노면 끝 원지반이 노면보다 높으면 같은 기울기로 늘여 원지반과 처음 만나는 자리까지 · 그 쪽 절토 사면 없음 · 샘플 반폭 안에서 못 만나면 옛 길 · 성토 쪽 그대로 · 결과 tilt_reach - 토공 면적 · 설계선이 늘인 노면을 따름(서버 정본 · [저장] 재계산 · 거울 시험 갈래) - 횡단 그림 — 콘크리트(포장층) · 월류 높이선이 늘인 끝까지(fordEdges) - 시험 test_294_3 새로 · 횡단 설계 거울 「물넘이포장 낮은 쪽 노면 연장」 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TteK8mfYZetk5P3bEZkyVZ --- B06_Section/B06_Section_Api_Types.ts | 2 + B06_Section/B06_Section_Engine_Design.py | 8 ++ .../B06_Section_Engine_Design_Geometry.py | 37 ++++++ .../B06_Section_UI_Cross_Ford_Pavement.ts | 11 +- common_util/common_util_cross_design.ts | 14 +++ .../common_util_cross_design_geometry.ts | 37 ++++++ .../tester/helper_294_3_ford_extend_chute.cjs | 62 ++++++++++ .../tester/test_294_3_ford_extend_chute.py | 112 ++++++++++++++++++ .../tester/test_b06_cross_design_mirror.py | 24 ++++ 9 files changed, 306 insertions(+), 1 deletion(-) create mode 100644 resources/tester/helper_294_3_ford_extend_chute.cjs create mode 100644 resources/tester/test_294_3_ford_extend_chute.py diff --git a/B06_Section/B06_Section_Api_Types.ts b/B06_Section/B06_Section_Api_Types.ts index d74e1ce27..f19214b97 100644 --- a/B06_Section/B06_Section_Api_Types.ts +++ b/B06_Section/B06_Section_Api_Types.ts @@ -501,6 +501,8 @@ export interface CrossDesign { /** 물넘이포장 · 맹암거 자리(286-2 · 287-4) — 측구 없음 · 노면 = 지대 낮은 쪽 한 경사(`tilt_pct`). */ tilted?: boolean; tilt_pct?: number; + /** 물넘이 낮은 쪽 늘인 노면 끝(294-3) — 그 쪽 절토 사면 대신 노면 · 콘크리트가 원지반까지. */ + tilt_reach?: { side: "left" | "right"; offset_m: number; elevation_m: number }; cut_area_m2: number; /** * 절토 내역(합 = `cut_area_m2`). 지표면~암반 경계선이 토사, 그 아래가 암이다. diff --git a/B06_Section/B06_Section_Engine_Design.py b/B06_Section/B06_Section_Engine_Design.py index 3f9e27daa..a83fe08e0 100644 --- a/B06_Section/B06_Section_Engine_Design.py +++ b/B06_Section/B06_Section_Engine_Design.py @@ -314,6 +314,7 @@ def compute_cross_design( shoulder_extend_left_m=shoulder_extend_left_m, shoulder_extend_right_m=shoulder_extend_right_m, tilted=bool(tilted), + ground_span=(valid[0][0], valid[-1][0]), ) # 적분 오프셋 = 지반 샘플 ∪ 설계 꼭짓점(샘플 범위 안쪽만). 꼭짓점을 넣어야 @@ -546,6 +547,13 @@ def compute_cross_design( } if tilted: result["tilted"] = True + for side, reach in geometry.reach.items(): # 낮은 쪽 늘인 노면 끝(294-3) + signed = reach if side == "left" else -reach + result["tilt_reach"] = { + "side": side, + "offset_m": round(signed, 4), + "elevation_m": round(geometry.road_z(signed), 4), + } if tilt_pct is not None: result["tilt_pct"] = round(float(tilt_pct), 4) if drop > 0: diff --git a/B06_Section/B06_Section_Engine_Design_Geometry.py b/B06_Section/B06_Section_Engine_Design_Geometry.py index 6131e19f7..e19dffc10 100644 --- a/B06_Section/B06_Section_Engine_Design_Geometry.py +++ b/B06_Section/B06_Section_Engine_Design_Geometry.py @@ -63,6 +63,7 @@ class _SectionGeometry: shoulder_extend_left_m: float = 0.0, shoulder_extend_right_m: float = 0.0, tilted: bool = False, + ground_span: tuple[float, float] | None = None, ) -> None: half_road = group["road_width_m"] / 2.0 # 곡선부 확폭은 **한쪽으로만** 붙는다(2026-09-06 사용자 확정: 곡선 바깥쪽). @@ -195,6 +196,36 @@ class _SectionGeometry: (edge_offset + outward * top, edge_z), ] + # 물넘이 낮은 쪽 연장(294-3) — 노면이 내려가는 쪽 노면 끝 원지반이 노면보다 높으면(절토) + # 노면을 같은 기울기로 늘여 원지반과 처음 만나는 자리까지(그 쪽 절토 사면 없음) · 반폭 + # (`ground_span`) 안에서 못 만나면 옛 길(절토 사면). ⚠ TS 짝 `SectionGeometry.reach`. + self.reach: dict[str, float] = {} + if tilted and ground_at is not None and abs(self.slope_per_offset) > 1e-12: + self._extend_low_side(ground_at, ground_span) + + def _extend_low_side( + self, ground_at: Callable[[float], float], span: tuple[float, float] | None + ) -> None: + side = "left" if self.slope_per_offset < 0 else "right" + sign = 1.0 if side == "left" else -1.0 + extent = self.left_extent if side == "left" else self.right_extent + limit = (span[1] if side == "left" else -span[0]) if span else extent + 50.0 + + def gap(dist: float) -> float: # > 0 = 원지반이 늘인 노면 위(절토) + return ground_at(sign * dist) - self.road_z(sign * dist) + + if gap(extent) <= 1e-3: + return + step = 0.05 + dist = extent + while dist < limit - 1e-9: + nxt = min(dist + step, limit) + g0, g1 = gap(dist), gap(nxt) + if g1 <= 0: + self.reach[side] = dist + (nxt - dist) * g0 / (g0 - g1) + return + dist = nxt + def road_z(self, offset_m: float) -> float: """노면(노견 포함) 설계고 — 중심 계획고에서 횡단경사로 기운 단일 평면.""" return self.z_center + self.slope_per_offset * offset_m @@ -430,6 +461,9 @@ class _SectionGeometry: return points[-1][1] role = self.left_role if side == "left" else self.right_role dist = abs(offset_m) + reach = self.reach.get(side) + if reach is not None: # 늘인 노면 → 그 바깥은 원지반(294-3) + return self.road_z(offset_m) if dist <= reach + 1e-9 else ground_m if role == "cut": # 지반과 1회 교차하면 그 이후 절토는 의미 없음 → 지반 추종(N-2-4). cross = self.cut_cross_dist(side) @@ -468,6 +502,9 @@ class _SectionGeometry: if cross is not None and offset > cross + 1e-9: break points.append(offset if side == "left" else -offset) + # 늘인 노면 끝(294-3) + for side, reach in self.reach.items(): + points.append(reach if side == "left" else -reach) # 절·성토 사면과 지반의 **첫** 교차점을 꼭짓점에 넣어 면적 절단을 정확히 한다(N-2-4). for side in ("left", "right"): role = self.left_role if side == "left" else self.right_role diff --git a/B06_Section/B06_Section_UI_Cross_Ford_Pavement.ts b/B06_Section/B06_Section_UI_Cross_Ford_Pavement.ts index 38c948fb4..37d5d4320 100644 --- a/B06_Section/B06_Section_UI_Cross_Ford_Pavement.ts +++ b/B06_Section/B06_Section_UI_Cross_Ford_Pavement.ts @@ -103,7 +103,7 @@ export function appendFordPavementOverlay( x: (offset: number) => number, y: (elevation: number) => number, ): boolean { - const edges = design.road_edges; + const edges = fordEdges(design); if (!spec || !edges) return false; const from = svg.children.length; // 노면공 항목은 안 넘김 — 물넘이 자체 포장(노면공 포장 고름과 안 섞임) @@ -134,6 +134,15 @@ export function appendFordPavementOverlay( return true; } +/** 콘크리트 · 노면 양 끝 — 낮은 쪽을 늘였으면(294-3 `tilt_reach`) 그 쪽은 원지반과 만난 자리까지. */ +export function fordEdges(design: CrossDesign): CrossDesign["road_edges"] | undefined { + const edges = design.road_edges; + const reach = design.tilt_reach; + if (!edges || !reach) return edges; + const end = { offset_m: reach.offset_m, elevation_m: reach.elevation_m }; + return reach.side === "left" ? { ...edges, left: end } : { ...edges, right: end }; +} + /** * 유출측(`inflowLeft` 반대) 노견 바깥 성토사면 선(136-2) — 설계선을 노견에서 바깥으로 따라 * 내려가는 동안만(오르면 절토라 멈춤). 성토가 없으면 빈 선(띠 안 그림). diff --git a/common_util/common_util_cross_design.ts b/common_util/common_util_cross_design.ts index f57a47453..2c6b95a76 100644 --- a/common_util/common_util_cross_design.ts +++ b/common_util/common_util_cross_design.ts @@ -194,6 +194,8 @@ export interface CrossDesignResult { /** 물넘이포장 · 맹암거 자리(286-2 · 287-4) — 측구 없음 · 지대 낮은 쪽 한 경사. */ tilted?: boolean; tilt_pct?: number; + /** 물넘이 낮은 쪽 늘인 노면 끝(294-3) — 그 쪽 절토 사면 대신 노면이 원지반까지. */ + tilt_reach?: { side: "left" | "right"; offset_m: number; elevation_m: number }; pavement_thickness_m?: number; rock_boundary_offset_m?: number; } @@ -377,6 +379,7 @@ export function computeCrossDesign( shoulderExtendLeftM: options.shoulderExtendLeftM ?? 0, shoulderExtendRightM: options.shoulderExtendRightM ?? 0, tilted: Boolean(options.tilted), + groundSpan: [valid[0][0], valid[valid.length - 1][0]], }); // 적분 오프셋 = 지반 샘플 ∪ 설계 꼭짓점(샘플 범위 안쪽만). @@ -592,6 +595,17 @@ export function computeCrossDesign( cut_slope_segments: geometry.cutSlopeSegments(), }; if (options.tilted) result.tilted = true; + // 낮은 쪽 늘인 노면 끝(294-3 · 파이썬 짝) + for (const side of ["left", "right"] as const) { + const reach = geometry.reach[side]; + if (!options.tilted || reach === undefined) continue; + const signed = side === "left" ? reach : -reach; + result.tilt_reach = { + side, + offset_m: round4(signed), + elevation_m: round4(geometry.roadZ(signed)), + }; + } if (options.tilted && options.tiltPct != null) result.tilt_pct = round4(options.tiltPct); if (drop > 0) result.surface_drop_m = round4(drop); if (options.berm) { diff --git a/common_util/common_util_cross_design_geometry.ts b/common_util/common_util_cross_design_geometry.ts index c32f80cfb..75358fc4b 100644 --- a/common_util/common_util_cross_design_geometry.ts +++ b/common_util/common_util_cross_design_geometry.ts @@ -147,6 +147,8 @@ export class SectionGeometry { shoulderExtendRightM?: number; /** 물넘이포장 · 맹암거 자리 — 측구 없음 · 노면 = 지대 낮은 쪽 한 경사(286-2 · 287-4 · 파이썬 짝 `tilted`). */ tilted?: boolean; + /** 원지반 샘플 범위(오프셋 최소 · 최대) — 낮은 쪽 연장 한계(294-3 · 파이썬 짝 `ground_span`). */ + groundSpan?: [number, number] | null; }) { const { group } = params; const halfRoad = group.road_width_m / 2; @@ -261,6 +263,36 @@ export class SectionGeometry { ]; } } + + // 물넘이 낮은 쪽 연장(294-3) — 노면이 내려가는 쪽 노면 끝 원지반이 노면보다 높으면(절토) 노면을 + // 같은 기울기로 늘여 원지반과 처음 만나는 자리까지(그 쪽 절토 사면 없음) · 반폭 안에서 못 만나면 + // 옛 길(절토 사면). ⚠ 파이썬 짝 `_extend_low_side`. + if (params.tilted && params.groundAt && Math.abs(this.slopePerOffset) > 1e-12) + this.extendLowSide(params.groundAt, params.groundSpan ?? null); + } + + /** 낮은 쪽 늘인 노면 끝(오프셋 절대값) — 쪽마다(294-3). */ + reach: { left?: number; right?: number } = {}; + + private extendLowSide(groundAt: (offsetM: number) => number, span: [number, number] | null) { + const side = this.slopePerOffset < 0 ? "left" : "right"; + const sign = side === "left" ? 1 : -1; + const extent = side === "left" ? this.leftExtent : this.rightExtent; + const limit = span ? (side === "left" ? span[1] : -span[0]) : extent + 50; + const gap = (dist: number) => groundAt(sign * dist) - this.roadZ(sign * dist); // > 0 = 절토 + if (gap(extent) <= 1e-3) return; + const step = 0.05; + let dist = extent; + while (dist < limit - 1e-9) { + const next = Math.min(dist + step, limit); + const g0 = gap(dist); + const g1 = gap(next); + if (g1 <= 0) { + this.reach[side] = dist + ((next - dist) * g0) / (g0 - g1); + return; + } + dist = next; + } } /** 노면(노견 포함) 설계고 — 중심 계획고에서 횡단경사로 기운 단일 평면. */ @@ -422,6 +454,8 @@ export class SectionGeometry { } const role = side === "left" ? this.leftRole : this.rightRole; const dist = Math.abs(offsetM); + const reach = this.reach[side]; + if (reach !== undefined) return dist <= reach + 1e-9 ? this.roadZ(offsetM) : groundM; // 294-3 if (role === "cut") { const cross = this.cutCrossDist(side); if (cross !== null && dist >= cross) return groundM; @@ -512,6 +546,9 @@ export class SectionGeometry { } } } + // 늘인 노면 끝(294-3) + if (this.reach.left !== undefined) points.push(this.reach.left); + if (this.reach.right !== undefined) points.push(-this.reach.right); for (const side of ["left", "right"]) { const role = side === "left" ? this.leftRole : this.rightRole; const cross = role === "cut" ? this.cutCrossDist(side) : this.fillCrossDist(side); diff --git a/resources/tester/helper_294_3_ford_extend_chute.cjs b/resources/tester/helper_294_3_ford_extend_chute.cjs new file mode 100644 index 000000000..646a89802 --- /dev/null +++ b/resources/tester/helper_294_3_ford_extend_chute.cjs @@ -0,0 +1,62 @@ +/* 294 물넘이 낮은 쪽 연장 · 도수로 끝단 · 띠 ↔ 사면 고름 헬퍼 — 진짜 TS 를 Node 로 돌려 JSON 한 벌로. */ +const fs = require("fs"); +const path = require("path"); +const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); + +const ROOT = path.join(__dirname, "..", ".."); +const cache = new Map(); +const load = (file) => { + if (cache.has(file)) return cache.get(file); + const js = ts.transpileModule(fs.readFileSync(file, "utf8"), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, + }).outputText; + const box = { exports: {} }; + cache.set(file, box.exports); + const near = (m) => { + if (m.startsWith("@config/")) return load(path.join(ROOT, "config", `${m.slice(8)}.ts`)); + if (m.startsWith("@util/")) return load(path.join(ROOT, "common_util", `${m.slice(6)}.ts`)); + if (m.startsWith(".")) return load(path.join(path.dirname(file), `${m}.ts`)); + return new Proxy({}, { get: () => () => {} }); + }; + new Function("exports", "module", "require", js)(box.exports, box, near); + return box.exports; +}; + +class Node { + constructor(tag) { + this.tag = tag; + this.attrs = {}; + this.children = []; + this.dataset = {}; + this.style = {}; + } + setAttribute(k, v) { + this.attrs[k] = String(v); + } + getAttribute(k) { + return this.attrs[k] ?? null; + } + append(...kids) { + this.children.push(...kids); + } +} +globalThis.document = { createElementNS: (_ns, tag) => new Node(tag) }; + +const input = JSON.parse(fs.readFileSync(process.argv[2], "utf8")); +const pave = load(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Ford_Pavement.ts")); +const design = input.design; // 서버가 낸 양측 절토 · 낮은 쪽 연장 설계 +const svg = new Node("g"); +pave.appendFordPavementOverlay( + svg, + { span_m: 8, depth_m: 0.3, slope_pct: 5 }, + design, + (o) => o, + (e) => e, +); +const paveLayer = svg.children.find((k) => (k.attrs.class ?? "").includes("ford-pavement")); + +const out = { + edges: pave.fordEdges(design), + pavePoints: paveLayer?.attrs.points ?? null, +}; +process.stdout.write(JSON.stringify(out)); diff --git a/resources/tester/test_294_3_ford_extend_chute.py b/resources/tester/test_294_3_ford_extend_chute.py new file mode 100644 index 000000000..90056560a --- /dev/null +++ b/resources/tester/test_294_3_ford_extend_chute.py @@ -0,0 +1,112 @@ +"""294 물넘이 낮은 쪽 연장 · 도수로/돌붙임 끝단 · 띠 ↔ 성토 사면 고름(요구 188-2 · 188-3 · 189 · 그림_188). + +못박는 것 + - 294-3 물넘이 측점 낮은 쪽 노면 끝 원지반이 노면보다 높으면(절토) 노면을 같은 기울기로 늘여 원지반과 + 처음 만나는 자리까지(그 쪽 절토 사면 없음 · `tilt_reach`) · 성토 쪽 그대로 · 반폭 안에서 못 만나면 옛 길 · + 서버 정본 ↔ TS 짝 같은 값 = 횡단 설계 거울(`test_b06_cross_design_mirror` 「물넘이포장 낮은 쪽 노면 + 연장」) · 그림 = 콘크리트(포장층)가 늘인 끝까지. + +진짜 TS 는 `helper_294_3_ford_extend_chute.cjs` 가 Node 로 돌림. 화면은 사용자 확인. +""" + +from __future__ import annotations + +import json +import subprocess +import sys +from pathlib import Path + +import pytest + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parents[1] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) +if str(HERE) not in sys.path: + sys.path.insert(0, str(HERE)) + +from B06_Section.B06_Section_Engine_Design import compute_cross_design # noqa: E402 +from config.config_system import STANDARD_CROSS_SECTION # noqa: E402 +from test_b06_cross_design_mirror import CASES, _dip_ground # noqa: E402 + +KW = { + "ground_type": "soil", + "section_mode": "left_cut", + "ditch_side": "left", + "paved": True, + "tilted": True, + "tilt_pct": 5.0, + "standard": STANDARD_CROSS_SECTION, +} + +pytestmark = pytest.mark.skipif( + not (ROOT / "config" / "node_modules" / "typescript").is_dir(), + reason="프론트엔드 의존성 미설치", +) + + +def _line(samples_fn) -> list[dict]: + return [ + {"offset_m": o / 2, "elevation_m": round(samples_fn(o / 2), 4), "valid": True} + for o in range(-24, 25) + ] + + +@pytest.fixture(scope="module") +def dip() -> dict: + return compute_cross_design(_dip_ground(), 100.4, **KW) + + +@pytest.fixture(scope="module") +def ts(dip, tmp_path_factory) -> dict: + source = tmp_path_factory.mktemp("s294") / "in.json" + source.write_text(json.dumps({"design": dip}), encoding="utf-8") + proc = subprocess.run( + ["node", str(HERE / "helper_294_3_ford_extend_chute.cjs"), str(source)], + capture_output=True, + text=True, + encoding="utf-8", + timeout=60, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +def test_양측_절토_낮은_쪽_늘인_끝은_원지반_교점(dip): + reach = dip["tilt_reach"] + # 원지반 101.05 + 0.2·o(−4.5 까지) · 노면 100.4 + 0.05·o → −4.3333 에서 만남 + assert reach["side"] == "right" + assert reach["offset_m"] == pytest.approx(-4.3333, abs=1e-4) + assert reach["elevation_m"] == pytest.approx(100.4 - 0.05 * 4.3333, abs=1e-4) + edge = dip["road_edges"]["right"]["offset_m"] + for p in dip["design_line"]: # 노견 끝 ~ 늘인 끝 = 노면 기울기 그대로(절토 사면 없음) + if reach["offset_m"] - 1e-6 <= p["offset_m"] <= edge: + assert p["elevation_m"] == pytest.approx(100.4 + 0.05 * p["offset_m"], abs=1e-4) + assert dip["ditch_enabled"] is False + + +def test_성토_쪽은_그대로(): + got = compute_cross_design(_line(lambda o: 100.0 + 0.35 * o), 100.4, **KW) + assert "tilt_reach" not in got # 낮은 쪽(우) 노면 끝 원지반이 노면 아래 = 성토 사면 + + +def test_반폭_안에서_못_만나면_옛_길(): + got = compute_cross_design(_line(lambda o: 101.0 + 0.02 * o), 100.4, **KW) + assert "tilt_reach" not in got # 늘인 노면이 끝까지 원지반 아래 = 절토 사면 그대로 + right = [ + p for p in got["design_line"] if p["offset_m"] < got["road_edges"]["right"]["offset_m"] + ] + assert any(p["elevation_m"] > got["road_edges"]["right"]["elevation_m"] for p in right) + + +def test_서버_TS_같은_값은_거울_시험(): + assert any(case["name"] == "물넘이포장 낮은 쪽 노면 연장" for case in CASES) + + +def test_콘크리트는_늘인_끝까지(dip, ts): + assert ts["edges"]["right"] == { + "offset_m": dip["tilt_reach"]["offset_m"], + "elevation_m": dip["tilt_reach"]["elevation_m"], + } + first = ts["pavePoints"].split()[1] # 포장층 윗면 우 끝 + assert float(first.split(",")[0]) == pytest.approx(-4.3333, abs=1e-4) diff --git a/resources/tester/test_b06_cross_design_mirror.py b/resources/tester/test_b06_cross_design_mirror.py index 229e8406b..22f84dad6 100644 --- a/resources/tester/test_b06_cross_design_mirror.py +++ b/resources/tester/test_b06_cross_design_mirror.py @@ -51,6 +51,16 @@ def _ground(slope: float, *, gap: tuple[float, float] | None = None) -> list[dic return samples +def _dip_ground() -> list[dict]: + """양측 절토 · 우측 노면 끝 너머(−4.5 m 밖)에서 원지반이 급히 내려가는 지반(294-3).""" + samples = [] + for index in range(49): + offset = -12.0 + index * 0.5 + z = 101.05 + 0.2 * offset if offset >= -4.5 else 100.15 + 1.0 * (offset + 4.5) + samples.append({"offset_m": offset, "elevation_m": round(z, 4), "valid": True}) + return samples + + # 갈래를 골고루 태운다 — 지반유형 셋 · 단면유형 · 측구 형식 · 포장 · 2단 경사 · 세월교 # 노면 하강 · 곡선부 확폭(바깥쪽 좌/우) · 지반선 결측 구간. CASES = [ @@ -245,6 +255,20 @@ CASES = [ "tilt_pct": 5.2408, }, }, + { + # 294-3 — 양측 절토 · 낮은 쪽(우) 노면을 늘여 원지반이 내려와 만나는 자리까지 + "name": "물넘이포장 낮은 쪽 노면 연장", + "samples": _dip_ground(), + "design_elevation_m": 100.4, + "options": { + "ground_type": "soil", + "section_mode": "left_cut", + "ditch_side": "left", + "paved": True, + "tilted": True, + "tilt_pct": 5.0, + }, + }, ] _TS_RUNNER = """