- 거리·기울기 재기가 예상노선(점선)에서도 됨 — 두 점은 한 노선 위 · 다른 노선을 짚으면 새로 시작 - 측점 눈금 라벨이 겹치면 빼지 않고 비켜 놓음(반대쪽 → 한 칸 더 멀리 · 가는 선으로 이음) — 공용 drawStationTicks - 오른쪽에 횡단이 뜬 측점에 좌·우 설계 반폭 선 — 노드를 끄는 동안 브라우저 셈으로 바로 따라감 - 측점 틀 파이썬·TS 짝(_interpolate_xy·_tangent_at ↔ stationFrameAt) + 거울 시험 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014WeXB1zh5hbYLgoeGS5kQu
78 lines
3.4 KiB
TypeScript
78 lines
3.4 KiB
TypeScript
/* =============================================================================
|
|
* 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<Vertex>): { 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<Vertex>,
|
|
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]] };
|
|
}
|