From 9d72d15cf015d849a8fef49b9ab42da0154a8f9d Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 17 Oct 2026 22:20:43 +0900 Subject: [PATCH] =?UTF-8?q?fix(B05=20=ED=8F=89=EB=A9=B4=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=EB=AC=BC=20=EB=A7=88=EC=BB=A4):=20=EC=97=B0=EB=8B=AC?= =?UTF-8?q?=EC=95=84=20=EB=B0=A9=ED=96=A5=ED=82=A4=20=EB=91=90=20=EA=B1=B8?= =?UTF-8?q?=EC=9D=8C=20=C2=B7=20=EC=A0=95=EB=B3=B4=20=EC=B0=BD=20=EB=B2=95?= =?UTF-8?q?=EC=84=A0=20=EB=81=9D=20=EB=B0=96=20=C2=B7=20=EC=98=A4=EB=A5=B8?= =?UTF-8?q?=EC=AA=BD=20=ED=8C=90=20=EC=95=84=EB=9E=98=20(399)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 관 목록이 늦게 따라와도 옮긴 자리 · 고름을 쥠(빠르게 두 번 = 2 m · 화면에서 한 번 빠지던 흠) - 정보 창 = 좌우 20 m 법선 끝 밖(최대 60 px) · z-index 1 로 오른쪽 횡단 판 아래 - 시험 test_399 늦게 오는 목록 갈래 더함 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte --- ...B05_Profile_UI_RouteEdit_StructureMarks.ts | 33 ++++++++++--- .../helper_399_plan_structure_marks.cjs | 47 ++++++++++++++++++- .../tester/test_399_plan_structure_marks.py | 6 +++ 3 files changed, 77 insertions(+), 9 deletions(-) diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_StructureMarks.ts b/B05_Profile/B05_Profile_UI_RouteEdit_StructureMarks.ts index d3a47406e..ef3b5d847 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_StructureMarks.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_StructureMarks.ts @@ -51,6 +51,8 @@ const MARK_HALF_PX = 5; const MARK_WIDTH_PX = 8; /** 고른 마커 바깥 테 — 법선과 같은 하늘색(391). */ const PICKED_RING = "#0ea5e9"; +/** 법선 좌우 길이(m) — `_Hover` 법선과 같은 값(391-1) · 정보 창을 그 밖으로. */ +const NORMAL_HALF_M = 20; /** 종단 보기가 건네는 것 — 알약 목록 · 종류 · 측점 간격 · 고름 · 옮김(알약과 같은 길). */ export interface PlanMarkSource { @@ -84,15 +86,22 @@ interface PlanMark { name: string; } -/** 옮기는 중(끌기 · 방향키 누름) — 놓을 때 · 뗄 때 `to` 로 한 번 옮김. 창(캔버스)마다 따로. */ +/** 옮기는 중(끌기 · 방향키 누름) — 놓을 때 · 뗄 때 `to` 로 한 번 옮김. 창(캔버스)마다 따로. + * `done` = 옮겼고 알약 목록이 아직 안 따라옴(관 목록은 늦게 옴) — 따라올 때까지 그 자리 · 그 고름 그대로. */ interface Moving { id: string; name: string; from: number; to: number; totalM: number; + done?: true; } const movings = new WeakMap(); +/** 끄는 중 · 누르는 중인 것(옮긴 뒤 기다리는 것은 아님). */ +const activeMoving = (canvas: HTMLCanvasElement): Moving | null => { + const moving = movings.get(canvas); + return moving && !moving.done ? moving : null; +}; const balloons = new WeakMap(); const lengthOf = (line: ReadonlyArray): number => @@ -112,7 +121,12 @@ function planMarks( const list = laneStructures(source.marks()); const types = new Map(source.types().map((type) => [type.type_id, type])); const texts = structurePillTexts(list, types); - const moving = movings.get(canvas) ?? null; + const held = movings.get(canvas) ?? null; + const lagging = list.some( + (structure) => structure.structure_id === held?.id && structureAnchorM(structure) !== held?.to, + ); + if (held?.done && !lagging) movings.set(canvas, null); // 목록이 따라옴(같은 id 가 없어도 끝) + const moving = held?.done && !lagging ? null : held; const marks = list .filter((structure) => structure.structure_id) .map((structure): PlanMark => { @@ -210,6 +224,7 @@ function showBalloon( const next = balloonElement(lines, mark.type); next.classList.add("is-center"); next.style.pointerEvents = "none"; + next.style.zIndex = "1"; // 오른쪽 횡단 판(z 2) · 펼친 판 아래로 — 지도 위에만 if (element) element.replaceWith(next); else canvas.parentElement.append(next); element = next; @@ -226,8 +241,12 @@ function showBalloon( element.style.left = `${x}px`; element.style.top = `${y}px`; element.style.bottom = "auto"; + // 법선(좌우 20 m) 끝 밖으로 띄움 — 크게 확대해도 60 px 넘게는 안 떨어짐 + const gap = Math.min(NORMAL_HALF_M * scene.pxPerMeter, 60) + 10; element.style.transform = - y < element.offsetHeight + 24 ? "translate(-50%, 14px)" : "translate(-50%, calc(-100% - 14px))"; + y < element.offsetHeight + gap + 10 + ? `translate(-50%, ${gap}px)` + : `translate(-50%, calc(-100% - ${gap}px))`; } export interface StructureMarkInput { @@ -280,9 +299,9 @@ export function bindStructureMarks( }; /** 놓음 · 뗌 — 옮겼으면 알약 끌기와 같은 길로 한 번. */ const finish = (): boolean => { - const moving = movings.get(canvas); + const moving = activeMoving(canvas); if (!moving) return false; - movings.set(canvas, null); + movings.set(canvas, moving.to !== moving.from ? { ...moving, done: true } : null); if (moving.to !== moving.from) { sources.get(draftProjectId())?.move(moving.id, moving.to); say(moving, true); @@ -336,7 +355,7 @@ export function bindStructureMarks( const target = event.target as HTMLElement | null; if (target?.closest?.("input, select, textarea, [contenteditable='true']")) return; if (event.repeat || holdKey || dragging() || params.measureMode()) return; - if (params.state().picked >= 0 || movings.get(canvas)) return; // 교각점 방향키 몫 + if (params.state().picked >= 0 || activeMoving(canvas)) return; // 교각점 방향키 몫 const found = planMarks(canvas); const mark = found?.marks.find((entry) => entry.id === found.picked); if (!mark) return; @@ -367,7 +386,7 @@ export function bindStructureMarks( return true; }, move(px, py) { - const moving = movings.get(canvas); + const moving = activeMoving(canvas); if (!moving || holdKey) return false; const hit = routePointAtScreen(lineNow(), params.toScreen, px, py, Infinity); const near = hit ? markChainage(hit.chainageM, moving.totalM) : moving.to; diff --git a/resources/tester/helper_399_plan_structure_marks.cjs b/resources/tester/helper_399_plan_structure_marks.cjs index 4233dd9be..2a25efea6 100644 --- a/resources/tester/helper_399_plan_structure_marks.cjs +++ b/resources/tester/helper_399_plan_structure_marks.cjs @@ -134,6 +134,7 @@ const types = [ { type_id: "box", name: "BOX", style: { color: "#e11d48" } }, { type_id: "pipe", name: "배수관", style: { color: "#2563eb" } }, ]; +let lag = false; const selects = []; const moves = []; let redrawn = 0; @@ -142,20 +143,35 @@ marks.setPlanMarkSource("p1", { types: () => types, intervalM: () => 20, select: (id) => selects.push(id), - move: (id, to) => moves.push([id, to]), + // 옮김 = 알약 목록이 그 자리로(`lag` 면 늦게 옴 — 관 목록) + move: (id, to) => { + moves.push([id, to]); + if (!lag) structures.find((s) => s.structure_id === id).chainage_m = to; + }, }); const canvas = { isConnected: true, parentElement: parent }; let line = EAST; let picked = -1; const status = { textContent: "" }; -const scene = () => ({ toScreen, view: { width: 400, height: 300 }, rotationRad: 0 }); +const scene = () => ({ + toScreen, + pxPerMeter: 2, + view: { width: 400, height: 300 }, + rotationRad: 0, +}); const style = { getPropertyValue: () => "" }; const draw = () => { const fake = fakeContext(canvas); marks.drawStructureMarks(fake.ctx, scene(), line, style); return fake.log; }; +/** 한 걸음 판정 뒤 — 그려서 기다림을 풀고 처음 자리로. */ +const settle = () => { + draw(); + structures[0].chainage_m = 40; + structures[1].chainage_m = 80.17; +}; const input = marks.bindStructureMarks( { canvas, @@ -213,16 +229,19 @@ out.dragStatus = status.textContent; out.movesBeforeRelease = moves.length; input.release(); out.dragMove = moves[moves.length - 1]; +settle(); out.dragDoneStatus = status.textContent; // 노선 밖 — 끝(200) · 시점(0)에 붙음 input.press(80, 0); input.move(9999, 0); input.release(); out.pastEnd = moves[moves.length - 1]; +settle(); input.press(80, 0); input.move(-500, 0); input.release(); out.beforeStart = moves[moves.length - 1]; +settle(); // 옛 소수 관(80.17) — 눌렀다 반올림 자리에서 떼면 안 옮김 · 1 m 끌면 정수 const count = moves.length; input.press(160.34, 0); @@ -233,18 +252,22 @@ input.press(160.34, 0); input.move(162.2, 0); input.release(); out.oldDecimalMoved = moves[moves.length - 1]; +settle(); // ④ 방향키 — 고른 마커(A) · 교각점 안 고름 = 노선 따라 1 m · 뗄 때 한 번 selection = { chainageM: 40, structureId: "A" }; key("keydown", "ArrowRight"); key("keyup", "ArrowRight"); out.keyRight = moves[moves.length - 1]; +settle(); key("keydown", "ArrowLeft"); key("keyup", "ArrowLeft"); out.keyLeft = moves[moves.length - 1]; +settle(); key("keydown", "ArrowUp"); // 노선과 직각 = → · ↑ 는 + key("keyup", "ArrowUp"); out.keyUpPerpendicular = moves[moves.length - 1]; +settle(); // 서→동이 아니라 동→서 노선 — 오른쪽(동) = 뒤로 line = [ [200, 0], @@ -253,7 +276,27 @@ line = [ key("keydown", "ArrowRight"); key("keyup", "ArrowRight"); out.keyRightWestward = moves[moves.length - 1]; +settle(); line = EAST; +// 관 목록이 늦게 옴 — 연달아 두 번 눌러도 두 걸음(옮긴 자리 · 고름을 목록이 따라올 때까지 쥠) +lag = true; +selection = { chainageM: 80.17, structureId: null }; +key("keydown", "ArrowRight"); +key("keyup", "ArrowRight"); +selection = { chainageM: 81, structureId: null }; // 관 고름이 새 자리로(followMovedPipe) +const heldLog = draw(); +out.lagHeldAt = heldLog.filter((s) => s.width === 15).map((s) => s.at); +key("keydown", "ArrowRight"); +key("keyup", "ArrowRight"); +out.lagTwoSteps = moves.slice(-2); +lag = false; +structures[1].chainage_m = 82; // 목록이 따라옴 +selection = { chainageM: 82, structureId: null }; +out.lagCaught = draw() + .filter((s) => s.width === 15) + .map((s) => s.at); +settle(); +selection = { chainageM: 40, structureId: "A" }; // 교각점 고름 = 교각점 방향키 몫(마커 안 옮김) picked = 2; const before = moves.length; diff --git a/resources/tester/test_399_plan_structure_marks.py b/resources/tester/test_399_plan_structure_marks.py index f0a6d07df..b1129ff5e 100644 --- a/resources/tester/test_399_plan_structure_marks.py +++ b/resources/tester/test_399_plan_structure_marks.py @@ -102,6 +102,12 @@ def test_방향키_노선_따라_1m(got) -> None: assert got["keyUpPerpendicular"] == ["A", 41] # 직각 = → · ↑ 가 + assert got["keyRightWestward"] == ["A", 39] # 노선이 서쪽으로 가면 → = 뒤로 assert got["keyWithNodePicked"] == 0 # 교각점 고름 = 교각점 방향키 몫 + + +def test_관_목록이_늦게_와도_연달아_두_걸음(got) -> None: + assert got["lagHeldAt"] == [[162, 0]] # 옮긴 자리 · 고름 그대로(목록은 아직 80.17) + assert got["lagTwoSteps"] == [["pipe-80.17", 81], ["pipe-80.17", 82]] + assert got["lagCaught"] == [[164, 0]] # 목록이 따라오면 목록 자리 assert got["keyNoPick"] == 0