/* ============================================================================= * B05_Profile_UI_RouteEdit_Keys.ts * 계획노선 편집 평면의 **교각점 방향키**(383-2 · 요구 241-16) — 고른 IP 를 화면 축으로 0.1 m * (Shift 1 m)씩. 누르는 동안 옮기기만 · 뗄 때 되돌리기 한 걸음(측점 스냅 켬이면 그때 MC 맞춤). * * 받는 때 = 마지막 누름이 평면 편집 창(`.b05-routeedit`) 안 · 입력칸 아님. 그때 방향키는 평면이 * 삼킴(IP 를 안 골랐어도) — 종단 계획고 ▲▼ · 횡단 조정창으로 새지 않음. 하한 검사 · 놓을 때 할 * 일은 끌기(`_Pointer`)와 같음. 걸음 · 결과 한 줄 셈은 `logic/B05_Profile_RouteEdit_KeyNudge`. * ========================================================================== */ import { lastPointerWithin } from "../B06_Section/B06_Section_UI_Cross_Panel_Base"; import { HOLD_DELAY_MS, HOLD_INTERVAL_MS } from "./B05_Profile_UI_Profile_Edit"; import type { RouteEditPointerParams } from "./B05_Profile_UI_RouteEdit_Pointer"; import { arrowStep, isArrowKey, nudgeMessage, nudgeReport, type ArrowKey, } from "./logic/B05_Profile_RouteEdit_KeyNudge"; import type { Vertex } from "./logic/B05_Profile_UI_RouteEdit_Curve"; import { curveShortfalls, shortfallCrossed } from "./logic/B05_Profile_UI_RouteEdit_Edits"; /** 누르는 한 번(누름 ~ 뗌) — 뗄 때 결과 한 줄을 셈. */ interface Hold { key: ArrowKey; shift: boolean; index: number; before: Vertex[]; start: Vertex; moved: boolean; } export function bindRouteEditKeys(params: RouteEditPointerParams, dragging: () => boolean): void { const { canvas, status } = params; let hold: Hold | null = null; let timer = 0; const metricAt = (x: number, y: number): Vertex => params.toMetric(...params.unrotate(x, y)); /** 한 걸음 — 하한을 새로 깨면 그 걸음만 되돌림(끌기와 같은 검사). */ function step(now: Hold): void { const state = params.state(); const node = state.planned[now.index]; if (!node) return; const [dx, dy] = arrowStep(now.key, now.shift, metricAt); const before = curveShortfalls(state.nodeInfo, state.limitRadiusM, state.limitArcM); state.planned[now.index] = [node[0] + dx, node[1] + dy]; params.markEdited(); const after = params.state(); if ( shortfallCrossed(before, curveShortfalls(after.nodeInfo, after.limitRadiusM, after.limitArcM)) ) { state.planned[now.index] = node; params.markEdited(); status.textContent = `${params.routeHead()} — 하한에 걸려 더 못 옮깁니다` + `(곡선반지름 ${state.limitRadiusM}m${state.limitArcM > 0 ? ` · 곡선 길이 ${state.limitArcM}m` : ""}).`; params.draw(); return; } now.moved = true; status.textContent = `${params.routeHead()} — 방향키로 옮기는 중(뗄 때 한 걸음). ${params.curveHint()}`; params.draw(); } /** 뗌 — 옮겼으면 되돌리기 한 걸음(스냅 켬이면 맞춤) · 횡단 · 기울기 다시 · 결과 한 줄. */ function release(): void { window.clearTimeout(timer); window.clearInterval(timer); const done = hold; hold = null; if (!done?.moved) return; const end = params.state().planned[done.index] ?? done.start; params.commit(); void params.crossPreview.refresh(); params.slopeRefresh(); const report = nudgeReport(done.before, params.state().planned, done.index, done.start, end); const message = nudgeMessage(report, params.stations() !== null); status.textContent = `${params.routeHead()} — ${message} ${params.curveHint()}`; params.draw(); } const detach = (): void => { window.removeEventListener("keydown", onKeyDown, true); window.removeEventListener("keyup", onKeyUp, true); window.removeEventListener("blur", release); }; function onKeyDown(event: KeyboardEvent): void { if (!canvas.isConnected) return detach(); if (!isArrowKey(event.key) || !lastPointerWithin(".b05-routeedit")) return; const target = event.target as HTMLElement | null; if (target?.closest?.("input, select, textarea, [contenteditable='true']")) return; // 평면이 삼킴 — 종단도(▲▼ 계획고 · `defaultPrevented` 면 비킴) · 횡단 조정창으로 안 감 event.preventDefault(); event.stopPropagation(); if (event.repeat || hold || dragging() || params.measureMode()) return; // 반복은 아래 타이머 const state = params.state(); const index = state.picked; const node = state.planned[index]; if (!node || state.apexLock[index] === true) return; const now: Hold = { key: event.key, shift: event.shiftKey, index, before: state.planned.map(([x, y]): Vertex => [x, y]), start: [node[0], node[1]], moved: false, }; hold = now; step(now); // 누르고 있으면 종단 편집과 같은 속도(0.5초 뒤 초당 10회) — OS 키 반복은 기기마다 갈림 timer = window.setTimeout(() => { timer = window.setInterval(() => hold === now && step(now), HOLD_INTERVAL_MS); }, HOLD_DELAY_MS); } function onKeyUp(event: KeyboardEvent): void { if (!canvas.isConnected) return detach(); if (!hold || event.key !== hold.key) return; event.preventDefault(); event.stopPropagation(); release(); } window.addEventListener("keydown", onKeyDown, true); window.addEventListener("keyup", onKeyUp, true); window.addEventListener("blur", release); }