Merge remote-tracking branch 'origin/main_laptop_1' into sub_laptop_1

This commit is contained in:
2026-09-07 05:21:01 +09:00
3 changed files with 54 additions and 9 deletions
@@ -267,6 +267,15 @@ class ElevationSampler:
if missing.any():
_, indices = self._tree.query(xy[missing])
values[missing] = self._z[indices]
# 볼록껍질 밖으로 빠진 점은 최근접 정점 값이라 **등고선 간격(5m)만큼 틀릴 수**
# 있고, 그러면 그 조각의 상·하류 판정이 갈린다. 구름을 좁힌 뒤로는 「둘레
# 100m 안에 등고선이 없는 자리」가 그 원인이 되므로 조용히 넘기지 않고 남긴다
# (2026-09-07 보조 창 제안 — 다른 현장에서 갈리는 것을 잡는 값싼 그물).
logger.warning(
"배수유역: 표고를 TIN 밖에서 읽음 — %d/%d점은 최근접 등고선 정점 값",
int(missing.sum()),
int(missing.size),
)
return values
+38 -8
View File
@@ -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)",
+7 -1
View File
@@ -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))