From a5a9a7cec0e7b2bc9f161d1f8cd89d954fe32469 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 12 Oct 2026 20:57:54 +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=EC=BD=98=ED=81=AC=EB=A6=AC=ED=8A=B8=20?= =?UTF-8?q?=EC=9C=97=EB=A9=B4=20=3D=20=EC=84=A4=EA=B3=84=20=EB=85=B8?= =?UTF-8?q?=EB=A9=B4=20=C2=B7=20=ED=8F=AC=EC=9E=A5=EC=B8=B5=20=EB=85=B8?= =?UTF-8?q?=EA=B2=AC=EA=B9=8C=EC=A7=80=20=C2=B7=20=EC=9B=94=EB=A5=98=20?= =?UTF-8?q?=EB=86=92=EC=9D=B4=20=EC=A0=90=EC=84=A0=EC=9D=80=20=EB=85=B8?= =?UTF-8?q?=EB=A9=B4=20=EC=9C=84=20(287-2=20=C2=B7=20287-4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 설계 계산 tilted · tilt_pct(파이썬 · TS 짝) — 물넘이 범위 노면 = 경사각도로 지대 낮은 쪽 한 경사 · 측구 없음 · 중심점 = 계획고(콘크리트 윗면) · 노면 끝에서 사면이 이어짐(짧으면 성토) - 서버 tilt_ranges · tilt_args · stored_tilt — 기본 설계 · [저장] 재계산 · 포장 범위 맞춤 · 저장분 재계산이 같은 값(옛 ford_paved 갈음) - 횡단 그림 — 포장층 = 노면공 포장 형상(appendPavementOverlay)을 노견까지 · 물넘이 표식 · 월류 높이선 = 노면 + 높이 같은 각도 · 옛 파임 · 노면 걷기 그림 걷음 - 3D 노면 파기 걷음(설계 노면이 곧 윗면) · 코리도 빌드 판 94 - 시험 test_287 · 286_1 · 횡단 설계 거울(기울임 갈래) · 물넘이 명세 시험 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TteK8mfYZetk5P3bEZkyVZ --- B05_Profile/B05_Profile_Router_Confirm.py | 3 +- .../B05_Profile_UI_Corridor_Envelope.ts | 3 +- .../B05_Profile_UI_Corridor_Station.ts | 19 +- B06_Section/B06_Section_Api_Types.ts | 3 + B06_Section/B06_Section_Cross_Refresh.ts | 5 +- B06_Section/B06_Section_Engine_Culvert.py | 9 +- B06_Section/B06_Section_Engine_Design.py | 25 ++- .../B06_Section_Engine_Design_Geometry.py | 12 +- .../B06_Section_Engine_SampleExtend.py | 3 +- B06_Section/B06_Section_Engine_SpoilFill.py | 8 +- B06_Section/B06_Section_Router.py | 5 +- B06_Section/B06_Section_Router_Design.py | 48 +++-- .../B06_Section_UI_Cross_Ford_Pavement.ts | 202 ++++-------------- B06_Section/B06_Section_UI_Cross_View.ts | 11 - .../B06_Section_UI_Style_Cross_Areas.css | 15 +- common_util/common_util_cross_design.ts | 20 +- .../common_util_cross_design_geometry.ts | 14 +- common_util/common_util_culvert_sets.ts | 2 +- .../tester/helper_286_1_ford_pavement.cjs | 20 +- resources/tester/helper_287_ford_pavement.cjs | 58 +++++ .../tester/test_286_1_ford_pavement_design.py | 31 ++- .../tester/test_287_ford_pavement_followup.py | 26 +++ .../tester/test_b06_cross_design_mirror.py | 19 +- 23 files changed, 273 insertions(+), 288 deletions(-) diff --git a/B05_Profile/B05_Profile_Router_Confirm.py b/B05_Profile/B05_Profile_Router_Confirm.py index 32ae9dd84..0fde4d5a6 100644 --- a/B05_Profile/B05_Profile_Router_Confirm.py +++ b/B05_Profile/B05_Profile_Router_Confirm.py @@ -107,6 +107,7 @@ async def sync_uphill_overrides_into_designs( compute_cross_design, curve_widening_args, stored_shoulder_extend, + stored_tilt, ) from B06_Section.B06_Section_Repository import get_cross_section_designs from B06_Section.B06_Section_Repository_Bulk import merge_cross_section_designs @@ -175,7 +176,7 @@ async def sync_uphill_overrides_into_designs( surface_drop_m=ford_drop_at(float(chainage), ford_drops), **curve_widening_args(cross_record), **stored_shoulder_extend(design), # 노견 확장(139-6) - ford_paved=bool(design.get("ford_paved")), # 물넘이 자리 측구 없음(286-2) + **stored_tilt(design), # 물넘이 · 맹암거 자리 측구 없음 · 기울기(286-2 · 287-4) ) next_design["status"] = design.get("status", "provisional") next_design["pavement_suggested"] = design.get("pavement_suggested", pavement_suggested) diff --git a/B05_Profile/B05_Profile_UI_Corridor_Envelope.ts b/B05_Profile/B05_Profile_UI_Corridor_Envelope.ts index 771f51404..3530d33cc 100644 --- a/B05_Profile/B05_Profile_UI_Corridor_Envelope.ts +++ b/B05_Profile/B05_Profile_UI_Corridor_Envelope.ts @@ -181,7 +181,8 @@ function fnv1a(text: string): string { // (세월교 유입·유출, BOX 좌·우, 본체는 `body`). 91 저장본엔 배수관 세트 키만 있다. // 93 = 측점 사이 행 자리를 **노선 길이 직선 보간**으로(115-5 P0-2) — 균일 Catmull-Rom 이 // 행을 최대 7.2 m 미끄러뜨리고 되접었다. 모든 행 자리가 바뀌어 저장본을 만료한다. -const BUILD_VERSION = 93; +// 94 = 물넘이포장 노면을 파지 않음(287-4) — 설계 노면이 콘크리트 윗면 · 저장본을 만료한다. +const BUILD_VERSION = 94; /** 종횡단 정본에서 코리도에 영향을 주는 입력만 요약해 해시 — 갱신 감지 기준. */ export function corridorHash(detail: SectionDetailResponse, routePoints: RoutePoint[]): string { diff --git a/B05_Profile/B05_Profile_UI_Corridor_Station.ts b/B05_Profile/B05_Profile_UI_Corridor_Station.ts index 4ef2cdb40..400218fa7 100644 --- a/B05_Profile/B05_Profile_UI_Corridor_Station.ts +++ b/B05_Profile/B05_Profile_UI_Corridor_Station.ts @@ -9,7 +9,6 @@ * (2026-08-23 사용자 지시)의 핵심. * ========================================================================== */ -import { fordDeckElevationAt } from "../B06_Section/B06_Section_UI_Cross_Ford_Pavement"; import type { CrossSection } from "../B06_Section/B06_Section_Api_Fetch"; import { structureSilhouettes } from "./B05_Profile_UI_Corridor_Station_Structure"; @@ -227,22 +226,8 @@ export function classifyStation(section: CrossSection): StationPieces | null { if (!design || !line || line.length < 2) return null; const roadL = design.road_edges.left.offset_m; const roadR = design.road_edges.right.offset_m; - // 물넘이포장 — 노면(노견 포함)이 파인다. 횡단도와 **같은 산식**으로 표고를 내려 - // 3D도 같은 자리에 같은 깊이로 팬다(2026-08-28 사용자 지시). 노면 밖 비탈은 그대로라 - // 노견 끝에 파임 벽(수직 단차)이 선다 — 횡단도 그림과 같은 표현이다. - const ford = section.ford_pavement; - const carved = - ford && ford.depth_m - ? line.map((point) => - point.offset_m <= roadL + 1e-9 && point.offset_m >= roadR - 1e-9 - ? { - ...point, - elevation_m: fordDeckElevationAt(design, ford, point.offset_m, point.elevation_m), - } - : point, - ) - : line; - const sorted = [...carved].sort((a, b) => a.offset_m - b.offset_m); + // 물넘이포장 — 설계 노면이 곧 콘크리트 윗면(287-4 · 서버가 지대 낮은 쪽 한 경사로 냄) · 파지 않음 + const sorted = [...line].sort((a, b) => a.offset_m - b.offset_m); const cwL = design.carriageway_edges?.left.offset_m ?? roadL; const cwR = design.carriageway_edges?.right.offset_m ?? roadR; diff --git a/B06_Section/B06_Section_Api_Types.ts b/B06_Section/B06_Section_Api_Types.ts index 5144da188..45a42aa34 100644 --- a/B06_Section/B06_Section_Api_Types.ts +++ b/B06_Section/B06_Section_Api_Types.ts @@ -495,6 +495,9 @@ export interface CrossDesign { right: { offset_m: number; elevation_m: number }; }; design_elevation_m: number; + /** 물넘이포장 · 맹암거 자리(286-2 · 287-4) — 측구 없음 · 노면 = 지대 낮은 쪽 한 경사(`tilt_pct`). */ + tilted?: boolean; + tilt_pct?: number; cut_area_m2: number; /** * 절토 내역(합 = `cut_area_m2`). 지표면~암반 경계선이 토사, 그 아래가 암이다. diff --git a/B06_Section/B06_Section_Cross_Refresh.ts b/B06_Section/B06_Section_Cross_Refresh.ts index 437e948a4..d4fd143f2 100644 --- a/B06_Section/B06_Section_Cross_Refresh.ts +++ b/B06_Section/B06_Section_Cross_Refresh.ts @@ -359,8 +359,9 @@ function refreshLocally(input: CrossRefreshInput): number[] | null { choice?.ditch_type ?? (typeof design.ditch_type === "string" ? design.ditch_type : "standard"), paved: pavedBySpecs(section), - // 물넘이 자리 = 측구 없음(286-2 · 서버 `ford_pavement_ranges` 짝 = 얹힌 제원 범위) - fordPaved: Boolean(section.ford_pavement), + // 물넘이 자리 = 측구 없음 · 지대 낮은 쪽 한 경사(286-2 · 287-4 · 서버 `tilt_ranges` 짝 = 얹힌 제원) + tilted: Boolean(section.ford_pavement), + tiltPct: section.ford_pavement?.slope_pct ?? null, standard, berm: bermAt(section.chainage_m, design), rockBoundaryOffsetM, diff --git a/B06_Section/B06_Section_Engine_Culvert.py b/B06_Section/B06_Section_Engine_Culvert.py index 1ccc86b17..43540c9f2 100644 --- a/B06_Section/B06_Section_Engine_Culvert.py +++ b/B06_Section/B06_Section_Engine_Culvert.py @@ -513,11 +513,10 @@ def _ford_set(options: dict[str, Any] | None) -> dict[str, Any]: def _ford_pavement_set(options: dict[str, Any] | None) -> dict[str, Any]: - """물넘이포장 1개소 — 노면을 월류 폭만큼, 노선 중심에서 월류 높이만큼 판 자리. + """물넘이포장 1개소 — 월류 폭 범위의 노면 = 콘크리트 윗면(287-4). - 구조물을 얹는 것이 아니라 **계획고를 파낸 노면**이라 다른 세트와 형상이 다르다. - 바닥은 유입(상류) 쪽이 높고 유출 쪽이 낮게 기운다 — 경사를 사용자가 비워 두면 - 화면이 노면 일반 기울기로 채운다(2026-08-28 사용자 확정). 파임 폭은 노견까지 전폭. + 설계 노면을 경사각도(`slope_pct`)로 지대 낮은 쪽 한 경사로 기울임(`tilt_ranges` → + `compute_cross_design(tilted=…)`) · 월류 높이(`depth_m`) = 노면 위 점선(그림 전용). """ defaults = _registry_defaults("ford_pavement") values = dict(options or {}) @@ -536,7 +535,7 @@ def _ford_pavement_set(options: dict[str, Any] | None) -> dict[str, Any]: { "type": "ford_pavement", "span_m": span, - # 노선 중심에서 잰 깊이. 없으면 화면이 파임을 그리지 않는다(수치를 지어내지 않는다). + # 월류 높이 — 노면 위 점선. 없으면 안 그린다(수치를 지어내지 않는다). "depth_m": depth if depth and depth > 0 else None, "slope_pct": slope, "defaulted": _defaulted( diff --git a/B06_Section/B06_Section_Engine_Design.py b/B06_Section/B06_Section_Engine_Design.py index 53d9f7efb..3f9e27daa 100644 --- a/B06_Section/B06_Section_Engine_Design.py +++ b/B06_Section/B06_Section_Engine_Design.py @@ -155,6 +155,13 @@ def stored_shoulder_extend(stored: dict[str, Any] | None) -> dict[str, float]: return out +def stored_tilt(stored: dict[str, Any] | None) -> dict[str, Any]: + """저장분의 구조물 자리 기울기(287-4 · 288-1) — `compute_cross_design(**...)` 로 넘긴다.""" + value = (stored or {}).get("tilt_pct") + pct = float(value) if isinstance(value, (int, float)) and not isinstance(value, bool) else None + return {"tilted": bool((stored or {}).get("tilted")), "tilt_pct": pct} + + def curve_widening_args(section: dict[str, Any] | None) -> dict[str, Any]: """측점 기록에서 곡선부 확폭 입력을 뽑는다 — `compute_cross_design(**...)` 로 넘긴다. @@ -194,7 +201,8 @@ def compute_cross_design( spoil_fill: dict[str, Any] | None = None, shoulder_extend_left_m: float = 0.0, shoulder_extend_right_m: float = 0.0, - ford_paved: bool = False, + tilted: bool = False, + tilt_pct: float | None = None, ) -> dict[str, Any]: """측점 하나의 표준횡단 설계선과 절·성토 단면적을 계산한다. @@ -211,8 +219,9 @@ def compute_cross_design( 입력이며, None·45m 이상이면 확폭이 없다. curve_outer_side: 곡선 **바깥쪽**("left"/"right"). 확폭은 그쪽으로만 붙는다 (2026-09-06 사용자 확정). 값이 없으면 확폭을 넣지 않는다. - ford_paved: 물넘이포장 범위 안 측점(286-2) — 측구 없음 · 결과에 `ford_paved` 로 되실음. - TS 짝 `fordPaved`. + tilted · tilt_pct: 물넘이포장 범위 · 맹암거 측점(286-2 · 287-4 · 288-1) — 측구 없음 · + 노면 = `tilt_pct`(비면 표준 횡단경사)로 지대 낮은 쪽 한 경사. 결과에 `tilted` · `tilt_pct` + 로 되실음(`stored_tilt`). TS 짝 `tilted` · `tiltPct`. surface_drop_m: 노면을 통째로 내리는 양(m) — 세월교 월류 높이. 구체 위 노면은 월류 높이만큼 낮게 앉으므로 계획고를 그만큼 내려 잡는다. 단면 전체가 평행 이동하므로 횡단경사·측구·사면 규칙은 그대로고 절·성토 면적만 따라 바뀐다(2026-08-30 사용자). @@ -246,6 +255,8 @@ def compute_cross_design( paved_group = _resolve_group("paved", standard) # 포장 중첩: 횡단경사와 포장층 두께만 포장 그룹을 따른다. cross_slope_pct = paved_group["cross_slope_pct"] if paved else group["cross_slope_pct"] + if tilted and tilt_pct is not None and float(tilt_pct) > 0: + cross_slope_pct = float(tilt_pct) # 구조물 자리 경사(287-4 · 288-1) resolved_ditch_side = _resolve_ditch_side(section_mode, ditch_side) valid = sorted( @@ -302,7 +313,7 @@ def compute_cross_design( berm=berm, shoulder_extend_left_m=shoulder_extend_left_m, shoulder_extend_right_m=shoulder_extend_right_m, - no_ditch=bool(ford_paved), + tilted=bool(tilted), ) # 적분 오프셋 = 지반 샘플 ∪ 설계 꼭짓점(샘플 범위 안쪽만). 꼭짓점을 넣어야 @@ -533,8 +544,10 @@ def compute_cross_design( # 뒤에 쓸 자리가 있다. **되살릴 때는 저장분에서 읽지 말고 계산해서 쓸 것.** "cut_slope_segments": geometry.cut_slope_segments(), } - if ford_paved: - result["ford_paved"] = True + if tilted: + result["tilted"] = True + if tilt_pct is not None: + result["tilt_pct"] = round(float(tilt_pct), 4) if drop > 0: # 내려 앉힌 양 — 프론트가 "월류가 없었다면" 노면을 점선으로 되그리는 데 쓴다. result["surface_drop_m"] = round(drop, 4) diff --git a/B06_Section/B06_Section_Engine_Design_Geometry.py b/B06_Section/B06_Section_Engine_Design_Geometry.py index 5b554b273..6131e19f7 100644 --- a/B06_Section/B06_Section_Engine_Design_Geometry.py +++ b/B06_Section/B06_Section_Engine_Design_Geometry.py @@ -62,7 +62,7 @@ class _SectionGeometry: berm: BermSpec | None = None, shoulder_extend_left_m: float = 0.0, shoulder_extend_right_m: float = 0.0, - no_ditch: bool = False, + tilted: bool = False, ) -> None: half_road = group["road_width_m"] / 2.0 # 곡선부 확폭은 **한쪽으로만** 붙는다(2026-09-06 사용자 확정: 곡선 바깥쪽). @@ -96,6 +96,11 @@ class _SectionGeometry: # 횡단경사: 측구 방향으로 내려가는 단일 사면 (좌=+offset 규약). slope = cross_slope_pct / 100.0 self.slope_per_offset = -slope if ditch_side == "left" else slope + # 물넘이포장 · 맹암거 자리(287-4 · 288-1) — 노면 = 한 경사로 지대 낮은 쪽(노면 끝 + # 원지반이 낮은 쪽)으로 기움 · 측구 없음(아래). ⚠ TS 짝 `SectionGeometry`. + if tilted and ground_at is not None: + low_left = ground_at(self.left_extent) < ground_at(-self.right_extent) + self.slope_per_offset = -slope if low_left else slope # 단면 유형 자동 판정(D-2): 각 측 절/성토 역할을 노면 끝 지반이 설계면보다 # 높은지(절토)/낮은지(성토)로 결정한다. 좌절/우절/양절/양성이 모두 지형에서 @@ -145,8 +150,9 @@ class _SectionGeometry: self.has_ditch = auto_ditch if (choice is None or both_fill) else choice #: 그 측점에 **사용자가 정한 선택**(없으면 자동). 결과와 갈라 내보낸다. self.ditch_choice = None if both_fill else choice - # 물넘이포장 자리(286-2) — 파인 노면이라 측구가 안 섬 · 선택은 그대로(시설을 걷으면 되살아남). - if no_ditch: + # 물넘이포장 · 맹암거 자리(286-2 · 288-1) — 측구가 안 섬 · 선택은 그대로(시설을 걷으면 + # 되살아남). + if tilted: self.has_ditch = False # 설계선 쉬프트 = 쪽마다 사면 이동(≥ 0 · 139-6 · 151-2) — 단면유형 · 측구 판정은 **옮기기 전** diff --git a/B06_Section/B06_Section_Engine_SampleExtend.py b/B06_Section/B06_Section_Engine_SampleExtend.py index 29eac4fb1..e2dc0da03 100644 --- a/B06_Section/B06_Section_Engine_SampleExtend.py +++ b/B06_Section/B06_Section_Engine_SampleExtend.py @@ -25,6 +25,7 @@ from B06_Section.B06_Section_Engine_Design import ( compute_cross_design, curve_widening_args, stored_shoulder_extend, + stored_tilt, ) from common_util.common_util_json import atomic_write_json @@ -158,7 +159,7 @@ def extend_unclosed_sections( berm=stored_berm(design), **curve_widening_args(section), **stored_shoulder_extend(design), # 노견 확장(139-6) - ford_paved=bool(design.get("ford_paved")), # 물넘이 자리 측구 없음(286-2) + **stored_tilt(design), # 물넘이 · 맹암거 자리 측구 없음 · 기울기(286-2 · 287-4) ) try: diff --git a/B06_Section/B06_Section_Engine_SpoilFill.py b/B06_Section/B06_Section_Engine_SpoilFill.py index c7b0c2d30..30d267132 100644 --- a/B06_Section/B06_Section_Engine_SpoilFill.py +++ b/B06_Section/B06_Section_Engine_SpoilFill.py @@ -23,7 +23,11 @@ from __future__ import annotations from pathlib import Path from typing import Any -from B06_Section.B06_Section_Engine_Design import compute_cross_design, stored_shoulder_extend +from B06_Section.B06_Section_Engine_Design import ( + compute_cross_design, + stored_shoulder_extend, + stored_tilt, +) from common_util.common_util_structure_face_role import structure_face_role #: 폭을 좁혀 가는 이분법 반복 수. TS 짝(`solveSpoilWidthM`)과 같은 값이다. @@ -217,7 +221,7 @@ def enforce_spoil_fills( }, **curve_widening_args(section), **stored_shoulder_extend(design), # 노견 확장(139-6) - ford_paved=bool(design.get("ford_paved")), # 물넘이 자리 측구 없음(286-2) + **stored_tilt(design), # 물넘이 · 맹암거 자리 측구 없음 · 기울기(286-2 · 287-4) ) except (ValueError, KeyError): return None diff --git a/B06_Section/B06_Section_Router.py b/B06_Section/B06_Section_Router.py index 6106b23c8..d640c495b 100644 --- a/B06_Section/B06_Section_Router.py +++ b/B06_Section/B06_Section_Router.py @@ -46,12 +46,13 @@ from B06_Section.B06_Section_Router_Design import ( from B06_Section.B06_Section_Router_Design import ( USER_TOUCHED_KEYS, ford_drop_at, - ford_pavement_ranges, ford_surface_drops, paved_at, pavement_ranges, stored_berm, stored_cut_slope, + tilt_args, + tilt_ranges, ) from B06_Section.B06_Section_Router_Design import ( attach_default_designs as _attach_default_designs, @@ -601,7 +602,7 @@ async def compute_cross_section_design( berm=stored_berm(stored_design or {}), **curve_widening_args(cross_record), **stored_shoulder_extend(stored_design or {}), # 노견 확장(139-6) - ford_paved=paved_at(request.chainage_m, ford_pavement_ranges(project_root)), + **tilt_args(request.chainage_m, tilt_ranges(project_root)), ) # B06 지정 시점은 잠정치. B07 도면 확정 시 동일 엔진으로 재계산해 confirmed로 승격한다. design["status"] = "provisional" diff --git a/B06_Section/B06_Section_Router_Design.py b/B06_Section/B06_Section_Router_Design.py index 392600390..878399892 100644 --- a/B06_Section/B06_Section_Router_Design.py +++ b/B06_Section/B06_Section_Router_Design.py @@ -13,6 +13,7 @@ from B06_Section.B06_Section_Engine_Design import ( compute_cross_design, curve_widening_args, stored_shoulder_extend, + stored_tilt, ) from common_util.common_util_cross_berm import ( BERM_DEFAULT_DEPTH_M, @@ -93,6 +94,27 @@ def ford_pavement_ranges(project_root: Path | None) -> list[tuple[float, float]] return [(at - half, at + half) for at, half in halves] +def tilt_ranges(project_root: Path | None) -> list[tuple[float, float, float | None]]: + """측구 없이 노면을 지대 낮은 쪽 한 경사로 기울이는 자리(287-4) — 물넘이포장 범위 · + 경사 = 경사각도(%).""" + if project_root is None: + return [] + out: list[tuple[float, float, float | None]] = [] + for at, spec in load_culvert_sets(project_root).items(): + if spec.get("type") == "ford_pavement": + half = float(spec.get("span_m") or 0.0) / 2.0 + out.append((at - half, at + half, spec.get("slope_pct"))) + return out + + +def tilt_args(chainage_m: float, ranges: list[tuple[float, float, float | None]]) -> dict[str, Any]: + """그 측점의 구조물 자리 기울기 — `compute_cross_design(**...)` 로 넘김 · + 저장분 짝 `stored_tilt`.""" + hit = next((r for r in ranges if paved_at(chainage_m, [(r[0], r[1])])), None) + pct = hit[2] if hit else None + return {"tilted": hit is not None, "tilt_pct": float(pct) if pct is not None else None} + + def ford_surface_drops(project_root: Path | None) -> list[tuple[float, float]]: """세월교가 앉은 측점의 노면 하강량 — [(누가거리, 월류 높이 m)]. @@ -215,7 +237,7 @@ def enforce_pavement_ranges( 사용자 조작값(반폭·구조물 조정)은 그대로 승계한다. """ ranges = pavement_ranges(project_root) - ford_ranges = ford_pavement_ranges(project_root) + tilts = tilt_ranges(project_root) ford_drops = ford_surface_drops(project_root) changed = 0 for section in cross_sections: @@ -224,8 +246,8 @@ def enforce_pavement_ranges( continue chainage = float(section.get("chainage_m", 0.0)) wanted = paved_at(chainage, ranges) - ford_paved = paved_at(chainage, ford_ranges) # 물넘이 자리 = 측구 없음(286-2) - if bool(design.get("paved")) == wanted and bool(design.get("ford_paved")) == ford_paved: + tilt = tilt_args(chainage, tilts) # 물넘이 자리 = 측구 없음 · 기울기(286-2 · 287-4) + if bool(design.get("paved")) == wanted and stored_tilt(design) == tilt: continue try: recomputed = compute_cross_design( @@ -248,7 +270,7 @@ def enforce_pavement_ranges( berm=stored_berm(design), **curve_widening_args(section), **stored_shoulder_extend(design), # 노견 확장(139-6) - ford_paved=ford_paved, + **tilt, ) except (ValueError, KeyError): continue @@ -277,7 +299,7 @@ def enforce_design_lines( from B06_Section.B06_Section_Engine_Structures_Wall import berm_span_at, load_berm_spans ford_drops = ford_surface_drops(project_root) - ford_ranges = ford_pavement_ranges(project_root) + tilts = tilt_ranges(project_root) spans = load_berm_spans(project_root) changed = 0 for section in cross_sections: @@ -308,7 +330,7 @@ def enforce_design_lines( berm=stored_berm(design), **curve_widening_args(section), **stored_shoulder_extend(design), - ford_paved=paved_at(chainage, ford_ranges), + **tilt_args(chainage, tilts), ) except (ValueError, KeyError): continue @@ -364,7 +386,7 @@ def enforce_ford_surface_drops( berm=stored_berm(design), **curve_widening_args(section), **stored_shoulder_extend(design), # 노견 확장(139-6) - ford_paved=bool(design.get("ford_paved")), + **stored_tilt(design), ) except (ValueError, KeyError): continue @@ -437,7 +459,7 @@ def attach_default_designs( modes = default_section_modes(longitudinal) pavement = pavement_suggestions(longitudinal) paved_ranges = pavement_ranges(project_root) if project_root else [] - ford_ranges = ford_pavement_ranges(project_root) + tilts = tilt_ranges(project_root) ford_drops = ford_surface_drops(project_root) for section in cross_sections: if section.get("design"): @@ -456,7 +478,7 @@ def attach_default_designs( rock_boundary_offset_m=STANDARD_ROCK_BOUNDARY_DEFAULT_OFFSET_M, surface_drop_m=ford_drop_at(chainage, ford_drops), **curve_widening_args(section), - ford_paved=paved_at(chainage, ford_ranges), + **tilt_args(chainage, tilts), ) design.update(status="provisional", pavement_suggested=suggested) section["design"] = design @@ -517,7 +539,7 @@ def recompute_designs_for_alignment( modes = default_section_modes(longitudinal) pavement = pavement_suggestions(longitudinal) paved_ranges = pavement_ranges(project_root) if project_root else [] - ford_ranges = ford_pavement_ranges(project_root) + tilts = tilt_ranges(project_root) ford_drops = ford_surface_drops(project_root) stored_by_chainage = { round(float(record["chainage_m"]), 3): (record.get("design") or {}) @@ -576,7 +598,7 @@ def recompute_designs_for_alignment( berm=session_berms.get(key) or stored_berm(stored), **curve_widening_args(section), **stored_shoulder_extend(stored), # 노견 확장(139-6) - ford_paved=paved_at(chainage, ford_ranges), + **tilt_args(chainage, tilts), ) except (ValueError, KeyError): continue @@ -621,7 +643,7 @@ def compute_default_designs( default_modes = default_section_modes(longitudinal) pavement = pavement_suggestions(longitudinal) paved_ranges = pavement_ranges(root) - ford_ranges = ford_pavement_ranges(root) + tilts = tilt_ranges(root) ford_drops = ford_surface_drops(root) results: list[tuple[float, dict[str, Any]]] = [] for chainage_m in chainages: @@ -661,7 +683,7 @@ def compute_default_designs( surface_drop_m=ford_drop_at(chainage_m, ford_drops), berm=stored_berm(user), **curve_widening_args(cross), - ford_paved=paved_at(chainage_m, ford_ranges), + **tilt_args(chainage_m, tilts), ) for key in ("berm", "cut_slope_ratio_user"): if user.get(key) is not None: diff --git a/B06_Section/B06_Section_UI_Cross_Ford_Pavement.ts b/B06_Section/B06_Section_UI_Cross_Ford_Pavement.ts index 89d038df6..ea30b9e4d 100644 --- a/B06_Section/B06_Section_UI_Cross_Ford_Pavement.ts +++ b/B06_Section/B06_Section_UI_Cross_Ford_Pavement.ts @@ -1,36 +1,28 @@ /* ============================================================================= * B06_Section_UI_Cross_Ford_Pavement.ts - * 물넘이포장 — 계획고를 파낸 노면을 횡단도에 그린다(2026-08-28 사용자 확정). + * 물넘이포장 — 횡단도 그림(287-2 · 287-4 · 그림_183). * - * 다른 시설은 구조물을 얹지만 물넘이는 **노면 자체가 내려앉는다**. 그래서 그림도 다르다. - * · 파임 범위 = 노견까지 **전폭**. - * · 깊이 = **노선 중심**에서 잰 월류 높이(`depth_m`). - * · 바닥은 **유입(상류)이 높고 유출이 낮게** 기운다. 경사를 비우면 그 측점의 노면 - * 횡단경사(`cross_slope_pct`)를 쓴다 — 사용자가 폼에서 바꿀 수 있다. - * · 기존 계획고는 **점선**, 물넘이 바닥은 **실선**. - * · 포장 필수 — 일반 포장층과 구분되게 진한 회색 + 빗금으로 채운다. - * - * 유입측 = 표고가 높은 쪽 — 설계선 양 끝(비탈이 원지반을 만나는 자리) 표고로 가름 · 노면은 경사각도 - * (`ford_slope_deg` → `slope_pct`)만큼 표고 낮은 쪽(유출)으로 기움(286-2). 양 끝이 같으면 `section_mode` - * (상단측 절토 = 상류). + * · 콘크리트 윗면 = 설계 노면 — 서버(`compute_cross_design(tilted=…)`) · 짝 TS 가 노면을 경사각도 + * (`ford_slope_deg` → `slope_pct`)로 지대 낮은 쪽 한 경사로 기울여 낸 면 · 설계 중심점이 그 위. + * 노면 밖은 설계선 그대로 — 노면 끝에서 성토 · 절토 사면이 이어짐(늘어나지 못한 자리 = 성토). + * · 포장층 = 노면공 포장 형상(`appendPavementOverlay`)을 노견까지. + * · 월류 높이선(회색 점선) = 노면 위로 월류 높이(`depth_m`)만큼 · 노면과 같은 각도. + * · 도수로 · 돌붙임 띠 = 유출(지대 낮은 쪽) 성토사면. * ========================================================================== */ import type { CrossDesign } from "./B06_Section_Api_Fetch"; import type { OffsetPoint } from "./B06_Section_UI_Cross_Culvert_Types"; +import { appendPavementOverlay } from "./B06_Section_UI_Cross_Design_Surface"; import { appendChuteBand } from "./B06_Section_UI_Cross_Wall_Hatch"; -const SVG_NS = "http://www.w3.org/2000/svg"; -/** 빗금 패턴 id는 문서 안에서 유일해야 한다 — 카드마다 하나씩 번호를 준다. */ -let hatchSeq = 0; - /** 백엔드 `section.ford_pavement` 제원(치수 결정은 서버 몫 — 여기서는 좌표만 만든다). */ export interface FordPavementSpec { /** 관 지점 누가거리 — 폭 절반 걸친 옆 카드도 같은 값. */ chainage_m?: number; span_m: number; - /** 노선 중심에서 잰 파임 깊이(m). 없으면 그리지 않는다 — 수치를 지어내지 않는다. */ + /** 월류 높이(m) — 노면 위 점선. 없으면 점선을 안 그림(수치를 지어내지 않는다). */ depth_m: number | null; - /** 유입 → 유출 바닥 경사(%). 비우면 노면 횡단경사를 쓴다. */ + /** 노면 경사(%) — 경사각도 환산 · 서버 설계 노면이 이 값으로 기욺. */ slope_pct: number | null; /** 정본에 안 적혀 기본값으로 그린 칸 「이름 값」 — 카드 머리에 「기본값(미확정)」. */ defaulted?: string[]; @@ -38,46 +30,19 @@ export interface FordPavementSpec { chute?: { structure: string; thickness_m: number; length_m?: number }; } -interface Edge { - offset_m: number; - elevation_m: number; -} - -/** 유입(상류)이 좌측(+offset)인가 — 설계선 양 끝 표고가 높은 쪽(286-2) · 같으면 단면유형. */ +/** 유입(상류)이 좌측(+offset)인가 — 기울인 노면이면 높은 노면 끝 · 아니면 설계선 양 끝 표고가 높은 + * 쪽(286-2) · 같으면 단면유형. */ export function inflowLeft(design: CrossDesign): boolean { + const edges = design.road_edges; + if (design.tilted && edges && Math.abs(edges.left.elevation_m - edges.right.elevation_m) > 1e-6) + return edges.left.elevation_m > edges.right.elevation_m; const line = [...(design.design_line ?? [])].sort((a, b) => a.offset_m - b.offset_m); const gap = line.length >= 2 ? line[line.length - 1].elevation_m - line[0].elevation_m : 0; return Math.abs(gap) > 1e-3 ? gap > 0 : design.section_mode !== "right_cut"; } -/** 유입측(상류) 부호 — 그 방향으로 갈수록 바닥이 높아진다. */ -function inflowSign(design: CrossDesign, offsetM: number): number { - const towardLeft = offsetM > 0; - return towardLeft === inflowLeft(design) ? 1 : -1; -} - -/** - * 파인 노면의 표고(m). 횡단도와 3D 코리도가 **같은 식**을 써야 두 그림이 어긋나지 않는다. - * 중심에서 깊이만큼 내리고, 유입측으로 갈수록 경사만큼 올린다. - */ -export function fordDeckElevationAt( - design: CrossDesign, - spec: FordPavementSpec, - offsetM: number, - fallbackElevationM = 0, -): number { - const depth = spec.depth_m ?? 0; - const slope = (spec.slope_pct ?? design.cross_slope_pct) / 100; - const center = design.design_elevation_m ?? fallbackElevationM; - return center - depth + slope * Math.abs(offsetM) * inflowSign(design, offsetM); -} - -function bottomAt(design: CrossDesign, spec: FordPavementSpec, edge: Edge): number { - return fordDeckElevationAt(design, spec, edge.offset_m, edge.elevation_m); -} - function line(points: string[], className: string): SVGPolylineElement { - const polyline = document.createElementNS(SVG_NS, "polyline"); + const polyline = document.createElementNS("http://www.w3.org/2000/svg", "polyline"); polyline.setAttribute("points", points.join(" ")); polyline.setAttribute("class", className); return polyline; @@ -127,8 +92,9 @@ export function appendFordSurfaceDropPlan( } /** - * 물넘이 파임을 그린다. 그렸으면 true — 호출부는 일반 포장층 박스를 건너뛴다 - * (같은 자리에 두 겹으로 깔리면 색 구분이 사라진다). + * 물넘이포장을 그린다(287-2 · 287-4) — 포장층(노면공 형상 · 노견까지) · 월류 높이선 · 도수로 띠. + * 그렸으면 true — 호출부는 일반 포장층을 건너뜀(두 겹으로 안 깔림). 포장층에 물넘이 표식 + * (`b06-chart__ford-pavement` · 287-5 고름이 노면공 포장 고름과 가름). */ export function appendFordPavementOverlay( svg: SVGElement, @@ -137,66 +103,34 @@ export function appendFordPavementOverlay( x: (offset: number) => number, y: (elevation: number) => number, ): boolean { - if (!spec || !spec.depth_m || !design.road_edges) return false; - const { left, right } = design.road_edges; - const bottomLeft = bottomAt(design, spec, left); - const bottomRight = bottomAt(design, spec, right); - - // ① 기존 계획고(점선) — 파기 전 노면이 어디였는지 남긴다. - svg.append( - line( - [ - `${x(left.offset_m)},${y(left.elevation_m)}`, - `${x(right.offset_m)},${y(right.elevation_m)}`, - ], - "b06-chart__ford-deck-plan", - ), - ); - - // ② 포장층 — 바닥선에서 두께만큼 아래로. 일반 포장과 색·빗금으로 구분한다. - const thickness = design.pavement_thickness_m ?? 0.2; - const hatchId = `b06-ford-hatch-${(hatchSeq += 1)}`; - const defs = document.createElementNS(SVG_NS, "defs"); - const pattern = document.createElementNS(SVG_NS, "pattern"); - pattern.setAttribute("id", hatchId); - pattern.setAttribute("patternUnits", "userSpaceOnUse"); - pattern.setAttribute("width", "6"); - pattern.setAttribute("height", "6"); - const stroke = document.createElementNS(SVG_NS, "path"); - stroke.setAttribute("d", "M0,6 L6,0"); - stroke.setAttribute("class", "b06-chart__ford-pavement-hatch"); - pattern.append(stroke); - defs.append(pattern); - svg.append(defs); - - const polygon = document.createElementNS(SVG_NS, "polygon"); - polygon.setAttribute( - "points", - [ - `${x(left.offset_m)},${y(bottomLeft)}`, - `${x(right.offset_m)},${y(bottomRight)}`, - `${x(right.offset_m)},${y(bottomRight - thickness)}`, - `${x(left.offset_m)},${y(bottomLeft - thickness)}`, - ].join(" "), - ); - polygon.setAttribute("class", "b06-chart__ford-pavement"); - polygon.setAttribute("fill", `url(#${hatchId})`); - svg.append(polygon); - - // ③ 물넘이 바닥(실선) — 횡단 기준선이라 가장 위에 올린다. - svg.append( - line( - [`${x(left.offset_m)},${y(bottomLeft)}`, `${x(right.offset_m)},${y(bottomRight)}`], - "b06-chart__ford-deck", - ), - ); + const edges = design.road_edges; + if (!spec || !edges) return false; + const from = svg.children.length; + // 노면공 항목은 안 넘김 — 물넘이 자체 포장(노면공 포장 고름과 안 섞임) + appendPavementOverlay(svg, { ...design, paved: true, carriageway_edges: edges }, x, y, null); + [...svg.children] + .slice(from) + .filter((el) => el.getAttribute("class") === "b06-chart__pavement") + .forEach((el) => el.setAttribute("class", "b06-chart__pavement b06-chart__ford-pavement")); + // 월류 높이선 — 콘크리트 윗면(설계 노면) 위로 월류 높이만큼 · 같은 각도(그림_183) + const depth = spec.depth_m ?? 0; + if (depth > 0) + svg.append( + line( + [ + `${x(edges.left.offset_m)},${y(edges.left.elevation_m + depth)}`, + `${x(edges.right.offset_m)},${y(edges.right.elevation_m + depth)}`, + ], + "b06-chart__ford-deck-plan", + ), + ); if (spec.chute) appendChuteBand(svg, [{ points: fordChuteLine(design, spec.chute) }], spec.chute, x, y); return true; } /** - * 유출측(`inflowSign` 반대) 노견 바깥 성토사면 선(136-2) — 설계선을 노견에서 바깥으로 따라 + * 유출측(`inflowLeft` 반대) 노견 바깥 성토사면 선(136-2) — 설계선을 노견에서 바깥으로 따라 * 내려가는 동안만(오르면 절토라 멈춤). 성토가 없으면 빈 선(띠 안 그림). */ export function outletFillLine(design: CrossDesign): OffsetPoint[] { @@ -242,60 +176,6 @@ export function fordChuteLine(design: CrossDesign, chute?: { length_m?: number } return out; } -/** - * 물넘이 자리 노면 · 측구 설계선을 걷는다(66-2 · 66-3) — 노면은 파인 바닥(실선)과 옛 계획고(점선)가 - * 대신하고, 측구는 서지 않음. `from` = 설계선 그리기 앞의 자식 수(그 뒤에 붙은 선만 본다). - * 측구 자리는 바닥 끝에서 절토 비탈 발까지 실선으로 잇는다. 면적 · 서버 설계선은 그대로(그림만). - */ -export function hideFordPavedRoad( - layer: SVGElement, - from: number, - design: CrossDesign, - spec: FordPavementSpec, - x: (offset: number) => number, - y: (elevation: number) => number, -): void { - const edges = design.road_edges; - const designLine = design.design_line; - if (!edges || !designLine?.length) return; - const ditch = design.ditch; - const ditchWidth = - design.ditch_enabled === false || ditch.type === "none" - ? 0 - : ditch.type === "standard" - ? ditch.top_width_m - : ditch.width_m; - // +offset = 좌측 · 측구는 노견 바깥 그 쪽에 붙는다 - const leftEdge = Math.max(edges.left.offset_m, edges.right.offset_m); - const rightEdge = Math.min(edges.left.offset_m, edges.right.offset_m); - const hi = leftEdge + (design.ditch_side === "left" ? ditchWidth : 0); - const lo = rightEdge - (design.ditch_side === "right" ? ditchWidth : 0); - const [xa, xb] = [x(lo), x(hi)]; - const inside = (px: number): boolean => - px >= Math.min(xa, xb) - 0.5 && px <= Math.max(xa, xb) + 0.5; - for (const node of [...layer.children].slice(from)) { - if (node.tagName !== "line" || !node.classList.contains("b06-chart__design-cross")) continue; - if (inside(Number(node.getAttribute("x1"))) && inside(Number(node.getAttribute("x2")))) - node.remove(); - } - if (!(ditchWidth > 0) || !design.ditch_side) return; - const edge = design.ditch_side === "left" ? leftEdge : rightEdge; - const foot = design.ditch_side === "left" ? hi : lo; - const sorted = [...designLine].sort((a, b) => a.offset_m - b.offset_m); - const at = sorted.find((point) => Math.abs(point.offset_m - foot) < 1e-3); - if (!at) return; - const edgeElevation = design.ditch_side === "left" ? edges.left : edges.right; - layer.append( - line( - [ - `${x(edge)},${y(fordDeckElevationAt(design, spec, edge, edgeElevation.elevation_m))}`, - `${x(foot)},${y(at.elevation_m)}`, - ], - "b06-chart__ford-deck", - ), - ); -} - /** * 좌측 패널의 물넘이 칸을 캐시 제원에 얹는다(66-2 · 66-3) — 월류 높이 = 파임 깊이 · 경사각(°) → %. * 서버 `_ford_pavement_set` 과 같은 환산(저장 뒤 서버가 정본으로 다시 냄). 바꿨으면 true. diff --git a/B06_Section/B06_Section_UI_Cross_View.ts b/B06_Section/B06_Section_UI_Cross_View.ts index 9571b8df4..da538e6af 100644 --- a/B06_Section/B06_Section_UI_Cross_View.ts +++ b/B06_Section/B06_Section_UI_Cross_View.ts @@ -17,7 +17,6 @@ import { } from "./B06_Section_UI_Cross_Areas"; import { appendFordPavementOverlay, - hideFordPavedRoad, appendFordSurfaceDropPlan, } from "./B06_Section_UI_Cross_Ford_Pavement"; import { ownWallsDrawAt } from "./B06_Section_UI_Cross_Culvert_Wire"; @@ -513,16 +512,6 @@ export function createCrossSectionCard( [...plotLayer.children] .slice(designFrom) .forEach((el) => el.setAttribute(DESIGN_LINE_ATTR, "")); - // 물넘이 자리 = 노면 · 측구 설계선 걷음(66-2 · 66-3 — 옛 계획고 점선이 드러남) - if (fordPaved && section.ford_pavement) - hideFordPavedRoad( - plotLayer, - designFrom, - section.design, - section.ford_pavement, - x, - toDisplayY, - ); // 사토장(유용토운반작업장) — 교본 6장 3절이 횡단도 표시를 요구한다. 값은 설계 결과에서 // 그대로 오고 여기서 다시 세지 않는다. 선 종류는 터파기 파선과 갈라 둔다. appendSpoilFillOverlay(plotLayer, section.design, x, toDisplayY); diff --git a/B06_Section/B06_Section_UI_Style_Cross_Areas.css b/B06_Section/B06_Section_UI_Style_Cross_Areas.css index a724bc1cc..c24ef3860 100644 --- a/B06_Section/B06_Section_UI_Style_Cross_Areas.css +++ b/B06_Section/B06_Section_UI_Style_Cross_Areas.css @@ -254,7 +254,7 @@ stroke-width: 2.2; } -/* 물넘이포장(2026-08-28): 파인 노면 — 기존 계획고 점선 + 바닥 실선 + 진한 회색 빗금 포장 */ +/* 물넘이포장(287-2 · 287-4): 노면공 포장층(노견까지) + 월류 높이 점선 */ .b06-chart__ford-deck-plan { fill: none; stroke: var(--color-text-secondary); @@ -262,24 +262,11 @@ stroke-dasharray: 5 4; } -.b06-chart__ford-deck { - fill: none; - stroke: var(--color-text-body); - stroke-width: 1.8; - stroke-linejoin: round; -} - .b06-chart__ford-pavement { stroke: var(--color-text-body); stroke-width: 1; } -.b06-chart__ford-pavement-hatch { - stroke: color-mix(in srgb, var(--color-text-body) 70%, transparent); - stroke-width: 1.2; - fill: none; -} - /* 배수관 세트(2026-08-19): 배관·기슭막이·돌붙임 — 분리 도형 3종 + 집수정 라벨 */ /* 관은 실무 도면처럼 점선 윤곽 2줄 느낌 — 채움은 식별용으로만 아주 옅게. */ .b06-chart__culvert-pipe { diff --git a/common_util/common_util_cross_design.ts b/common_util/common_util_cross_design.ts index b80dbd405..f57a47453 100644 --- a/common_util/common_util_cross_design.ts +++ b/common_util/common_util_cross_design.ts @@ -98,8 +98,10 @@ export interface CrossDesignOptions { ditchChoice?: boolean | null; /** 세월교 월류 높이만큼 노면을 통째로 내린다(m). */ surfaceDropM?: number; - /** 물넘이포장 범위 안 측점 — 측구 없음(286-2). 짝: 파이썬 `compute_cross_design(ford_paved=…)`. */ - fordPaved?: boolean; + /** 물넘이포장 범위 · 맹암거 측점 — 측구 없음 · 노면 = `tiltPct`(비면 표준 횡단경사)로 지대 낮은 쪽 한 경사 + * (286-2 · 287-4 · 288-1). 짝: 파이썬 `compute_cross_design(tilted=…, tilt_pct=…)`. */ + tilted?: boolean; + tiltPct?: number | null; /** 이 측점의 평면 곡선반경(m) — 곡선부 확폭을 정하는 입력. 직선이면 null. */ planRadiusM?: number | null; /** 곡선 **바깥쪽**("left"/"right") — 확폭이 붙는 쪽(2026-09-06 사용자 확정). */ @@ -189,8 +191,9 @@ export interface CrossDesignResult { /** 이 측점에 놓인 소단 제원 — 옹벽 의무 판정이 사면을 도막으로 끊는 데 쓴다. */ berm?: { width_m: number; interval_m: number; slope_deg: number; depth_m?: number }; surface_drop_m?: number; - /** 물넘이포장 범위 안 측점(286-2) — 측구 없음. */ - ford_paved?: boolean; + /** 물넘이포장 · 맹암거 자리(286-2 · 287-4) — 측구 없음 · 지대 낮은 쪽 한 경사. */ + tilted?: boolean; + tilt_pct?: number; pavement_thickness_m?: number; rock_boundary_offset_m?: number; } @@ -316,7 +319,9 @@ export function computeCrossDesign( } const pavedGroup = resolveGroup("paved", options.standard); // 포장 중첩: 횡단경사와 포장층 두께만 포장 그룹을 따른다. - const crossSlopePct = paved ? pavedGroup.cross_slope_pct : group.cross_slope_pct; + const tiltPct = options.tilted && (options.tiltPct ?? 0) > 0 ? options.tiltPct! : null; + // 구조물 자리 경사(287-4 · 288-1 · 파이썬 짝) + const crossSlopePct = tiltPct ?? (paved ? pavedGroup.cross_slope_pct : group.cross_slope_pct); const resolvedDitchSide = resolveDitchSide(sectionMode, options.ditchSide); const valid: Array<[number, number]> = samples @@ -371,7 +376,7 @@ export function computeCrossDesign( berm: options.berm ?? null, shoulderExtendLeftM: options.shoulderExtendLeftM ?? 0, shoulderExtendRightM: options.shoulderExtendRightM ?? 0, - noDitch: Boolean(options.fordPaved), + tilted: Boolean(options.tilted), }); // 적분 오프셋 = 지반 샘플 ∪ 설계 꼭짓점(샘플 범위 안쪽만). @@ -586,7 +591,8 @@ export function computeCrossDesign( design_line: designLine, cut_slope_segments: geometry.cutSlopeSegments(), }; - if (options.fordPaved) result.ford_paved = true; + if (options.tilted) result.tilted = true; + 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 e6704af6a..c32f80cfb 100644 --- a/common_util/common_util_cross_design_geometry.ts +++ b/common_util/common_util_cross_design_geometry.ts @@ -145,8 +145,8 @@ export class SectionGeometry { /** 설계선 쉬프트 = 쪽마다 노견 확장(m · ≥ 0 · 139-6). */ shoulderExtendLeftM?: number; shoulderExtendRightM?: number; - /** 물넘이포장 자리 — 측구 없음(286-2 · 파이썬 짝 `no_ditch`). */ - noDitch?: boolean; + /** 물넘이포장 · 맹암거 자리 — 측구 없음 · 노면 = 지대 낮은 쪽 한 경사(286-2 · 287-4 · 파이썬 짝 `tilted`). */ + tilted?: boolean; }) { const { group } = params; const halfRoad = group.road_width_m / 2; @@ -171,6 +171,12 @@ export class SectionGeometry { // 횡단경사: 측구 방향으로 내려가는 단일 사면 (좌=+offset 규약). const slope = params.crossSlopePct / 100; this.slopePerOffset = params.ditchSide === "left" ? -slope : slope; + // 물넘이포장 · 맹암거 자리(287-4 · 288-1) — 노면 = 한 경사로 지대 낮은 쪽(노면 끝 원지반이 낮은 쪽) + // 으로 기움 · 측구 없음(아래 · 파이썬 짝) + if (params.tilted && params.groundAt) { + const lowLeft = params.groundAt(this.leftExtent) < params.groundAt(-this.rightExtent); + this.slopePerOffset = lowLeft ? -slope : slope; + } // 단면유형 자동 판정(D-2) — 노면 끝 지반이 설계면보다 높으면 절토, 낮으면 성토. const groundAt = params.groundAt; @@ -215,8 +221,8 @@ export class SectionGeometry { this.hasDitch = choice === null || bothFill ? autoDitch : choice; /** 그 측점에 **사용자가 정한 선택**(없으면 자동). 결과와 갈라 내보낸다. */ this.ditchChoice = bothFill ? null : choice; - // 물넘이포장 자리(286-2) — 파인 노면이라 측구가 안 섬 · 선택은 그대로(파이썬 짝) - if (params.noDitch) this.hasDitch = false; + // 물넘이포장 · 맹암거 자리(286-2 · 288-1) — 측구가 안 섬 · 선택은 그대로(파이썬 짝) + if (params.tilted) this.hasDitch = false; // 설계선 쉬프트 = 쪽마다 사면 이동(≥ 0 · 139-6 · 151-2) — 단면유형 · 측구 판정은 **옮기기 전** // 노견에서(위) · 노견(폭 고정) · 측구 · 사면이 같이 바깥으로 · 늘어난 폭은 차도. ⚠ 파이썬 짝 `_SectionGeometry`. diff --git a/common_util/common_util_culvert_sets.ts b/common_util/common_util_culvert_sets.ts index 7cf5c21bc..b58a26deb 100644 --- a/common_util/common_util_culvert_sets.ts +++ b/common_util/common_util_culvert_sets.ts @@ -505,7 +505,7 @@ function fordPavementSet( { type: "ford_pavement", span_m: span, - // 노선 중심에서 잰 깊이. 없으면 화면이 파임을 그리지 않는다(수치를 지어내지 않는다). + // 월류 높이 — 노면 위 점선(287-4). 없으면 안 그린다(수치를 지어내지 않는다). depth_m: depth && depth > 0 ? depth : null, slope_pct: used !== null ? round(Math.tan((used * Math.PI) / 180) * 100, 4) : legacy, defaulted: defaulted(values, [ diff --git a/resources/tester/helper_286_1_ford_pavement.cjs b/resources/tester/helper_286_1_ford_pavement.cjs index 1327b58f8..dc98731df 100644 --- a/resources/tester/helper_286_1_ford_pavement.cjs +++ b/resources/tester/helper_286_1_ford_pavement.cjs @@ -25,23 +25,8 @@ const station = loadTs(path.join(ROOT, "common_util", "common_util_structure_sta const ford = loadTs(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Ford_Pavement.ts"), { "./B06_Section_UI_Cross_Wall_Hatch": { appendChuteBand() {} }, + "./B06_Section_UI_Cross_Design_Surface": { appendPavementOverlay() {} }, }); -// 286-2 노면 기울기 — 설계선 양 끝 표고가 높은 쪽 = 유입 · 표고 낮은 쪽(유출)으로 기움 -const SPEC = { span_m: 6, depth_m: 0.3, slope_pct: 5 }; -const designOf = (left, right, mode) => ({ - design_elevation_m: 100, - cross_slope_pct: 2, - section_mode: mode, - design_line: [ - { offset_m: -10, elevation_m: right }, - { offset_m: 10, elevation_m: left }, - ], -}); -const deck = (design) => ({ - left: ford.fordDeckElevationAt(design, SPEC, 3), - 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 = { @@ -73,8 +58,5 @@ const out = { 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")), - deckRightHigh: deck(designOf(95, 105, "left_cut")), - deckFlat: deck(designOf(100, 100, "right_cut")), }; process.stdout.write(JSON.stringify(out)); diff --git a/resources/tester/helper_287_ford_pavement.cjs b/resources/tester/helper_287_ford_pavement.cjs index 67e3847c5..fbf56b8de 100644 --- a/resources/tester/helper_287_ford_pavement.cjs +++ b/resources/tester/helper_287_ford_pavement.cjs @@ -36,7 +36,65 @@ const change = (form, c, i) => { return form.state().chute[0].area; }; +// 287-2 · 287-4 그림 — DOM 흉내(속성 · 자식만) +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 pave = load(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Ford_Pavement.ts")); +// 서버가 지대 낮은 쪽(우)으로 5 % 기울인 노면 · 노견 끝 ±3 · 차도 끝 ±2.5 +const tilted = { + section_mode: "left_cut", + tilted: true, + cross_slope_pct: 5, + design_elevation_m: 100, + pavement_thickness_m: 0.2, + paved: true, + road_edges: { + left: { offset_m: 3, elevation_m: 100.15 }, + right: { offset_m: -3, elevation_m: 99.85 }, + }, + carriageway_edges: { + left: { offset_m: 2.5, elevation_m: 100.125 }, + right: { offset_m: -2.5, elevation_m: 99.875 }, + }, + design_line: [ + { offset_m: 6, elevation_m: 103 }, + { offset_m: 3, elevation_m: 100.15 }, + { offset_m: -3, elevation_m: 99.85 }, + { offset_m: -7, elevation_m: 97.2 }, + ], +}; +const svg = new Node("g"); +const drew = pave.appendFordPavementOverlay( + svg, + { span_m: 8, depth_m: 0.4, slope_pct: 5 }, + tilted, + (o) => o, + (e) => e, +); +const ofClass = (c) => svg.children.filter((k) => (k.attrs.class ?? "").split(" ").includes(c)); + const out = { + drew, + pavePoints: ofClass("b06-chart__ford-pavement").map((k) => k.attrs.points), + planPoints: ofClass("b06-chart__ford-deck-plan").map((k) => k.attrs.points), + inflowLeft: pave.inflowLeft(tilted), // 287-1 길이 넣음 = 월류 폭 × 길이 · 비움 = 기본값 · 폭 바꿈 = 길이 있을 때만 다시 셈 · 폭 비면 기본 폭 areaByLength: change(formOf({ width: "6" }, { length_m: "4", area: "30" }), "chute", "length_m"), areaCleared: change(formOf({ width: "6" }, { length_m: "", area: "24" }), "chute", "length_m"), diff --git a/resources/tester/test_286_1_ford_pavement_design.py b/resources/tester/test_286_1_ford_pavement_design.py index b19354ba6..68c52322a 100644 --- a/resources/tester/test_286_1_ford_pavement_design.py +++ b/resources/tester/test_286_1_ford_pavement_design.py @@ -103,14 +103,12 @@ def _ground(slope: float) -> list[dict]: def test_물넘이_자리_측구_없음(): kwargs = {"ground_type": "soil", "section_mode": "left_cut", "ditch_side": "left"} plain = compute_cross_design(_ground(0.35), 100.4, **kwargs) - ford = compute_cross_design(_ground(0.35), 100.4, ford_paved=True, **kwargs) - assert plain["ditch_enabled"] is True and "ford_paved" not in plain - assert ford["ditch_enabled"] is False and ford["ford_paved"] is True + ford = compute_cross_design(_ground(0.35), 100.4, tilted=True, **kwargs) + assert plain["ditch_enabled"] is True and "tilted" not in plain + assert ford["ditch_enabled"] is False and ford["tilted"] is True assert ford["ditch_area_m2"] == 0 # 사용자가 측구를 켜도 물넘이 자리는 안 섬 - forced = compute_cross_design( - _ground(0.35), 100.4, ford_paved=True, ditch_choice=True, **kwargs - ) + forced = compute_cross_design(_ground(0.35), 100.4, tilted=True, ditch_choice=True, **kwargs) assert forced["ditch_enabled"] is False @@ -125,21 +123,22 @@ def test_물넘이_범위_서버_판정(tmp_path): assert paved_at(230.0, ranges) and paved_at(226.0, ranges) and not paved_at(240.0, ranges) -def test_노면은_표고_낮은_쪽으로(ts): - # 좌 높음(입력 단면유형이 반대여도 지형) · 우 높음 · 같으면 단면유형(right_cut = 우 상류) - assert ts["deckLeftHigh"]["left"] > ts["deckLeftHigh"]["right"] - assert ts["deckRightHigh"]["right"] > ts["deckRightHigh"]["left"] - assert ts["deckFlat"]["right"] > ts["deckFlat"]["left"] - # 경사 5 % · 노선 중심 100 − 깊이 0.3 · 3 m 에서 ±0.15 - assert ts["deckLeftHigh"]["left"] == pytest.approx(99.85) - assert ts["deckLeftHigh"]["right"] == pytest.approx(99.55) +def test_노면은_표고_낮은_쪽으로(): + # 287-4 — 설계 노면 = 경사각도(%)로 지대 낮은 쪽 한 경사 · 중심 = 계획고(콘크리트 윗면) + kwargs = {"ground_type": "soil", "section_mode": "left_cut", "ditch_side": "left"} + for slope, high in ((0.35, "left"), (-0.35, "right")): + got = compute_cross_design(_ground(slope), 100.4, tilted=True, tilt_pct=5.0, **kwargs) + left, right = (got["road_edges"][k]["elevation_m"] for k in ("left", "right")) + assert (left > right) == (high == "left") + assert got["design_elevation_m"] == 100.4 and got["cross_slope_pct"] == 5.0 + assert got["tilt_pct"] == 5.0 def test_저장_뒤_서버가_물넘이_측구를_맞춤(): router = _read("B06_Section", "B06_Section_Router_Design.py") - assert 'bool(design.get("ford_paved")) == ford_paved' in router + assert "stored_tilt(design) == tilt" in router refresh = _read("B06_Section", "B06_Section_Cross_Refresh.ts") - assert "fordPaved: Boolean(section.ford_pavement)," in refresh + assert "tilted: Boolean(section.ford_pavement)," in refresh CHUTE = {"ford_chute_1_structure": "돌붙임", "ford_chute_1_thickness": 30} diff --git a/resources/tester/test_287_ford_pavement_followup.py b/resources/tester/test_287_ford_pavement_followup.py index 1ee9e5f15..94b01daa2 100644 --- a/resources/tester/test_287_ford_pavement_followup.py +++ b/resources/tester/test_287_ford_pavement_followup.py @@ -3,6 +3,9 @@ 못박는 것 - 287-1 도수로/돌붙임 길이 ↔ 면적 — 길이 넣음 = 월류 폭 × 길이 · 비움 = 칸 기본값 · 월류 폭을 바꾸면 길이가 있을 때만 다시 셈 · 폭이 비면 물넘이 기본 폭. + - 287-2 · 287-4 콘크리트 윗면 = 설계 노면(서버 · TS 짝이 경사각도로 지대 낮은 쪽 한 경사 · 중심점이 그 위) + · 포장층 = 노면공 포장 형상 노견까지(노면공 항목과 안 섞임) · 월류 높이 점선 = 노면 위 같은 각도 · + 3D 노면을 파지 않음 · 설계 측구 없음. 진짜 TS 는 `helper_287_ford_pavement.cjs` 가 Node 로 돌림. 화면은 사용자 확인. """ @@ -55,3 +58,26 @@ def test_도수로_길이_면적(ts): def test_물넘이_폼이_길이_면적을_잇는다(): form = _read("B05_Profile", "B05_Profile_UI_Drainage_Facility.ts") assert "syncChuteArea(paveForm, c, item), paveFlowBox.onChange(c, item)" in form + + +def _pts(text: str) -> list[float]: + return [float(v) for pair in text.split() for v in pair.split(",")] + + +def test_포장층_노견까지_점선은_노면_위(ts): + assert ts["drew"] is True + # 포장층 하나 = 노견 끝(±3)까지 · 윗면 = 설계 노면 · 두께 0.2 + assert len(ts["pavePoints"]) == 1 + assert _pts(ts["pavePoints"][0]) == pytest.approx([3, 100.15, -3, 99.85, -3, 99.65, 3, 99.95]) + # 월류 높이선 = 노면 + 0.4 · 같은 각도 + assert _pts(ts["planPoints"][0]) == pytest.approx([3, 100.55, -3, 100.25]) + assert ts["inflowLeft"] is True # 노면 높은 쪽 = 유입 · 도수로 띠 = 낮은 쪽 + + +def test_3D_노면을_파지_않고_옛_파임_그림을_걷음(): + station = _read("B05_Profile", "B05_Profile_UI_Corridor_Station.ts") + assert "fordDeckElevationAt" not in station + pave = _read("B06_Section", "B06_Section_UI_Cross_Ford_Pavement.ts") + assert "hideFordPavedRoad" not in pave and "fordDeckElevationAt" not in pave + view = _read("B06_Section", "B06_Section_UI_Cross_View.ts") + assert "hideFordPavedRoad" not in view diff --git a/resources/tester/test_b06_cross_design_mirror.py b/resources/tester/test_b06_cross_design_mirror.py index b3fe1763d..229e8406b 100644 --- a/resources/tester/test_b06_cross_design_mirror.py +++ b/resources/tester/test_b06_cross_design_mirror.py @@ -228,7 +228,21 @@ CASES = [ "section_mode": "left_cut", "ditch_side": "left", "paved": True, - "ford_paved": True, + "tilted": True, + }, + }, + { + # 287-4 — 물넘이포장 자리 노면 = 경사각도로 지대 낮은 쪽 한 경사(지반이 좌로 오름 = 우가 낮음) + "name": "물넘이포장 자리 노면 기울임", + "samples": _ground(0.35), + "design_elevation_m": 100.4, + "options": { + "ground_type": "soil", + "section_mode": "left_cut", + "ditch_side": "left", + "paved": True, + "tilted": True, + "tilt_pct": 5.2408, }, }, ] @@ -254,7 +268,8 @@ const camel = { curve_widening_m: "curveWideningM", shoulder_extend_left_m: "shoulderExtendLeftM", shoulder_extend_right_m: "shoulderExtendRightM", - ford_paved: "fordPaved", + tilted: "tilted", + tilt_pct: "tiltPct", }; // 사토장 제원만 칸 이름이 다르다 — 파이썬은 snake, TS 는 camel 이다. const spoilCamel = { side: "side", width_m: "widthM", slope_ratio_n: "slopeRatioN" };