feat(562-1): D03 수동 그리기 셈 짝 — 길이 · 원지반 기울기 · 길이 고정 · 기울기 고정 · 양각기 · 선 위 거리 ↔ 점 · 측점
- logic/D03_PlanAnalysis_Draw.ts ↔ D03_PlanAnalysis_Draw.py 같은 이름 짝 · 높이는 인자 heightAt - 거울 시험 test_562_1_plan_draw.py — 두 쪽 값 대조 + 손셈 값 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
"""D 예정노선 수동 직선 그리기 셈 — [저장] 정본 쪽 (PLAN 562-1 · 요구 389 12 · 13).
|
||||
|
||||
구간 길이 · 원지반 기울기 · 길이 고정 · 기울기 고정 · 둘 다(양각기) · 선 위 거리 ↔ 점 · 측점.
|
||||
높이는 인자 `height_at(x, y)`(nan = 지형 밖).
|
||||
|
||||
⚠ 파이썬·TS 짝 — `logic/D03_PlanAnalysis_Draw.ts` 같은 이름(camel). 한쪽을 고치면 다른 쪽도 고치고
|
||||
거울 시험 `resources/tester/test_562_1_plan_draw.py`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from collections.abc import Callable, Sequence
|
||||
|
||||
Vertex = tuple[float, float]
|
||||
HeightAt = Callable[[float, float], float]
|
||||
|
||||
# 기울기 고정 반직선 걸음(m) · 이분 횟수 · 양각기 원 표본 수 — 짝 TS 상수
|
||||
SLOPE_STEP_M = 1
|
||||
BISECT = 40
|
||||
COMPASS_SAMPLES = 360
|
||||
EPS = 1e-9
|
||||
|
||||
|
||||
def _finite(value: float) -> bool:
|
||||
return math.isfinite(value)
|
||||
|
||||
|
||||
def segment_length(a: Sequence[float], b: Sequence[float]) -> float:
|
||||
"""수평 길이(m)."""
|
||||
dx = b[0] - a[0]
|
||||
dy = b[1] - a[1]
|
||||
return math.sqrt(dx * dx + dy * dy)
|
||||
|
||||
|
||||
def ground_slope_pct(a: Sequence[float], b: Sequence[float], height_at: HeightAt) -> float | None:
|
||||
"""원지반 기울기(%) = (z끝 − z시작) / 수평 길이 × 100 · + 오르막 · None = 지형 밖 · 길이 0."""
|
||||
length = segment_length(a, b)
|
||||
if length < EPS:
|
||||
return None
|
||||
za = height_at(a[0], a[1])
|
||||
zb = height_at(b[0], b[1])
|
||||
if not _finite(za) or not _finite(zb):
|
||||
return None
|
||||
return ((zb - za) / length) * 100
|
||||
|
||||
|
||||
def fixed_length_point(
|
||||
start: Sequence[float], cursor: Sequence[float], length_m: float
|
||||
) -> Vertex | None:
|
||||
"""길이 고정 — 마우스 방향으로 그 길이 점 · 마우스 = 시작이면 None."""
|
||||
d = segment_length(start, cursor)
|
||||
if d < EPS:
|
||||
return None
|
||||
return (
|
||||
start[0] + ((cursor[0] - start[0]) / d) * length_m,
|
||||
start[1] + ((cursor[1] - start[1]) / d) * length_m,
|
||||
)
|
||||
|
||||
|
||||
def fixed_slope_point(
|
||||
start: Sequence[float],
|
||||
cursor: Sequence[float],
|
||||
slope_pct: float,
|
||||
height_at: HeightAt,
|
||||
max_m: float = 1000,
|
||||
) -> Vertex | None:
|
||||
"""기울기 고정 — 마우스 방향 반직선 위 |기울기| 가 목표가 되는 첫 자리(1 m 걸음 · 이분) ·
|
||||
`max_m` 안에 없거나 지형 밖에 닿으면 None."""
|
||||
d = segment_length(start, cursor)
|
||||
z0 = height_at(start[0], start[1])
|
||||
if d < EPS or not _finite(z0):
|
||||
return None
|
||||
ux = (cursor[0] - start[0]) / d
|
||||
uy = (cursor[1] - start[1]) / d
|
||||
grade = abs(slope_pct) / 100
|
||||
|
||||
def at(t: float) -> Vertex:
|
||||
return (start[0] + ux * t, start[1] + uy * t)
|
||||
|
||||
def f(t: float) -> float:
|
||||
p = at(t)
|
||||
return abs(height_at(p[0], p[1]) - z0) - grade * t
|
||||
|
||||
lo = 0.0
|
||||
flo = math.nan
|
||||
i = 1
|
||||
while i * SLOPE_STEP_M <= max_m:
|
||||
t = i * SLOPE_STEP_M
|
||||
ft = f(t)
|
||||
if not _finite(ft):
|
||||
return None
|
||||
if ft == 0:
|
||||
return at(t)
|
||||
if i > 1 and (flo < 0) != (ft < 0):
|
||||
hi = t
|
||||
for _ in range(BISECT):
|
||||
mid = (lo + hi) / 2
|
||||
fm = f(mid)
|
||||
if not _finite(fm):
|
||||
return None
|
||||
if (fm < 0) == (flo < 0):
|
||||
lo = mid
|
||||
flo = fm
|
||||
else:
|
||||
hi = mid
|
||||
return at((lo + hi) / 2)
|
||||
lo = t
|
||||
flo = ft
|
||||
i += 1
|
||||
return None
|
||||
|
||||
|
||||
def compass_candidates(
|
||||
start: Sequence[float], length_m: float, slope_pct: float, height_at: HeightAt
|
||||
) -> list[Vertex]:
|
||||
"""둘 다(양각기) — 반지름 `length_m` 원 위에서 |기울기| = 목표인 후보들
|
||||
(방위 표본 · 이분 · 동쪽부터 반시계)."""
|
||||
z0 = height_at(start[0], start[1])
|
||||
if not length_m > EPS or not _finite(z0):
|
||||
return []
|
||||
rise = (abs(slope_pct) / 100) * length_m
|
||||
step = (2 * math.pi) / COMPASS_SAMPLES
|
||||
|
||||
def at(angle: float) -> Vertex:
|
||||
return (start[0] + math.cos(angle) * length_m, start[1] + math.sin(angle) * length_m)
|
||||
|
||||
def h(angle: float) -> float:
|
||||
p = at(angle)
|
||||
return abs(height_at(p[0], p[1]) - z0) - rise
|
||||
|
||||
values = [h(k * step) for k in range(COMPASS_SAMPLES)]
|
||||
out: list[Vertex] = []
|
||||
for k in range(COMPASS_SAMPLES):
|
||||
hk = values[k]
|
||||
hn = values[(k + 1) % COMPASS_SAMPLES]
|
||||
if not _finite(hk) or not _finite(hn):
|
||||
continue
|
||||
if hk == 0:
|
||||
out.append(at(k * step))
|
||||
continue
|
||||
if hn == 0 or (hk < 0) == (hn < 0):
|
||||
continue
|
||||
lo = k * step
|
||||
hi = lo + step
|
||||
flo = hk
|
||||
ok = True
|
||||
for _ in range(BISECT):
|
||||
mid = (lo + hi) / 2
|
||||
fm = h(mid)
|
||||
if not _finite(fm):
|
||||
ok = False
|
||||
break
|
||||
if (fm < 0) == (flo < 0):
|
||||
lo = mid
|
||||
flo = fm
|
||||
else:
|
||||
hi = mid
|
||||
if ok:
|
||||
out.append(at((lo + hi) / 2))
|
||||
return out
|
||||
|
||||
|
||||
def compass_point(
|
||||
start: Sequence[float],
|
||||
cursor: Sequence[float],
|
||||
length_m: float,
|
||||
slope_pct: float,
|
||||
height_at: HeightAt,
|
||||
) -> Vertex | None:
|
||||
"""둘 다 — 커서에 가장 가까운 양각기 후보 · 없으면 None."""
|
||||
best: Vertex | None = None
|
||||
best_d = math.inf
|
||||
for p in compass_candidates(start, length_m, slope_pct, height_at):
|
||||
d = segment_length(p, cursor)
|
||||
if d < best_d:
|
||||
best = p
|
||||
best_d = d
|
||||
return best
|
||||
|
||||
|
||||
def line_length(line: Sequence[Sequence[float]]) -> float:
|
||||
"""선 길이(m) — 앞에서부터 더함."""
|
||||
total = 0.0
|
||||
for i in range(1, len(line)):
|
||||
total += segment_length(line[i - 1], line[i])
|
||||
return total
|
||||
|
||||
|
||||
def point_at_distance(line: Sequence[Sequence[float]], m: float) -> Vertex:
|
||||
"""선 위 거리 → 점(0 · 끝 밖은 끝에서 멈춤) · 선 점 하나 이상."""
|
||||
if m <= 0:
|
||||
return (line[0][0], line[0][1])
|
||||
acc = 0.0
|
||||
for i in range(1, len(line)):
|
||||
a = line[i - 1]
|
||||
b = line[i]
|
||||
seg = segment_length(a, b)
|
||||
if seg > 0 and acc + seg >= m:
|
||||
t = (m - acc) / seg
|
||||
return (a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t)
|
||||
acc += seg
|
||||
return (line[-1][0], line[-1][1])
|
||||
|
||||
|
||||
def distance_along(line: Sequence[Sequence[float]], point: Sequence[float]) -> float:
|
||||
"""점 → 선 위 거리(m) — 가장 가까운 선 위 자리(같으면 앞 구간)."""
|
||||
best = 0.0
|
||||
best_d = math.inf
|
||||
acc = 0.0
|
||||
for i in range(1, len(line)):
|
||||
a = line[i - 1]
|
||||
b = line[i]
|
||||
seg = segment_length(a, b)
|
||||
t = 0.0
|
||||
if seg > 0:
|
||||
t = ((point[0] - a[0]) * (b[0] - a[0]) + (point[1] - a[1]) * (b[1] - a[1])) / (
|
||||
seg * seg
|
||||
)
|
||||
t = min(1.0, max(0.0, t))
|
||||
p = (a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t)
|
||||
d = segment_length(p, point)
|
||||
if d < best_d:
|
||||
best_d = d
|
||||
best = acc + seg * t
|
||||
acc += seg
|
||||
return best
|
||||
|
||||
|
||||
def slice_line(line: Sequence[Sequence[float]], from_m: float, to_m: float) -> list[Vertex]:
|
||||
"""선 조각 — `from_m`~`to_m` 사이(끝점 + 그 사이 꺾임점)."""
|
||||
lo = min(from_m, to_m)
|
||||
hi = max(from_m, to_m)
|
||||
out = [point_at_distance(line, lo)]
|
||||
acc = 0.0
|
||||
for i in range(1, len(line) - 1):
|
||||
acc += segment_length(line[i - 1], line[i])
|
||||
if lo + EPS < acc < hi - EPS:
|
||||
out.append((line[i][0], line[i][1]))
|
||||
out.append(point_at_distance(line, hi))
|
||||
return out
|
||||
|
||||
|
||||
def station_chainages(length_m: float, interval_m: float = 20) -> list[float]:
|
||||
"""측점 누가 — No.0(0 m) · 간격 배수 · 끝(선마다 0 부터)."""
|
||||
out: list[float] = [0]
|
||||
k = 1
|
||||
while k * interval_m < length_m - 1e-6:
|
||||
out.append(k * interval_m)
|
||||
k += 1
|
||||
if length_m > 1e-6:
|
||||
out.append(length_m)
|
||||
return out
|
||||
Reference in New Issue
Block a user