From 28e4b0eaf3b8428bfd03cc0b830c65b6929e9f65 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 18 Oct 2026 10:32:36 +0900 Subject: [PATCH] =?UTF-8?q?fix(B05=20=EB=85=B8=EC=84=A0=20=EC=B6=94?= =?UTF-8?q?=EC=B2=9C=20=EC=B8=A1=EC=A0=90=20=EB=A7=9E=EC=B6=A4):=20?= =?UTF-8?q?=ED=95=A9=EC=B9=9C=20=EA=BC=B4=20=EB=A8=BC=EC=A0=80=20=ED=8C=90?= =?UTF-8?q?=EC=97=90=20=EC=B8=A1=EC=A0=90=20=EB=B0=96=20=EC=83=9D=EA=B8=B0?= =?UTF-8?q?=EB=A9=B4=20=EC=98=9B=20=EC=B0=A8=EB=A1=80=20=ED=8C=90=EA=B3=BC?= =?UTF-8?q?=20=EA=B2=AC=EC=A4=8C=20=C2=B7=20=EC=B6=94=EC=B2=9C=20=EC=B0=BD?= =?UTF-8?q?=20=EC=84=A4=EB=AA=85=20=C2=B7=20ALGO=5FVERSION=207=20(406-4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 측점 맞춤 — 같은 쪽 합친 꼴부터 놓다 뒤 곡선이 측점을 잃으면 옛 차례(그대로 → 합침) 판도 맞춰 측점 밖 적은 쪽 · 같으면 곡선 적은 쪽 - 검증_sub4 다섯 갈래 측점 밖 ≤ v5(weighted 5 → 1) - 추천 창 「측점 맞춤」 설명 — 6m 한도 문구를 지금 규칙(같은 쪽 합침 · 거리 한도 없음 · 복도 · 계류 · 기울기 · 토공)으로 - 시험 test_406_1 — 측점에 놓을 수 없는 쌍 안 합침 · 언덕 세 갈래 다른 선 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL --- ...B05_Profile_Engine_RouteInitial_Station.py | 27 +++++++++--- .../B05_Profile_Engine_RouteInitial_Store.py | 4 +- ...B05_Profile_UI_RouteEdit_Recommend_Help.ts | 2 +- resources/tester/test_406_1_route_merge.py | 41 +++++++++++++++++++ 4 files changed, 66 insertions(+), 8 deletions(-) diff --git a/B05_Profile/B05_Profile_Engine_RouteInitial_Station.py b/B05_Profile/B05_Profile_Engine_RouteInitial_Station.py index f309673f6..94a4a7cc8 100644 --- a/B05_Profile/B05_Profile_Engine_RouteInitial_Station.py +++ b/B05_Profile/B05_Profile_Engine_RouteInitial_Station.py @@ -26,6 +26,7 @@ R = 일반측점이 기본 제약(383-1 · 요구 241-13) — 추천 후보마 · 잼을 지나는 놓기가 없으면 덜 나쁜 놓기(복도 안 · 더 넘는 기울기 `SOFT_SLACKS` 단계까지) — MC 를 다 측점에 놓는 첫 단계 판. · 그래도 못 놓으면 더 안 넘기는 놓기만 쓰고 그 곡선은 그 자리(측점 밖 · 기울기 먼저). + · 합친 꼴 먼저 판에 측점 밖이 생기면 옛 차례(그대로 → 합침) 판도 맞춰 측점 밖이 적은 쪽(406-4). 예상노선 추적 · 새 초기 노선은 안 줌(이탈 한도 그대로). 고른 값은 편집 갈래(`simplify=False` · 곡선 켬 · `radii`)로 다시 짠다 — 화면이 곡선을 교각점 @@ -317,6 +318,7 @@ class _Snapper: self.guard = guard self.keep = False self.slack = math.inf # 못 지나는 놓기가 더 넘겨도 되는 기울기(`SOFT_SLACKS`) + self.merged_first = guard is not None # 같은 쪽으로 합친 꼴부터 놓아 봄(406) def _steeper(self, local: list[Xy]) -> bool: """맞춘 조각이 원래 선 같은 자리보다 상한을 더 넘나(직접 잼이면 그 잼 전부).""" @@ -418,15 +420,16 @@ class _Snapper: break variants.append(trial) if self.guard is not None: - # 직접 잼(406) — 같은 쪽으로 가장 많이 합친 꼴부터(S자 뺌 · 남은 꼴은 옛 차례) · 잼 - # 지나는 놓기를 모두 본 뒤 덜 나쁜 놓기(MC 는 늘 측점에) + # 직접 잼(406) — 같은 쪽으로 가장 많이 합친 꼴부터(`merged_first` · S자 뺌 · 남은 꼴은 + # 옛 차례) · 잼 지나는 놓기를 모두 본 뒤 덜 나쁜 놓기(MC 는 늘 측점에) same = 1 while same < len(variants): p = variants[same - 1].points if _turn_sign(*p[k - 1 : k + 2]) != _turn_sign(*p[k : k + 3]): break same += 1 - variants = variants[:same][::-1] + variants[same:] + if self.merged_first: + variants = variants[:same][::-1] + variants[same:] for soft in (False, True) if self.guard is not None else (False,): for trial in variants: yield from self._placements(state, trial, soft) @@ -567,9 +570,23 @@ def snap_curves_to_stations( """ if not outline.curves: return outline - snapper = _Snapper(outline, min_radius_m, max_gap_m, excess, guard) + snapped = _snap(_Snapper(outline, min_radius_m, max_gap_m, excess, guard), min_radius_m) + if guard is not None and (snapped is None or off_station_count(snapped) > 0): + # 합친 꼴부터 놓다 뒤 곡선이 측점을 잃으면 옛 차례(그대로 → 합침)로도 맞춰 측점 밖이 적은 + # 쪽 · 같으면 곡선이 적은 쪽(406-4 · 합친 곡선 때문에 측점 밖이 늘지 않게) + plain = _Snapper(outline, min_radius_m, max_gap_m, excess, guard) + plain.merged_first = False + found = [line for line in (snapped, _snap(plain, min_radius_m)) if line is not None] + snapped = min( + found, key=lambda line: (off_station_count(line), len(line.curves)), default=None + ) + return snapped + + +def _snap(snapper: _Snapper, min_radius_m: float) -> PlannedPolyline | None: + """다 놓는 길(직접 잼이면 `SOFT_SLACKS` 단계) → 부분 맞춤 차례로 맞춘 선 · 못 하면 None.""" state = None - for slack in SOFT_SLACKS if guard is not None else (math.inf,): + for slack in SOFT_SLACKS if snapper.guard is not None else (math.inf,): snapper.slack = slack state = state or snapper.run(keep=False) snapper.slack = 0.0 diff --git a/B05_Profile/B05_Profile_Engine_RouteInitial_Store.py b/B05_Profile/B05_Profile_Engine_RouteInitial_Store.py index a996cc4cb..49767ab1d 100644 --- a/B05_Profile/B05_Profile_Engine_RouteInitial_Store.py +++ b/B05_Profile/B05_Profile_Engine_RouteInitial_Store.py @@ -33,8 +33,8 @@ from B05_Profile.B05_Profile_Engine_RouteInitial import ( from common_util.common_util_json import atomic_write_json # 3 = 곡선 MC 일반측점(351-2) · 4 = 다투는 곡선 끔(364-5) · 5 = 후보마다 맞춤(383-1) · -# 6 = 같은 쪽 곡선 합치기 · 거리 한도 대신 직접 잼(406) -ALGO_VERSION = 6 +# 6 = 같은 쪽 곡선 합치기 · 거리 한도 대신 직접 잼(406) · 7 = 측점 밖이 늘면 옛 차례 맞춤(406-4) +ALGO_VERSION = 7 STORE_DIR = "B05_Profile/route_recommend" # 종류(갈래 · 지형) 마다 남길 벌 수 — 꺼낼 때 수정시각을 새로 해 최근에 쓴 것이 남음 KEEP_PER_KIND = 2 diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Recommend_Help.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Recommend_Help.ts index fe01727b7..dfcdc918c 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Recommend_Help.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Recommend_Help.ts @@ -103,7 +103,7 @@ const CRITERIA_HELP: Array<{ name: string; text: string }> = [ }, { name: "측점 맞춤", - text: "켬(기본) = 곡선 가운데(MC)를 일반측점(20m) 위로 옮긴 선 — 교각점을 6m 안에서 밀고 반지름을 맞추며, 측점이 모자라면 곡선을 합침. 못 옮긴 곡선은 그 자리(「측점 밖 곡선 N개」). 비교값은 옮긴 선으로 다시 셈. 끔 = 옮기기 전 선. [적용]은 보이는 쪽", + text: "켬(기본) = 곡선 가운데(MC)를 일반측점(20m) 위로 옮긴 선 — 교각점을 밀고 반지름을 맞추며, 같은 쪽으로 도는 이웃 곡선은 큰 곡선 하나로 합침(S자 사이 직선은 남김). 미는 거리 한도는 없고 탐색 복도 · 계류 이격 · 기울기 · 토공이 나빠지지 않는 자리만(예상노선 추적은 6m 안). 못 옮긴 곡선은 그 자리(「측점 밖 곡선 N개」). 비교값은 옮긴 선으로 다시 셈. 끔 = 옮기기 전 선. [적용]은 보이는 쪽", }, ]; diff --git a/resources/tester/test_406_1_route_merge.py b/resources/tester/test_406_1_route_merge.py index 742ea0bc7..35a1145b9 100644 --- a/resources/tester/test_406_1_route_merge.py +++ b/resources/tester/test_406_1_route_merge.py @@ -5,11 +5,13 @@ · 같은 쪽으로 도는 이웃 곡선 둘 → 바깥 두 직선 교점 하나(곡선 하나) · S자는 안 합침. · 막힘 — 지반 기울기(골짜기) · 복도 밖(지형 없음) · 계류 버퍼 안 나란히 · 노선 전체 잣대. · 큰 R(`widen`) 직접 잼 = 평지면 이탈 한도(4 m) 판보다 큼. +· 406-4 — 측점에 놓을 수 없는 쌍은 처음부터 안 합침 · 언덕에서 세 갈래가 다른 선. """ import numpy as np from shapely.geometry import LineString, Point +from B05_Profile.B05_Profile_Engine_RouteInitial import generate_initial_route from B05_Profile.B05_Profile_Engine_RouteInitial_Merge import ( GRADE_NOISE, Guard, @@ -122,3 +124,42 @@ def test_widen_guard_larger_than_gap_limit() -> None: gap = widen(fitted, path, RADIUS) direct = widen(fitted, path, RADIUS, guard=_guard(path)) assert direct.curves[0].radius_m > gap.curves[0].radius_m + 20.0 + + +def test_406_4_unplaceable_pair_stays() -> None: + """합친 곡선 자리만 지형이 있음(그 둘레 3 m) — 가운데 꼴은 지나도 IP 를 밀어 측점에 놓은 꼴이 + 복도 밖이면 처음부터 안 합침(합친 뒤 푸는 것 아님).""" + merged = _merge(SAME, _guard(SAME)) + keep = [LineString(merged.vertices), LineString(_outline(SAME).vertices)] + near = np.vectorize(lambda px, py: min(line.distance(Point(px, py)) for line in keep)) + guard = _guard(SAME, valid=lambda xx, yy: near(xx, yy) <= 3.0) + cross = _line_intersection(*SAME) + piece = guard.piece(SAME[0], cross, SAME[-1], merged.curves[0].radius_m) + assert guard._look(piece)[3] # 가운데 꼴은 복도 안 + assert len(_merge(SAME, guard).curves) == 2 + + +def test_406_4_branches_differ_on_hill() -> None: + """제약을 지키는 길이 있는 언덕 — 영선 · 토공 최소 · 기울기 최소가 다른 선(후보 거름 뒤에도).""" + x = np.arange(0.0, 802.0, 2.0) + y = np.arange(0.0, 402.0, 2.0) + xx, yy = np.meshgrid(x, y) + hill = Terrain( + x, + y, + 40.0 * np.exp(-((xx - 400.0) ** 2 + (yy - 200.0) ** 2) / 90.0**2), + np.ones(xx.shape, dtype=bool), + ) + line = [(float(v), 200.0) for v in np.arange(50.0, 752.0, 4.0)] + lines = { + mode: LineString( + generate_initial_route( + line, hill, [], mode=mode, weights=None, max_grade=GRADE, min_radius_m=RADIUS + )["planned"] + ) + for mode in ("gradeline", "earthwork", "gentle") + } + names = list(lines) + for i, a in enumerate(names): + for b in names[i + 1 :]: + assert lines[a].hausdorff_distance(lines[b]) > 10.0, (a, b)