From a8a8e4faba2e6f2ab4b083ad0b36c6e113d529cb Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 17 Oct 2026 20:11:30 +0900 Subject: [PATCH] =?UTF-8?q?fix(B05=20=ED=8F=89=EB=A9=B4=20=EC=B8=A1?= =?UTF-8?q?=EC=A0=90=20=EC=8A=A4=EB=83=85):=20=EB=81=8C=EA=B8=B0=20=C2=B7?= =?UTF-8?q?=20=EB=B0=A9=ED=96=A5=ED=82=A4=20=ED=95=98=ED=95=9C=20=EA=B2=80?= =?UTF-8?q?=EC=82=AC=20=3D=20=EB=A7=9E=EC=B6=94=EA=B8=B0=20=EC=A0=84=20?= =?UTF-8?q?=EB=85=B8=EC=84=A0=20=E2=80=94=2010=20m=20=C2=B7=20=EC=B8=A1?= =?UTF-8?q?=EC=A0=90=20No.=20=EB=AF=B8=EB=A6=AC=EB=B3=B4=EA=B8=B0=EA=B0=80?= =?UTF-8?q?=20IP=20=EB=A5=BC=20=EB=B0=80=EC=96=B4=20=ED=97=9B=EA=B1=B8?= =?UTF-8?q?=EB=A6=AC=EB=8D=98=20=EA=B2=83=20(390-5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD --- B05_Profile/B05_Profile_UI_RouteEdit.ts | 1 + B05_Profile/B05_Profile_UI_RouteEdit_Keys.ts | 22 ++++++++++++++----- .../B05_Profile_UI_RouteEdit_Pointer.ts | 22 ++++++++++++++++--- resources/tester/test_390_5_snap_step_ops.py | 3 +++ 4 files changed, 39 insertions(+), 9 deletions(-) diff --git a/B05_Profile/B05_Profile_UI_RouteEdit.ts b/B05_Profile/B05_Profile_UI_RouteEdit.ts index 149e8076c..a8339783d 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit.ts @@ -534,6 +534,7 @@ export async function mountRouteEditor( const base = stationSnap ? history?.current?.()?.planned : undefined; if (base) planned = fit.fitDrag(snapshotNow(), base, index, fitLimits()); if (base) markEdited(); + return base !== undefined; }, }); diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Keys.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Keys.ts index b90b7fb78..42d4ea83d 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Keys.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Keys.ts @@ -29,8 +29,8 @@ interface Hold { index: number; before: Vertex[]; start: Vertex; - /** 키로 옮긴 자리(맞추기 전) — 다음 걸음은 여기서(맞춤이 쌓이지 않음). */ - at: Vertex; + /** 스냅 미리보기 전 노선(맞추기 전) — 다음 걸음 · 하한 검사는 여기서(맞춤이 쌓이지 않음). */ + raw: Vertex[] | null; moved: boolean; } @@ -41,15 +41,25 @@ export function bindRouteEditKeys(params: RouteEditPointerParams, dragging: () = 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] = [now.at[0] + dx, now.at[1] + dy]; + state.planned[now.index] = [node[0] + dx, node[1] + dy]; params.markEdited(); const after = params.state(); if ( @@ -57,6 +67,7 @@ export function bindRouteEditKeys(params: RouteEditPointerParams, dragging: () = ) { state.planned[now.index] = node; params.markEdited(); + fitPreview(now, state.planned); status.textContent = `${params.routeHead()} — 하한에 걸려 더 못 옮깁니다` + `(곡선반지름 ${state.limitRadiusM}m${state.limitArcM > 0 ? ` · 곡선 길이 ${state.limitArcM}m` : ""}).`; @@ -64,8 +75,7 @@ export function bindRouteEditKeys(params: RouteEditPointerParams, dragging: () = return; } now.moved = true; - now.at = state.planned[now.index]; - params.fitDrag(now.index); + fitPreview(now, state.planned); status.textContent = `${params.routeHead()} — 방향키로 옮기는 중(뗄 때 한 걸음). ${params.curveHint()}`; params.draw(); } @@ -112,7 +122,7 @@ export function bindRouteEditKeys(params: RouteEditPointerParams, dragging: () = index, before: state.planned.map(([x, y]): Vertex => [x, y]), start: [node[0], node[1]], - at: [node[0], node[1]], + raw: null, moved: false, }; hold = now; diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Pointer.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Pointer.ts index de3998356..f7b350137 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Pointer.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Pointer.ts @@ -101,8 +101,9 @@ export interface RouteEditPointerParams { commit: () => void; /** 스냅 간격(m · 390-5) — 스냅 켬이면 방향키 한 걸음. */ snapStepM: () => number; - /** 끄는 중 · 방향키 걸음 미리보기(390-5) — 스냅 켬이면 IP `index` 자리 그대로 MC 를 간격 배수에. */ - fitDrag: (index: number) => void; + /** 끄는 중 · 방향키 걸음 미리보기(390-5) — 스냅 켬이면 IP `index` 자리 그대로 MC 를 간격 배수에 · + * 맞췄으면 true(끔이면 false · 아무 일 없음). */ + fitDrag: (index: number) => boolean; } export interface RouteEditPointer { @@ -188,6 +189,8 @@ export function bindRouteEditPointer(params: RouteEditPointerParams): RouteEditP /** 이번 끌기에서 **실제로 움직였나** — 그냥 눌러 고르기만 한 것은 되돌릴 걸음이 아니다 * (2026-09-07 실화면: 노드를 클릭만 해도 [되돌리기]가 켜졌다). */ let dragMoved = false; + /** 스냅 미리보기 전 자리(390-5) — 하한 검사는 맞춘 선이 아닌 이것으로(맞춤이 IP 를 ±간격/2 밂). */ + let dragRaw: Vertex[] | null = null; canvas.addEventListener("pointerdown", (event) => { if (event.button !== 0) return; @@ -208,6 +211,7 @@ export function bindRouteEditPointer(params: RouteEditPointerParams): RouteEditP const hitNode = pick && "node" in pick ? pick.node : -1; // 교각점을 고정한 자리는 **고르기만** 되고 안 끌린다(2026-09-12 사용자 지시 ①). dragNode = hitNode >= 0 && state.apexLock[hitNode] !== true ? hitNode : -1; + dragRaw = null; dragHandle = hitNode >= 0 ? null : handleAt(px, py); dragMoved = false; if (hitNode >= 0) { @@ -274,6 +278,12 @@ export function bindRouteEditPointer(params: RouteEditPointerParams): RouteEditP canvas.setPointerCapture(event.pointerId); }); + /** 스냅 미리보기 — 맞추기 전 자리를 쥔 뒤 맞춤(끔이면 쥐지 않음). */ + const fitPreview = (planned: Vertex[]): void => { + const raw = planned.map((vertex): Vertex => [vertex[0], vertex[1]]); + dragRaw = params.fitDrag(dragNode) ? raw : null; + }; + canvas.addEventListener("pointermove", (event) => { const rect = canvas.getBoundingClientRect(); const [px, py] = params.unrotate(event.clientX - rect.left, event.clientY - rect.top); @@ -303,6 +313,10 @@ export function bindRouteEditPointer(params: RouteEditPointerParams): RouteEditP return; } if (dragNode >= 0) { + if (dragRaw) { + dragRaw.forEach((vertex, index) => (state.planned[index] = vertex)); + params.markEdited(); + } // 옮기기 **전**에 하한을 지키던 자리 — 이미 밑돌던 자리는 그대로 고칠 수 있어야 하므로 // **지키던 자리가 넘어가는 것만** 막는다(계획서 0-9 ④). const before = curveShortfalls(state.nodeInfo, state.limitRadiusM, state.limitArcM); @@ -320,13 +334,14 @@ export function bindRouteEditPointer(params: RouteEditPointerParams): RouteEditP // 접선 자리가 모자라 R 이 하한 아래로 눌리는 자리다 — 그 걸음만 되돌린다. state.planned[dragNode] = previous; params.markEdited(); + fitPreview(state.planned); status.textContent = `${params.routeHead()} — 하한에 걸려 더 못 옮깁니다` + `(곡선반지름 ${state.limitRadiusM}m${state.limitArcM > 0 ? ` · 곡선 길이 ${state.limitArcM}m` : ""}).`; params.draw(); return; } - params.fitDrag(dragNode); // 스냅 켬 = 끄는 동안 MC 를 간격 배수에(390-5 · 놓을 때 정본) + fitPreview(state.planned); // 스냅 켬 = 끄는 동안 MC 를 간격 배수에(390-5 · 놓을 때 정본) // 끄는 동안에도 상태줄이 살아 있어야 한다 — 예전에는 여기서 아무 말이 없어 // 「곡선이 사라졌다」는 인상만 남았다(2026-09-07 사용자 지적 ②). status.textContent = `${params.routeHead()} — 옮기는 중. ${params.curveHint()}`; @@ -370,6 +385,7 @@ export function bindRouteEditPointer(params: RouteEditPointerParams): RouteEditP dragNode = -1; dragHandle = null; dragMoved = false; + dragRaw = null; // 끄는 동안 숨겨 둔 기울기 라벨 — 놓은 노선 높이를 한 번 묻게 다시 그린다(계획서 18 ①). if (moved) { params.slopeRefresh(); diff --git a/resources/tester/test_390_5_snap_step_ops.py b/resources/tester/test_390_5_snap_step_ops.py index 8c2f210f2..3b7c3e9fe 100644 --- a/resources/tester/test_390_5_snap_step_ops.py +++ b/resources/tester/test_390_5_snap_step_ops.py @@ -98,6 +98,9 @@ def test_끌기_방향키_편집기_이음() -> None: assert "params.fitDrag(dragNode)" in pointer assert "params.fitDrag(now.index)" in keys and "params.snapStepM()" in keys assert "fit.fitDrag(snapshotNow(), base, index, fitLimits())" in edit + # 하한 검사는 맞추기 전 노선으로(맞춤이 IP 를 ±간격/2 밀어 10 m · 측점 No. 에서 헛걸림) + assert "dragRaw.forEach((vertex, index) => (state.planned[index] = vertex))" in pointer + assert "now.raw.forEach((vertex, index) => (state.planned[index] = vertex))" in keys assert edit.count("\n") <= 695