- StationFit reach = intervalM(반 간격이면 MC 18+9 ~ 19+1 못 맞춤) - 끌기 · 방향키 — 미리보기 측점 밖 곡선 수가 누를 때보다 늘면 그 걸음 되돌림 · dragMoved 는 막힘 검사 뒤(433-14) - 계산 줄 stations 단계 · follow 새 측점 — 초안 없으면 저장 노선 station_snap · station_snap_step(snapOptions) - 시험 — 390_5 끌기 훑기(놓은 뒤 측점 밖 0 · 자동 · 고정 R) · 344_3 저장 스냅 · 266 사면 이동 한 걸음 · 다시 그림 한 번 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
161 lines
6.9 KiB
TypeScript
161 lines
6.9 KiB
TypeScript
/* =============================================================================
|
|
* B05_Profile_UI_RouteEdit_Keys.ts
|
|
* 계획노선 편집 평면의 **교각점 방향키**(383-2 · 요구 241-16) — 고른 IP 를 화면 축으로 0.1 m
|
|
* (Shift 1 m)씩 · 측점 스냅 켬이면 스냅 간격씩(390-5 · 걸음마다 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;
|
|
/** 스냅 미리보기 전 노선(맞추기 전) — 다음 걸음 · 하한 검사는 여기서(맞춤이 쌓이지 않음). */
|
|
raw: Vertex[] | null;
|
|
/** 누를 때 측점 밖 곡선 수(441) — 미리보기가 이보다 늘면 그 걸음 되돌림. */
|
|
off: number;
|
|
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 fitPreview(now: Hold, planned: Vertex[]): void {
|
|
const raw = planned.map((vertex): Vertex => [vertex[0], vertex[1]]);
|
|
now.raw = params.fitDrag(now.index) ? raw : null;
|
|
}
|
|
|
|
/** 한 걸음 — 하한을 새로 깨면 그 걸음만 되돌림(끌기와 같은 검사). */
|
|
function step(now: Hold): void {
|
|
const state = params.state();
|
|
if (now.raw) {
|
|
now.raw.forEach((vertex, index) => (state.planned[index] = vertex));
|
|
params.markEdited();
|
|
}
|
|
const node = state.planned[now.index];
|
|
if (!node) return;
|
|
const snapStepM = params.stations() !== null ? params.snapStepM() : undefined;
|
|
const [dx, dy] = arrowStep(now.key, now.shift, metricAt, snapStepM);
|
|
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();
|
|
fitPreview(now, state.planned);
|
|
status.textContent =
|
|
`${params.routeHead()} — 하한에 걸려 더 못 옮깁니다` +
|
|
`(곡선반지름 ${state.limitRadiusM}m${state.limitArcM > 0 ? ` · 곡선 길이 ${state.limitArcM}m` : ""}).`;
|
|
params.draw();
|
|
return;
|
|
}
|
|
fitPreview(now, state.planned);
|
|
if (now.raw && params.offStation() > now.off) {
|
|
// 맞춰도 MC 가 측점 밖에 남는 자리 — 끌기와 같게 그 걸음만 되돌림(441)
|
|
const planned = params.state().planned;
|
|
now.raw.forEach((vertex, index) => (planned[index] = vertex));
|
|
planned[now.index] = node;
|
|
fitPreview(now, planned);
|
|
status.textContent = `${params.routeHead()} — 곡선 가운데를 측점에 못 맞춰 더 못 옮깁니다.`;
|
|
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; // B06 겹침 평면도 같음(436)
|
|
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]],
|
|
raw: null,
|
|
off: params.offStation(),
|
|
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);
|
|
}
|