From 486616aabdae29ac7579241753a4ac828bd96d5c Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 17 Oct 2026 12:21:00 +0900 Subject: [PATCH] =?UTF-8?q?fix(B06=20=EB=A7=B9=EC=95=94=EA=B1=B0):=20371-3?= =?UTF-8?q?=20=EB=85=B8=EB=A9=B4=20=3D=20=ED=91=9C=EC=A4=80=20=EA=B2=BD?= =?UTF-8?q?=EC=82=AC=20=C2=B7=20=EC=95=94=EA=B1=B0=20=EA=B2=BD=EC=82=AC=20?= =?UTF-8?q?=3D=20=ED=98=95=EC=83=81=20=ED=95=98=EB=8B=A8=20=EA=B2=BD?= =?UTF-8?q?=EA=B3=84(=EB=86=92=EC=9D=80=20=EC=AA=BD=20=EB=85=B8=EB=A9=B4?= =?UTF-8?q?=20=EB=81=9D=20=E2=88=92=20=EB=86=92=EC=9D=B4=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EB=82=B4=EB=A6=BC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 암거 경사가 노면으로 가던 길 끊음(tilt_ranges None ↔ facilityRanges null · Cross_Refresh · design_pipeline_cross) · 측구 없음 · 낮은 쪽 한 경사 그대로 - underdrainOutline 하단 = 높은 쪽 노면 끝 z − H + 암거 경사 · 절토 교점 · 성토 교점 · 돌 띠 따라감 · 경사 비면 옛 동작 - 저장분은 tilt_pct 달라 서버가 다시 셈 · B06 · B07 같은 함수(묶음 다시 지음) - 시험 test_288 하단 경계 · 349_0n 맹암거 자리 경사 비움 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Se7ytqbohmBQnfYpwYFnYf --- A00_Common/design/design_pipeline_cross.ts | 2 +- B06_Section/B06_Section_Cross_Refresh.ts | 2 +- B06_Section/B06_Section_Engine_Tilt.py | 5 ++- .../B06_Section_UI_Cross_Underdrain.ts | 45 ++++++++++++++----- .../logic/B06_Section_Cross_Enforce.ts | 3 +- .../common_util_structure_underdrain.ts | 2 +- resources/tester/helper_288_underdrain.cjs | 2 + resources/tester/test_288_underdrain_cross.py | 34 ++++++++++---- .../test_349_0n_cross_enforce_mirror.py | 3 ++ 9 files changed, 72 insertions(+), 26 deletions(-) diff --git a/A00_Common/design/design_pipeline_cross.ts b/A00_Common/design/design_pipeline_cross.ts index 4ef809138..b047fcfcd 100644 --- a/A00_Common/design/design_pipeline_cross.ts +++ b/A00_Common/design/design_pipeline_cross.ts @@ -66,7 +66,7 @@ function carried(section: CrossSection): Record { : null, paved: Boolean(section.pavement?.length) || Boolean(section.ford_pavement), tilted: Boolean(section.ford_pavement || section.underdrain), - tiltPct: section.ford_pavement?.slope_pct ?? section.underdrain?.slope_pct ?? null, + tiltPct: section.ford_pavement?.slope_pct ?? null, // 맹암거 = 표준 경사(371-3) surfaceDropM: drop > 0 ? drop : 0, }; } diff --git a/B06_Section/B06_Section_Cross_Refresh.ts b/B06_Section/B06_Section_Cross_Refresh.ts index d71763f7c..2f72e8c68 100644 --- a/B06_Section/B06_Section_Cross_Refresh.ts +++ b/B06_Section/B06_Section_Cross_Refresh.ts @@ -435,7 +435,7 @@ function refreshLocally(input: CrossRefreshInput): number[] | null { paved: pavedBySpecs(section), // 물넘이 자리 = 측구 없음 · 지대 낮은 쪽 한 경사(286-2 · 287-4 · 서버 `tilt_ranges` 짝 = 얹힌 제원) tilted: Boolean(section.ford_pavement || section.underdrain), - tiltPct: section.ford_pavement?.slope_pct ?? section.underdrain?.slope_pct ?? null, + tiltPct: section.ford_pavement?.slope_pct ?? null, // 맹암거 = 표준 경사(371-3) standard, berm: bermAt(section.chainage_m, design), rockBoundaryOffsetM, diff --git a/B06_Section/B06_Section_Engine_Tilt.py b/B06_Section/B06_Section_Engine_Tilt.py index e67df46af..d0bd3b129 100644 --- a/B06_Section/B06_Section_Engine_Tilt.py +++ b/B06_Section/B06_Section_Engine_Tilt.py @@ -1,7 +1,7 @@ """구조물 자리 기울기 — 물넘이포장 · 맹암거 측점의 노면(287-4 · 288-1). 그 자리 횡단 설계 = 측구 없음 · 노면 = 한 경사로 지대 낮은 쪽(`compute_cross_design(tilted=…, -tilt_pct=…)`). 경사 = 물넘이 경사각도(%) · 맹암거 암거 경사(%) · 비면 표준 횡단경사. +tilt_pct=…)`). 경사 = 물넘이 경사각도(%) · 맹암거 = 표준 횡단경사(암거 경사는 형상 하단 · 371-3). · 물넘이포장 = 관 지점 정본(pipe_points) 범위(기준 ± 월류 폭/2). · 맹암거 = 구조물 정본(structures.json) A군 점형 · 기준 측점 한 곳 — 횡단 dict 에 `underdrain`(높이 · 경사)을 얹어 횡단도가 돌 무늬 형상을 그림(TS 짝 @@ -78,7 +78,8 @@ def tilt_ranges(project_root: Path | None) -> list[TiltRange]: half = float(spec.get("span_m") or 0.0) / 2.0 out.append((at - half, at + half, spec.get("slope_pct"))) for drain in load_underdrains(project_root): - out.append((drain["chainage_m"], drain["chainage_m"], drain["slope_pct"])) + # 노면 = 표준 경사 · 암거 경사는 맹암거 하단 경계만(371-3 · TS `facilityRanges` 짝) + out.append((drain["chainage_m"], drain["chainage_m"], None)) return out diff --git a/B06_Section/B06_Section_UI_Cross_Underdrain.ts b/B06_Section/B06_Section_UI_Cross_Underdrain.ts index cb41c0033..4b43d4798 100644 --- a/B06_Section/B06_Section_UI_Cross_Underdrain.ts +++ b/B06_Section/B06_Section_UI_Cross_Underdrain.ts @@ -2,8 +2,9 @@ * B06_Section_UI_Cross_Underdrain.ts * 맹암거 횡단 형상(288-1 · 요구 183-3). * - * · 노면 = 설계 노면 — 서버 · TS 짝이 암거 경사로 지대 낮은 쪽 한 경사로 기울여 냄 · 측구 없음. - * · 옵셋선 = 노면을 암거 높이만큼 아래로 같은 각도. + * · 노면 = 설계 노면 — 서버 · TS 짝이 표준 경사로 지대 낮은 쪽 한 경사로 기울여 냄 · 측구 없음. + * · 옵셋선(하단 경계) = 높은 쪽 노면 끝에서 암거 높이 아래 · 암거 경사로 낮은 쪽으로 내림(371-3) · + * 경사가 비면 노면과 같은 각도. * · 성토 쪽 = 옵셋선을 성토 사면과 만날 때까지 · 절토 쪽 = 절토 사면을 아래로 늘여 옵셋선과 만남. * · 양측절토(낮은 쪽 늘인 노면 `tilt_reach`) = 늘인 노면이 원지반과 만나는 점에서 수직으로 끊음(356-1). * · 노면 · 양쪽 선 · 옵셋선이 둘러싼 안 = 돌 무늬(306-1) — 켜마다 반 칸 엇갈리고 이웃과 겹친 돌 · @@ -42,16 +43,18 @@ export function drawCrossedStones(group: SVGElement, frame: BandFrame, heightM: type Edge = { offset_m: number; elevation_m: number }; +/** 하단 경계(옵셋선) — 표고 = `z0 + grade · (o − o0)`. */ +type Bottom = { o0: number; z0: number; grade: number }; + /** 한쪽 둘레(노면 끝 → 옵셋선과 만나는 자리) — `outward` = 바깥 부호(+1 좌 · −1 우). */ function sidePath( edge: Edge, outward: 1 | -1, line: readonly Edge[], - roadGrade: number, - heightM: number, + bottom: Bottom, ): OffsetPoint[] { const start = { offset: edge.offset_m, elevation: edge.elevation_m }; - const offZ = (o: number) => edge.elevation_m + roadGrade * (o - edge.offset_m) - heightM; + const offZ = (o: number) => bottom.z0 + bottom.grade * (o - bottom.o0); const outside = line .filter((p) => (p.offset_m - edge.offset_m) * outward > 1e-6) .sort((a, b) => (a.offset_m - b.offset_m) * outward); @@ -60,7 +63,8 @@ function sidePath( if (first.elevation_m >= edge.elevation_m) { // 절토 — 사면을 안쪽 아래로 늘여 옵셋선과 만남 const k = (first.elevation_m - edge.elevation_m) / (first.offset_m - edge.offset_m); - const d = Math.abs(k - roadGrade) < 1e-9 ? 0 : -heightM / (k - roadGrade); + const g = bottom.grade; + const d = Math.abs(k - g) < 1e-9 ? 0 : (offZ(edge.offset_m) - edge.elevation_m) / (k - g); const offset = edge.offset_m + d; return [start, { offset, elevation: offZ(offset) }]; } @@ -83,25 +87,38 @@ function sidePath( return path; } +/** 하단 경계 — 높은 쪽(상류) 노면 끝 − 높이에서 암거 경사로 낮은 쪽으로 내림(371-3) · 경사가 비면 노면과 평행. */ +function bottomOf(left: Edge, right: Edge, heightM: number, slopePct: number | null): Bottom { + const roadGrade = (left.elevation_m - right.elevation_m) / (left.offset_m - right.offset_m); + const high = left.elevation_m >= right.elevation_m ? left : right; + const low = high === left ? right : left; + const grade = + slopePct && slopePct > 0 + ? (Math.sign(low.offset_m - high.offset_m) * -slopePct) / 100 + : roadGrade; + return { o0: high.offset_m, z0: high.elevation_m - heightM, grade }; +} + /** 맹암거 형상 둘레 — 좌 노면 끝 → 우 노면 끝 → 우 둘레 → 옵셋선 → 좌 둘레(위로). 높이 없으면 null. */ export function underdrainOutline( design: CrossDesign, heightM: number | null, + slopePct: number | null = null, ): OffsetPoint[] | null { const edges = design.road_edges; if (!edges || !(heightM && heightM > 0)) return null; const { left, right } = edges; const span = left.offset_m - right.offset_m; if (!(Math.abs(span) > 1e-9)) return null; - const grade = (left.elevation_m - right.elevation_m) / span; + const bottom = bottomOf(left, right, heightM, slopePct); // 양측절토(356-1) — 낮은 쪽 늘인 노면이 원지반과 만나는 점(tilt_reach)에서 끊음 · 너머 점 거름 → 꼬리가 수직으로 닫음 const reach = design.tilt_reach; const outward = reach?.side === "left" ? 1 : -1; const line = (design.design_line ?? []).filter( (p) => !reach || (p.offset_m - reach.offset_m) * outward <= 1e-6, ); - const leftPath = sidePath(left, 1, line, grade, heightM); - const rightPath = sidePath(right, -1, line, grade, heightM); + const leftPath = sidePath(left, 1, line, bottom); + const rightPath = sidePath(right, -1, line, bottom); return [ { offset: left.offset_m, elevation: left.elevation_m }, ...rightPath, @@ -117,7 +134,7 @@ export function appendUnderdrainOverlay( x: (offset: number) => number, y: (elevation: number) => number, ): SVGGElement | null { - const outline = spec ? underdrainOutline(design, spec.height_m) : null; + const outline = spec ? underdrainOutline(design, spec.height_m, spec.slope_pct) : null; if (!spec || !outline) return null; const px = (p: OffsetPoint): [number, number] => [x(p.offset), y(p.elevation)]; const points = outline.map((p) => px(p).join(",")).join(" "); @@ -136,15 +153,19 @@ export function appendUnderdrainOverlay( clip.append(clipShape); const stones = document.createElementNS(SVG_NS, "g"); stones.setAttribute("clip-path", `url(#${clipId})`); - const height = spec.height_m!; const offsets = outline.map((p) => p.offset); const [lo, hi] = [Math.min(...offsets), Math.max(...offsets)]; const { left, right } = design.road_edges; const grade = (left.elevation_m - right.elevation_m) / (left.offset_m - right.offset_m); const roadZ = (o: number) => left.elevation_m + grade * (o - left.offset_m); + const bottom = bottomOf(left, right, spec.height_m!, spec.slope_pct); + const bottomZ = (o: number) => bottom.z0 + bottom.grade * (o - bottom.o0); + // 돌 띠 = 노면 ~ 하단 경계(두께가 자리마다 다름 · 켜 수는 두꺼운 쪽) + const height = Math.max(roadZ(lo) - bottomZ(lo), roadZ(hi) - bottomZ(hi), spec.height_m!); const at = (u: number, v: number): OffsetPoint => { const offset = lo + (hi - lo) * u; - return { offset, elevation: roadZ(offset) - height * (v + 0.5) }; + const top = roadZ(offset); + return { offset, elevation: top + (bottomZ(offset) - top) * (v + 0.5) }; }; const [ax, ay] = px(at(0, 0)); const [bx, by] = px(at(1, 0)); diff --git a/B06_Section/logic/B06_Section_Cross_Enforce.ts b/B06_Section/logic/B06_Section_Cross_Enforce.ts index 6eec745e3..8976ddb5b 100644 --- a/B06_Section/logic/B06_Section_Cross_Enforce.ts +++ b/B06_Section/logic/B06_Section_Cross_Enforce.ts @@ -63,8 +63,9 @@ export function facilityRanges( const depth = num(spec.overflow_depth_m) ?? 0; if (spec.type === "ford" && depth > 0) drops.push([at, depth]); } + // 노면 = 표준 경사 · 암거 경사는 맹암거 하단 경계만(371-3 · 파이썬 `tilt_ranges` 짝) for (const drain of underdrainSpecsFrom(structures)) - tilt.push([drain.chainage_m, drain.chainage_m, drain.slope_pct]); + tilt.push([drain.chainage_m, drain.chainage_m, null]); return { paved, tilt, drops }; } diff --git a/common_util/common_util_structure_underdrain.ts b/common_util/common_util_structure_underdrain.ts index e992610e6..36179f3b3 100644 --- a/common_util/common_util_structure_underdrain.ts +++ b/common_util/common_util_structure_underdrain.ts @@ -12,7 +12,7 @@ import { catalogRow } from "./common_util_structure_catalog"; const EDGE_TOLERANCE_M = 0.02; -/** 맹암거 제원 — 높이 · 경사가 비면 null(높이 없으면 형상 없음 · 경사 없으면 표준 횡단경사). */ +/** 맹암거 제원 — 높이 · 경사가 비면 null(높이 없으면 형상 없음 · 경사 = 형상 하단 경계 · 없으면 노면과 평행 · 371-3). */ export interface UnderdrainSpec { structure_id: string | null; chainage_m: number; diff --git a/resources/tester/helper_288_underdrain.cjs b/resources/tester/helper_288_underdrain.cjs index eaf9c50ed..bcabb6a73 100644 --- a/resources/tester/helper_288_underdrain.cjs +++ b/resources/tester/helper_288_underdrain.cjs @@ -107,6 +107,8 @@ process.stdout.write( attached, sections: sections.map((s) => s.underdrain ?? null), outline: draw.underdrainOutline(design, 1), + // 371-3 암거 경사 3 % — 하단 = 높은 쪽(좌) 노면 끝 − 1 m 에서 3 % 로 낮은 쪽(우)으로 내림 + sloped: draw.underdrainOutline(design, 1, 3), bothCut: draw.underdrainOutline(bothCut, 1), noHeight: draw.underdrainOutline(design, null), drew: !!group, diff --git a/resources/tester/test_288_underdrain_cross.py b/resources/tester/test_288_underdrain_cross.py index 50c5526c8..c048d4bf6 100644 --- a/resources/tester/test_288_underdrain_cross.py +++ b/resources/tester/test_288_underdrain_cross.py @@ -1,10 +1,12 @@ """288 맹암거 횡단 형상(요구 183-3 · 9). 못박는 것 - - 288-1 맹암거 기준 측점 = 측구 없음 · 노면 = 암거 경사로 지대 낮은 쪽 한 경사(물넘이 286 · 287 길 — - 서버 `tilt_ranges` · `tilt_args` · TS 짝 `tilted`) · 제원 얹기 서버 `attach_underdrains` ↔ TS - `attachUnderdrainSpecs` 같은 값 · 형상 = 노면을 암거 높이만큼 아래로 옵셋 → 성토 쪽은 성토 사면과 - 만남 · 절토 쪽은 절토 사면을 늘여 만남 · 그 안 돌 무늬 · 높이 비면 안 그림. + - 288-1 맹암거 기준 측점 = 측구 없음 · 노면 = 표준 경사로 지대 낮은 쪽 한 경사 + (물넘이 286 · 287 길 — 서버 `tilt_ranges` · `tilt_args` · TS 짝 `tilted` · 암거 경사는 + 노면에 안 감 371-3) · 제원 얹기 서버 `attach_underdrains` ↔ TS `attachUnderdrainSpecs` + 같은 값 · 형상 = 노면을 암거 높이만큼 아래로 옵셋 → 성토 쪽은 성토 사면과 만남 · + 절토 쪽은 절토 사면을 늘여 만남 · 그 안 돌 무늬 · 높이 비면 안 그림. + - 371-3 암거 경사 = 형상 하단 경계 — 높은 쪽 노면 끝 − 높이에서 암거 경사로 낮은 쪽으로 내림. - 288-2 맹암거 형상 누름 = 그 구조물 id 고름(구조물 목록 창구 · 알약 · 폼 · 측점선 따라옴) · 물넘이와 같은 판정면(보라 손잡이 아래 · 끌기 = 고름 아님 · 287-5) · 노면공 포장 범위 고름과 따로. @@ -86,8 +88,9 @@ def ts(tmp_path_factory) -> dict: def test_맹암거_자리_기울기_서버(project): ranges = tilt.tilt_ranges(project) - assert ranges == [(40.0, 40.0, 4.0), (80.0, 80.0, None)] - assert tilt.tilt_args(40.0, ranges) == {"tilted": True, "tilt_pct": 4.0} + # 371-3 — 암거 경사(4 %)는 노면에 안 감 · 노면 = 표준 경사 + assert ranges == [(40.0, 40.0, None), (80.0, 80.0, None)] + assert tilt.tilt_args(40.0, ranges) == {"tilted": True, "tilt_pct": None} assert tilt.tilt_args(80.01, ranges) == {"tilted": True, "tilt_pct": None} assert tilt.tilt_args(60.0, ranges) == {"tilted": False, "tilt_pct": None} @@ -110,8 +113,9 @@ def test_맹암거_측점_설계_측구_없음_낮은_쪽_기울기(): for o in range(-24, 25) ] kwargs = {"ground_type": "soil", "section_mode": "left_cut", "ditch_side": "left"} - got = compute_cross_design(samples, 100.4, tilted=True, tilt_pct=4.0, **kwargs) - assert got["ditch_enabled"] is False and got["cross_slope_pct"] == 4.0 + got = compute_cross_design(samples, 100.4, tilted=True, tilt_pct=None, **kwargs) + standard = compute_cross_design(samples, 100.4, **kwargs)["cross_slope_pct"] + assert got["ditch_enabled"] is False and got["cross_slope_pct"] == standard left, right = (got["road_edges"][k]["elevation_m"] for k in ("left", "right")) assert left > right # 지반이 좌로 오름 = 우가 낮음 @@ -131,6 +135,20 @@ def test_형상_옵셋선_성토_절토와_만남(ts): assert ts["drew"] is True and ts["stones"] > 0 +def test_하단_경계는_암거_경사(ts): + """371-3 — 노면 5 % · 암거 3 % · 높이 1 m: 하단 = 좌(높은 쪽) 끝 99.15 에서 3 % 로 우로 내림.""" + outline = [(p["offset"], p["elevation"]) for p in ts["sloped"]] + expected = [ + (3, 100.15), + (-3, 99.85), + (-4.391304, 98.928261), # 우 성토 사면 ∩ 하단(99.15 − 0.03 × 7.391) + (1.872180, 99.116165), # 좌 절토 사면을 늘인 선 ∩ 하단 + ] + assert len(outline) == len(expected) + for got, want in zip(outline, expected): + assert got == pytest.approx(want, abs=1e-5) + + def test_양측절토_늘인_노면_원지반_교점에서_끊음(ts): """356-1 — 낮은 쪽 노면 끝도 절토(tilt_reach) = reach 에서 수직으로 끊음 · 너머 원지반 따라가지 않음.""" outline = [(p["offset"], p["elevation"]) for p in ts["bothCut"]] diff --git a/resources/tester/test_349_0n_cross_enforce_mirror.py b/resources/tester/test_349_0n_cross_enforce_mirror.py index ddf794d18..b692157b6 100644 --- a/resources/tester/test_349_0n_cross_enforce_mirror.py +++ b/resources/tester/test_349_0n_cross_enforce_mirror.py @@ -221,6 +221,9 @@ def test_포장_기울임_하강_판정_서버와_같음(r, server): hits["tilted"] += py["tilted"] hits["drop"] += py["drop"] > 0 assert all(hits.values()), hits # 셋 다 실제로 걸림 + # 371-3 — 맹암거 자리 = 기울임 · 경사 비움(노면 표준 · 암거 경사는 형상 하단 경계) + drain = [py for ts_at, py in zip(r["ts"]["at"], r["rules"]) if ts_at["chainage"] == 160.0] + assert drain == [{"paved": False, "tilted": True, "tilt_pct": None, "drop": 0.0}] KEYS = (