"""포장 구간 정본 + 물넘이포장 횡단 스펙 — 2026-08-28 사용자 확정 스펙. - 포장은 **사용자가 구간으로 지정**한다(구조물 정본 G군). 종단경사 자동 판정은 더 이상 포장을 켜지 않고 경고(`pavement_suggested`)로만 남는다. - 물넘이포장은 노면을 월류 폭만큼 판 자리다 — 범위 안 모든 횡단도에 붙고, 그 범위는 언제나 포장이다. """ import json import math from B06_Section.B06_Section_Engine_Culvert import ( FORD_PAVEMENT_DEFAULT_WIDTH_M, attach_culvert_sets, load_culvert_sets, ) from B06_Section.B06_Section_Router_Design import paved_at, pavement_ranges def _write_pipe_points(root, points): edits = root / "B04_PreProcess" / "drainage" / "edits" edits.mkdir(parents=True, exist_ok=True) (edits / "pipe_points.json").write_text( json.dumps({"route_signature": "무관", "points": points}, ensure_ascii=False), encoding="utf-8", ) def _write_structures(root, structures): route_dir = root / "B05_Profile" / "route" route_dir.mkdir(parents=True, exist_ok=True) (route_dir / "structures.json").write_text( json.dumps({"revision": 1, "structures": structures}, ensure_ascii=False), encoding="utf-8", ) FORD = { "chainage_m": 100.0, "source": "user", "facility": "ford_pavement", "options": {"ford_width_m": 6.0, "ford_height_m": 0.4, "ford_slope_pct": 3.0}, } # ── 물넘이 스펙 ──────────────────────────────────────────────────────────── def test_ford_pavement_becomes_its_own_spec(tmp_path): _write_pipe_points(tmp_path, [FORD]) (spec,) = load_culvert_sets(tmp_path).values() assert spec["type"] == "ford_pavement" assert spec["span_m"] == 6.0 assert spec["depth_m"] == 0.4 assert spec["slope_pct"] == 3.0 def test_ford_pavement_without_values_keeps_default_width_and_no_depth(tmp_path): """깊이는 지어내지 않는다 — 없으면 None이라 화면이 파임을 그리지 않는다. 경사는 빈 값이면 정의 기본 3° → %(65-5 · 66-2).""" _write_pipe_points(tmp_path, [{"chainage_m": 50.0, "facility": "ford_pavement"}]) (spec,) = load_culvert_sets(tmp_path).values() assert spec["span_m"] == FORD_PAVEMENT_DEFAULT_WIDTH_M assert spec["depth_m"] is None assert spec["slope_pct"] == round(math.tan(math.radians(3)) * 100.0, 4) def test_ford_pavement_attaches_to_every_section_in_span(tmp_path): """월류 폭 6m → 기준측점 ±3m 안의 측점 전부에 붙는다(다른 시설은 소유 측점 한 곳).""" _write_pipe_points(tmp_path, [FORD]) sections = [{"chainage_m": value} for value in (94.0, 97.5, 100.0, 102.5, 106.0)] attach_culvert_sets(tmp_path, sections) attached = [s["chainage_m"] for s in sections if "ford_pavement" in s] assert attached == [97.5, 100.0, 102.5] assert all("culvert" not in s and "ford" not in s for s in sections) # ── 포장 구간 ────────────────────────────────────────────────────────────── def test_pavement_range_comes_from_group_g_structure(tmp_path): _write_structures( tmp_path, [ { "type_id": "pavement_concrete", "chainage_m": 200.0, "start_m": 180.0, "end_m": 230.0, "placement": "interval", } ], ) assert pavement_ranges(tmp_path) == [(180.0, 230.0)] def test_ford_pavement_range_is_always_paved(tmp_path): _write_pipe_points(tmp_path, [FORD]) ranges = pavement_ranges(tmp_path) assert ranges == [(97.0, 103.0)] assert paved_at(100.0, ranges) is True assert paved_at(97.0, ranges) is True assert paved_at(96.9, ranges) is False def test_other_groups_are_not_pavement(tmp_path): _write_structures( tmp_path, [ { "type_id": "revetment", "chainage_m": 100.0, "start_m": 100.0, "end_m": 115.0, "placement": "interval", } ], ) assert pavement_ranges(tmp_path) == [] def test_missing_truth_files_are_not_an_error(tmp_path): assert pavement_ranges(tmp_path) == [] # ── 포장 판정 규칙 ───────────────────────────────────────────────────────── def test_outside_range_is_unpaved(): """포장 = 노면공 · 물넘이 범위만(138-1) — 밖은 비포장(옛 저장값 인자 없음).""" ranges = [(10.0, 20.0)] assert paved_at(30.0, ranges) is False assert paved_at(15.0, ranges) is True assert paved_at(20.02, ranges) is True # 끝 허용오차 = 브라우저 짝 0.02 m assert paved_at(20.03, ranges) is False assert paved_at(15.0, []) is False def test_enforce_turns_paved_on_and_off(monkeypatch): """재계산은 켬↔끔 양쪽 — 범위 밖 옛 `paved=true` 는 비포장으로 되돌림(138-1).""" from pathlib import Path from B06_Section import B06_Section_Router_Design as design_mod monkeypatch.setattr(design_mod, "pavement_ranges", lambda root: [(0.0, 50.0)]) monkeypatch.setattr(design_mod, "ford_surface_drops", lambda root: []) monkeypatch.setattr(design_mod, "design_elevation_from_longitudinal", lambda lon, ch: 100.0) monkeypatch.setattr(design_mod, "curve_widening_args", lambda section: {}) monkeypatch.setattr( design_mod, "compute_cross_design", lambda samples, elev, **kw: {"paved": kw["paved"]} ) sections = [ {"chainage_m": 20.0, "design": {"paved": False}}, # 범위 안 · 끔 → 켬 {"chainage_m": 80.0, "design": {"paved": True}}, # 범위 밖 · 켬 → 끔 {"chainage_m": 30.0, "design": {"paved": True}}, # 그대로 {"chainage_m": 90.0, "design": {"paved": False}}, # 그대로 ] assert design_mod.enforce_pavement_ranges({}, sections, Path("."), None) == 2 assert [s["design"]["paved"] for s in sections] == [True, False, True, False] # 노면공을 다 지워도(범위 없음) 옛 켬은 꺼진다 monkeypatch.setattr(design_mod, "pavement_ranges", lambda root: []) assert design_mod.enforce_pavement_ranges({}, sections, Path("."), None) == 2 assert not any(s["design"]["paved"] for s in sections) # ── 독립 기슭막이 ───────────────────────────────────────────────────────── # # 2026-08-28 이관으로 기슭막이는 구조물 정본 D군을 떠나 관 지점 시설이 됐다. 전용 엔진 # (`B06_Section_Engine_Revetment`)은 호출자가 사라져 2026-09-01 삭제했다 — 벽은 관 세트 # (`B06_Section_Engine_Culvert._revet_set`)가 만든다. 그쪽 검증은 # `test_revet_span_source.py`에 있다.