"""구조물 자리 기울기 — 물넘이포장 · 맹암거 측점의 노면(287-4 · 288-1). 그 자리 횡단 설계 = 측구 없음 · 노면 = 한 경사로 지대 낮은 쪽(`compute_cross_design(tilted=…, tilt_pct=…)`). 경사 = 물넘이 경사각도(%) · 맹암거 암거 경사(%) · 비면 표준 횡단경사. · 물넘이포장 = 관 지점 정본(pipe_points) 범위(기준 ± 월류 폭/2). · 맹암거 = 구조물 정본(structures.json) A군 점형 · 기준 측점 한 곳 — 횡단 dict 에 `underdrain`(높이 · 경사)을 얹어 횡단도가 돌 무늬 형상을 그림(TS 짝 `common_util_structure_underdrain.ts`). 판정 폭 = 포장 범위와 같은 0.02 m(`paved_at`). """ from __future__ import annotations import logging from pathlib import Path from typing import Any from B05_Profile.B05_Profile_Structures_Repository import load_structures from B06_Section.B06_Section_Engine_Culvert import load_culvert_sets logger = logging.getLogger(__name__) UNDERDRAIN_TYPE_ID = "cross_underdrain" _EDGE_TOLERANCE_M = 0.02 TiltRange = tuple[float, float, float | None] def _number(value: Any) -> float | None: try: parsed = float(value) except (TypeError, ValueError): return None return parsed if parsed == parsed else None def load_underdrains(project_root: Path) -> list[dict[str, Any]]: """구조물 정본의 맹암거 — `{structure_id, chainage_m, height_m, slope_pct}` · 실패하면 빈 목록.""" try: structures = load_structures(str(project_root))[1] except Exception: # noqa: BLE001 — 정본을 못 읽어도 설계 계산은 이어 간다 logger.exception("B06 맹암거를 읽지 못했습니다 (없음으로 본다)") return [] found: list[dict[str, Any]] = [] for structure in structures: if structure.type_id != UNDERDRAIN_TYPE_ID: continue options = structure.options or {} height = _number(options.get("culvert_height_m")) slope = _number(options.get("culvert_slope_pct")) found.append( { "structure_id": structure.structure_id, "chainage_m": float(structure.anchor_m()), "height_m": height if height and height > 0 else None, "slope_pct": slope if slope and slope > 0 else None, } ) return found def tilt_ranges(project_root: Path | None) -> list[TiltRange]: """측구 없이 노면을 지대 낮은 쪽 한 경사로 기울이는 자리 — (시작, 끝, 경사 %).""" if project_root is None: return [] out: list[TiltRange] = [] 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"))) for drain in load_underdrains(project_root): out.append((drain["chainage_m"], drain["chainage_m"], drain["slope_pct"])) return out def tilt_args(chainage_m: float, ranges: list[TiltRange]) -> dict[str, Any]: """그 측점의 구조물 자리 기울기 — `compute_cross_design(**...)` 로 넘김 · 저장분 짝 `stored_tilt`.""" tol = _EDGE_TOLERANCE_M hit = next((r for r in ranges if r[0] - tol <= chainage_m <= r[1] + tol), 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 attach_underdrains(project_root: Path, cross_sections: list[dict[str, Any]]) -> int: """맹암거 기준 측점의 횡단 dict 에 `underdrain` 을 얹는다. 얹은 개수를 돌려준다.""" drains = load_underdrains(project_root) attached = 0 for section in cross_sections: chainage = _number(section.get("chainage_m")) if chainage is None: continue hit = next( (d for d in drains if abs(d["chainage_m"] - chainage) <= _EDGE_TOLERANCE_M), None ) if hit: section["underdrain"] = dict(hit) attached += 1 return attached