diff --git a/B03_FileInput/B03_FileInput_Service_Chain.py b/B03_FileInput/B03_FileInput_Service_Chain.py index 8b21e1af..bc4394ae 100644 --- a/B03_FileInput/B03_FileInput_Service_Chain.py +++ b/B03_FileInput/B03_FileInput_Service_Chain.py @@ -39,10 +39,16 @@ def _planned_route_points_in_project_crs( """ from B04_PreProcess.B04_PreProcess_Engine_Extent import project_epsg_from_prj from common_util.common_util_route_geometry import ( + densify_route, find_planned_route_file, read_planned_route_csv, trim_route_to_surface, ) + from config.config_system import ( + ROUTE_DIRECT_LINK_CELL_FACTOR, + ROUTE_GRID_RES_M, + ROUTE_PLANNED_DENSIFY_SAFETY, + ) route_file = find_planned_route_file(project_root / "B03_FileInput" / "input") planned = read_planned_route_csv(route_file) if route_file else None @@ -71,6 +77,13 @@ def _planned_route_points_in_project_crs( logger.warning("자동 설계 체인 노선 트림 건너뜀 — 지표면을 열지 못했습니다: %s", exc) else: points = trim_route_to_surface(points, sampler) + # 예정노선은 정점이 성기다(용화 평균 17.6m). 그대로 넘기면 구간마다 격자 + # 탐색을 타고 급경사 현에서 끊긴다. 문턱 아래로 간격만 좁히면 계획노선이 + # 손대지 않은 채 채택된다 — 같은 직선 위에 점을 더 찍는 것이라 형상 불변. + points = densify_route( + points, + ROUTE_DIRECT_LINK_CELL_FACTOR * ROUTE_GRID_RES_M * ROUTE_PLANNED_DENSIFY_SAFETY, + ) if len(points) < 2: logger.warning( "자동 설계 체인 중단(노선이 지표면 밖): 계획노선과 라이다 측량 범위가" diff --git a/common_util/common_util_route_geometry.py b/common_util/common_util_route_geometry.py index 5297b7e6..39511de4 100644 --- a/common_util/common_util_route_geometry.py +++ b/common_util/common_util_route_geometry.py @@ -182,6 +182,36 @@ def trim_route_to_surface( return kept +def densify_route( + points: list[tuple[float, float]], max_spacing_m: float +) -> list[tuple[float, float]]: + """원래 정점은 모두 남기고, 간격이 `max_spacing_m`을 넘는 구간에만 점을 끼워 넣는다. + + **평면 형상은 바뀌지 않는다** — 같은 직선 위에 점을 더 찍을 뿐이다. + B05 경로 탐색은 제어점 간격이 `ROUTE_DIRECT_LINK_CELL_FACTOR × ROUTE_GRID_RES_M` + 이하일 때만 격자 탐색을 건너뛰고 원좌표를 그대로 잇는다 + (`B05_Profile_Engine_Solver.py:394`). 예정노선처럼 정점이 성긴 선(용화 평균 17.6m)은 + 그 문턱을 넘어 탐색을 타고, 급경사 현에서 "통과 경로 없음"으로 끊긴다. + 간격만 좁혀 주면 계획노선이 손대지 않은 채 그대로 채택된다 (2026-09-01 사용자 확정). + """ + if len(points) < 2 or max_spacing_m <= 0: + return list(points) + dense: list[tuple[float, float]] = [points[0]] + for start, end in zip(points, points[1:]): + distance = math.dist(start, end) + steps = int(math.ceil(distance / max_spacing_m)) + for step in range(1, steps): + ratio = step / steps + dense.append( + ( + start[0] + (end[0] - start[0]) * ratio, + start[1] + (end[1] - start[1]) * ratio, + ) + ) + dense.append(end) + return dense + + def _trim_ends( points: list[tuple[float, float]], head_m: float, tail_m: float ) -> list[tuple[float, float]]: diff --git a/config/config_system.py b/config/config_system.py index 67750843..8a142020 100644 --- a/config/config_system.py +++ b/config/config_system.py @@ -285,6 +285,9 @@ ROUTE_REQUIRED_POINT_TOLERANCE_M = float(os.getenv("ROUTE_REQUIRED_POINT_TOLERAN # 제어점 쌍이 이 배수×비용면 셀보다 가까우면 격자 탐색 없이 직결한다 — 원청 계획노선 # 보존 (2026-08-30 사용자 확정. 조밀 기준선은 격자 중간점이 못 끼어들어 평면 불변). ROUTE_DIRECT_LINK_CELL_FACTOR = float(os.getenv("ROUTE_DIRECT_LINK_CELL_FACTOR", "2.0")) +# 예정노선을 자동 체인에 넘기기 전, 직결 문턱의 이 비율까지 정점 간격을 좁힌다. +# 문턱과 정확히 같게 두면 부동소수 오차 한 번에 탐색으로 넘어가 노선이 바뀐다. +ROUTE_PLANNED_DENSIFY_SAFETY = float(os.getenv("ROUTE_PLANNED_DENSIFY_SAFETY", "0.9")) # 임도 종류 — 현행 규칙(별표2)의 3종. `branch`(지선)는 규칙에서 폐지됐으나 기존 # 저장분이 남아 있어 값으로는 계속 받는다(화면 선택지에서는 뺀다, 2026-08-19). ROUTE_GRADE_CLASSES = ("trunk", "fire", "work", "branch")