/* ============================================================================= * B05_Profile_UI_RouteEdit_Frame.ts * 측점 한 자리의 **평면 틀**(중심점 · 좌측 축) — 노선을 끄는 동안 브라우저가 바로 셈한다. * * ⚠ 파이썬·TS 짝 — `B05_Profile_Engine_Sections_Core.py` 의 `_interpolate_xy`·`_tangent_at` * (`generate_sections` 가 측점 자리·좌측 축을 내는 식). 한쪽을 고치면 다른 쪽도 고치고 * 거울 시험 `resources/tester/test_b05_routeedit_frame_mirror.py` 를 돌림. * ========================================================================== */ type Vertex = [number, number]; /** 접선을 재는 앞뒤 거리(m) — 파이썬 `max(SECTION_LONG_SAMPLE_INTERVAL_M, 0.5)` 와 같은 값. */ export const SECTION_LONG_SAMPLE_INTERVAL_M = 1.0; export interface StationFrame { origin: Vertex; /** 좌측(+offset) 단위 벡터 — 우측은 반대 방향. */ left: Vertex; } /** 길이 0 인 선분을 걸러 낸 정점과 누가거리 — 파이썬 `_clean_polyline` 과 같은 거름. */ function cleanPolyline(line: ReadonlyArray): { points: Vertex[]; chainage: number[] } { const points: Vertex[] = []; const chainage: number[] = []; line.forEach((vertex, index) => { if (index > 0) { const last = points[points.length - 1]; const step = Math.hypot(vertex[0] - last[0], vertex[1] - last[1]); if (step <= 1e-8) return; chainage.push(chainage[chainage.length - 1] + step); } else { chainage.push(0); } points.push([vertex[0], vertex[1]]); }); return { points, chainage }; } /** `np.interp` 와 같은 한 점 보간 — 범위 밖은 끝값에 붙는다. */ function interpolate(points: Vertex[], chainage: number[], target: number): Vertex { const last = chainage.length - 1; if (target <= chainage[0]) return points[0]; if (target >= chainage[last]) return points[last]; let index = 1; while (chainage[index] < target) index += 1; const span = chainage[index] - chainage[index - 1]; const ratio = span > 0 ? (target - chainage[index - 1]) / span : 0; return [ points[index - 1][0] + (points[index][0] - points[index - 1][0]) * ratio, points[index - 1][1] + (points[index][1] - points[index - 1][1]) * ratio, ]; } /** 누가거리 `chainageM` 측점의 중심점과 좌측 축. 노선이 없거나 방향을 못 재면 null. * 노선보다 긴 누가거리는 끝점에 붙인다 — 노선이 짧아지면 서버도 마지막 측점을 준다. */ export function stationFrameAt( line: ReadonlyArray, target: number, probeM: number = Math.max(SECTION_LONG_SAMPLE_INTERVAL_M, 0.5), ): StationFrame | null { const { points, chainage } = cleanPolyline(line); if (points.length < 2) return null; const total = chainage[chainage.length - 1]; const chainageM = Math.min(Math.max(target, 0), total); let before = Math.max(0, chainageM - probeM); let after = Math.min(total, chainageM + probeM); if (after - before <= 1e-9) { before = Math.max(0, chainageM - 1e-3); after = Math.min(total, chainageM + 1e-3); } const from = interpolate(points, chainage, before); const to = interpolate(points, chainage, after); const length = Math.hypot(to[0] - from[0], to[1] - from[1]); if (length <= 1e-9) return null; const tangent: Vertex = [(to[0] - from[0]) / length, (to[1] - from[1]) / length]; return { origin: interpolate(points, chainage, chainageM), left: [-tangent[1], tangent[0]] }; }