feat(B05 노선 추천 측점 맞춤): 거리 한도 걷어 내고 직접 잼 · 합친 꼴부터 놓기 · ALGO_VERSION 6 (406-2)
- 큰 R(widen) · 측점 맞춤(Station) — 추천 후보는 이탈 한도(4 m · 6 m) 대신 Guard 직접 잼 · 예상노선 추적 · 새 초기 노선은 그대로 - 측점 맞춤 — 같은 쪽으로 가장 많이 합친 꼴부터 · 자리 = 기울기 → 가까운 측점 → 토공 가늠 나은 곳 · 잼 못 지나면 덜 나쁜 놓기 0 → 1 → 3 %p 단계 · 그래도 못 놓으면 기울기 먼저(그 자리) - Pool._trace 큰 R 판 뒤 합치기 → 측점 맞춤 · 엔진 grade_excess 걷음 - 검증_sub4 v5 대비: gradeline · earthwork · gentle 측점 밖 0 · 기울기 위반 5/4/4 → 1 · 같은 쪽 짧은 직선 15/17/14 → 8 · 절성토 ±1 % · weighted 곡선 35 → 26 · 측점 밖 1 → 5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL
This commit is contained in:
@@ -56,16 +56,8 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
from scipy.interpolate import RegularGridInterpolator
|
||||
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial_Metrics import (
|
||||
GRADE_SMOOTH_M,
|
||||
SAMPLE_M,
|
||||
STATION_M,
|
||||
_resample,
|
||||
route_metrics,
|
||||
smoothed_ground,
|
||||
)
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial_Metrics import GRADE_SMOOTH_M, route_metrics
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial_Pool import (
|
||||
Pool,
|
||||
build_pool,
|
||||
@@ -328,30 +320,10 @@ def generate_initial_route(
|
||||
target_grade=goal_grade,
|
||||
)
|
||||
|
||||
smooth_z = RegularGridInterpolator(
|
||||
(terrain.y, terrain.x),
|
||||
smoothed_ground(terrain, grade_smooth),
|
||||
bounds_error=False,
|
||||
fill_value=None,
|
||||
)
|
||||
|
||||
def grade_excess(points: list[tuple[float, float]]) -> float:
|
||||
# 측점 맞춤(351-2)이 옮긴 조각 — 20 m 사이 고른 지반 기울기(비교값과 같은 잣대)가 상한을
|
||||
# 넘은 만큼. 20 m 보다 짧으면 양 끝 사이.
|
||||
xy, chain = _resample(points, SAMPLE_M)
|
||||
z = smooth_z(xy[:, ::-1])
|
||||
span = min(int(round(STATION_M / SAMPLE_M)), len(z) - 1)
|
||||
if span < 1:
|
||||
return 0.0
|
||||
run = np.maximum(chain[span:] - chain[:-span], 1e-9)
|
||||
return max(0.0, float((np.abs(z[span:] - z[:-span]) / run).max()) - max_grade)
|
||||
|
||||
def fit(outline: Any, follow: bool = False) -> Any:
|
||||
# R = 일반측점(383-1) — 갈래는 옮긴 조각이 원래보다 더 가파르면 그 곡선만 그 자리(추적은
|
||||
# 제약을 안 따짐 · 새 초기 노선 `snapped_initial` 과 같은 잣대).
|
||||
moved = snap_curves_to_stations(
|
||||
outline, min_radius_m, excess=None if follow else grade_excess
|
||||
)
|
||||
def fit(outline: Any, guard: Any = None) -> Any:
|
||||
# R = 일반측점(383-1) — 갈래 후보는 직접 잼(`guard` · 406 · 거리 한도 없음) · 추적은 제약을
|
||||
# 안 따짐(새 초기 노선 `snapped_initial` 과 같은 잣대 · 거리 한도 그대로).
|
||||
moved = snap_curves_to_stations(outline, min_radius_m, guard=guard)
|
||||
return outline if moved is None else moved
|
||||
|
||||
unsnapped: dict[int, Any] = {}
|
||||
@@ -361,9 +333,7 @@ def generate_initial_route(
|
||||
# 규칙 · 같은 입력 → 같은 선. 제약은 따지지 않고 넘은 곳만 목록에 뜸.
|
||||
outline = build_planned_polyline(expected, min_radius_m=min_radius_m)
|
||||
route = (outline, measure(outline, bound=None, follow=True))
|
||||
return settle(
|
||||
route, lambda line: fit(line, True), measure, unsnapped, bound=None, follow=True
|
||||
)
|
||||
return settle(route, fit, measure, unsnapped, bound=None, follow=True)
|
||||
|
||||
def profiled(route: Any, exact: bool = True) -> None:
|
||||
if route is not None:
|
||||
|
||||
@@ -33,6 +33,7 @@ from typing import Any
|
||||
import numpy as np
|
||||
from shapely.geometry import LineString
|
||||
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial_Merge import Guard, merge_same_side
|
||||
from B05_Profile.B05_Profile_Engine_RouteInitial_Metrics import (
|
||||
COST_CROSSING,
|
||||
COST_EARTH_M3,
|
||||
@@ -80,8 +81,8 @@ TRACE_DENSIFY_M = 2.0
|
||||
|
||||
Measure = Callable[..., dict[str, Any]]
|
||||
Route = tuple[Any, dict[str, Any]]
|
||||
# 선 → 곡선 MC 를 일반측점에 맞춘 선(못 맞추면 그 선 그대로)
|
||||
Snap = Callable[[Any], Any]
|
||||
# (선, 직접 잼) → 곡선 MC 를 일반측점에 맞춘 선(못 맞추면 그 선 그대로)
|
||||
Snap = Callable[[Any, Any], Any]
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -105,6 +106,8 @@ class Pool:
|
||||
follow: Route | None = None
|
||||
# 제약 안에서 갈 수 있는 띠 폭 어림 — 후보끼리 가장 먼 거리(m)
|
||||
band_width_m: float = 0.0
|
||||
# 큰 R · 합치기 · 측점 맞춤이 거리 한도 대신 쓰는 직접 잼(406 · `..._Merge.Guard`)
|
||||
guard: Guard | None = None
|
||||
|
||||
|
||||
def settle(route: Route, snap: Snap, measure: Measure, raw: dict[int, Route], **kw: Any) -> Route:
|
||||
@@ -125,8 +128,13 @@ def _trace(
|
||||
# 경로를 예정노선 규칙으로 단순화 · 묶음 · R 맞춤) 차례로, 위반이 최소 R 판(노드마다 곡선 ·
|
||||
# 옛 규칙)보다 많지 않은 첫 판. 모두 화면이 [확인] 때 같은 선으로 다시 짜는 꼴.
|
||||
# 같은 노드 · 같은 잼이면 다시 재지 않음(쓸기는 이웃 비중이 같은 길을 자주 냄 · 묶음마다 따로).
|
||||
# 고른 판을 측점에 맞춤(`settle` · 383-1).
|
||||
# 고른 판의 같은 쪽 이웃 곡선을 합치고(406 · `..._Merge`) 측점에 맞춤(`settle` · 383-1).
|
||||
# 큰 R · 합치기 · 측점 맞춤 모두 거리 한도 대신 직접 잼(`pool.guard`) · 비중 계산은 예상노선
|
||||
# 이격도(그 갈래 잣대 — 예상 몫을 줄이면 이격이 늘기만).
|
||||
traced = pool.traced
|
||||
guard = pool.guard
|
||||
if guard is not None and kw.get("weighted"):
|
||||
guard = guard.keeping_offset()
|
||||
key = (tuple(nodes), tuple(sorted(kw.items())))
|
||||
if key not in traced:
|
||||
floor = build_planned_polyline(
|
||||
@@ -135,7 +143,11 @@ def _trace(
|
||||
least = (floor, measure(floor, **kw))
|
||||
path = densify_route(nodes, TRACE_DENSIFY_M)
|
||||
fitted = build_planned_polyline(path, min_radius_m=min_radius_m)
|
||||
larger = (widen(fitted, path, min_radius_m), widen(floor, path, min_radius_m), fitted)
|
||||
larger = (
|
||||
widen(fitted, path, min_radius_m, guard=guard),
|
||||
widen(floor, path, min_radius_m, guard=guard),
|
||||
fitted,
|
||||
)
|
||||
chosen = next(
|
||||
(
|
||||
route
|
||||
@@ -144,7 +156,9 @@ def _trace(
|
||||
),
|
||||
least,
|
||||
)
|
||||
traced[key] = settle(chosen, pool.snap, measure, pool.raw, **kw)
|
||||
if guard is not None:
|
||||
chosen = merge_same_side(chosen, guard, min_radius_m, lambda line: measure(line, **kw))
|
||||
traced[key] = settle(chosen, lambda line: pool.snap(line, guard), measure, pool.raw, **kw)
|
||||
return traced[key]
|
||||
|
||||
|
||||
@@ -224,7 +238,7 @@ def build_pool(
|
||||
snap: Snap,
|
||||
) -> Pool:
|
||||
"""후보 묶음 — 제약 탐색 후보 → 끝 노선 셋으로 비중 크기 맞춤 → 토공 ↔ 기울기 쓸기를 더함."""
|
||||
pool = Pool(graph, snap, follow=follow)
|
||||
pool = Pool(graph, snap, follow=follow, guard=Guard(graph.grid, max_grade))
|
||||
pool.candidates, pool.bound, pool.limit = _candidates(pool, max_grade, min_radius_m, measure)
|
||||
candidates, bound, limit = pool.candidates, pool.bound, pool.limit
|
||||
ends = [follow[0], pick("earthwork", candidates)[0], pick("gentle", candidates)[0]]
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
[맞춘 R, 접선 자리 ÷ tan(IA/2)] 안에서 탐색 경로 점과 가장 먼 거리 ≤ `ROUTE_RECOMMEND_MAX_GAP_M`
|
||||
인 가장 큰 값(1 m 내림)으로 고른다. 맞는 값이 없으면 맞춘 R 그대로.
|
||||
|
||||
추천 후보(`guard` 줌 · PLAN 406)는 거리 한도 대신 직접 잼 — 곡선 조각이 같은 자리 탐색 경로보다
|
||||
복도 밖 · 기울기 · 계류 · 토공이 나빠지지 않는 가장 큰 R(`..._Merge.Guard` · 자리 R ~ 맞춘 R 사이
|
||||
`GUARD_STEPS` 벌 · 1 m 내림).
|
||||
|
||||
고른 R 은 편집 갈래(`simplify=False` · `radii`)로 못박아 다시 짠다 — 화면이 곡선을 교각점 하나로
|
||||
펴서(`flattenServerPlan`) [확인]이 다시 짜는 것과 같은 입력이라 저장 뒤 R 이 응답 R 과 같다.
|
||||
"""
|
||||
@@ -13,6 +17,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
from shapely.geometry import LineString, Point
|
||||
@@ -25,6 +30,8 @@ from common_util.common_util_route_polyline import (
|
||||
from config.config_system_design import ROUTE_RECOMMEND_MAX_GAP_M
|
||||
|
||||
Xy = tuple[float, float]
|
||||
# 직접 잼 R 후보 수(자리 R ~ 맞춘 R · 큰 것부터)
|
||||
GUARD_STEPS = 12
|
||||
|
||||
|
||||
def _flat(outline: PlannedPolyline) -> tuple[list[Xy], list[bool], list[float | None]]:
|
||||
@@ -77,8 +84,12 @@ def widen(
|
||||
path: list[Xy],
|
||||
min_radius_m: float,
|
||||
max_gap_m: float = ROUTE_RECOMMEND_MAX_GAP_M,
|
||||
*,
|
||||
guard: Any = None,
|
||||
) -> PlannedPolyline:
|
||||
"""곡선마다 허용 이탈 안 가장 큰 R(1 m 내림)로 못박아 다시 짠 선. 곡선이 없으면 그대로."""
|
||||
"""곡선마다 허용 이탈 안 가장 큰 R(1 m 내림)로 못박아 다시 짠 선. 곡선이 없으면 그대로.
|
||||
|
||||
`guard` 를 주면 이탈 한도 대신 직접 잼(`..._Merge.Guard.curve_ok`)을 지나는 가장 큰 R."""
|
||||
if not outline.curves:
|
||||
return outline
|
||||
points, flags, radii = _flat(outline)
|
||||
@@ -102,6 +113,20 @@ def widen(
|
||||
samples = coords[(along >= min(ends)) & (along <= max(ends))]
|
||||
if not candidates.size or not len(samples):
|
||||
continue
|
||||
if guard is not None:
|
||||
if len(samples) < 2:
|
||||
continue
|
||||
steps = np.geomspace(room, float(radii[index]), GUARD_STEPS)
|
||||
reference = [tuple(p) for p in samples]
|
||||
radii[index] = next(
|
||||
(
|
||||
r
|
||||
for r in sorted({float(math.floor(r)) for r in steps}, reverse=True)
|
||||
if r > radii[index] and guard.curve_ok(before, apex, after, r, reference)
|
||||
),
|
||||
radii[index],
|
||||
)
|
||||
continue
|
||||
ok = candidates[_gap(samples, before, apex, after, candidates) <= max_gap_m]
|
||||
if ok.size:
|
||||
radii[index] = float(ok[0])
|
||||
|
||||
@@ -20,6 +20,14 @@ R = 일반측점이 기본 제약(383-1 · 요구 241-13) — 추천 후보마
|
||||
그 전에 이웃 곡선과 한 측점을 다투면 덜 꺾인 쪽을 끔 — 꺾임점째 빼거나 꺾임만(364-5).
|
||||
· 그것도 못 하면 None — 부르는 쪽이 원래 선을 씀.
|
||||
|
||||
추천 후보(`guard` 줌 · PLAN 406 · 요구 251-2)는 이탈 한도 없음 — 옮긴 조각을 직접 잼
|
||||
(`..._Merge.Guard` · 복도 · 기울기 · 계류 · 토공이 같은 자리 원래 조각보다 안 나빠짐)으로 거름:
|
||||
· 같은 쪽으로 가장 많이 합친 꼴부터 놓아 봄(S자는 안 합침) · 자리는 그 잣대가 가장 나은 곳부터.
|
||||
· 잼을 지나는 놓기가 없으면 덜 나쁜 놓기(복도 안 · 더 넘는 기울기 `SOFT_SLACKS` 단계까지) —
|
||||
MC 를 다 측점에 놓는 첫 단계 판.
|
||||
· 그래도 못 놓으면 더 안 넘기는 놓기만 쓰고 그 곡선은 그 자리(측점 밖 · 기울기 먼저).
|
||||
예상노선 추적 · 새 초기 노선은 안 줌(이탈 한도 그대로).
|
||||
|
||||
고른 값은 편집 갈래(`simplify=False` · 곡선 켬 · `radii`)로 다시 짠다 — 화면이 곡선을 교각점
|
||||
하나로 펴서(`flattenServerPlan`) [확인] 때 다시 짜는 입력과 같아 저장 뒤에도 MC 가 측점 위(TS 짝
|
||||
불필요). 누가거리는 짜인 선의 정점(원호 5° 현)을 따라 잼 — 측점 생성과 같은 잣대.
|
||||
@@ -30,6 +38,7 @@ from __future__ import annotations
|
||||
import math
|
||||
from collections.abc import Callable, Iterator
|
||||
from dataclasses import dataclass, replace
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import shapely
|
||||
@@ -68,6 +77,9 @@ RADIUS_STEPS = 8
|
||||
BACKTRACK_LIMIT = 30
|
||||
# 한 꺾임 꼴(그대로 · 합친 꼴마다)에서 볼 놓기 수 — 막히면 R · 측점만 바꾸기보다 합치기로
|
||||
PER_VARIANT = 3
|
||||
# 직접 잼(406) — 잼을 못 지나는 놓기가 더 넘겨도 되는 기울기(비율) 단계 · 앞 단계로 다 놓으면
|
||||
# 그 판 · 다 못 놓으면 더 안 넘기는 놓기만 쓰고 못 놓는 곡선은 그 자리(부분 맞춤)
|
||||
SOFT_SLACKS = (0.0, 0.01, 0.03)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -293,30 +305,43 @@ class _Snapper:
|
||||
min_radius_m: float,
|
||||
max_gap_m: float,
|
||||
excess: Excess | None,
|
||||
guard: Any = None,
|
||||
) -> None:
|
||||
points, flags, radii = _flat(outline)
|
||||
points = [(round(x, 4), round(y, 4)) for x, y in points] # 화면이 펴는 자릿수
|
||||
self.start = _State(_Plan(points, flags, radii), 1, points[0], 0.0, 0.0, 0.0)
|
||||
self.reference = _Reference(outline.vertices)
|
||||
self.min_radius = min_radius_m
|
||||
self.max_gap = max_gap_m
|
||||
self.max_gap = math.inf if guard is not None else max_gap_m
|
||||
self.excess = excess
|
||||
self.guard = guard
|
||||
self.keep = False
|
||||
self.slack = math.inf # 못 지나는 놓기가 더 넘겨도 되는 기울기(`SOFT_SLACKS`)
|
||||
|
||||
def _steeper(self, local: list[Xy]) -> bool:
|
||||
"""맞춘 조각이 원래 선 같은 자리보다 상한을 더 넘나."""
|
||||
"""맞춘 조각이 원래 선 같은 자리보다 상한을 더 넘나(직접 잼이면 그 잼 전부)."""
|
||||
if self.guard is not None:
|
||||
return not self.guard.ok(local, self.reference.window(local))
|
||||
if self.excess is None:
|
||||
return False
|
||||
return self.excess(local) > self.excess(self.reference.window(local)) + 1e-6
|
||||
|
||||
def _far(self, local: list[Xy]) -> bool:
|
||||
"""원래 선에서 이탈 한도 밖(직접 잼이면 그 잼)."""
|
||||
if self.guard is not None:
|
||||
return self._steeper(local)
|
||||
return self.reference.gap(local) > self.max_gap
|
||||
|
||||
@staticmethod
|
||||
def _kink(state: _State, plan: _Plan) -> _State:
|
||||
at = plan.points[state.k]
|
||||
end_s = state.end_s + math.dist(state.end_xy, at)
|
||||
return _State(plan, state.k + 1, at, end_s, 0.0, state.last_station, state.placed)
|
||||
|
||||
def _placements(self, state: _State, plan: _Plan) -> Iterator[_State]:
|
||||
"""곡선 k 를 남은 측점에 놓은 걸음들 — 가까운 측점 · 지금 R 에 가까운 것부터."""
|
||||
def _placements(self, state: _State, plan: _Plan, soft: bool = False) -> Iterator[_State]:
|
||||
"""곡선 k 를 남은 측점에 놓은 걸음들 — 가까운 측점 · 지금 R 에 가까운 것부터.
|
||||
|
||||
직접 잼(406)이면 잼을 지나는 자리만 · `soft` 면 못 지나는 자리(복도 안)만 — 잣대 나은 순."""
|
||||
k = state.k
|
||||
before, at, after = plan.points[k - 1], plan.points[k], plan.points[k + 1]
|
||||
start = _try(state, plan, k, at, plan.radii[k] or self.min_radius)
|
||||
@@ -344,13 +369,23 @@ class _Snapper:
|
||||
new_at, new_corner, new_mc = solved
|
||||
# 뒤 직선 절반에 여유를 남긴 것 먼저 — 다음 IP 가 뒤로도 밀리게
|
||||
tight = math.dist(new_at, after) / 2 - new_corner.tangent < SLACK_M
|
||||
rank = (tight, abs(station - mc), abs(radius - corner.radius))
|
||||
found.append((rank, new_at, new_corner, new_mc, station))
|
||||
middle = ((new_at[0] + after[0]) / 2, (new_at[1] + after[1]) / 2)
|
||||
local = [state.end_xy, *new_corner.arc, middle]
|
||||
rank: tuple = (tight, abs(station - mc), abs(radius - corner.radius))
|
||||
if self.guard is not None:
|
||||
# 직접 잼(406) — 잣대 가장 나은 자리부터 · 복도 밖은 버림
|
||||
passed, (out, along, steeper, earth) = self.guard.judge(
|
||||
local, self.reference.window(local)
|
||||
)
|
||||
if out or passed == soft or steeper > self.slack:
|
||||
continue
|
||||
rank = (along, steeper, tight, rank[1], earth, rank[2])
|
||||
found.append((rank, local, new_at, new_corner, new_mc, station))
|
||||
given = 0
|
||||
for _, new_at, new_corner, new_mc, station in sorted(found, key=lambda item: item[0]):
|
||||
middle = ((new_at[0] + after[0]) / 2, (new_at[1] + after[1]) / 2)
|
||||
local = [state.end_xy, *new_corner.arc, middle]
|
||||
if self.reference.gap(local) > self.max_gap or self._steeper(local):
|
||||
for _, local, new_at, new_corner, new_mc, station in sorted(
|
||||
found, key=lambda item: item[0]
|
||||
):
|
||||
if self.guard is None and (self._far(local) or self._steeper(local)):
|
||||
continue
|
||||
placed = plan.copy()
|
||||
placed.points[k], placed.radii[k] = new_at, round(new_corner.radius, 3)
|
||||
@@ -376,17 +411,29 @@ class _Snapper:
|
||||
kink.flags[k] = False # 꺾임만(나란한 직선 등 곡선 못 끼움 포함)
|
||||
yield self._kink(state, kink)
|
||||
return
|
||||
trial = plan.copy()
|
||||
for _ in range(MERGE_TRIES + 1):
|
||||
yield from self._placements(state, trial)
|
||||
trial = trial.copy()
|
||||
variants = [plan.copy()]
|
||||
for _ in range(MERGE_TRIES):
|
||||
trial = variants[-1].copy()
|
||||
if not _absorb_next(trial, k):
|
||||
break
|
||||
variants.append(trial)
|
||||
if self.guard is not None:
|
||||
# 직접 잼(406) — 같은 쪽으로 가장 많이 합친 꼴부터(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:]
|
||||
for soft in (False, True) if self.guard is not None else (False,):
|
||||
for trial in variants:
|
||||
yield from self._placements(state, trial, soft)
|
||||
# 곡선 끔 — 법이 곡선을 안 둬도 되는 완만한 꺾임 · 원래 선에서 한도 안일 때만
|
||||
middle = ((at[0] + after[0]) / 2, (at[1] + after[1]) / 2)
|
||||
if (
|
||||
_inner_angle_deg(before, at, after) >= STRAIGHT_INNER_ANGLE_DEG
|
||||
and self.reference.gap([state.end_xy, at, middle]) <= self.max_gap
|
||||
if _inner_angle_deg(before, at, after) >= STRAIGHT_INNER_ANGLE_DEG and not self._far(
|
||||
[state.end_xy, at, middle]
|
||||
):
|
||||
kink = plan.copy()
|
||||
kink.flags[k], kink.radii[k] = False, None
|
||||
@@ -427,7 +474,7 @@ class _Snapper:
|
||||
# 꺾임점째 뺌(이웃 곡선 접선 자리가 넓어짐) → 안 되면 꺾임만 남김. 이 꺾임점은 앞
|
||||
# 곡선이 꺾임만일 때만 뺌(앞 곡선 EC 가 이 꺾임점 쪽 직선 위)
|
||||
movable = small > k or not plan.flags[k - 1]
|
||||
if movable and self.reference.gap(chord) <= self.max_gap and not self._steeper(chord):
|
||||
if movable and not self._far(chord) and not self._steeper(chord):
|
||||
gone = plan.copy()
|
||||
del gone.points[small], gone.flags[small], gone.radii[small]
|
||||
yield replace(state, plan=gone)
|
||||
@@ -436,7 +483,7 @@ class _Snapper:
|
||||
at,
|
||||
((at[0] + b[0]) / 2, (at[1] + b[1]) / 2),
|
||||
]
|
||||
if self.reference.gap(piece) > self.max_gap or self._steeper(piece):
|
||||
if self._far(piece) or self._steeper(piece):
|
||||
continue
|
||||
off = plan.copy()
|
||||
off.flags[small], off.radii[small] = False, None
|
||||
@@ -455,7 +502,8 @@ class _Snapper:
|
||||
corner, mc = found
|
||||
at, after = plan.points[k], plan.points[k + 1]
|
||||
middle = ((at[0] + after[0]) / 2, (at[1] + after[1]) / 2)
|
||||
if self.reference.gap([state.end_xy, *corner.arc, middle]) > self.max_gap:
|
||||
# 직접 잼(406)은 안 거름 — 옮기지 않은 곡선(앞 곡선이 민 만큼만 달라짐)
|
||||
if self.guard is None and self._far([state.end_xy, *corner.arc, middle]):
|
||||
return
|
||||
kept = plan.copy()
|
||||
kept.radii[k] = round(corner.radius, 3)
|
||||
@@ -508,17 +556,24 @@ def snap_curves_to_stations(
|
||||
max_gap_m: float = ROUTE_STATION_SNAP_MAX_GAP_M,
|
||||
*,
|
||||
excess: Excess | None = None,
|
||||
guard: Any = None,
|
||||
) -> PlannedPolyline | None:
|
||||
"""곡선 MC 를 일반측점에 놓아 다시 짠 선 · 못 하면 None(원래 선을 씀). 곡선 없으면 그대로.
|
||||
|
||||
모두 놓는 길을 먼저 찾고, 없으면 못 놓는 곡선만 그 자리에 둔 부분 맞춤(`off_station_count`).
|
||||
`excess`(점 묶음 → 기울기가 상한을 넘은 만큼)를 주면 옮긴 조각이 원래 자리보다 더 넘을 때 그
|
||||
놓기를 버림 — 급한 산지에서 측점 맞춤이 기울기를 망치지 않게.
|
||||
놓기를 버림 — 급한 산지에서 측점 맞춤이 기울기를 망치지 않게. `guard`(추천 후보 · 406)를 주면
|
||||
이탈 한도 · `excess` 대신 그 직접 잼으로 거르고 자리를 고름.
|
||||
"""
|
||||
if not outline.curves:
|
||||
return outline
|
||||
snapper = _Snapper(outline, min_radius_m, max_gap_m, excess)
|
||||
state = snapper.run(keep=False) or snapper.run(keep=True)
|
||||
snapper = _Snapper(outline, min_radius_m, max_gap_m, excess, guard)
|
||||
state = None
|
||||
for slack in SOFT_SLACKS if guard is not None else (math.inf,):
|
||||
snapper.slack = slack
|
||||
state = state or snapper.run(keep=False)
|
||||
snapper.slack = 0.0
|
||||
state = state or snapper.run(keep=True)
|
||||
if state is None:
|
||||
return None
|
||||
snapped = build_planned_polyline(
|
||||
|
||||
@@ -32,8 +32,9 @@ 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)
|
||||
ALGO_VERSION = 5
|
||||
# 3 = 곡선 MC 일반측점(351-2) · 4 = 다투는 곡선 끔(364-5) · 5 = 후보마다 맞춤(383-1) ·
|
||||
# 6 = 같은 쪽 곡선 합치기 · 거리 한도 대신 직접 잼(406)
|
||||
ALGO_VERSION = 6
|
||||
STORE_DIR = "B05_Profile/route_recommend"
|
||||
# 종류(갈래 · 지형) 마다 남길 벌 수 — 꺼낼 때 수정시각을 새로 해 최근에 쓴 것이 남음
|
||||
KEEP_PER_KIND = 2
|
||||
|
||||
Reference in New Issue
Block a user