diff --git a/B05_Profile/B05_Profile_Router_Replan.py b/B05_Profile/B05_Profile_Router_Replan.py index 79cf02fe..a905e9bb 100644 --- a/B05_Profile/B05_Profile_Router_Replan.py +++ b/B05_Profile/B05_Profile_Router_Replan.py @@ -30,6 +30,7 @@ import asyncio import csv +import functools import logging import time from pathlib import Path @@ -163,10 +164,23 @@ async def _min_plan_radius_m(project_id: UUID) -> float: return legal_plan_radius_min_m(resolve_design_speed(grade_class, design_speed), terrain) -def _write_planned_polyline(path: Path, points: list[tuple[float, float]], radius_m: float) -> dict: - """점 묶음을 폴리라인으로 바꿔 CSV 로 쓴다. 노드 요약을 돌려준다.""" - result = build_planned_polyline(points, min_radius_m=radius_m) +def _nodes_path(path: Path) -> Path: + """그 폴리라인을 낳은 **노드** 파일 자리 — `planned_route.csv` → `planned_route_nodes.csv`.""" + return path.with_name(f"{path.stem}_nodes.csv") + + +def _write_planned_polyline( + path: Path, points: list[tuple[float, float]], radius_m: float, *, simplify: bool = True +) -> dict: + """점 묶음을 폴리라인으로 바꿔 CSV 로 쓴다. 노드 요약을 돌려준다. + + **노드도 함께 남긴다** — 노드는 폴리라인에서 되뽑을 수 없다. 폴리라인에는 원호 위 점이 + 섞여 있어 다시 단순화하면 꺾임점이 조금씩 지워지고, 그것을 반복하면 [확인]을 누를 + 때마다 선이 깎인다(2026-09-07 실측: 정점 136 → 134 → 119). 낳은 값을 그대로 보관한다. + """ + result = build_planned_polyline(points, min_radius_m=radius_m, simplify=simplify) write_route_csv(path, [{"x": x, "y": y} for x, y in result.vertices]) + write_route_csv(_nodes_path(path), [{"x": node.x, "y": node.y} for node in result.nodes]) return { "nodes": len(result.nodes), "curves": result.curve_count, @@ -342,11 +356,24 @@ async def read_route_plan(project_id: UUID) -> dict[str, Any] | JSONResponse: initial = await asyncio.to_thread(_vertices_of, planned_route_initial_path(project_root)) planned = working or initial or expected - # 노드는 **원본 점**에서 뽑는다 — 폴리라인 정점에는 원호 위 점이 섞여 있어 편집 대상이 - # 아니다. 고친 적이 있으면 그때 보낸 노드가 곧 수정본의 씨앗이므로 같은 규칙으로 다시 냄. - node_source = expected if not working else working + # 노드는 **저장해 둔 것을 그대로** 쓴다 — 폴리라인에서 되뽑으면 안 된다. 정점에 원호 + # 위 점이 섞여 있어 다시 단순화하면 꺾임점이 지워지고, 그 결과로 만든 폴리라인을 또 + # 단순화하게 되어 [확인]마다 선이 깎인다(2026-09-07 실측: 정점 136 → 134 → 119). + saved_nodes = await asyncio.to_thread( + _vertices_of, + _nodes_path( + planned_route_working_path(project_root) + if working + else planned_route_initial_path(project_root) + ), + ) + node_source = saved_nodes or (working or expected) outline = await asyncio.to_thread( - build_planned_polyline, [(x, y) for x, y in node_source], min_radius_m=radius_m + build_planned_polyline, + [(x, y) for x, y in node_source], + min_radius_m=radius_m, + # 저장해 둔 노드면 이미 꺾임점이라 다시 뽑지 않는다. 없을 때(옛 프로젝트)만 뽑는다. + simplify=not saved_nodes, ) return { "status": "success", @@ -382,12 +409,15 @@ async def replan_route( await asyncio.to_thread(_ensure_planned_initial, project_root, radius_m) # 화면이 보낸 것은 **노드(꺾임점)** 다 — 같은 R 규칙으로 다시 폴리라인을 만든다. # 노드만 옮기면 선이 저절로 규칙을 지키는 것이 이 구조의 목적이다(2026-09-06 사용자). + # 받은 것은 이미 꺾임점이므로 **다시 뽑지 않는다** — 뽑으면 사용자가 둔 노드가 지워진다. nodes = [(vertex.x, vertex.y) for vertex in request.vertices] # 체인이 끊기면 되돌릴 수 있게 이전 수정본을 손에 쥔다 — 실패했는데 노선만 바뀌면 # 종횡단과 어긋난 채 굳고, 다시 누를수록 어긋남이 쌓인다(2026-09-06 실측). working_path = planned_route_working_path(project_root) previous = working_path.read_bytes() if working_path.is_file() else None - summary = await asyncio.to_thread(_write_planned_polyline, working_path, nodes, radius_m) + summary = await asyncio.to_thread( + functools.partial(_write_planned_polyline, simplify=False), working_path, nodes, radius_m + ) written = summary["vertices"] logger.info( "계획노선 갈아 끼움: project_id=%s 노드 %d → 정점 %d (곡선 %d · 위반 %d · R %.1fm)", diff --git a/common_util/common_util_route_polyline.py b/common_util/common_util_route_polyline.py index 292bc93a..8f0dbd11 100644 --- a/common_util/common_util_route_polyline.py +++ b/common_util/common_util_route_polyline.py @@ -236,6 +236,7 @@ def build_planned_polyline( min_radius_m: float, hairpin_min_radius_m: float = 10.0, straight_inner_angle_deg: float = STRAIGHT_INNER_ANGLE_DEG, + simplify: bool = True, ) -> PlannedPolyline: """점 묶음을 계획노선 폴리라인으로 바꾼다. @@ -244,8 +245,13 @@ def build_planned_polyline( **먼저 꺾임점을 뽑는다**(`simplify_to_nodes`) — 예상노선은 조밀한 점군이라 그대로 두면 곡선을 끼울 접선 자리가 없어 반지름이 뭉개진다(용화 실측: 점마다 끼우면 R 2~6m). + + `simplify=False` 는 **이미 꺾임점인 것을 넘길 때** 쓴다 — 사용자가 옮긴 노드가 그렇다. + ⚠ 이 갈래가 없으면 [확인]을 누를 때마다 선이 깎인다(2026-09-07 실측: 정점 136 → 134 → + 119, 노드 22 → 21). 원호 점이 섞인 폴리라인을 다시 단순화하면 꺾임점이 조금씩 지워지고, + 그 결과로 만든 폴리라인을 또 단순화하기 때문이다. **단순화는 원본 점군에서 한 번만.** """ - cleaned = simplify_to_nodes(dedupe_points(points)) + cleaned = simplify_to_nodes(dedupe_points(points)) if simplify else dedupe_points(points) if len(cleaned) < 3: nodes = [RouteNode(x=x, y=y) for x, y in cleaned] return PlannedPolyline(nodes=nodes, vertices=list(cleaned))