From 8a7d972ce8ab75f96d568aee1bfec407437d9eb7 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 31 Oct 2026 22:20:13 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix(B05=20=ED=8F=89=EB=A9=B4):=20=EC=98=88?= =?UTF-8?q?=EC=A0=95=EB=85=B8=EC=84=A0=20=EA=B7=B8=EB=A6=AC=EA=B8=B0=20?= =?UTF-8?q?=E2=80=94=20=ED=99=95=EC=9D=B8=EC=B0=BD=20=EB=8F=99=EC=95=88=20?= =?UTF-8?q?=EC=83=88=20=EC=98=88=EC=A0=95=EB=85=B8=EC=84=A0=20=EB=B3=B4?= =?UTF-8?q?=EC=9E=84=20=C2=B7=20=EC=A7=80=ED=98=95=20=EA=B2=A9=EC=9E=90=20?= =?UTF-8?q?=EB=B0=9B=EC=9D=80=20=EB=92=A4=20=EB=AF=B8=EB=A6=AC=EB=B3=B4?= =?UTF-8?q?=EA=B8=B0=20=EB=8B=A4=EC=8B=9C=20=C2=B7=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EC=B0=BD=EA=B5=AC=20(PLAN=20626-3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 확인창이 떠 있는 동안 바뀔 예정노선 통째를 그림(묻는 동안 무엇이 바뀌는지) - 설계 지형 격자를 받기 전 부채꼴이 「자리 없음」으로 남던 것 — 받은 뒤 미리보기 다시 셈 - 지도 돌림을 함수 하나로 받음(RouteEdit 683 줄) · 검증 창구 `expectedProbe`(상태 · 보이는 예정노선 · 화면 px) - 예정노선 판 자리 = 도구 줄 아래(떠 있는 판 absolute · 높이 100 % 풂) Co-Authored-By: Claude Opus 5.5 --- B05_Profile/B05_Profile_UI_ExpectedDraw.ts | 45 +++++++++++++------ B05_Profile/B05_Profile_UI_RouteEdit.ts | 3 +- .../B05_Profile_UI_Style_RouteEdit.css | 3 ++ 3 files changed, 35 insertions(+), 16 deletions(-) diff --git a/B05_Profile/B05_Profile_UI_ExpectedDraw.ts b/B05_Profile/B05_Profile_UI_ExpectedDraw.ts index 4463a59fb..7fc96d09e 100644 --- a/B05_Profile/B05_Profile_UI_ExpectedDraw.ts +++ b/B05_Profile/B05_Profile_UI_ExpectedDraw.ts @@ -36,6 +36,7 @@ import { import type { DrawPreview } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Draw"; import { planColors } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Map"; import { requestJson, type RoutePlanResponse } from "./B05_Profile_Api_Replan"; +import type { MapRotation } from "./B05_Profile_UI_RouteEdit_Rotate"; import { loadDesignTerrainGrid, type DesignTerrainGrid } from "./B05_Profile_Api_TerrainGrid"; import { flattenServerPlan, type FlattenedPlan } from "./logic/B05_Profile_UI_RouteEdit_Edits"; import { @@ -63,10 +64,9 @@ export interface ExpectedDrawParams { host: HTMLElement; toScreen: (vertex: Vertex) => [number, number]; toMetric: (px: number, py: number) => Vertex; - /** 보이는 화면 px → 그리기 px(돌린 지도). */ - unrotate: (px: number, py: number) => [number, number]; + /** 지도 돌림 — 보이는 px ↔ 그리기 px · 글자 세울 각(만든 뒤에 서므로 함수로). */ + rotation: () => Pick; pxPerMeter: () => number; - uprightRad: () => number; redraw: () => void; /** 판을 켬 — 편집기가 고른 노드 · 곡선 판을 풂. */ onOpen: () => void; @@ -105,6 +105,8 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw { let busy = false; let stroke: Stroke | null = null; let preview: DrawPreview | null = null; + /** 확인창이 떠 있는 동안 보일 새 예정노선(묻는 동안 무엇이 바뀌는지 보이게). */ + let asking: Vertex[] | null = null; let fan: Fan | null = null; let hoverAt: { px: number; py: number; shift: boolean } | null = null; let grid: DesignTerrainGrid | null = null; @@ -155,7 +157,7 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw { canvas.style.removeProperty("cursor"); if (open) { params.onOpen(); - void loadDesignTerrainGrid(params.projectId).then((loaded) => (grid = loaded)); + void loadDesignTerrainGrid(params.projectId).then((loaded) => ((grid = loaded), rehover())); } params.redraw(); } @@ -268,6 +270,8 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw { /** 다시 추적 — 확인창 → `/route/follow`(저장 0) → 편집기 한 걸음. `next` = [] 면 파일입력 예정노선. */ async function follow(next: Vertex[]): Promise { stop(); + asking = next.length ? next : original; + params.redraw(); const ok = await askConfirm({ title: "평면선 다시 추적", message: @@ -276,6 +280,8 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw { "[저장] 전까지는 되돌리기로 물릴 수 있습니다.", okLabel: "다시 추적", }); + asking = null; + params.redraw(); if (!ok) return; busy = true; try { @@ -308,7 +314,7 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw { // ── 누름 · 키 — 판이 켜진 동안 먼저 받음(계획선 · 구조물 · 재기로 안 샘) ── const at = (event: MouseEvent): [number, number] => { const rect = canvas.getBoundingClientRect(); - return params.unrotate(event.clientX - rect.left, event.clientY - rect.top); + return params.rotation().unrotate(event.clientX - rect.left, event.clientY - rect.top); }; const claim = (event: Event): void => { event.stopImmediatePropagation(); @@ -376,7 +382,7 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw { toScreen: params.toScreen, pxPerMeter: 1, upright(context: CanvasRenderingContext2D, x: number, y: number, paint: () => void): void { - const radians = params.uprightRad(); + const radians = params.rotation().uprightRad(); if (!radians) return paint(); context.save(); context.translate(x, y); @@ -405,8 +411,8 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw { path(context, line()); // 고치는 예정노선 = 맨 위(D9) strokeCased(context, colors.measure, 2.5, [8, 5]); const points = strokePoints(); - if (points.length > 1) { - path(context, points); + if ((asking ?? points).length > 1) { + path(context, asking ?? points); // 묻는 동안 = 새 예정노선 통째 strokeCased(context, colors.active, 3); } points.forEach((point, i) => { @@ -421,6 +427,22 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw { context.restore(); } + const state = (): ReturnType => ({ + open, + stroke: stroke?.mode ?? null, + points: strokePoints().length, + preview, + }); + // 검증 창구 — 판 상태 · 보이는 예정노선 · 사업지 좌표 → 보이는 화면 px + Object.assign(canvas, { + expectedProbe: { + state, + line, + edit: () => edit, + screen: (vertex: Vertex) => params.rotation().rerotate(...params.toScreen(vertex)), + }, + }); + return { load(nextOriginal, nextSaved) { original = nextOriginal; @@ -432,11 +454,6 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw { }, line, paint, - state: () => ({ - open, - stroke: stroke?.mode ?? null, - points: strokePoints().length, - preview, - }), + state, }; } diff --git a/B05_Profile/B05_Profile_UI_RouteEdit.ts b/B05_Profile/B05_Profile_UI_RouteEdit.ts index 245e6fe86..a77be295d 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit.ts @@ -126,9 +126,8 @@ export async function mountRouteEditor( host: overlay.querySelector(".b05-routeedit__tools")!, toScreen: (vertex) => toScreen(vertex), toMetric: (px, py) => toMetric(px, py), - unrotate: (px, py) => rotation.unrotate(px, py), + rotation: () => rotation, pxPerMeter: () => pxPerMeter(), - uprightRad: () => rotation.uprightRad(), redraw: () => draw(), onOpen: () => ((picked = -1), syncCurveBar()), onFollow: (flat, line) => diff --git a/B05_Profile/B05_Profile_UI_Style_RouteEdit.css b/B05_Profile/B05_Profile_UI_Style_RouteEdit.css index f14502b92..c4d82b5cb 100644 --- a/B05_Profile/B05_Profile_UI_Style_RouteEdit.css +++ b/B05_Profile/B05_Profile_UI_Style_RouteEdit.css @@ -489,7 +489,10 @@ html.is-bm-route .b05-routeedit__tools { } /* 626-3 예정노선 그리기 판 — 도구 줄 아래(떠 있는 판) · 단추 줄 */ +/* 도구 줄 칸(세로 줄) 안 차례대로 — 판 기본 absolute · 높이 100 % 를 풂 */ .b05-routeedit__expected-draw { + position: relative; + max-height: none; pointer-events: auto; width: 300px; } From 3de262a4a9a9bcf98cd0659c3f2b2c15c6b2efdd Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 31 Oct 2026 22:28:41 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix(D03=20=EA=B8=8B=EA=B8=B0):=20=EB=86=93?= =?UTF-8?q?=EC=9D=B8=20=EB=85=B8=EB=93=9C=20+/=E2=88=92=20=EC=83=89=20?= =?UTF-8?q?=EB=B3=B4=EC=9D=B4=EA=B2=8C=20=E2=80=94=20=EC=84=A0=EB=B3=B4?= =?UTF-8?q?=EB=8B=A4=20=EA=B5=B5=EC=9D=80=20=EB=B6=80=ED=98=B8=20=EC=83=89?= =?UTF-8?q?=20=EC=A0=90=20+=20=EA=B2=80=EC=9D=80=20=ED=85=8C=20=C2=B7=20?= =?UTF-8?q?=EA=B3=A0=EB=A5=B8=20=EC=84=A0=EC=9D=80=20=EB=85=B8=EB=9E=91=20?= =?UTF-8?q?=EA=B0=95=EC=A1=B0=20=EC=9C=84=EC=97=90=20=EC=A0=90=20=EB=8B=A4?= =?UTF-8?q?=EC=8B=9C=20=C2=B7=20=EC=86=90=EC=9E=A1=EC=9D=B4=20=3D=20?= =?UTF-8?q?=EB=B9=88=20=EB=84=A4=EB=AA=A8=20(PLAN=20625-3=20=C2=B7=20?= =?UTF-8?q?=EC=9A=94=EA=B5=AC=20535-2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 까닭 — 놓인 점 반지름 3 px · 테 없음 = − 하늘(#29e0ff)이 노선 민트 선(#3df5c4)에 묻힘 · 고른 선은 흰 네모 손잡이와 노랑 강조(8 px)가 점을 덮음. - DrawPaint `paintNodes` 떼어 냄(점 5 px · fillCased) · Edit 가 고른 선만 강조 위에 다시 그림(heightAt 인자 덧) - 시험 test_625_draw 놓인 노드 지킴 하나 덧 Co-Authored-By: Claude Opus 5.5 --- .../D03_PlanAnalysis_UI_DrawPaint.ts | 31 +++++++++++++------ D03_PlanAnalysis/D03_PlanAnalysis_UI_Edit.ts | 22 +++++++------ D03_PlanAnalysis/D03_PlanAnalysis_UI_Page.ts | 1 + resources/tester/test_625_draw.py | 16 ++++++++++ 4 files changed, 51 insertions(+), 19 deletions(-) diff --git a/D03_PlanAnalysis/D03_PlanAnalysis_UI_DrawPaint.ts b/D03_PlanAnalysis/D03_PlanAnalysis_UI_DrawPaint.ts index c584139bd..473c58f78 100644 --- a/D03_PlanAnalysis/D03_PlanAnalysis_UI_DrawPaint.ts +++ b/D03_PlanAnalysis/D03_PlanAnalysis_UI_DrawPaint.ts @@ -36,6 +36,8 @@ import type { DrawPreview } from "./D03_PlanAnalysis_UI_Draw"; export const SNAP_PX = 12; /** 선 위 누름으로 볼 반경(px). */ export const LINE_HIT_PX = 8; +/** 놓인 꺾임점 점 반지름(px) — 선 테(3 + 4 px)보다 굵게 · 고른 선 손잡이는 이 점을 감싸는 빈 네모. */ +export const NODE_DOT_PX = 5; /** 구간 길이 라벨을 보일 화면 길이(px). */ const LABEL_MIN_PX = 70; @@ -213,24 +215,33 @@ export function paintNetwork( ); if (map.slopes) segmentLabels(context, map, line.vertices, z); }); - // 꺾임점 = 들어온 구간 기울기 부호 색(625-2 B4) · 기록과 어긋난 노드(옮김) = 경고 고리(599-2) - context.strokeStyle = colors.warn; - context.lineWidth = 2; - for (const line of network.lines) + paintNodes(context, map, network.lines, z); + drawPieces(context, map, network); + extra(); +} + +/** 꺾임점 = 들어온 구간 기울기 부호 색(625-2 B4) · 선보다 굵은 점 + 검은 테 = 선 색 · 위성 위에서도 가름(625-3) · + * 기록과 어긋난 노드(옮김) = 경고 고리(599-2) · 고른 선은 고름 강조 위에 다시 그림(`D03_PlanAnalysis_UI_Edit`). */ +export function paintNodes( + context: CanvasRenderingContext2D, + map: Pick, + lines: ReadonlyArray, + z: HeightAt, +): void { + for (const line of lines) line.vertices.forEach((v, i) => { if (!i) return; const [sx, sy] = map.toScreen(v); context.beginPath(); - context.arc(sx, sy, 3, 0, Math.PI * 2); - context.fillStyle = slopeColor(groundSlopePct(line.vertices[i - 1], v, z)); - context.fill(); + context.arc(sx, sy, NODE_DOT_PX, 0, Math.PI * 2); + fillCased(context, slopeColor(groundSlopePct(line.vertices[i - 1], v, z))); if (!nodeOff(line, i, z)) return; context.beginPath(); - context.arc(sx, sy, 7, 0, Math.PI * 2); + context.arc(sx, sy, NODE_DOT_PX + 4, 0, Math.PI * 2); + context.lineWidth = 2; + context.strokeStyle = planColors().warn; context.stroke(); }); - drawPieces(context, map, network); - extra(); } /** 지점 핀 겹(585-1) — 꼭지 = 지점 · 둥근 머리 · 「지점 N」 · 화면 px 고정 크기 · 돌린 지도에서도 섬. */ diff --git a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Edit.ts b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Edit.ts index 4feb44586..43c33632b 100644 --- a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Edit.ts +++ b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Edit.ts @@ -20,7 +20,7 @@ import { showToast } from "@ui/ui_template_elements"; import { themeColor } from "@ui/ui_template_palette"; import { keyIs } from "../A00_Common/key_registry"; -import { distanceAlong, pointAtDistance } from "./logic/D03_PlanAnalysis_Draw"; +import { distanceAlong, pointAtDistance, type HeightAt } from "./logic/D03_PlanAnalysis_Draw"; import { insertVertex, moveMark, @@ -46,7 +46,9 @@ import { } from "./logic/D03_PlanAnalysis_Select"; import { lineAt, + NODE_DOT_PX, paintMark, + paintNodes, planLabel, screenGap, SNAP_PX, @@ -70,6 +72,8 @@ export interface PlanEditParams { /** 지점(긋기에 붙는 점 · 591-2). */ base: () => Vertex[]; commit: (next: PlanNetwork) => void; + /** 원지반 높이(고른 선 꺾임점 부호 색 · 625-3). */ + heightAt: () => HeightAt; /** 끌기 중 — 화면만 바꿈(되돌리기 · 캐시 없음). */ show: (next: PlanNetwork) => void; /** 지점 자리 노드를 끎 — 지점 · 노선 한 걸음 · 화면만. */ @@ -329,18 +333,18 @@ export function createPlanEdit(params: PlanEditParams): PlanEdit { context.stroke(); strokeCased(context, colors.active, PICK_LINE_PX); } - // 꺾임점 손잡이(고른 것이 든 선) + // 꺾임점 손잡이(고른 것이 든 선) = 부호 색 점(고름 강조 위에 다시) + 감싸는 빈 네모(625-3) const lines = pickedLines(network); - context.lineWidth = 2; - context.fillStyle = "#ffffff"; - for (const line of network.lines) { - if (!lines.has(line.id)) continue; + const handled = network.lines.filter((line) => lines.has(line.id)); + paintNodes(context, map, handled, params.heightAt()); + const half = NODE_DOT_PX + 3; + for (const line of handled) for (const point of line.vertices) { const [sx, sy] = map.toScreen(point); - context.fillRect(sx - 4, sy - 4, 8, 8); - context.strokeRect(sx - 4, sy - 4, 8, 8); + context.beginPath(); + context.rect(sx - half, sy - half, half * 2, half * 2); + strokeCased(context, colors.active, 2); } - } // 고른 노드 · 마크 테 const node = picked?.kind === "node" ? picked.node : null; const nodePoint = node diff --git a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Page.ts b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Page.ts index 48689d050..77b3e017a 100644 --- a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Page.ts +++ b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Page.ts @@ -146,6 +146,7 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise { network: () => network, base: () => points, commit, + heightAt: () => z, show: (next) => { network = normalizePieces(next, yearStart); map.redraw(); diff --git a/resources/tester/test_625_draw.py b/resources/tester/test_625_draw.py index c0822d833..3bdb02a28 100644 --- a/resources/tester/test_625_draw.py +++ b/resources/tester/test_625_draw.py @@ -159,3 +159,19 @@ def test_가파른_땅_지형_밖_고를_자리_없음(r): for key in ("steep", "offTerrain"): f = r[key] assert not f["flat"] and not f["nodes"] and not f["arcs"] + + +def test_놓인_노드_부호_색_선_위_고름_위에도_보임(): + """625-3 — 놓인 꺾임점 = 선 테(3 + 4 px)보다 굵은 부호 색 점 + 검은 테 · 고른 선은 노랑 강조 위에 점을 다시 그리고 손잡이 = 빈 네모.""" + paint = (ROOT / "D03_PlanAnalysis" / "D03_PlanAnalysis_UI_DrawPaint.ts").read_text( + encoding="utf-8" + ) + edit = (ROOT / "D03_PlanAnalysis" / "D03_PlanAnalysis_UI_Edit.ts").read_text(encoding="utf-8") + assert int(re.search(r"NODE_DOT_PX = (\d+)", paint).group(1)) * 2 > 3 + 4 + body = paint[paint.index("export function paintNodes") :] + body = body[: body.index("\n}\n")] + assert "fillCased(context, slopeColor(groundSlopePct(" in body + assert "paintNodes(context, map, network.lines, z)" in paint + draw = edit[edit.index("draw(context, map) {") :] + assert draw.index("sel.paths()") < draw.index("paintNodes(") < draw.index("context.rect(") + assert "fillRect" not in draw[: draw.index("고른 노드 · 마크 테")]