"""B06 포장 · 세월교 구간 읽기와 판정 — `B06_Section_Router_Design.py` 에서 나눔(472-4 · 동작 같음). 짝(349-0n): `pavement_ranges` · `ford_surface_drops` · `paved_at` · `ford_drop_at` ↔ `B06_Section/logic/B06_Section_Cross_Enforce.ts` · 거울 시험 `test_349_0n_cross_enforce_mirror.py`. 부르는 쪽은 `B06_Section_Router_Design` 이름으로 씀(다시 내보냄 · 시험이 그 이름을 갈아 끼움). """ import logging from pathlib import Path from typing import Any from B05_Profile.B05_Profile_Structures_Repository import load_structures from B05_Profile.B05_Profile_Structures_Schema import structure_type_map from B06_Section.B06_Section_Engine_Culvert_Sets import load_culvert_sets from common_util.common_util_structure_station import EDGE_TOLERANCE_M logger = logging.getLogger(__name__) # 세월교 노면 하강이 걸리는 측점 판정 허용 오차(m) — 세트 부착(`attach_culvert_sets`)과 # 같은 기준이라 "구체가 그려진 측점"과 "노면이 내려간 측점"이 어긋나지 않는다. _FORD_DROP_TOLERANCE_M = 0.02 def pavement_suggestions(longitudinal: dict[str, Any]) -> dict[float, bool]: mapping: dict[float, bool] = {} stations = longitudinal.get("stations") for station in stations if isinstance(stations, list) else []: suggested = station.get("pavement_suggested") if isinstance(suggested, bool): mapping[round(float(station.get("chainage_m", 0.0)), 3)] = suggested return mapping def pavement_ranges(project_root: Path) -> list[tuple[float, float]]: """포장으로 볼 누가거리 구간 — 구조물 정본 G군 + 물넘이포장(포장 필수). 포장 지정은 **사용자가 구간으로 준다**(2026-08-28 사용자 확정). 종단경사 자동 판정은 더 이상 포장을 켜지 않는다 — 경고 표기(`pavement_suggested`)만 남는다. 물넘이포장은 콘크리트 등으로 노면을 만드는 시설이라 범위 안이 항상 포장이다. 읽기 실패는 비치명 — 빈 목록이면 사용자 지정이 없는 것과 같다. """ ranges: list[tuple[float, float]] = [] try: types = structure_type_map() for structure in load_structures(str(project_root))[1]: definition = types.get(structure.type_id) if definition is None or definition.group != "G": continue anchor = structure.chainage_m start = structure.start_m if structure.start_m is not None else anchor end = structure.end_m if structure.end_m is not None else anchor if start is None or end is None: continue ranges.append((min(float(start), float(end)), max(float(start), float(end)))) except Exception: # noqa: BLE001 — 정본을 못 읽어도 설계 계산은 이어 간다 logger.exception("B06 포장 구간을 읽지 못했습니다 (사용자 지정 없음으로 본다)") 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]]: """세월교가 앉은 측점의 노면 하강량 — [(누가거리, 월류 높이 m)]. 구체 위 노면은 월류 높이만큼 낮게 앉는다(2026-08-30 사용자 확정). 계획고를 그만큼 내려 잡아야 측벽·바닥판이 맞고 절·성토 면적도 따라온다. 포장 구간과 같은 방식으로 관 지점 정본에서 읽는다 — 읽기 실패는 비치명(하강 없음으로 본다). """ if project_root is None: return [] drops: list[tuple[float, float]] = [] try: for chainage, spec in load_culvert_sets(project_root).items(): if spec.get("type") != "ford": continue depth = float(spec.get("overflow_depth_m") or 0.0) if depth > 0: drops.append((chainage, depth)) except Exception: # noqa: BLE001 — 정본을 못 읽어도 설계 계산은 이어 간다 logger.exception("B06 세월교 월류 높이를 읽지 못했습니다 (노면 하강 없음으로 본다)") return drops def ford_drop_at(chainage_m: float, drops: list[tuple[float, float]]) -> float: """그 측점의 노면 하강량(m). 구체가 앉은 측점만 — 붙는 기준은 세트 부착과 같다.""" for at, depth in drops: if abs(chainage_m - at) <= _FORD_DROP_TOLERANCE_M: return depth return 0.0 # 범위 끝 허용오차(m) — 같은 자리 거리 한 곳(323-7) _PAVED_EDGE_TOLERANCE_M = EDGE_TOLERANCE_M def paved_at(chainage_m: float, ranges: list[tuple[float, float]]) -> bool: """이 측점을 포장으로 볼 것인가 — 노면공 · 물넘이 범위 안만 포장, 밖은 비포장(저장값 무시). 브라우저 짝 = `B06_Section_Cross_Refresh.ts` `pavedBySpecs`(같은 규칙 · 138-1). """ tol = _PAVED_EDGE_TOLERANCE_M return any(start - tol <= chainage_m <= end + tol for start, end in ranges)