Files
Aislo/B05_Profile/B05_Profile_UI_RouteEdit_Keys.ts
T
eomsangdonandClaude Opus 5.5 e0abf8c5bf feat(383-2): 평면 교각점 방향키 — 화면 축 0.1 m · Shift ×10 · 뗄 때 한 걸음(스냅이면 맞춤 + 실제 이동 · 함께 밀린 IP)
- 새 파일 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
2026-10-17 15:11:46 +09:00

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);
}