diff --git a/B03_FileInput/B03_FileInput_Service_Chain.py b/B03_FileInput/B03_FileInput_Service_Chain.py index e1fb177d..55e24128 100644 --- a/B03_FileInput/B03_FileInput_Service_Chain.py +++ b/B03_FileInput/B03_FileInput_Service_Chain.py @@ -263,7 +263,7 @@ async def run_auto_design_chain( # [초기화]가 재계산으로 얼버무리지 않게 한다(2026-09-02 사용자 확정). try: async with pool.acquire() as connection: - await save_initial_snapshot(connection, project_root, route_id) + await save_initial_snapshot(connection, project_root, route_id, points) except Exception as exc: # noqa: BLE001 — 스냅샷 실패가 체인을 막지는 않는다 logger.exception("초기값 스냅샷 실패: project_id=%s", project_id) mark_design_failed(project_root, f"초기값 스냅샷 저장 실패: {exc}") diff --git a/B04_PreProcess/B04_PreProcess_Engine_SheetSurface.py b/B04_PreProcess/B04_PreProcess_Engine_SheetSurface.py index 6e3d1c3d..566c2c33 100644 --- a/B04_PreProcess/B04_PreProcess_Engine_SheetSurface.py +++ b/B04_PreProcess/B04_PreProcess_Engine_SheetSurface.py @@ -510,7 +510,8 @@ def run_sheet_surface_analysis( 반환 형식은 `run_surface_analysis()`와 같다(save_surface_analysis_to_db 호환). """ - from common_util.common_util_route_geometry import read_planned_route + from B04_PreProcess.B04_PreProcess_Engine_Extent import project_epsg_from_prj + from common_util.common_util_route_geometry import load_design_route def _report(percent: int, stage: str, message: str) -> None: if on_progress is not None: @@ -522,10 +523,14 @@ def run_sheet_surface_analysis( processed_dir.mkdir(parents=True, exist_ok=True) models_dir.mkdir(parents=True, exist_ok=True) - planned = read_planned_route(route_csv_path) + # 노선을 **사업지(.prj) 좌표계로 옮긴 뒤** 도엽을 뜬다. 원본 좌표 그대로 뜨면 노선이 + # 5179, 지표면이 5176처럼 갈려 설계 계통(`load_design_route`)이 재투영한 노선이 지표면 + # 밖으로 나가고 트림이 노선을 통째로 지운다(2026-09-03 실측: shapefile 노선 + LAS 없는 + # 설계). LAS 경로·`build_sheet_surface_from_route`와 같은 창구를 쓰는 것이 요지다. + planned = load_design_route(project_root) if planned is None or len(planned.vertices) < 2: raise ValueError(f"계획 노선 파일을 읽지 못했습니다: {route_csv_path.name}") - epsg = planned.epsg or 5186 + crs = planned.crs_input or project_epsg_from_prj(project_root) route_xy = np.array([(v.x, v.y) for v in planned.vertices], dtype=np.float64) bounds_dict = { @@ -545,16 +550,15 @@ def run_sheet_surface_analysis( project_root, processed_dir, bounds_dict, - route_csv_path.parent, + # 노선 세트 폴더의 PRJ는 노선 좌표계다 — 지형 PRJ를 고르게 지형 폴더를 준다. + project_root / "B03_FileInput" / "input" / "prj", rebuild=False, - default_epsg=f"EPSG:{epsg}", + default_epsg=crs, report=_report, ) _report(70, "surface_model", "도엽등고선 3D 서피스 생성 중") - models = build_sheet_surface_model( - project_root, processed_dir, models_dir, route_xy, f"EPSG:{epsg}" - ) + models = build_sheet_surface_model(project_root, processed_dir, models_dir, route_xy, crs) if not models: raise ValueError("도엽등고선으로 지표면을 만들지 못했습니다 — 도엽 확보를 확인하세요.") diff --git a/common_util/common_util_initial_snapshot.py b/common_util/common_util_initial_snapshot.py index e9a56e5c..90eadb9c 100644 --- a/common_util/common_util_initial_snapshot.py +++ b/common_util/common_util_initial_snapshot.py @@ -9,6 +9,7 @@ CLAUDE.md 5장(조작·데이터 흐름 정책)의 **초기값** 층이다. 자 `pipe_points.json` 편집분이 그대로 남아 초기값과 다른 결과가 나온다(2026-08-29). """ +import csv import json import shutil from pathlib import Path @@ -19,6 +20,10 @@ import aiomysql # 스냅샷 폴더는 워크플로우 단계가 아니므로 PROJECT_STORAGE_LAYOUT_V2에 넣지 않는다. SNAPSHOT_DIRNAME = "initial_snapshot" _DB_DUMP_NAME = "db.json" +# 설계가 쓰는 계획노선 정본 — shapefile로 온 노선도 여기서는 CSV 한 벌이다. +# 사업지(.prj) 좌표계로 옮기고 지표면 밖을 잘라 조밀화까지 끝낸 값이라, 설계 계통은 +# 이 파일만 읽으면 매번 같은 노선을 본다(2026-09-03 사용자 확정). +DESIGN_ROUTE_CSV_NAME = "planned_route.csv" # 초기 설계 체인이 도는 동안만 존재하는 마커(진입 차단 판정용). DESIGNING_LOCK_NAME = "initial_design.lock" # 초기 설계 체인이 실패로 끝났음을 남기는 마커. @@ -40,6 +45,11 @@ def snapshot_dir(project_root: Path) -> Path: return Path(project_root) / SNAPSHOT_DIRNAME +def design_route_csv_path(project_root: Path) -> Path: + """설계용 계획노선 CSV 정본의 자리.""" + return snapshot_dir(Path(project_root)) / DESIGN_ROUTE_CSV_NAME + + def designing_lock_path(project_root: Path) -> Path: """초기 설계 체인이 도는 동안만 존재하는 마커. @@ -158,14 +168,24 @@ def _is_json_native(value: Any) -> bool: async def save_initial_snapshot( - connection: aiomysql.Connection, project_root: Path, route_id: int + connection: aiomysql.Connection, + project_root: Path, + route_id: int, + design_route_points: list[dict[str, float]] | None = None, ) -> None: - """자동설계 체인 성공 직후 한 번 부른다. 이미 있으면 덮어쓰지 않는다.""" + """자동설계 체인 성공 직후 한 번 부른다. 이미 있으면 덮어쓰지 않는다. + + `design_route_points`는 체인이 이미 만들어 둔 계획노선 정점(사업지 좌표계·트림·조밀화 + 후)이다. 받으면 CSV 정본으로 함께 남긴다 — 노선이 shapefile로 왔더라도 설계 계통이 + 읽는 것은 이 CSV 한 벌이다. + """ root = Path(project_root) target = snapshot_dir(root) if has_initial_snapshot(root): return target.mkdir(parents=True, exist_ok=True) + if design_route_points: + _write_design_route_csv(target / DESIGN_ROUTE_CSV_NAME, design_route_points) for tree in _FILE_TREES: _copy_tree(root / tree, target / tree.replace("/", "__")) @@ -181,6 +201,17 @@ async def save_initial_snapshot( (target / _DB_DUMP_NAME).write_text(json.dumps(dump, ensure_ascii=False), encoding="utf-8") +def _write_design_route_csv(path: Path, points: list[dict[str, float]]) -> None: + """계획노선 정점을 CSV로 적는다. 열 이름은 `read_planned_route_csv()`가 아는 것으로.""" + with path.open("w", encoding="utf-8", newline="") as file: + writer = csv.writer(file) + writer.writerow(("sequence", "x", "y")) + writer.writerows( + (index, round(point["x"], 4), round(point["y"], 4)) + for index, point in enumerate(points) + ) + + def wipe_edited_masters(project_root: Path) -> list[str]: """사용자 편집 정본을 걷어낸다 — 재계산으로 **진짜 초기값**을 만들기 위한 사전 정리. diff --git a/common_util/common_util_route_geometry.py b/common_util/common_util_route_geometry.py index 3dfd7032..eb02f0d2 100644 --- a/common_util/common_util_route_geometry.py +++ b/common_util/common_util_route_geometry.py @@ -227,12 +227,29 @@ def load_design_route( 주지 않으면 읽어서 좌표계만 맞춘 원본을 돌려준다. """ from B04_PreProcess.B04_PreProcess_Engine_Extent import project_epsg_from_prj + from common_util.common_util_initial_snapshot import design_route_csv_path from config.config_system import ( ROUTE_DIRECT_LINK_CELL_FACTOR, ROUTE_GRID_RES_M, ROUTE_PLANNED_DENSIFY_SAFETY, ) + # 체인이 남긴 CSV 정본이 있으면 그것이 설계 노선이다 — 좌표계 변환·트림·조밀화가 이미 + # 끝난 값이라 다시 하지 않는다(노선이 shapefile로 왔어도 여기서는 CSV 한 벌이다). + # 지표면·노선이 바뀌면 `discard_initial_snapshot()`이 폴더째 지우므로 이 경로가 저절로 + # 닫히고 원본 재판독으로 되돌아간다. 트림 **전** 원본이 필요한 호출(도엽 범위 — + # surface_params 없음)은 여기를 타지 않는다. + if surface_params: + master = design_route_csv_path(project_root) + if master.is_file(): + stored = read_planned_route_csv(master) + if stored is not None and len(stored.vertices) >= 2: + return replace_vertices( + stored, + [(v.x, v.y) for v in stored.vertices], + crs_input=project_epsg_from_prj(project_root), + ) + route_file = find_planned_route_file(project_root / "B03_FileInput" / "input") planned = read_planned_route(route_file) if route_file else None if planned is None or len(planned.vertices) < 2: