From 6d25379469794acaed19a5b3026b63cece5b244a Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 12 Oct 2026 20:15:17 +0900 Subject: [PATCH] =?UTF-8?q?feat(b05,b06):=20=EB=AC=BC=EB=84=98=EC=9D=B4?= =?UTF-8?q?=ED=8F=AC=EC=9E=A5=20=EB=8F=84=EC=88=98=EB=A1=9C/=EB=8F=8C?= =?UTF-8?q?=EB=B6=99=EC=9E=84=20=3D=20=EB=8B=A4=EB=A5=B8=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=EB=AC=BC=EA=B3=BC=20=EA=B0=99=EC=9D=80=20=EC=B9=B8=20?= =?UTF-8?q?+=20=EB=A7=A8=20=EC=95=84=EB=9E=98=20=E3=80=8C=EA=B8=B8?= =?UTF-8?q?=EC=9D=B4=20(m)=E3=80=8D=20=E2=80=94=20=EA=B7=B8=EB=A6=BC=20?= =?UTF-8?q?=EC=A0=84=EC=9A=A9=20(286-3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 명세 — 도수로/돌붙임 칸 = CHUTE_ITEMS(구조 · 형태 · 두께 · 면적) + 길이(ford_chute_{n}_length_m) - 서버 _chute · TS chute 짝 — 길이 있으면 제원에 length_m - 횡단 · 3D 띠 — 유출 성토사면을 노견에서 사면 따라 그 길이까지(비거나 사면보다 길면 원지반까지) - 길이 = 그림 전용 · 집계 열 · B08 돌붙임 수량(면적)에 안 실림 - 시험 test_286_1 · 물넘이 명세 시험 기대값 새 칸으로 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TteK8mfYZetk5P3bEZkyVZ --- .../design/design_structure_ford_pavement.ts | 25 +++---- ...05_Profile_UI_Corridor_Structures_Chute.ts | 4 +- B06_Section/B06_Section_Api_Types.ts | 2 + B06_Section/B06_Section_Engine_Culvert.py | 9 ++- .../B06_Section_UI_Cross_Ford_Pavement.ts | 30 ++++++++- common_util/common_util_culvert_sets.ts | 12 +++- .../tester/helper_286_1_ford_pavement.cjs | 28 ++++++++ .../tester/test_286_1_ford_pavement_design.py | 66 +++++++++++++++++++ .../test_52_3_structure_spec_ford_pavement.py | 9 ++- 9 files changed, 155 insertions(+), 30 deletions(-) diff --git a/A00_Common/design/design_structure_ford_pavement.ts b/A00_Common/design/design_structure_ford_pavement.ts index 8f3f729bf..8db636fa2 100644 --- a/A00_Common/design/design_structure_ford_pavement.ts +++ b/A00_Common/design/design_structure_ford_pavement.ts @@ -2,13 +2,16 @@ * design_structure_ford_pavement.ts * 횡단배수 › 물넘이 포장 명세 — 사용자 구조물 표 03(`docs/raw/구조물정의_표/03_횡단배수_물넘이포장.md`) 그대로. * - * 컨테이너 둘: 경사각도(필수 · 각도 손 입력 · 도 + 월류 폭 · 높이 · 65-5) · 도수로/돌붙임(선택 · 구조 = 도수로 · 돌붙임). - * 저장 = pipe_points.json `options` — 새 키 `ford_slope_deg` · 선택 컨테이너 `ford_chute_{n}_structure` · + * 컨테이너 둘: 경사각도(필수 · 각도 손 입력 · 도 + 월류 폭 · 높이 · 65-5) · 도수로/돌붙임(선택 · 다른 구조물과 + * 같은 칸 `CHUTE_ITEMS` + 맨 아래 길이 · 286-3). + * 저장 = pipe_points.json `options` — 새 키 `ford_slope_deg` · 선택 컨테이너 `ford_chute_{n}_*` · * 월류 폭 · 높이 = 세월교와 같은 키(`ford_width_m` · `ford_height_m`) · 같은 계산(`design_structure_ford_calc`). * 경사각도는 옛 「바닥 경사(%)」(`ford_slope_pct` · B06 파임 기울기)와 뜻이 같은지 또렷하지 않아 잇지 않음. - * import 없음 — 시험이 이 파일만 옮겨 돌림. + * 길이(`ford_chute_{n}_length_m`) = 횡단 · 3D 띠를 사면 따라 그릴 길이 — 그림 전용(수량 · 내역 · 일위대가에 안 씀) · + * 비면 원지반까지. * ========================================================================== */ +import { CHUTE_ITEMS } from "./design_structure_guard"; import type { StructureSpec } from "./design_structure_spec"; export const FORD_PAVEMENT_SPEC: StructureSpec = { @@ -32,20 +35,8 @@ export const FORD_PAVEMENT_SPEC: StructureSpec = { prefix: "ford_chute", link: "gfordpavechute", max: 1, - // 구조(기본 도수로) · 두께(cm · 기본 45 · 173-5) — 배관 · 박스 · 세월교 `CHUTE_ITEMS` 와 같은 칸(90-2) - items: [ - { - id: "structure", - label: "구조", - kind: "select", - choices: [ - { value: "도수로", label: "도수로" }, - { value: "돌붙임", label: "돌붙임" }, - ], - default: "도수로", - }, - { id: "thickness", label: "두께 (cm)", kind: "number", step: 1, default: "45" }, - ], + // 배관 · 박스 · 세월교와 같은 칸(구조 · 형태 · 두께 · 면적 · 90-2) + 맨 아래 길이(그림 전용 · 286-3) + items: [...CHUTE_ITEMS, { id: "length_m", label: "길이 (m)", kind: "number", step: 0.1 }], }, ], // 표 03 에 없는 옛 칸 — 화면에서 빼고 값은 남김(지난 정의 값) diff --git a/B05_Profile/B05_Profile_UI_Corridor_Structures_Chute.ts b/B05_Profile/B05_Profile_UI_Corridor_Structures_Chute.ts index 20cbd37d8..1cbe71281 100644 --- a/B05_Profile/B05_Profile_UI_Corridor_Structures_Chute.ts +++ b/B05_Profile/B05_Profile_UI_Corridor_Structures_Chute.ts @@ -11,7 +11,7 @@ import type { ChuteSpec, CrossSection } from "../B06_Section/B06_Section_Api_Fetch"; import type { OffsetPoint } from "../B06_Section/B06_Section_UI_Cross_Culvert_Types"; import { chuteBandOutline } from "../B06_Section/B06_Section_UI_Cross_Wall_Hatch"; -import { outletFillLine } from "../B06_Section/B06_Section_UI_Cross_Ford_Pavement"; +import { fordChuteLine } from "../B06_Section/B06_Section_UI_Cross_Ford_Pavement"; /** 성토 조각 하나 — B06 오버레이가 띠를 그리는 선(절토 조각은 뺀다). */ interface FillSegment { @@ -51,6 +51,6 @@ export function fordPavementChute( ); if (owner !== section) return null; } - const bands = chuteBands([{ points: outletFillLine(section.design) }], pave.chute); + const bands = chuteBands([{ points: fordChuteLine(section.design, pave.chute) }], pave.chute); return bands.length ? { bands, halfM: Math.max(pave.span_m, 0.5) / 2 } : null; } diff --git a/B06_Section/B06_Section_Api_Types.ts b/B06_Section/B06_Section_Api_Types.ts index d00053611..5144da188 100644 --- a/B06_Section/B06_Section_Api_Types.ts +++ b/B06_Section/B06_Section_Api_Types.ts @@ -228,6 +228,8 @@ export interface CulvertSideSpec { export interface ChuteSpec { structure: string; thickness_m: number; + /** 사면 따라 그릴 길이(m · 물넘이 포장 286-3) — 그림 전용 · 없으면 원지반까지. */ + length_m?: number; } /** 배수관 측점의 세트(배관·기슭막이·보호공) 제원 — 횡단 카드 오버레이 입력. */ diff --git a/B06_Section/B06_Section_Engine_Culvert.py b/B06_Section/B06_Section_Engine_Culvert.py index 4f50a3c08..1ccc86b17 100644 --- a/B06_Section/B06_Section_Engine_Culvert.py +++ b/B06_Section/B06_Section_Engine_Culvert.py @@ -251,7 +251,8 @@ def _side_spec( def _chute(values: dict[str, Any], head: str = "chute_add") -> dict[str, Any] | None: """도수로 · 돌붙임(90-2) — 첫 상자(`{head}_{n}_*` · 물넘이 포장 = `ford_chute` · 136-2) 구조 · 두께(cm → m · 빈 값 30 cm) · 구조를 안 고르면 None. 횡단이 유출 성토사면을 따라 띠로 그림. - TS 짝 `chute`.""" + 길이(`{head}_{n}_length_m` · 286-3) = 사면 따라 그릴 길이 — 그림 전용(수량 · 내역에 안 씀) · + 비면 원지반까지. TS 짝 `chute`.""" pattern = re.compile(rf"^{head}_(\d+)_structure$") picked = sorted( (int(hit.group(1)), str(value).strip()) @@ -263,7 +264,11 @@ def _chute(values: dict[str, Any], head: str = "chute_add") -> dict[str, Any] | n, structure = picked[0] cm = _number(values.get(f"{head}_{n}_thickness"), None) cm = cm if cm and cm > 0 else CHUTE_THICKNESS_CM - return {"structure": structure, "thickness_m": round(cm / 100.0, 3)} + found = {"structure": structure, "thickness_m": round(cm / 100.0, 3)} + length = _number(values.get(f"{head}_{n}_length_m"), None) + if length and length > 0: + found["length_m"] = round(length, 3) + return found def _with_chute( diff --git a/B06_Section/B06_Section_UI_Cross_Ford_Pavement.ts b/B06_Section/B06_Section_UI_Cross_Ford_Pavement.ts index 5ca23d584..89d038df6 100644 --- a/B06_Section/B06_Section_UI_Cross_Ford_Pavement.ts +++ b/B06_Section/B06_Section_UI_Cross_Ford_Pavement.ts @@ -34,8 +34,8 @@ export interface FordPavementSpec { slope_pct: number | null; /** 정본에 안 적혀 기본값으로 그린 칸 「이름 값」 — 카드 머리에 「기본값(미확정)」. */ defaulted?: string[]; - /** 도수로 · 돌붙임(`ford_chute_{n}_*` · 136-2) — 유출측 성토사면 띠. */ - chute?: { structure: string; thickness_m: number }; + /** 도수로 · 돌붙임(`ford_chute_{n}_*` · 136-2) — 유출측 성토사면 띠 · 길이 = 사면 따라(286-3 · 비면 원지반까지). */ + chute?: { structure: string; thickness_m: number; length_m?: number }; } interface Edge { @@ -190,7 +190,8 @@ export function appendFordPavementOverlay( "b06-chart__ford-deck", ), ); - if (spec.chute) appendChuteBand(svg, [{ points: outletFillLine(design) }], spec.chute, x, y); + if (spec.chute) + appendChuteBand(svg, [{ points: fordChuteLine(design, spec.chute) }], spec.chute, x, y); return true; } @@ -218,6 +219,29 @@ export function outletFillLine(design: CrossDesign): OffsetPoint[] { return line; } +/** + * 물넘이 도수로 · 돌붙임 선(286-3) — 유출 성토사면 선을 노견에서 사면 따라 `length_m` 까지 자름 · + * 비거나 사면보다 길면 원지반까지. 횡단 띠 · 3D 띠가 같이 씀(그림 전용). + */ +export function fordChuteLine(design: CrossDesign, chute?: { length_m?: number }): OffsetPoint[] { + const line = outletFillLine(design); + let left = chute?.length_m ?? 0; + if (!(left > 0)) return line; + const out = line.slice(0, 1); + for (let i = 1; i < line.length && left > 1e-9; i += 1) { + const [a, b] = [line[i - 1], line[i]]; + const step = Math.hypot(b.offset - a.offset, b.elevation - a.elevation); + if (step < 1e-9) continue; + const t = Math.min(left / step, 1); + out.push({ + offset: a.offset + (b.offset - a.offset) * t, + elevation: a.elevation + (b.elevation - a.elevation) * t, + }); + left -= step; + } + return out; +} + /** * 물넘이 자리 노면 · 측구 설계선을 걷는다(66-2 · 66-3) — 노면은 파인 바닥(실선)과 옛 계획고(점선)가 * 대신하고, 측구는 서지 않음. `from` = 설계선 그리기 앞의 자식 수(그 뒤에 붙은 선만 본다). diff --git a/common_util/common_util_culvert_sets.ts b/common_util/common_util_culvert_sets.ts index e1a92706a..7cf5c21bc 100644 --- a/common_util/common_util_culvert_sets.ts +++ b/common_util/common_util_culvert_sets.ts @@ -199,11 +199,12 @@ function sideSpec( } /** 도수로 · 돌붙임(90-2) — 첫 상자(`{head}_{n}_*` · 물넘이 포장 = `ford_chute` · 136-2) 구조 · 두께 - * (cm → m · 빈 값 45 cm) · 구조를 안 고르면 null. 횡단이 유출 성토사면을 따라 띠로 그림. 파이썬 짝 `_chute`. */ + * (cm → m · 빈 값 45 cm) · 구조를 안 고르면 null. 횡단이 유출 성토사면을 따라 띠로 그림 · 길이 + * (`{head}_{n}_length_m` · 286-3) = 사면 따라 그릴 길이(그림 전용 · 비면 원지반까지). 파이썬 짝 `_chute`. */ export function chute( values: Record, head = "chute_add", -): { structure: string; thickness_m: number } | null { +): { structure: string; thickness_m: number; length_m?: number } | null { const pattern = new RegExp(`^${head}_(\\d+)_structure$`); const picked = Object.entries(values) .flatMap(([key, value]) => { @@ -215,7 +216,12 @@ export function chute( if (!picked.length) return null; const [n, structure] = picked[0]; const cm = num(values[`${head}_${n}_thickness`], null); - return { structure, thickness_m: round((cm && cm > 0 ? cm : CHUTE_THICKNESS_CM) / 100, 3) }; + const found = { + structure, + thickness_m: round((cm && cm > 0 ? cm : CHUTE_THICKNESS_CM) / 100, 3), + }; + const length = num(values[`${head}_${n}_length_m`], null); + return length && length > 0 ? { ...found, length_m: round(length, 3) } : found; } /** 도수로 · 돌붙임이 있으면 세트에 `chute` 로 실음(90-2). */ diff --git a/resources/tester/helper_286_1_ford_pavement.cjs b/resources/tester/helper_286_1_ford_pavement.cjs index 4174223a4..1327b58f8 100644 --- a/resources/tester/helper_286_1_ford_pavement.cjs +++ b/resources/tester/helper_286_1_ford_pavement.cjs @@ -42,7 +42,35 @@ const deck = (design) => ({ right: ford.fordDeckElevationAt(design, SPEC, -3), }); +// 286-3 도수로 · 돌붙임 길이 — 유출(좌 · 우가 높음) 성토사면 1:1.5(5 m 내려감 · 사면 길이 √(7.5²+5²) ≈ 9.01) +const sets = loadTs(path.join(ROOT, "common_util", "common_util_culvert_sets.ts")); +const slopeDesign = { + design_elevation_m: 100, + cross_slope_pct: 2, + section_mode: "left_cut", + road_edges: { + left: { offset_m: 2.5, elevation_m: 100 }, + right: { offset_m: -2.5, elevation_m: 100 }, + }, + design_line: [ + { offset_m: 10, elevation_m: 95 }, + { offset_m: 2.5, elevation_m: 100 }, + { offset_m: -2.5, elevation_m: 100 }, + { offset_m: -6, elevation_m: 104 }, + ], +}; +const chuteOf = (extra) => + sets.chute( + { ford_chute_1_structure: "돌붙임", ford_chute_1_thickness: 30, ...extra }, + "ford_chute", + ); + const out = { + chuteNoLength: chuteOf({}), + chuteLength: chuteOf({ ford_chute_1_length_m: 3 }), + lineNoLength: ford.fordChuteLine(slopeDesign, chuteOf({})), + lineLength: ford.fordChuteLine(slopeDesign, chuteOf({ ford_chute_1_length_m: 3 })), + lineTooLong: ford.fordChuteLine(slopeDesign, chuteOf({ ford_chute_1_length_m: 50 })), fordMarks: station.pipeStationMarks({ chainage_m: 230, facility: "ford_pavement" }), planted: station.plantedChainages(input.pipes, input.structures, input.types), deckLeftHigh: deck(designOf(105, 95, "right_cut")), diff --git a/resources/tester/test_286_1_ford_pavement_design.py b/resources/tester/test_286_1_ford_pavement_design.py index 30e2c247c..b19354ba6 100644 --- a/resources/tester/test_286_1_ford_pavement_design.py +++ b/resources/tester/test_286_1_ford_pavement_design.py @@ -5,6 +5,8 @@ `resolve_extra_stations` 같은 값 · 라벨 「물넘이포장」 · B05 · B06 거름 걷음. - 286-2 물넘이 자리 = 측구 없음(서버 정본 · 화면 짝 · 거울 = `test_b06_cross_design_mirror`) · 노면 = 경사각도로 표고 낮은 쪽(설계선 양 끝 표고 · 같으면 단면유형). + - 286-3 도수로/돌붙임 = 다른 구조물과 같은 칸 + 맨 아래 「길이 (m)」(`ford_chute_{n}_length_m`) · + 비면 원지반까지 · 있으면 사면 따라 그 길이 · 서버 · TS 같은 값 · 집계 · B08 수량에 안 실림. 진짜 TS 는 `helper_286_1_ford_pavement.cjs` 가 Node 로 돌림. 화면은 사용자 확인. """ @@ -27,7 +29,10 @@ from B05_Profile.B05_Profile_Engine_Sections import resolve_extra_stations # no from B05_Profile.B05_Profile_Structures_Schema import structure_type_map # noqa: E402 from B06_Section.B06_Section_Engine_Design import compute_cross_design # noqa: E402 from B06_Section.B06_Section_Router_Design import ford_pavement_ranges, paved_at # noqa: E402 +from B06_Section.B06_Section_Engine_Culvert import _chute # noqa: E402 +from B08_Quantity.B08_Quantity_Router_Summary import _chutes # noqa: E402 from common_util.common_util_drainage_pipes import parse_pipe_points # noqa: E402 +from M02_MasterTemplete.M02_Table_Fill_Spec import container_rows # noqa: E402 PIPES = [ {"chainage_m": 95.0, "source": "user"}, @@ -135,3 +140,64 @@ def test_저장_뒤_서버가_물넘이_측구를_맞춤(): assert 'bool(design.get("ford_paved")) == ford_paved' in router refresh = _read("B06_Section", "B06_Section_Cross_Refresh.ts") assert "fordPaved: Boolean(section.ford_pavement)," in refresh + + +CHUTE = {"ford_chute_1_structure": "돌붙임", "ford_chute_1_thickness": 30} + + +def test_도수로_길이_서버_TS_같은_값(ts): + assert ( + _chute(CHUTE, "ford_chute") + == ts["chuteNoLength"] + == { + "structure": "돌붙임", + "thickness_m": 0.3, + } + ) + long = {**CHUTE, "ford_chute_1_length_m": 3} + assert _chute(long, "ford_chute") == ts["chuteLength"] + assert ts["chuteLength"]["length_m"] == 3 + + +def test_도수로_길이_없음_원지반까지_있음_그_길이(ts): + # 유출 = 좌(우가 표고 높음) · 노견(2.5, 100) → 원지반(10, 95) + assert ts["lineNoLength"] == [ + {"offset": 2.5, "elevation": 100}, + {"offset": 10, "elevation": 95}, + ] + start, end = ts["lineLength"] + assert start == {"offset": 2.5, "elevation": 100} + assert (end["offset"] - 2.5) ** 2 + (100 - end["elevation"]) ** 2 == pytest.approx(9.0) + assert end["offset"] == pytest.approx(2.5 + 3 * 7.5 / 9.0139, abs=1e-3) + assert ts["lineTooLong"] == ts["lineNoLength"] # 사면보다 길면 원지반에서 멈춤 + + +def test_컨테이너_같은_칸_맨_아래_길이(): + spec = _read("A00_Common", "design", "design_structure_ford_pavement.ts") + assert ( + 'items: [...CHUTE_ITEMS, { id: "length_m", label: "길이 (m)", kind: "number", step: 0.1 }],' + in spec + ) + + +def test_길이는_집계_수량에_안_실림(): + options = {**CHUTE, "ford_chute_1_bond": "찰붙임", "ford_chute_1_area": 12} + item = {"type_id": "ford_pavement", "options": options, "복사본": "pipe@50.0"} + plain = container_rows(item, {"gfordpavechute1": "SS1"}) + long = container_rows( + {**item, "options": {**options, "ford_chute_1_length_m": 7}}, {"gfordpavechute1": "SS1"} + ) + # 집계 열이 읽는 칸(length_m · 쪽별 revet_length_m)은 그대로 — 길이는 안 읽히는 이름으로만 + for key in ("length_m", "inlet_revet_length_m", "outlet_revet_length_m", "area_m"): + assert plain[0]["options"].get(key) == long[0]["options"].get(key) + + class Sheet: + def __init__(self) -> None: + self.rows: list[tuple] = [] + + def add(self, at, key, sort, group, label, value, combos) -> None: + self.rows.append((key, label, value)) + + sheet = Sheet() + _chutes(sheet, 50.0, {**options, "ford_chute_1_length_m": 7}, {"gfordpavechute1": "SS1"}, {}) + assert sheet.rows == [("chute|찰붙임", "㎡", 12.0)] # 돌붙임 수량 = 면적 · 길이 안 탐 diff --git a/resources/tester/test_52_3_structure_spec_ford_pavement.py b/resources/tester/test_52_3_structure_spec_ford_pavement.py index 6162dece5..eddc984ea 100644 --- a/resources/tester/test_52_3_structure_spec_ford_pavement.py +++ b/resources/tester/test_52_3_structure_spec_ford_pavement.py @@ -25,7 +25,9 @@ const load = (file) => { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText; const box = { exports: {} }; - new Function("exports", "module", "require", js)(box.exports, box, () => ({})); + // 같은 폴더 명세 import(`CHUTE_ITEMS` · 286-3)만 따라 옮김 + const near = (m) => (m.startsWith("./") ? load(path.join(path.dirname(file), `${m}.ts`)) : {}); + new Function("exports", "module", "require", js)(box.exports, box, near); return box.exports; }; const s = load(path.join(ROOT, "A00_Common/design/design_structure_spec.ts")); @@ -75,8 +77,9 @@ def test_저장_값은_도_와_인스턴스_키(out): "width": "6", "height": "0.3", } # 월류 폭 · 높이 = 옛 키 그대로 읽음(65-5) - # 두께(cm · 90-2) — 빈 칸은 빈 값(새 상자만 기본 30) - assert out["state"] == {"slope": [slope], "chute": [{"structure": "돌붙임", "thickness": ""}]} + # 다른 구조물과 같은 칸(구조 · 형태 · 두께 · 면적) + 길이(286-3) — 빈 칸은 빈 값(새 상자만 기본값) + chute = {"structure": "돌붙임", "bond": "", "thickness": "", "area": "", "length_m": ""} + assert out["state"] == {"slope": [slope], "chute": [chute]} assert out["written"] == { "ford_slope_deg": 15, "ford_width_m": 6,