- 새 파일 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
94 lines
3.9 KiB
TypeScript
94 lines
3.9 KiB
TypeScript
/* =============================================================================
|
|
* B05_Profile/logic/B05_Profile_RouteEdit_KeyNudge.ts
|
|
* 교각점 방향키(383-2 · 요구 241-16) — 화면 축 걸음 · 뗄 때 맞춘 결과 한 줄.
|
|
*
|
|
* · 한 번 0.1 m · Shift ×10 · 위 = 화면 위(지도를 돌려도 화면 기준).
|
|
* · 측점 스냅 켬이면 뗄 때 MC 를 일반측점에 맞춤 → 실제 이동 · 함께 밀린 IP 를 알림.
|
|
* 파이썬 짝 아님. 순수 계산 — 화면 · fetch · sessionStorage 없음. 부르는 쪽은 `_UI_RouteEdit_Keys`.
|
|
* ========================================================================== */
|
|
|
|
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 를 도면 좌표로 바꿔
|
|
* 그 방향 단위 벡터 × 걸음. 돌린 지도 · 확대와 무관하게 화면 축.
|
|
*/
|
|
export function arrowStep(
|
|
key: ArrowKey,
|
|
shift: boolean,
|
|
metricAt: (x: number, y: number) => Vertex,
|
|
): 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 = 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<Vertex>,
|
|
after: ReadonlyArray<Vertex>,
|
|
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}곳.`;
|
|
}
|