feat(B05 평면 구조물 마커): 구조물 측점 마커 · 고름 정보 창 · 20 m 법선 · 끌기 · 방향키 1 m (399)

- 마커 = 종단 알약과 같은 목록(laneStructures) · 노선 위 알약 꼴(종류 색) · 스냅 · 간격과 무관
- 고름 = 알약 고름과 같은 길(onStructureSelect · 공용 고름) · 알약 벌룬 그대로 · 좌우 20 m 법선
- 끌기 · 방향키 = 노선 따라 정수 m 1 m 씩 · 시점 · 종점 밖 안 감 · 놓을 때 알약 끌기와 같은 길로 한 번
- 집기 차례 = 교각점 · 곡선 손잡이 다음 · 옛 마름모 지움
- 시험 test_399_plan_structure_marks · test_391 마름모 줄 고침

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte
This commit is contained in:
2026-10-17 21:43:51 +09:00
co-authored by Claude Opus 5.5
parent 614f12bb88
commit c3efa6dd7e
10 changed files with 938 additions and 56 deletions
@@ -12,6 +12,7 @@ import { pickPx } from "../A00_Common/touch_bridge";
import { hitPreparedLayer, type ViewState } from "../B04_PreProcess/B04_PreProcess_UI_MapRender";
import type { RouteEditContours } from "./B05_Profile_UI_RouteEdit_Contour";
import { bindRouteEditKeys } from "./B05_Profile_UI_RouteEdit_Keys";
import { bindStructureMarks } from "./B05_Profile_UI_RouteEdit_StructureMarks";
import {
dragHandleTo as curveDragTo,
type EditedCurve,
@@ -191,6 +192,8 @@ export function bindRouteEditPointer(params: RouteEditPointerParams): RouteEditP
let dragMoved = false;
/** 스냅 미리보기 전 자리(390-5) — 하한 검사는 맞춘 선이 아닌 이것으로(맞춤이 IP 를 ±간격/2 밂). */
let dragRaw: Vertex[] | null = null;
/** 구조물 측점 마커(399) — 집기 차례 = 교각점 · 손잡이 다음 · 끌기 · 방향키 1 m. */
const marks = bindStructureMarks(params, () => dragNode >= 0 || dragHandle !== null);
canvas.addEventListener("pointerdown", (event) => {
if (event.button !== 0) return;
@@ -236,6 +239,8 @@ export function bindRouteEditPointer(params: RouteEditPointerParams): RouteEditP
})()
) {
/* 여기로는 안 온다 — 위 갈래는 선택만 풀고 다음 갈래로 넘긴다. */
} else if (marks.press(px, py)) {
/* 구조물 마커를 골랐다 — 끌면 노선 따라 1 m 씩(399). */
} else if (
// 측점 눈금을 누르면 그 측점 횡단을 따로 띄운다(계획서 0-9 ⑧). 노드·손잡이 다음이다.
(() => {
@@ -288,6 +293,7 @@ export function bindRouteEditPointer(params: RouteEditPointerParams): RouteEditP
const rect = canvas.getBoundingClientRect();
const [px, py] = params.unrotate(event.clientX - rect.left, event.clientY - rect.top);
const state = params.state();
if (marks.move(px, py)) return;
if (dragHandle) {
// 곡선 시작·끝점을 끈다 — 그쪽 직선 각도와 반지름이 함께 바뀐다(2026-09-07 사용자 확정).
const node = dragHandle.node;
@@ -361,7 +367,7 @@ export function bindRouteEditPointer(params: RouteEditPointerParams): RouteEditP
? state.apexLock[overNode]
? "not-allowed"
: "grab"
: handleAt(px, py)
: handleAt(px, py) || marks.over(px, py)
? "grab"
: over
? "pointer"
@@ -373,6 +379,7 @@ export function bindRouteEditPointer(params: RouteEditPointerParams): RouteEditP
const endDrag = (event: PointerEvent): void => {
if (canvas.hasPointerCapture(event.pointerId)) canvas.releasePointerCapture(event.pointerId);
marks.release();
// 끌기 **한 번**이 되돌리기 한 걸음이다 — 프레임마다 쌓으면 한 번 물리는 데 수십 번
// 눌러야 한다. 놓는 순간에만 쌓는다.
if (dragMoved) {