diff --git a/B05_Profile/B05_Profile_Engine.py b/B05_Profile/B05_Profile_Engine.py index 46f360d9e..875f38043 100644 --- a/B05_Profile/B05_Profile_Engine.py +++ b/B05_Profile/B05_Profile_Engine.py @@ -11,6 +11,7 @@ from typing import Any from B05_Profile.B05_Profile_Debug import log_b05_debug from B05_Profile.B05_Profile_Engine_RidgeValley import solve_ridge_valley_route +from B05_Profile.B05_Profile_Engine_Sections import ROUTE_DATA_FILE from B05_Profile.B05_Profile_Engine_Solver import solve_optimal_route from common_util.common_util_json import atomic_write_json @@ -142,7 +143,7 @@ def run_route_design( route_dir = project_root / _ROUTE_SUBDIR route_dir.mkdir(parents=True, exist_ok=True) - geojson_path = route_dir / "route_main.geojson" + geojson_path = project_root / ROUTE_DATA_FILE atomic_write_json(geojson_path, _route_geojson(polyline)) log_b05_debug( logger, diff --git a/B05_Profile/B05_Profile_Engine_Sections.py b/B05_Profile/B05_Profile_Engine_Sections.py index c4c5f6871..bbaf3707f 100644 --- a/B05_Profile/B05_Profile_Engine_Sections.py +++ b/B05_Profile/B05_Profile_Engine_Sections.py @@ -30,8 +30,9 @@ from B05_Profile.B05_Profile_Structures_Schema import ( from common_util.common_util_drainage_pipes import ( PipePoint, load_pipe_points_file, - parse_pipe_points, pipe_anchor_clearances, + pipe_points_path_in, + read_pipe_points_file, route_signature, ) from common_util.common_util_json import atomic_write_json @@ -54,6 +55,8 @@ logger = logging.getLogger(__name__) _STAGE_SUBDIR = Path("B06_Section") _MODELS_SUBDIR = Path("B04_PreProcess") / "models" +# 노선 파일 하나 — 노선 풀기(`B05_Profile_Engine`)가 쓰고 DB `routes.route_data_path` 가 가리킴 +ROUTE_DATA_FILE = "B05_Profile/route/route_main.geojson" def _load_route_polyline(project_root: Path, route_data_path: str) -> list[list[float]]: @@ -243,6 +246,20 @@ def _load_pipe_points(project_root: Path, polyline: list[list[float]]) -> list[P return points +def route_pipe_points(project_root: Path, route_data_path: str | None = None) -> list[PipePoint]: + """관 지점 정본을 **지금 노선에 얹어** 읽음(472 덧 · 관 = 좌표 주인) — 측점 계획 · 관 지점 GET · + 화면과 같은 이월(`_load_pipe_points`) · 지문이 어긋난 옛 저장분도 같은 자리. + + 노선 경로 안 줌 = 노선 파일 하나(`ROUTE_DATA_FILE`) · 노선을 못 읽으면 저장분 그대로.""" + try: + polyline = _load_route_polyline(project_root, route_data_path or ROUTE_DATA_FILE) + except (OSError, ValueError): + if route_data_path: + logger.warning("관 지점: 노선을 못 읽어 저장분 그대로 씀 (%s)", route_data_path) + return read_pipe_points_file(pipe_points_path_in(project_root)) + return _load_pipe_points(project_root, polyline) + + def pipe_station_label(pipe: PipePoint, types: dict[str, Any]) -> str | None: """관 지점 측점 라벨 = 표 카드 이름(322-1 · 배관 = 「관종 D관경」 · 빈칸 = 정의 기본값).""" if pipe.facility not in types: @@ -250,24 +267,16 @@ def pipe_station_label(pipe: PipePoint, types: dict[str, Any]) -> str | None: return card_title(pipe.facility, pipe.options) -def relabel_pipe_stations(project_root: Path, cross_sections: list[dict[str, Any]]) -> None: +def relabel_pipe_stations( + project_root: Path, cross_sections: list[dict[str, Any]], route_data_path: str | None = None +) -> None: """횡단 측점 라벨을 **지금 관 지점 종류**로 맞춘다(54-4). 라벨은 측점 생성 때 적혀 파일에 남는다 — 저장으로 종류만 바꾸면(배관 → BOX암거) 측점은 다시 안 만들어져 카드 제목이 옛 종류로 남는다. 관 지점 라벨(종류 이름 · 「관종 D관경」)인 - 측점만 고치고 다른 구조물 라벨은 건드리지 않는다. + 측점만 고치고 다른 구조물 라벨은 건드리지 않는다. 관 = 지금 노선에 얹은 자리(472 덧). """ - path = ( - project_root - / "B04_PreProcess" - / DRAINAGE_CACHE_DIRNAME - / DRAINAGE_EDITS_DIRNAME - / DRAINAGE_PIPE_POINTS_FILENAME - ) - try: - points = parse_pipe_points(json.loads(path.read_text(encoding="utf-8")).get("points")) - except (OSError, ValueError, AttributeError): - return + points = route_pipe_points(project_root, route_data_path) types = structure_type_map() pipe_names = {row["name"] for row in catalog().values() if row["owner"] == "pipe_points"} for section in cross_sections: diff --git a/B05_Profile/B05_Profile_Structures_Router.py b/B05_Profile/B05_Profile_Structures_Router.py index 12e8e060b..9b1059aa3 100644 --- a/B05_Profile/B05_Profile_Structures_Router.py +++ b/B05_Profile/B05_Profile_Structures_Router.py @@ -298,12 +298,13 @@ async def migrate_structures(project_id: UUID, payload: StructureMigrateRequest) migrated_before = load_migrated_legacy(root) # 정본이 이미 심는 자리(측점 계획 · 323-7) — 이름이 아니라 자리로 가름(옛 이름 되짚기 걷음) # · 추가 측점(368-2 · 이름 없는 일반측점)도 심는 자리 — 「기타」 구조물로 옮기지 않음 + from B05_Profile.B05_Profile_Engine_Sections import route_pipe_points from B05_Profile.B05_Profile_Plus_Stations import plus_chainages planted = [ float(station["chainage_m"]) for station in plan_stations( - read_pipe_points_file(pipe_points_path_in(Path(root))), + route_pipe_points(Path(root)), # 관 = 지금 노선에 얹은 자리(472 덧) existing, plus_chainages(root), ) diff --git a/B06_Section/B06_Section_Engine_Attach.py b/B06_Section/B06_Section_Engine_Attach.py index 032a8fd95..b76e642e5 100644 --- a/B06_Section/B06_Section_Engine_Attach.py +++ b/B06_Section/B06_Section_Engine_Attach.py @@ -15,14 +15,9 @@ 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 import ( - attach_culvert_specs, - culvert_sets_from, - pipe_points_file, -) +from B06_Section.B06_Section_Engine_Culvert_Sets import attach_culvert_specs, culvert_sets_from from B06_Section.B06_Section_Engine_Structures_Wall import attach_wall_specs, wall_specs_from from B06_Section.B06_Section_Engine_Tilt import attach_underdrain_specs, underdrains_from -from common_util.common_util_drainage_pipes import read_pipe_points_file from common_util.common_util_structure_catalog import catalog_row, field_name, type_ids_with from common_util.common_util_structure_pavement import ( attach_pavement_specs, @@ -141,32 +136,14 @@ def attach_shapes( return misses -def saved_pipes(project_root: Path, route_data_path: str | None = None) -> list[Any]: - """관 지점 정본 — 노선 경로를 주면 그 노선에 얹어 읽음(472 덧) · 없으면 파일 그대로. - - 측점 계획(`_load_pipe_points`) · 관 지점 GET · 화면과 같은 이월 — 지문이 어긋난 옛 저장분도 - 측점이 선 자리에 관 세트가 얹힘.""" - if route_data_path: - from B05_Profile.B05_Profile_Engine_Sections import ( - _load_pipe_points, - _load_route_polyline, - ) - - try: - return _load_pipe_points( - project_root, _load_route_polyline(project_root, route_data_path) - ) - except (FileNotFoundError, ValueError): - logger.warning("B06 관 지점: 노선을 못 읽어 저장분 그대로 얹음 (%s)", route_data_path) - return read_pipe_points_file(pipe_points_file(project_root)) - - def attach_saved_shapes( project_root: Path, sections: list[dict[str, Any]], route_data_path: str | None = None ) -> None: - """저장분 현황(관 지점 · 구조물 정본)으로 얹음 — 관 = 그 노선에 얹은 것(`saved_pipes`) · + """저장분 현황(관 지점 · 구조물 정본)으로 얹음 — 관 = 그 노선에 얹은 것(`route_pipe_points`) · 구조물 정본을 못 읽으면 관만(횡단 조회는 이어 감).""" - pipes = saved_pipes(project_root, route_data_path) + from B05_Profile.B05_Profile_Engine_Sections import route_pipe_points + + pipes = route_pipe_points(project_root, route_data_path) try: structures = load_structures(str(project_root))[1] except Exception: # noqa: BLE001 — 정본을 못 읽어도 횡단 조회는 이어 간다 diff --git a/B06_Section/B06_Section_Engine_Culvert.py b/B06_Section/B06_Section_Engine_Culvert.py index 2ff50745b..47b03b31a 100644 --- a/B06_Section/B06_Section_Engine_Culvert.py +++ b/B06_Section/B06_Section_Engine_Culvert.py @@ -7,6 +7,7 @@ B05 배수유역도가 찍은 관 지점(`pipe_points.json` = 배관 정본)을 ⚠ TS 짝: `common_util/common_util_culvert_sets.ts` (2026-09-06). 한쪽만 고치면 화면과 저장본이 갈린다 — 거울 테스트 `tmp/tests/test_b06_culvert_sets_mirror.py` 로 지킨다. 매 상세 조회마다 도는 자리라 Node 왕복 대신 짝을 골랐다(CLAUDE.md 5장 「계산 자리」 ①). +관 지점 읽기 · 측점에 얹기 = `B06_Section_Engine_Culvert_Sets.py`(472 덧 나눔). 근거(지식DB): · 관은 **지반선상 매설** — 뜨게 매설은 부적합 (교본 8장, 그림 8-3·8-9) @@ -21,7 +22,6 @@ B05 배수유역도가 찍은 관 지점(`pipe_points.json` = 배관 정본)을 from __future__ import annotations -import json import logging import math import re @@ -29,9 +29,6 @@ from pathlib import Path from typing import Any from B05_Profile.B05_Profile_Structures_Schema import structure_type_map -from common_util.common_util_drainage_pipes import parse_pipe_points -from common_util.common_util_structure_catalog import catalog_row -from common_util.common_util_structure_station import EDGE_TOLERANCE_M, PIPE_MATCH_M from config.config_system import ( DRAINAGE_CACHE_DIRNAME, DRAINAGE_EDITS_DIRNAME, @@ -40,21 +37,6 @@ from config.config_system import ( 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"}) - # ⚠ 교차 참조 ① 관 위 최소 토피(m). 임도 배수관 토피 규정이 지식DB에 없어 별표2 # 교량·암거 "복토 시 흙 두께 50㎝ 이상"을 끌어왔다. B05 계획고 하향 차단 기준이다. MIN_PIPE_COVER_M = 0.5 @@ -589,112 +571,3 @@ def _box_set(options: dict[str, Any] | None) -> dict[str, Any]: def box_body_height(box: dict[str, Any]) -> float: """박스 본체 전체 높이(상판 + 내공 + 저판) — 흙막이 세트의 관경 자리(73-4).""" return round(box["top_thickness_m"] + box["inner_height_m"] + box["slab_thickness_m"], 3) - - -def load_culvert_sets(project_root: Path) -> dict[float, dict[str, Any]]: - """관 지점 정본을 읽어 누가거리별 세트 제원을 돌려준다. - - 노선 지문은 보지 않는다 — 횡단면 자체가 현 노선에서 만들어진 것이라, 지문이 - 어긋난 옛 관 지점은 매칭될 측점이 없어 자연히 걸러진다. 파일이 없거나 깨졌으면 빈 값. - """ - path = pipe_points_file(project_root) - if not path.is_file(): - return {} - try: - document = json.loads(path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError): - logger.warning("B06 배수관 세트: 관 지점 파일을 읽지 못했습니다 (%s)", path) - return {} - return culvert_sets_from(parse_pipe_points(document.get("points"))) - - -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 diff --git a/B06_Section/B06_Section_Engine_Culvert_Sets.py b/B06_Section/B06_Section_Engine_Culvert_Sets.py new file mode 100644 index 000000000..30ba9753b --- /dev/null +++ b/B06_Section/B06_Section_Engine_Culvert_Sets.py @@ -0,0 +1,146 @@ +"""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 diff --git a/B06_Section/B06_Section_Engine_Tilt.py b/B06_Section/B06_Section_Engine_Tilt.py index d0bd3b129..9e78a1546 100644 --- a/B06_Section/B06_Section_Engine_Tilt.py +++ b/B06_Section/B06_Section_Engine_Tilt.py @@ -18,7 +18,7 @@ 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 +from B06_Section.B06_Section_Engine_Culvert_Sets import load_culvert_sets from common_util.common_util_structure_catalog import catalog_row from common_util.common_util_structure_station import EDGE_TOLERANCE_M diff --git a/B06_Section/B06_Section_Router.py b/B06_Section/B06_Section_Router.py index 1c3f93d08..76195d937 100644 --- a/B06_Section/B06_Section_Router.py +++ b/B06_Section/B06_Section_Router.py @@ -261,7 +261,8 @@ def _read_section_detail( raise ValueError("종횡단 상세 파일 형식이 올바르지 않습니다.") # 측점 그림 자료(관 세트 · 벽 · 포장 · 맹암거 · 빈칸 경고) = 형상 얹기 한 벌(323-4 · 화면 짝 attachShapes) attach_saved_shapes(root, cross_sections, route_data_path) - relabel_pipe_stations(root, cross_sections) # 저장으로 종류만 바꾼 관 — 카드 제목(54-4) + # 저장으로 종류만 바꾼 관 — 카드 제목(54-4) + relabel_pipe_stations(root, cross_sections, route_data_path) return {"longitudinal": longitudinal, "cross_sections": cross_sections} diff --git a/B06_Section/B06_Section_Router_Design_Ranges.py b/B06_Section/B06_Section_Router_Design_Ranges.py index 5dd62deef..9f2d7f426 100644 --- a/B06_Section/B06_Section_Router_Design_Ranges.py +++ b/B06_Section/B06_Section_Router_Design_Ranges.py @@ -11,7 +11,7 @@ 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 import load_culvert_sets +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__) diff --git a/B07_DesignDetail/B07_DesignDetail_Engine_Cad_CulvertNote.py b/B07_DesignDetail/B07_DesignDetail_Engine_Cad_CulvertNote.py index 2406f9922..26e85673f 100644 --- a/B07_DesignDetail/B07_DesignDetail_Engine_Cad_CulvertNote.py +++ b/B07_DesignDetail/B07_DesignDetail_Engine_Cad_CulvertNote.py @@ -14,17 +14,17 @@ B06 [상세] 모달을 한 번도 안 연 측점(칸 = 없음)에 B07 이 같은 좌표 = 도면 mm(글 2.0 · 줄 3.0) · 칸 왼쪽 위 = (0, 0) · 아래로 음수. """ -import json import logging import math import re from pathlib import Path from typing import Any +from B05_Profile.B05_Profile_Engine_Sections import route_pipe_points from B05_Profile.B05_Profile_Structures_Repository import load_structures from B05_Profile.B05_Profile_Structures_Schema import load_definition, load_structure_types -from B06_Section.B06_Section_Engine_Culvert import _culvert_set, pipe_points_file -from common_util.common_util_drainage_pipes import PIPE_FACILITY_PIPE, parse_pipe_points +from B06_Section.B06_Section_Engine_Culvert import _culvert_set +from common_util.common_util_drainage_pipes import PIPE_FACILITY_PIPE from common_util.common_util_node_bundle import run_bundle_json from common_util.common_util_structure_station import PIPE_MATCH_M # 같은 자리 폭 한 곳(323-7) @@ -272,11 +272,7 @@ def with_option_lines(things: list[Thing]) -> list[Thing]: def load_note_sources(project_root: Path) -> tuple[list[Pipe], list[Thing]]: """정본 재료 — (원형 마크 배수관 · 관경 안 고름 = 없음, 글 줄 재료 = 관 지점 + 구조물 배치).""" - try: - document = json.loads(pipe_points_file(project_root).read_text(encoding="utf-8")) - points = parse_pipe_points(document.get("points")) - except (OSError, json.JSONDecodeError): - points = [] + points = route_pipe_points(project_root) # 관 = 지금 노선에 얹은 자리(472 덧) pipes: list[Pipe] = [] for point in points: options = dict(point.options or {}) diff --git a/common_util/common_util_culvert_sets.ts b/common_util/common_util_culvert_sets.ts index 65b792b79..63fcccee8 100644 --- a/common_util/common_util_culvert_sets.ts +++ b/common_util/common_util_culvert_sets.ts @@ -2,7 +2,7 @@ * common_util_culvert_sets.ts * 배수관 세트(배관·기슭막이·보호공·세월교·BOX암거·물넘이포장) 제원 — **브라우저 몫**. * - * ⚠ 파이썬 `B06_Section/B06_Section_Engine_Culvert.py` 와 **한 벌**이다(2026-09-06). + * ⚠ 파이썬 `B06_Section/B06_Section_Engine_Culvert.py` · `_Culvert_Sets.py`(읽기 · 얹기) 와 **한 벌**이다(2026-09-06). * 한쪽만 고치면 화면과 저장본이 갈린다. 거울 테스트: `resources/tester/test_b06_culvert_sets_mirror.py` * * 왜 짝으로 두나(CLAUDE.md 5장 「계산 자리」) — 이 계산은 종횡단 **상세를 읽을 때마다** diff --git a/docs/wiki/concepts/design_page/calculations.md b/docs/wiki/concepts/design_page/calculations.md index 51328fdfa..56bbef6b8 100644 --- a/docs/wiki/concepts/design_page/calculations.md +++ b/docs/wiki/concepts/design_page/calculations.md @@ -44,7 +44,7 @@ source: ["docs/raw/조사/2026-10-19_472_구조다시세움_목록.md", "PLAN.md | F1 | 시설 따라감(노선 바뀜) | `common_util_route_follow.ts:291` · `logic/B05_Profile_Pipeline_Follow.ts` · `logic/B05_Profile_Structure_Follow.ts` | `common_util_route_follow.py:120` | — | 짝 · `test_410_follow_xy` · `test_344_2_route_pairs` | | F2 | 추가 측점 따라감 | `B05_Profile_Api_PlusStations.ts` followPlusDraft | `B05_Profile_Plus_Stations.py:82` | — | 짝 · `test_459_route_seat_mirror` | | F3 | 시설 측점 계획 | `common_util_structure_station.ts` | `common_util_structure_station.py` | — | 짝 · `test_322_3_plan_mirror` | -| F4 | 형상 얹기(관 세트 · 벽 · 포장 그림 자료) — 관 = 지금 노선에 얹은 저장분(서버 `saved_pipes` = 측점 계획 · 관 지점 GET 과 같은 이월) | `common_util_structure_attach.ts:130` | `B06_Section_Engine_Attach.py:114` | — | 짝 · `test_323_4_attach_mirror` · `test_472_d_pipe_route_attach` | +| F4 | 형상 얹기(관 세트 · 벽 · 포장 그림 자료) — 관 = 지금 노선에 얹은 저장분(서버 `route_pipe_points` = 측점 계획 · 관 지점 GET 과 같은 이월) | `common_util_structure_attach.ts:130` | `B06_Section_Engine_Attach.py:114` | — | 짝 · `test_323_4_attach_mirror` · `test_472_d_pipe_route_attach` | | F5 | 관 세트(관경 · 날개벽 · 집수정) | `common_util_culvert_sets.ts` · `logic/B06_Section_UI_Cross_Culvert_Const.ts` | `B06_Section_Engine_Culvert.py` | Const = sc · co · ex | 짝 · `test_b06_culvert_sets_mirror` · `test_b06_pipe_wall_height_mirror` | | F6 | 벽 · 포장 | `common_util_structure_walls.ts` · `common_util_structure_pavement.ts` | `B06_Section_Engine_Structures_Wall.py` · `common_util_structure_pavement.py` | — | 짝 · `test_b06_structure_walls_mirror` · `test_b06_structure_pavement_mirror` | | F7 | 관 길이 · 구조물 면적 · 다단 벽 | `B06_Section_Structure_Layouts.ts:95 · 287` · `design_structure_pipe_calc.ts` · `common_util_cross_structure_areas.ts` | — | sc | ② · `test_b06_pipe_length` · `test_299_ford_span_pipe` | diff --git a/docs/wiki/concepts/design_page/rules.md b/docs/wiki/concepts/design_page/rules.md index 9fe7fc23d..b75363c00 100644 --- a/docs/wiki/concepts/design_page/rules.md +++ b/docs/wiki/concepts/design_page/rules.md @@ -32,7 +32,7 @@ source: ["CLAUDE.md §5", "docs/raw/조사/2026-10-19_465_요구272_값안변함 | B05 · B06 캐시 = 한 벌 · [저장] 한 번에 함께 나감 | `design_shared.ts:395-448` · `design_save.ts:237` | — | | 관 · 구조물 주인 = 가게 칸 하나 — 화면 사본은 가게 알림으로 맞춤(제 쓰기는 같은 값 · 남 고침은 다시 실음) | `design_view_profile.ts` 가게 구독 · `design_view_cross.ts` onShop · `B06_Section_UI_Page_Structures_Panel.ts` seen | `test_472_5_shop_owner` | | 정본 자리 = 값을 낳은 주인 — 노선 시설 = `pipe_points.json` · `structures.json` · 횡단 값 = 횡단 패치 | `B05_Profile_Api_Pipes_Draft.ts:30-37` · `B05_Profile_Structures_Router.py:386-405` · `B06_Section_Section_Store.ts:449` | — | -| 관 자리 읽기 한 벌 = 지금 노선에 얹은 관 지점(측점 계획 · 관 지점 GET · 서버 상세 형상 얹기 · 화면) — 지문 어긋난 옛 저장분도 같은 자리 | `B06_Section_Engine_Attach.py` saved_pipes · `B05_Profile_Engine_Sections.py:210` | `test_472_d_pipe_route_attach` | +| 관 자리 읽기 한 벌 = 지금 노선에 얹은 관 지점(측점 계획 · 관 지점 GET · 서버 상세 형상 얹기 · 카드 이름 · 세월교 노면 하강 · 물넘이포장 구간 · B07 관 글 · 화면) — 지문 어긋난 옛 저장분도 같은 자리 · 관 파일 누가거리를 그대로 읽지 않음 | `B05_Profile_Engine_Sections.py:249` route_pipe_points | `test_472_d_pipe_route_attach` · `test_472_d_route_pipe_readers` | ## 바꿀 때 할 일 diff --git a/docs/wiki/pages/B06_Section/B06_backend.md b/docs/wiki/pages/B06_Section/B06_backend.md index 5d6502d4c..ed79f6e38 100644 --- a/docs/wiki/pages/B06_Section/B06_backend.md +++ b/docs/wiki/pages/B06_Section/B06_backend.md @@ -27,8 +27,8 @@ source: ["docs/raw/verification/2026-07-19_verify_B05_B06_UI_Station_Improvement | `compute_cross_design()` | `B06_Section_Engine_Design.py` | **[2단계 경사 & N-2-4 연동]** 측점별 표준단면 절/성토 단면적 및 기하선 연산 엔진 (리핑암/발파암 2단계 절토 경사, 단면유형 자동 판정, `ditch_enabled` 토글) | | `_split_cut_areas()` | `[[B06_Section/backend/B06_Section_Engine_Areas]]` | **[신설/2026-08-02]** 700줄 충족을 위해 분리된 암 경계선 기준 절토면적 토사/암반 정밀 적분 연산 | | `design_elevation_from_longitudinal()` | `B06_Section_Engine_Design.py` | 종단 계획선 계획고 선형보간 계산 헬퍼 | -| `attach_culvert_sets()` | `B06_Section_Engine_Culvert.py` | 구조물 세트를 상세 횡단 dict에 결합; BOX암거·세월교는 소유 측점만 사용 | -| `_SPAN_LINKED_TYPES` | `B06_Section_Engine_Culvert.py` | 인접 측점 연동 타입 집합; 현재 비어 있으며 기슭막이는 별도 프론트 경로 사용 | +| `attach_culvert_sets()` | `B06_Section_Engine_Culvert_Sets.py` | 구조물 세트를 상세 횡단 dict에 결합; BOX암거·세월교는 소유 측점만 사용 | +| `_SPAN_LINKED_TYPES` | `B06_Section_Engine_Culvert_Sets.py` | 인접 측점 연동 타입 집합; 현재 비어 있으며 기슭막이는 별도 프론트 경로 사용 | | *종횡단 생성 엔진* | `B05_Profile_Engine_Sections.py` 등 | 기존 B06 종횡단 생성 엔진 파일 3종은 **B05 노선 설계 최적화 시점에 실시간 통합 계산**하도록 B05로 이관되었습니다. | ## 라우터·workflow (조회, 확정, 타 프로젝트 불러오기 및 재생성) diff --git a/resources/tester/diag_structures.py b/resources/tester/diag_structures.py index d1a53b9bf..9a1f8198c 100644 --- a/resources/tester/diag_structures.py +++ b/resources/tester/diag_structures.py @@ -72,7 +72,7 @@ def main() -> int: print(f"3) cross_*.json 파일={len(files)}개 구조물 표식={len(with_structure)}건 {with_structure}") # 4) B06 읽기 경로 — 실제로 세트가 붙는가 - from B06_Section.B06_Section_Engine_Culvert import load_culvert_sets + from B06_Section.B06_Section_Engine_Culvert_Sets import load_culvert_sets sets = load_culvert_sets(project) print(f"4) load_culvert_sets 세트={len(sets)}건") diff --git a/resources/tester/test_299_ford_span_pipe.py b/resources/tester/test_299_ford_span_pipe.py index e38fd3317..c9757fa07 100644 --- a/resources/tester/test_299_ford_span_pipe.py +++ b/resources/tester/test_299_ford_span_pipe.py @@ -26,7 +26,8 @@ for path in (ROOT, Path(__file__).resolve().parent): sys.path.insert(0, str(path)) import test_b06_culvert_sets_mirror as mirror # noqa: E402 -from B06_Section.B06_Section_Engine_Culvert import attach_culvert_sets, pipe_points_file # noqa: E402 +from B06_Section.B06_Section_Engine_Culvert import pipe_points_file # noqa: E402 +from B06_Section.B06_Section_Engine_Culvert_Sets import attach_culvert_sets # noqa: E402 # 검증 프로젝트 꼴 — 20 · 90 물넘이(폭 10) · 95 배관(도수로) · 100 배관 + 100.3 박스(유출 흙막이) · 60 세월교(흙막이) POINTS = [ diff --git a/resources/tester/test_322_0_golden.py b/resources/tester/test_322_0_golden.py index 545bbde74..de282db89 100644 --- a/resources/tester/test_322_0_golden.py +++ b/resources/tester/test_322_0_golden.py @@ -24,7 +24,8 @@ import pytest from B05_Profile.B05_Profile_Engine_Sections import relabel_pipe_stations, resolve_extra_stations from B05_Profile.B05_Profile_Structures_Repository import structures_file_path from B05_Profile.B05_Profile_Structures_Schema import load_definition -from B06_Section.B06_Section_Engine_Culvert import attach_culvert_sets, pipe_points_file +from B06_Section.B06_Section_Engine_Culvert import pipe_points_file +from B06_Section.B06_Section_Engine_Culvert_Sets import attach_culvert_sets from B06_Section.B06_Section_Engine_Structures_Wall import attach_wall_structures from B06_Section.B06_Section_Engine_Tilt import attach_underdrains from common_util.common_util_drainage_pipes import parse_pipe_points diff --git a/resources/tester/test_349_0n_cross_enforce_mirror.py b/resources/tester/test_349_0n_cross_enforce_mirror.py index 5834a690a..e5ee6a178 100644 --- a/resources/tester/test_349_0n_cross_enforce_mirror.py +++ b/resources/tester/test_349_0n_cross_enforce_mirror.py @@ -28,7 +28,7 @@ import B06_Section.B06_Section_Engine_Tilt as tilt_mod # noqa: E402 import B06_Section.B06_Section_Router_Design as design_mod # noqa: E402 import B06_Section.B06_Section_Router_Design_Ranges as ranges_mod # noqa: E402 from B05_Profile.B05_Profile_Structures_Schema import StructureInstance # noqa: E402 -from B06_Section.B06_Section_Engine_Culvert import culvert_sets_from # noqa: E402 +from B06_Section.B06_Section_Engine_Culvert_Sets import culvert_sets_from # noqa: E402 from B06_Section.B06_Section_Engine_Design import compute_cross_design # noqa: E402 from common_util.common_util_drainage_pipes import parse_pipe_points # noqa: E402 from config.config_system_design import STANDARD_CROSS_SECTION # noqa: E402 diff --git a/resources/tester/test_349_0p_fractional_station.py b/resources/tester/test_349_0p_fractional_station.py index 3e2edb595..7b9eaeba6 100644 --- a/resources/tester/test_349_0p_fractional_station.py +++ b/resources/tester/test_349_0p_fractional_station.py @@ -29,7 +29,7 @@ import B06_Section.B06_Section_Engine_Tilt as tilt_mod # noqa: E402 import B06_Section.B06_Section_Router_Design as design_mod # noqa: E402 import B06_Section.B06_Section_Router_Design_Ranges as ranges_mod # noqa: E402 from B05_Profile.B05_Profile_Structures_Schema import StructureInstance # noqa: E402 -from B06_Section.B06_Section_Engine_Culvert import culvert_sets_from # noqa: E402 +from B06_Section.B06_Section_Engine_Culvert_Sets import culvert_sets_from # noqa: E402 from B06_Section.B06_Section_Engine_Design import compute_cross_design # noqa: E402 from common_util.common_util_drainage_pipes import parse_pipe_points # noqa: E402 from config.config_system_design import ( # noqa: E402 diff --git a/resources/tester/test_472_d_pipe_route_attach.py b/resources/tester/test_472_d_pipe_route_attach.py index 10a16b47d..862700204 100644 --- a/resources/tester/test_472_d_pipe_route_attach.py +++ b/resources/tester/test_472_d_pipe_route_attach.py @@ -70,11 +70,14 @@ def test_상세_읽기_관_세트_자리_측점_계획과_같음(tmp_path: Path) assert owners == {351.0: 351.0} -def test_노선_모름_저장분_그대로(tmp_path: Path) -> None: +def test_노선_경로_안_줌_노선_파일_하나(tmp_path: Path) -> None: + # 472 덧(관 파일 누가거리 남은 자리) — 경로 안 줌 = 노선 파일 하나(`ROUTE_DATA_FILE`) · 옛 단언 + # 「파일 누가거리 394」는 측점 계획과 갈리던 옛 동작이라 고침 _project(tmp_path) sections = _sections() - attach_saved_shapes(tmp_path, sections) # 노선 경로 없음 = 파일 누가거리(394 · 제 측점 없음) - assert not any("culvert" in s for s in sections) + attach_saved_shapes(tmp_path, sections) + owners = {s["chainage_m"]: s["culvert"]["chainage_m"] for s in sections if "culvert" in s} + assert owners == {351.0: 351.0} def test_노선_파일_없음_저장분_그대로(tmp_path: Path) -> None: diff --git a/resources/tester/test_472_d_route_pipe_readers.py b/resources/tester/test_472_d_route_pipe_readers.py new file mode 100644 index 000000000..1cd7ff7f5 --- /dev/null +++ b/resources/tester/test_472_d_route_pipe_readers.py @@ -0,0 +1,128 @@ +"""PLAN 472 덧 (요구 272) — 관 파일 누가거리를 그대로 읽던 남은 서버 자리 = 지금 노선 관 자리. + +관 지점 정본의 노선 지문이 지금 노선과 어긋난 저장분(옛 노선 누가거리 · 좌표는 지금 노선) — +측점 계획(`_load_pipe_points`) · 관 지점 GET · 화면 · 서버 상세 얹기(`attach_saved_shapes`)는 좌표로 +얹는데 세월교 노면 하강(`ford_surface_drops`) · 물넘이포장 구간(`ford_pavement_ranges` · +`tilt_ranges`) · 카드 이름(`relabel_pipe_stations`) · B07 관 글 재료(`load_note_sources`)는 +파일 누가거리로 읽어 [저장] 재계산 · 상세 값이 화면과 갈렸음. +관 = 좌표 주인 · 파일 지문이 어긋나도 같은 값. + + ./venv/Scripts/python.exe -m pytest resources/tester/test_472_d_route_pipe_readers.py -q +""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) + +from B05_Profile.B05_Profile_Engine_Sections import relabel_pipe_stations # noqa: E402 +from B06_Section.B06_Section_Engine_Attach import attach_saved_shapes # noqa: E402 +from B06_Section.B06_Section_Engine_Tilt import tilt_ranges # noqa: E402 +from B06_Section.B06_Section_Router_Design_Ranges import ( # noqa: E402 + ford_drop_at, + ford_pavement_ranges, + ford_surface_drops, + paved_at, + pavement_ranges, +) +from B07_DesignDetail.B07_DesignDetail_Engine_Cad_CulvertNote import ( # noqa: E402 + load_note_sources, +) + +ROUTE = "B05_Profile/route/route_main.geojson" + +# (옛 노선 누가거리, 지금 노선 x) — 지금 노선 = x 축 0 → 1000 · 지금 누가거리 = x +PIPES = [ + (194.0, 151.0, "box_culvert", {}), + (394.0, 351.0, "ford_bridge", {"ford_height_m": 0.5, "ford_width_m": 10}), + (594.0, 551.0, "ford_pavement", {"ford_width_m": 10, "ford_slope_deg": 3}), + (894.0, 751.0, "pipe", {"pipe_kind": "파형강관", "pipe_diameter_mm": "1000"}), +] + + +def _project(root: Path, stale: bool) -> Path: + route = root / ROUTE + route.parent.mkdir(parents=True) + route.write_text( + json.dumps( + { + "type": "Feature", + "geometry": {"type": "LineString", "coordinates": [[0, 0, 0], [1000, 0, 0]]}, + } + ), + encoding="utf-8", + ) + edits = root / "B04_PreProcess" / "drainage" / "edits" + edits.mkdir(parents=True) + points = [ + { + "id": f"p{int(x)}", + "chainage_m": old if stale else x, + "x": x, + "y": 0.3, + "facility": facility, + "options": options, + } + for old, x, facility, options in PIPES + ] + (edits / "pipe_points.json").write_text( + json.dumps({"route_signature": "옛-노선", "points": points}), encoding="utf-8" + ) + return root + + +def _read(root: Path) -> dict: + sections = [ + {"chainage_m": 151.0, "structure": "파형강관 D900"}, + {"chainage_m": 351.0, "center_x": 351.0, "center_y": 0.0}, + {"chainage_m": 551.0, "center_x": 551.0, "center_y": 0.0}, + {"chainage_m": 751.0, "center_x": 751.0, "center_y": 0.0}, + ] + relabel_pipe_stations(root, sections) + attach_saved_shapes(root, sections) # 노선 경로 안 줌 = 노선 파일 하나 + pipes, _things = load_note_sources(root) + drops = ford_surface_drops(root) + return { + "drops": drops, + "ford_pavement": ford_pavement_ranges(root), + "tilts": [(a, b) for a, b, _ in tilt_ranges(root)], + "label_151": sections[0].get("structure"), + "ford_at_351": sections[1].get("ford", {}).get("chainage_m"), + "drop_351": ford_drop_at(351.0, drops), + "paved_551": paved_at(551.0, pavement_ranges(root)), + "note_pipes": [at for at, _spec, _options in pipes], + } + + +def test_지문_어긋난_관_파일_지금_노선_자리(tmp_path: Path) -> None: + got = _read(_project(tmp_path, stale=True)) + assert got["drops"] == [(351.0, 0.5)] + assert got["ford_pavement"] == [(546.0, 556.0)] + assert got["tilts"] == [(546.0, 556.0)] + assert got["label_151"] == "BOX암거" + assert got["ford_at_351"] == 351.0 + assert got["drop_351"] == 0.5 + assert got["paved_551"] is True + assert got["note_pipes"] == [751.0] + + +def test_지문_어긋나도_같은_값(tmp_path: Path) -> None: + stale = _read(_project(tmp_path / "옛", stale=True)) + fresh = _read(_project(tmp_path / "새", stale=False)) + assert stale == fresh + + +def test_노선_파일_없음_저장분_그대로(tmp_path: Path) -> None: + root = _project(tmp_path, stale=True) + (root / ROUTE).unlink() + assert ford_surface_drops(root) == [(394.0, 0.5)] + + +if __name__ == "__main__": + sys.exit(pytest.main([__file__, "-q"])) diff --git a/resources/tester/test_b06_box_set.py b/resources/tester/test_b06_box_set.py index ca213b8a2..7ecfc3efa 100644 --- a/resources/tester/test_b06_box_set.py +++ b/resources/tester/test_b06_box_set.py @@ -20,8 +20,8 @@ if str(ROOT) not in sys.path: from B06_Section.B06_Section_Engine_Culvert import ( # noqa: E402 BOX_COVER_M, _box_set, - attach_culvert_sets, ) +from B06_Section.B06_Section_Engine_Culvert_Sets import attach_culvert_sets # noqa: E402 def test_box_set_defaults() -> None: diff --git a/resources/tester/test_b06_culvert_set.py b/resources/tester/test_b06_culvert_set.py index fd0882acd..313f018a5 100644 --- a/resources/tester/test_b06_culvert_set.py +++ b/resources/tester/test_b06_culvert_set.py @@ -17,9 +17,9 @@ from B06_Section.B06_Section_Engine_Culvert import ( # noqa: E402 MIN_PIPE_COVER_M, REVET_FACE_SLOPE, _culvert_set, - attach_culvert_sets, pipe_points_file, ) +from B06_Section.B06_Section_Engine_Culvert_Sets import attach_culvert_sets # noqa: E402 def test_cross_reference_constants_match_plan(): diff --git a/resources/tester/test_b06_culvert_sets_mirror.py b/resources/tester/test_b06_culvert_sets_mirror.py index 7ed0cc3fb..84e8b0b14 100644 --- a/resources/tester/test_b06_culvert_sets_mirror.py +++ b/resources/tester/test_b06_culvert_sets_mirror.py @@ -19,10 +19,8 @@ if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) from B05_Profile.B05_Profile_Structures_Schema import structure_type_map # noqa: E402 -from B06_Section.B06_Section_Engine_Culvert import ( # noqa: E402 - attach_culvert_sets, - pipe_points_file, -) +from B06_Section.B06_Section_Engine_Culvert import pipe_points_file # noqa: E402 +from B06_Section.B06_Section_Engine_Culvert_Sets import attach_culvert_sets # noqa: E402 TSC = PROJECT_ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc" diff --git a/resources/tester/test_b06_ford_set.py b/resources/tester/test_b06_ford_set.py index 7d031e73f..44768f597 100644 --- a/resources/tester/test_b06_ford_set.py +++ b/resources/tester/test_b06_ford_set.py @@ -21,8 +21,8 @@ from B06_Section.B06_Section_Engine_Culvert import ( # noqa: E402 FORD_SLAB_THICKNESS_M, FORD_WALL_THICKNESS_M, _ford_set, - attach_culvert_sets, ) +from B06_Section.B06_Section_Engine_Culvert_Sets import attach_culvert_sets # noqa: E402 def test_ford_set_defaults() -> None: diff --git a/resources/tester/test_b06_pavement_and_ford.py b/resources/tester/test_b06_pavement_and_ford.py index e01503854..55c2e4ed0 100644 --- a/resources/tester/test_b06_pavement_and_ford.py +++ b/resources/tester/test_b06_pavement_and_ford.py @@ -9,11 +9,8 @@ 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_Engine_Culvert import FORD_PAVEMENT_DEFAULT_WIDTH_M +from B06_Section.B06_Section_Engine_Culvert_Sets import attach_culvert_sets, load_culvert_sets from B06_Section.B06_Section_Router_Design import paved_at, pavement_ranges diff --git a/resources/tester/test_b06_pipe_length.py b/resources/tester/test_b06_pipe_length.py index 4cab50137..3e7832c62 100644 --- a/resources/tester/test_b06_pipe_length.py +++ b/resources/tester/test_b06_pipe_length.py @@ -101,7 +101,7 @@ def test_소유_측점에만_싣는다(): layouts = _LAYOUTS.read_text(encoding="utf-8") assert "ownerChainage" in layouts, "소유 측점을 안 가리고 있다" assert "isOwner(section.culvert.chainage_m)" in layouts, "관이 놓인 자리를 안 읽고 있다" - py = (PROJECT_ROOT / "B06_Section" / "B06_Section_Engine_Culvert.py").read_text( + py = (PROJECT_ROOT / "B06_Section" / "B06_Section_Engine_Culvert_Sets.py").read_text( encoding="utf-8" ) ts = (PROJECT_ROOT / "common_util" / "common_util_culvert_sets.ts").read_text(encoding="utf-8")