- 새 파일 B05_Profile_UI_RouteEdit_Keys.ts(창 keydown 가로채기 · 마지막 누름 평면 안 · 고정 · 입력칸 제외 · 하한 검사) + logic/KeyNudge - 평면을 누른 뒤 방향키는 평면이 삼킴 — 종단 ▲▼ · 횡단 조정창으로 안 샘 - B06 Panel_Base lastPointerWithin 넓힘 · Pointer.ts 걸기 한 줄 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
131 lines
5.4 KiB
TypeScript
131 lines
5.4 KiB
TypeScript
/* =============================================================================
|
|
* 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);
|
|
}
|