/* ============================================================================= * B05_Profile/logic/B05_Profile_RouteEdit_KeyNudge.ts * 교각점 방향키(383-2 · 요구 241-16) — 화면 축 걸음 · 뗄 때 맞춘 결과 한 줄. * * · 한 번 0.1 m · Shift ×10 · 위 = 화면 위(지도를 돌려도 화면 기준). * · 측점 스냅 켬이면 한 번 = 스냅 간격(0.1 · 1 · 10 m · 측점 No. · Shift 안 씀 · 390-5) · 걸음마다 * MC 를 그 간격 배수에 맞춘 자리로 보임 · 뗄 때 정본 맞춤 → 실제 이동 · 함께 밀린 IP 를 알림. * · 반지름 · 곡선 길이 입력 뒤(408-1)도 같은 셈으로 맞춤이 민 교점 · 못 맞춘 곡선을 알림(`fitNote`). * 파이썬 짝 아님. 순수 계산 — 화면 · fetch · sessionStorage 없음. 부르는 쪽은 `_UI_RouteEdit_Keys`. * ========================================================================== */ import { stationLabel } from "../../common_util/common_util_station"; import type { Vertex } from "./B05_Profile_UI_RouteEdit_Curve"; /** 방향키 한 번(m) · Shift 배수. */ export const KEY_STEP_M = 0.1; export const KEY_SHIFT_TIMES = 10; /** 다른 IP 가 이보다 더 움직였으면 「함께 밀림」(m). */ const SAME_M = 0.005; /** 고른 IP 가 이보다 덜 움직였으면 「제자리」(m) — MC 측점 허용(`MC_TOL_M`)만큼. */ const STAY_M = 0.05; export type ArrowKey = "ArrowUp" | "ArrowDown" | "ArrowLeft" | "ArrowRight"; export function isArrowKey(key: string): key is ArrowKey { return key === "ArrowUp" || key === "ArrowDown" || key === "ArrowLeft" || key === "ArrowRight"; } /** * 방향키 한 번의 도면 좌표 걸음 — `metricAt(화면 x, y)` 로 화면 위 · 오른쪽 1 px 를 도면 좌표로 바꿔 * 그 방향 단위 벡터 × 걸음. 돌린 지도 · 확대와 무관하게 화면 축. `snapStepM` = 스냅 켬일 때 간격 * (있으면 걸음 = 그 값 · Shift 무시). */ export function arrowStep( key: ArrowKey, shift: boolean, metricAt: (x: number, y: number) => Vertex, snapStepM?: number, ): Vertex { const [x0, y0] = metricAt(0, 0); const [dx, dy] = key === "ArrowUp" ? [0, -1] : key === "ArrowDown" ? [0, 1] : key === "ArrowLeft" ? [-1, 0] : [1, 0]; const [x1, y1] = metricAt(dx, dy); const length = Math.hypot(x1 - x0, y1 - y0); if (!(length > 0)) return [0, 0]; const step = snapStepM ?? KEY_STEP_M * (shift ? KEY_SHIFT_TIMES : 1); return [((x1 - x0) / length) * step, ((y1 - y0) / length) * step]; } export interface NudgeReport { /** 누르기 전 자리에서 뗀 뒤(맞춘 뒤) 자리까지(m). */ movedM: number; /** 고른 IP 말고 자리가 바뀐 IP 수(맞춤이 함께 민 것). */ pushed: number; } /** 뗀 뒤 결과 — `before` = 누르기 전 노드 · `after` = 맞춘 뒤 노드 · `start` = 고른 IP 처음 자리 · * `end` = 키로 옮긴 자리(맞추기 전). 노드 수가 바뀌었으면(합침) `end` 에 가장 가까운 노드를 고른 IP 로. */ export function nudgeReport( before: ReadonlyArray, after: ReadonlyArray, picked: number, start: Vertex, end: Vertex, ): NudgeReport { const same = before.length === after.length; let index = picked; if (!same || !after[index]) { index = 0; after.forEach((node, i) => { if ( Math.hypot(node[0] - end[0], node[1] - end[1]) < Math.hypot(after[index][0] - end[0], after[index][1] - end[1]) ) index = i; }); } const node = after[index] ?? start; const moved = (a: Vertex, b: Vertex): boolean => Math.hypot(a[0] - b[0], a[1] - b[1]) > SAME_M; const pushed = same ? after.filter((xy, i) => i !== index && moved(xy, before[i])).length : after.filter((xy, i) => i !== index && before.every((old) => moved(xy, old))).length; return { movedM: Math.hypot(node[0] - start[0], node[1] - start[1]), pushed }; } /** 상태줄 한 마디 — 스냅 켬이면 실제 이동 · 함께 밀린 IP · 제자리면 미세 조정 안내. */ export function nudgeMessage(report: NudgeReport, snapping: boolean): string { if (!snapping) return `교각점을 ${report.movedM.toFixed(1)} m 옮겼습니다(방향키 0.1 m · Shift 1 m).`; if (report.movedM < STAY_M) return "같은 측점 — 스냅 끄고 미세 조정하세요."; return `측점에 맞춰 실제 이동 ${report.movedM.toFixed(1)} m · 함께 밀린 IP ${report.pushed}곳.`; } /** 반지름 · 곡선 길이 입력 뒤 맞춤 결과 꼬리(408-1) — 손으로 옮긴 교점이 없으니 고른 IP 도 「밀림」에 셈 · * 못 맞춘 곡선 = 측점 밖 MC(`offStationMcs`)를 측점 표기로(셋까지 · 나머지 「외 N」). * `handMoved` = 끌기 · 손잡이로 고른 IP 를 손이 옮김(430 · 놓을 때) — 고른 IP 는 「밀림」에 안 셈. */ export function fitNote( before: ReadonlyArray, after: ReadonlyArray, picked: number, offMcs: ReadonlyArray, stationM: number, stepM: number, handMoved = false, ): string { const start = before[picked] ?? [0, 0]; const report = nudgeReport(before, after, picked, start, start); const pushed = report.pushed + (!handMoved && report.movedM > SAME_M ? 1 : 0); const names = offMcs.map((mc) => `No.${stationLabel(mc, stationM, stepM < 1 ? 1 : 0)}`); const more = names.length > 3 ? ` 외 ${names.length - 3}` : ""; const off = names.length ? ` · 못 맞춘 곡선 ${names.slice(0, 3).join(", ")}${more}` : ""; return ` · 함께 밀린 교점 ${pushed}곳${off}`; }