fix(B05 평면): 예정노선 그리기 — 확인창 동안 새 예정노선 보임 · 지형 격자 받은 뒤 미리보기 다시 · 검증 창구 (PLAN 626-3)

- 확인창이 떠 있는 동안 바뀔 예정노선 통째를 그림(묻는 동안 무엇이 바뀌는지)
- 설계 지형 격자를 받기 전 부채꼴이 「자리 없음」으로 남던 것 — 받은 뒤 미리보기 다시 셈
- 지도 돌림을 함수 하나로 받음(RouteEdit 683 줄) · 검증 창구 `expectedProbe`(상태 · 보이는 예정노선 · 화면 px)
- 예정노선 판 자리 = 도구 줄 아래(떠 있는 판 absolute · 높이 100 % 풂)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-31 22:20:13 +09:00
co-authored by Claude Opus 5.5
parent 84e9151b7c
commit 8a7d972ce8
3 changed files with 35 additions and 16 deletions
+31 -14
View File
@@ -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<MapRotation, "unrotate" | "rerotate" | "uprightRad">;
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<void> {
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<ExpectedDraw["state"]> => ({
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,
};
}
+1 -2
View File
@@ -126,9 +126,8 @@ export async function mountRouteEditor(
host: overlay.querySelector<HTMLElement>(".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) =>
@@ -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;
}