"""B06 관 세트 읽기 · 측점에 얹기 — `B06_Section_Engine_Culvert.py` 에서 나눔(472 덧 · 동작 같음). 관 지점 정본(`pipe_points.json`) → 누가거리별 세트 제원(`culvert_sets_from`) → 횡단 dict 에 `culvert` · `ford` · `box` · `ford_pavement` 키로 얹는다(`attach_culvert_specs`). 세트 제원 만들기 (`_culvert_set` 등)는 `B06_Section_Engine_Culvert.py`. ⚠ TS 짝: `common_util/common_util_culvert_sets.ts` `buildCulvertSets` · `attachCulvertSets` — 거울 시험 `resources/tester/test_b06_culvert_sets_mirror.py`. """ from __future__ import annotations import logging from pathlib import Path from typing import Any from B06_Section.B06_Section_Engine_Culvert import ( _box_set, _culvert_set, _ford_pavement_set, _ford_set, _number, _revet_set, ) from common_util.common_util_structure_catalog import catalog_row from common_util.common_util_structure_station import EDGE_TOLERANCE_M, PIPE_MATCH_M logger = logging.getLogger(__name__) # 관 지점과 횡단 측점을 같은 자리로 볼 허용 오차(m) — 같은 자리 거리 한 곳(323-7) _CHAINAGE_TOLERANCE_M = EDGE_TOLERANCE_M # 세트 종류 -> 횡단 dict 키. 그림이 다른 시설끼리 소비처가 섞이지 않게 나눠 둔다. _SECTION_KEYS = {"ford": "ford", "box": "box", "ford_pavement": "ford_pavement"} # 구조물 종류별 **인접 측점 연동**(2026-08-25 사용자 확정). 이름을 넣으면 도로 방향 # 폭(`span_m`)의 절반만큼 앞뒤 측점에도 같은 스펙이 붙어 횡단도가 한 벌 더 그려진다. # 세월교·BOX암거는 끔 — 소유 측점 한 곳에만 선다. 3D는 그 측점에서 `span_m` 전체를 # 스윕하므로 길이가 줄지 않는다(인접 측점 기준으로 자르지 않는다). # 물넘이포장만 켠다 — 노면을 월류 폭만큼 파는 시설이라 **범위 안 모든 횡단도**에 # 파임이 그려져야 한다(2026-08-28 사용자 확정). # ※ 기슭막이 연동은 이 경로가 아니다 — 프론트 `culvertOwnerFor`가 따로 판정한다. _SPAN_LINKED_TYPES: frozenset[str] = frozenset({"ford_pavement"}) def load_culvert_sets(project_root: Path) -> dict[float, dict[str, Any]]: """관 지점 정본 → 누가거리별 세트 제원 — 관 = 지금 노선에 얹은 자리(472 덧 · 측점 계획과 같음). 파일 누가거리를 그대로 읽으면 지문이 어긋난 옛 저장분에서 세월교 노면 하강 · 물넘이포장 구간이 측점 계획 · 화면과 다른 자리에 걸렸다. 파일이 없거나 깨졌으면 빈 값. """ from B05_Profile.B05_Profile_Engine_Sections import route_pipe_points return culvert_sets_from(route_pipe_points(project_root)) def culvert_sets_from(points: list[Any]) -> dict[float, dict[str, Any]]: """관 지점 목록 → 누가거리(소수 2자리)별 세트 제원(TS `buildCulvertSets` 짝 · 형상 얹기 한 벌 323-4).""" sets: dict[float, dict[str, Any]] = {} for point in points: # 그릴 세트 = 구조물 표 형상(322-1 · TS `buildCulvertSets` 짝) shape = (catalog_row(point.facility) or {}).get("shape") if shape == "pipe_set": if (point.options or {}).get("pipe_diameter_mm") in (None, ""): continue # 관경 안 고름 = 관 없음(54-39) spec = _culvert_set(point.options) elif shape == "ford": spec = _ford_set(point.options) elif shape == "box": spec = _box_set(point.options) elif shape == "ford_pavement": # 구조물이 아니라 파인 노면이다 — 스펙 모양도 소비처도 다르다(2026-08-28). spec = _ford_pavement_set(point.options) elif shape == "hidden_revet": # 독립 기슭막이 — 배관 세트 모양이되 관을 숨긴다. `culvert` 키로 얹혀 배관 # 경로(컴퓨트·렌더·패널·연동·경사·3D·수량)를 그대로 탄다. spec = _revet_set(point.options) else: continue sets[round(float(point.chainage_m), 2)] = spec return sets def attach_culvert_sets(project_root: Path, cross_sections: list[dict[str, Any]]) -> int: """배관이 놓인 측점의 횡단 dict에 `culvert` 키를 얹는다. 얹은 개수를 돌려준다.""" return attach_culvert_specs(cross_sections, load_culvert_sets(project_root)) def attach_culvert_specs( cross_sections: list[dict[str, Any]], sets: dict[float, dict[str, Any]], missed: list[float] | None = None, ) -> int: """세트를 측점에 얹음(TS `attachCulvertSets` 짝) · 얹은 수 · 주인 먼 세트 = `missed`.""" if not sets: return 0 attached = 0 # 주인 측점 = 가장 가까운 측점 하나(`PIPE_MATCH_M` 안 · 정수 m 측점 위 소수 관도) · # 더 멀면 그 시설 측점이 없음(노선 바뀜) — 연동 이웃에도 안 얹음. owner: dict[float, float] = {} for pipe_chainage in sets: nearest = None for section in cross_sections: value = _number(section.get("chainage_m"), None) if value is None: continue if nearest is None or abs(value - pipe_chainage) < abs(nearest - pipe_chainage): nearest = value if nearest is not None and abs(nearest - pipe_chainage) < PIPE_MATCH_M: owner[pipe_chainage] = nearest elif missed is not None: missed.append(pipe_chainage) for section in cross_sections: chainage = _number(section.get("chainage_m"), None) if chainage is None: continue # ⚠ 키가 다른 시설은 같은 측점에 함께 얹는다(299-1) — 첫 시설 하나에서 멈추면 물넘이포장 # 연동 범위(폭 절반) 안 측점의 **자기 관**이 빠졌다(95 m 배관이 90 m 물넘이에 가려짐). # 같은 키끼리는 소유 측점 것 → 흙막이 아닌 자기 시설 → 가까운 것 → 관 지점 차례. best: dict[str, tuple[tuple[Any, ...], dict[str, Any]]] = {} for order, (pipe_chainage, spec) in enumerate(sets.items()): if pipe_chainage not in owner: continue # 연동 대상 종류만 폭의 절반까지 옆 측점에 걸친다(`_SPAN_LINKED_TYPES`). reach = _CHAINAGE_TOLERANCE_M if spec.get("type") in _SPAN_LINKED_TYPES: reach += (_number(spec.get("span_m"), 0.0) or 0.0) / 2 owned = owner.get(pipe_chainage) == chainage if not (abs(chainage - pipe_chainage) <= reach or owned): continue # 세월교·BOX암거·물넘이는 배수관과 그림이 달라 키를 나눈다 — 소비처가 섞이지 않는다. entries = [(_SECTION_KEYS.get(str(spec.get("type")), "culvert"), spec, False)] # 세월교 · 박스 흙막이(73-4) — 배관 흙막이와 같은 자리(`culvert`)에 함께 얹음 · # 자기 관과 부딪히면 관 먼저(흙막이가 덮으면 관 그림 · 연장이 빠짐). if spec.get("guard"): entries.append(("culvert", spec["guard"], True)) for key, value, is_guard in entries: rank = (not owned, is_guard, abs(chainage - pipe_chainage), order) if key not in best or rank < best[key][0]: # ⚠ 스펙에 **그 시설이 놓인 누가거리**를 함께 얹음 — 없으면 소비처가 소유 측점을 # 못 가려 **같은 시설을 여러 측점에서 셈**(폭 절반 연동 · 관 9개에 길이 10곳). best[key] = (rank, {**value, "chainage_m": pipe_chainage}) for key, (_, value) in best.items(): section[key] = value attached += bool(best) return attached