diff --git a/B05_Profile/B05_Profile_Router_Confirm.py b/B05_Profile/B05_Profile_Router_Confirm.py index ea39175f2..32ae9dd84 100644 --- a/B05_Profile/B05_Profile_Router_Confirm.py +++ b/B05_Profile/B05_Profile_Router_Confirm.py @@ -175,6 +175,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) ) next_design["status"] = design.get("status", "provisional") next_design["pavement_suggested"] = design.get("pavement_suggested", pavement_suggested) diff --git a/B06_Section/B06_Section_Cross_Refresh.ts b/B06_Section/B06_Section_Cross_Refresh.ts index 987e4cfdc..437e948a4 100644 --- a/B06_Section/B06_Section_Cross_Refresh.ts +++ b/B06_Section/B06_Section_Cross_Refresh.ts @@ -359,6 +359,8 @@ 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), standard, berm: bermAt(section.chainage_m, design), rockBoundaryOffsetM, diff --git a/B06_Section/B06_Section_Engine_Design.py b/B06_Section/B06_Section_Engine_Design.py index db44611ca..53d9f7efb 100644 --- a/B06_Section/B06_Section_Engine_Design.py +++ b/B06_Section/B06_Section_Engine_Design.py @@ -194,6 +194,7 @@ 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, ) -> dict[str, Any]: """측점 하나의 표준횡단 설계선과 절·성토 단면적을 계산한다. @@ -210,6 +211,8 @@ def compute_cross_design( 입력이며, None·45m 이상이면 확폭이 없다. curve_outer_side: 곡선 **바깥쪽**("left"/"right"). 확폭은 그쪽으로만 붙는다 (2026-09-06 사용자 확정). 값이 없으면 확폭을 넣지 않는다. + ford_paved: 물넘이포장 범위 안 측점(286-2) — 측구 없음 · 결과에 `ford_paved` 로 되실음. + TS 짝 `fordPaved`. surface_drop_m: 노면을 통째로 내리는 양(m) — 세월교 월류 높이. 구체 위 노면은 월류 높이만큼 낮게 앉으므로 계획고를 그만큼 내려 잡는다. 단면 전체가 평행 이동하므로 횡단경사·측구·사면 규칙은 그대로고 절·성토 면적만 따라 바뀐다(2026-08-30 사용자). @@ -299,6 +302,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), ) # 적분 오프셋 = 지반 샘플 ∪ 설계 꼭짓점(샘플 범위 안쪽만). 꼭짓점을 넣어야 @@ -529,6 +533,8 @@ def compute_cross_design( # 뒤에 쓸 자리가 있다. **되살릴 때는 저장분에서 읽지 말고 계산해서 쓸 것.** "cut_slope_segments": geometry.cut_slope_segments(), } + if ford_paved: + result["ford_paved"] = True 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 3249f58da..5b554b273 100644 --- a/B06_Section/B06_Section_Engine_Design_Geometry.py +++ b/B06_Section/B06_Section_Engine_Design_Geometry.py @@ -62,6 +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, ) -> None: half_road = group["road_width_m"] / 2.0 # 곡선부 확폭은 **한쪽으로만** 붙는다(2026-09-06 사용자 확정: 곡선 바깥쪽). @@ -144,6 +145,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: + 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 b3523a2e6..29eac4fb1 100644 --- a/B06_Section/B06_Section_Engine_SampleExtend.py +++ b/B06_Section/B06_Section_Engine_SampleExtend.py @@ -158,6 +158,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) ) try: diff --git a/B06_Section/B06_Section_Engine_SpoilFill.py b/B06_Section/B06_Section_Engine_SpoilFill.py index 28b280e5c..c7b0c2d30 100644 --- a/B06_Section/B06_Section_Engine_SpoilFill.py +++ b/B06_Section/B06_Section_Engine_SpoilFill.py @@ -217,6 +217,7 @@ def enforce_spoil_fills( }, **curve_widening_args(section), **stored_shoulder_extend(design), # 노견 확장(139-6) + ford_paved=bool(design.get("ford_paved")), # 물넘이 자리 측구 없음(286-2) ) except (ValueError, KeyError): return None diff --git a/B06_Section/B06_Section_Router.py b/B06_Section/B06_Section_Router.py index 3247a715e..6106b23c8 100644 --- a/B06_Section/B06_Section_Router.py +++ b/B06_Section/B06_Section_Router.py @@ -46,6 +46,7 @@ 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, @@ -600,6 +601,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)), ) # 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 65a26467b..392600390 100644 --- a/B06_Section/B06_Section_Router_Design.py +++ b/B06_Section/B06_Section_Router_Design.py @@ -77,12 +77,20 @@ def pavement_ranges(project_root: Path) -> list[tuple[float, float]]: ranges.append((min(float(start), float(end)), max(float(start), float(end)))) except Exception: # noqa: BLE001 — 정본을 못 읽어도 설계 계산은 이어 간다 logger.exception("B06 포장 구간을 읽지 못했습니다 (사용자 지정 없음으로 본다)") - for chainage, spec in load_culvert_sets(project_root).items(): - if spec.get("type") != "ford_pavement": - continue - half = float(spec.get("span_m") or 0.0) / 2.0 - ranges.append((chainage - half, chainage + half)) - return ranges + return ranges + ford_pavement_ranges(project_root) + + +def ford_pavement_ranges(project_root: Path | None) -> list[tuple[float, float]]: + """물넘이포장 범위(기준 ± 월류 폭/2) — 포장 필수 · 측구 없음(286-2). 판정은 `paved_at`.""" + if project_root is None: + return [] + sets = load_culvert_sets(project_root).items() + halves = [ + (at, float(s.get("span_m") or 0.0) / 2.0) + for at, s in sets + if s.get("type") == "ford_pavement" + ] + return [(at - half, at + half) for at, half in halves] def ford_surface_drops(project_root: Path | None) -> list[tuple[float, float]]: @@ -207,6 +215,7 @@ def enforce_pavement_ranges( 사용자 조작값(반폭·구조물 조정)은 그대로 승계한다. """ ranges = pavement_ranges(project_root) + ford_ranges = ford_pavement_ranges(project_root) ford_drops = ford_surface_drops(project_root) changed = 0 for section in cross_sections: @@ -215,7 +224,8 @@ def enforce_pavement_ranges( continue chainage = float(section.get("chainage_m", 0.0)) wanted = paved_at(chainage, ranges) - if bool(design.get("paved")) == wanted: + ford_paved = paved_at(chainage, ford_ranges) # 물넘이 자리 = 측구 없음(286-2) + if bool(design.get("paved")) == wanted and bool(design.get("ford_paved")) == ford_paved: continue try: recomputed = compute_cross_design( @@ -238,6 +248,7 @@ def enforce_pavement_ranges( berm=stored_berm(design), **curve_widening_args(section), **stored_shoulder_extend(design), # 노견 확장(139-6) + ford_paved=ford_paved, ) except (ValueError, KeyError): continue @@ -266,6 +277,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) spans = load_berm_spans(project_root) changed = 0 for section in cross_sections: @@ -296,6 +308,7 @@ def enforce_design_lines( berm=stored_berm(design), **curve_widening_args(section), **stored_shoulder_extend(design), + ford_paved=paved_at(chainage, ford_ranges), ) except (ValueError, KeyError): continue @@ -351,6 +364,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")), ) except (ValueError, KeyError): continue @@ -423,6 +437,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) ford_drops = ford_surface_drops(project_root) for section in cross_sections: if section.get("design"): @@ -441,6 +456,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), ) design.update(status="provisional", pavement_suggested=suggested) section["design"] = design @@ -501,6 +517,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) ford_drops = ford_surface_drops(project_root) stored_by_chainage = { round(float(record["chainage_m"]), 3): (record.get("design") or {}) @@ -559,6 +576,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), ) except (ValueError, KeyError): continue @@ -603,6 +621,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) ford_drops = ford_surface_drops(root) results: list[tuple[float, dict[str, Any]]] = [] for chainage_m in chainages: @@ -642,6 +661,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), ) 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 f833f5624..5ca23d584 100644 --- a/B06_Section/B06_Section_UI_Cross_Ford_Pavement.ts +++ b/B06_Section/B06_Section_UI_Cross_Ford_Pavement.ts @@ -10,7 +10,9 @@ * · 기존 계획고는 **점선**, 물넘이 바닥은 **실선**. * · 포장 필수 — 일반 포장층과 구분되게 진한 회색 + 빗금으로 채운다. * - * 유입측 판정은 `section_mode`(상단측 절토 = 등고가 높은 쪽 = 상류)를 따른다. + * 유입측 = 표고가 높은 쪽 — 설계선 양 끝(비탈이 원지반을 만나는 자리) 표고로 가름 · 노면은 경사각도 + * (`ford_slope_deg` → `slope_pct`)만큼 표고 낮은 쪽(유출)으로 기움(286-2). 양 끝이 같으면 `section_mode` + * (상단측 절토 = 상류). * ========================================================================== */ import type { CrossDesign } from "./B06_Section_Api_Fetch"; @@ -41,11 +43,17 @@ interface Edge { elevation_m: number; } +/** 유입(상류)이 좌측(+offset)인가 — 설계선 양 끝 표고가 높은 쪽(286-2) · 같으면 단면유형. */ +export function inflowLeft(design: CrossDesign): boolean { + 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 inflowLeft = design.section_mode !== "right_cut"; const towardLeft = offsetM > 0; - return towardLeft === inflowLeft ? 1 : -1; + return towardLeft === inflowLeft(design) ? 1 : -1; } /** @@ -193,7 +201,7 @@ export function appendFordPavementOverlay( export function outletFillLine(design: CrossDesign): OffsetPoint[] { const edges = design.road_edges; if (!edges || !design.design_line?.length) return []; - const outward = design.section_mode !== "right_cut" ? -1 : 1; // +offset = 좌측 · 유입 반대 + const outward = inflowLeft(design) ? -1 : 1; // +offset = 좌측 · 유입 반대 const edge = outward > 0 ? Math.max(edges.left.offset_m, edges.right.offset_m) diff --git a/common_util/common_util_cross_design.ts b/common_util/common_util_cross_design.ts index b9b99b1d8..b80dbd405 100644 --- a/common_util/common_util_cross_design.ts +++ b/common_util/common_util_cross_design.ts @@ -98,6 +98,8 @@ export interface CrossDesignOptions { ditchChoice?: boolean | null; /** 세월교 월류 높이만큼 노면을 통째로 내린다(m). */ surfaceDropM?: number; + /** 물넘이포장 범위 안 측점 — 측구 없음(286-2). 짝: 파이썬 `compute_cross_design(ford_paved=…)`. */ + fordPaved?: boolean; /** 이 측점의 평면 곡선반경(m) — 곡선부 확폭을 정하는 입력. 직선이면 null. */ planRadiusM?: number | null; /** 곡선 **바깥쪽**("left"/"right") — 확폭이 붙는 쪽(2026-09-06 사용자 확정). */ @@ -187,6 +189,8 @@ 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; pavement_thickness_m?: number; rock_boundary_offset_m?: number; } @@ -367,6 +371,7 @@ export function computeCrossDesign( berm: options.berm ?? null, shoulderExtendLeftM: options.shoulderExtendLeftM ?? 0, shoulderExtendRightM: options.shoulderExtendRightM ?? 0, + noDitch: Boolean(options.fordPaved), }); // 적분 오프셋 = 지반 샘플 ∪ 설계 꼭짓점(샘플 범위 안쪽만). @@ -581,6 +586,7 @@ export function computeCrossDesign( design_line: designLine, cut_slope_segments: geometry.cutSlopeSegments(), }; + if (options.fordPaved) result.ford_paved = true; 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 b67b2d67e..e6704af6a 100644 --- a/common_util/common_util_cross_design_geometry.ts +++ b/common_util/common_util_cross_design_geometry.ts @@ -145,6 +145,8 @@ export class SectionGeometry { /** 설계선 쉬프트 = 쪽마다 노견 확장(m · ≥ 0 · 139-6). */ shoulderExtendLeftM?: number; shoulderExtendRightM?: number; + /** 물넘이포장 자리 — 측구 없음(286-2 · 파이썬 짝 `no_ditch`). */ + noDitch?: boolean; }) { const { group } = params; const halfRoad = group.road_width_m / 2; @@ -213,6 +215,8 @@ export class SectionGeometry { this.hasDitch = choice === null || bothFill ? autoDitch : choice; /** 그 측점에 **사용자가 정한 선택**(없으면 자동). 결과와 갈라 내보낸다. */ this.ditchChoice = bothFill ? null : choice; + // 물넘이포장 자리(286-2) — 파인 노면이라 측구가 안 섬 · 선택은 그대로(파이썬 짝) + if (params.noDitch) this.hasDitch = false; // 설계선 쉬프트 = 쪽마다 사면 이동(≥ 0 · 139-6 · 151-2) — 단면유형 · 측구 판정은 **옮기기 전** // 노견에서(위) · 노견(폭 고정) · 측구 · 사면이 같이 바깥으로 · 늘어난 폭은 차도. ⚠ 파이썬 짝 `_SectionGeometry`. diff --git a/resources/tester/helper_286_1_ford_pavement.cjs b/resources/tester/helper_286_1_ford_pavement.cjs index 8cbdde1bd..4174223a4 100644 --- a/resources/tester/helper_286_1_ford_pavement.cjs +++ b/resources/tester/helper_286_1_ford_pavement.cjs @@ -23,8 +23,30 @@ function loadTs(file, stubs = {}) { const input = JSON.parse(fs.readFileSync(process.argv[2], "utf8")); const station = loadTs(path.join(ROOT, "common_util", "common_util_structure_station.ts")); +const ford = loadTs(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Ford_Pavement.ts"), { + "./B06_Section_UI_Cross_Wall_Hatch": { appendChuteBand() {} }, +}); +// 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), +}); + const out = { 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/test_286_1_ford_pavement_design.py b/resources/tester/test_286_1_ford_pavement_design.py index 41018dbb9..30e2c247c 100644 --- a/resources/tester/test_286_1_ford_pavement_design.py +++ b/resources/tester/test_286_1_ford_pavement_design.py @@ -3,6 +3,8 @@ 못박는 것 - 286-1 물넘이포장도 기준 측점 하나 — TS `pipeStationMarks` · `plantedChainages` ↔ 서버 `resolve_extra_stations` 같은 값 · 라벨 「물넘이포장」 · B05 · B06 거름 걷음. + - 286-2 물넘이 자리 = 측구 없음(서버 정본 · 화면 짝 · 거울 = `test_b06_cross_design_mirror`) · + 노면 = 경사각도로 표고 낮은 쪽(설계선 양 끝 표고 · 같으면 단면유형). 진짜 TS 는 `helper_286_1_ford_pavement.cjs` 가 Node 로 돌림. 화면은 사용자 확인. """ @@ -23,6 +25,8 @@ if str(ROOT) not in sys.path: from B05_Profile.B05_Profile_Engine_Sections import resolve_extra_stations # noqa: E402 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 common_util.common_util_drainage_pipes import parse_pipe_points # noqa: E402 PIPES = [ @@ -82,3 +86,52 @@ def test_B05_B06_물넘이_거름_걷음(): assert '"ford_pavement"' not in view drafts = _read("B06_Section", "B06_Section_Section_Drafts.ts") assert 'if (facility === "ford_pavement") return null;' not in drafts + + +def _ground(slope: float) -> list[dict]: + return [ + {"offset_m": o / 2, "elevation_m": round(100.0 + slope * o / 2, 4), "valid": True} + for o in range(-24, 25) + ] + + +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 + assert ford["ditch_area_m2"] == 0 + # 사용자가 측구를 켜도 물넘이 자리는 안 섬 + forced = compute_cross_design( + _ground(0.35), 100.4, ford_paved=True, ditch_choice=True, **kwargs + ) + assert forced["ditch_enabled"] is False + + +def test_물넘이_범위_서버_판정(tmp_path): + edits = tmp_path / "B04_PreProcess" / "drainage" / "edits" + edits.mkdir(parents=True) + (edits / "pipe_points.json").write_text( + json.dumps({"points": [{**PIPES[1], "options": {"ford_width_m": 8}}]}), encoding="utf-8" + ) + ranges = ford_pavement_ranges(tmp_path) + assert ranges == [(226.0, 234.0)] + 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_저장_뒤_서버가_물넘이_측구를_맞춤(): + router = _read("B06_Section", "B06_Section_Router_Design.py") + 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 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 e5a020a1e..6162dece5 100644 --- a/resources/tester/test_52_3_structure_spec_ford_pavement.py +++ b/resources/tester/test_52_3_structure_spec_ford_pavement.py @@ -158,14 +158,15 @@ class Node { } globalThis.document = { createElementNS: (_ns, tag) => new Node(tag) }; const pave = load(path.join(ROOT, "B06_Section/B06_Section_UI_Cross_Ford_Pavement.ts")); -// +offset = 좌측 · 좌 성토 1:1.5 · 우 절토 오름 — 유입 = 좌(left_cut 아님) → 유출 = 우 -const design = (mode) => ({ +// +offset = 좌측 · 좌 성토 1:1.5 · 우 절토 오름 — 유입 = 표고 높은 쪽(설계선 끝 · 286-2) = 우 → 유출 = 좌 +// `rightEnd` = 우 끝 표고 — 좌 끝(97)과 같으면 단면유형으로 가름(left_cut = 유입 좌 → 유출 우) +const design = (mode, rightEnd = 99) => ({ section_mode: mode, cross_slope_pct: 2, design_elevation_m: 100, pavement_thickness_m: 0.2, road_edges: { left: { offset_m: 2.5, elevation_m: 100 }, right: { offset_m: -2.5, elevation_m: 100 } }, design_line: [ { offset_m: 7, elevation_m: 97 }, { offset_m: 2.5, elevation_m: 100 }, { offset_m: -2.5, elevation_m: 100 }, { offset_m: -5.5, elevation_m: 98 }, - { offset_m: -6.5, elevation_m: 98 }, { offset_m: -8, elevation_m: 99 }, + { offset_m: -6.5, elevation_m: 98 }, { offset_m: -8, elevation_m: rightEnd }, ], }); const spec = { span_m: 9, depth_m: 0.3, slope_pct: 2, chute: { structure: "돌붙임", thickness_m: 0.3 } }; @@ -176,7 +177,7 @@ const run = (mode, withChute = true) => { return band ? band.attrs.points : null; }; process.stdout.write(JSON.stringify({ - outLeft: pave.outletFillLine(design("left_cut")), outRight: pave.outletFillLine(design("right_cut")), + outLeft: pave.outletFillLine(design("left_cut", 97)), outRight: pave.outletFillLine(design("left_cut")), band: run("left_cut"), none: run("left_cut", false), })); """ @@ -188,13 +189,14 @@ def test_유출측_성토사면_띠_136_2(): ) assert proc.returncode == 0, proc.stderr got = json.loads(proc.stdout) - # 유입 = 좌 → 유출 = 우(− 바깥) · 노견에서 내려가는 동안만(평지 · 오름에서 멈춤) + # 양 끝 같음 → 단면유형 left_cut = 유입 좌 → 유출 = 우(− 바깥) · 노견에서 내려가는 동안만 assert got["outLeft"] == [ {"offset": -2.5, "elevation": 100}, {"offset": -5.5, "elevation": 98}, {"offset": -6.5, "elevation": 98}, + {"offset": -8, "elevation": 97}, ] - # right_cut = 유입 우 → 유출 좌 성토 1:1.5 + # 우 끝이 높음 = 유입 우(입력 left_cut 이어도 지형 · 286-2) → 유출 좌 성토 1:1.5 assert got["outRight"] == [{"offset": 2.5, "elevation": 100}, {"offset": 7, "elevation": 97}] - assert got["band"] and got["band"].startswith("-2.5,100 -5.5,98") + assert got["band"] and got["band"].startswith("2.5,100 7,97") assert got["none"] is None diff --git a/resources/tester/test_b06_cross_design_mirror.py b/resources/tester/test_b06_cross_design_mirror.py index d5169229f..b3fe1763d 100644 --- a/resources/tester/test_b06_cross_design_mirror.py +++ b/resources/tester/test_b06_cross_design_mirror.py @@ -218,6 +218,19 @@ CASES = [ "shoulder_extend_left_m": 0.4, }, }, + { + # 286-2 — 물넘이포장 자리 = 측구 없음(좌절토라 자동이면 측구가 섬) + "name": "물넘이포장 자리 측구 없음", + "samples": _ground(0.35), + "design_elevation_m": 100.4, + "options": { + "ground_type": "soil", + "section_mode": "left_cut", + "ditch_side": "left", + "paved": True, + "ford_paved": True, + }, + }, ] _TS_RUNNER = """ @@ -241,6 +254,7 @@ const camel = { curve_widening_m: "curveWideningM", shoulder_extend_left_m: "shoulderExtendLeftM", shoulder_extend_right_m: "shoulderExtendRightM", + ford_paved: "fordPaved", }; // 사토장 제원만 칸 이름이 다르다 — 파이썬은 snake, TS 는 camel 이다. const spoilCamel = { side: "side", width_m: "widthM", slope_ratio_n: "slopeRatioN" };