Merge remote-tracking branch 'origin/sub_laptop_6' into main_laptop_1

This commit is contained in:
2026-11-01 12:29:17 +09:00
14 changed files with 317 additions and 178 deletions
-14
View File
@@ -57,20 +57,6 @@ export const MAP_TOOLBAR: readonly (readonly MapToolRow[])[] = [
},
{ key: "snap-step", title: "스냅 간격", does: "스냅 간격 고르기(고르기 메뉴)", pages: B05 },
],
[
{
key: "expected-draw",
title: "예정노선 변경",
does: "예정노선 변경 켬끔(626-3 · 걷음 = 635-3)",
pages: B05,
},
{
key: "expected-original",
title: "예정노선 원본",
does: "예정노선(원본) 옅은 점선 켬끔(626-3 · 걷음 = 635-3)",
pages: B05,
},
],
[
{
key: "recommend",
+152 -110
View File
@@ -1,39 +1,41 @@
/* =============================================================================
* B05_Profile_UI_ExpectedDraw.ts
* B05 평면 예정노선 변경(PLAN 626-3 · 요구 530-11 · 12 · 535-1 · 536 · 결정 D3 · D4 · D9) — 도구줄 「예정노선 변경」 켬끔.
* 켬 = 예정노선이 맨 위 · 예정노선 페이지(D03) 긋기 · 고르기 · 고치기를 **그대로**(같은 함수 · 단추 · 단축키):
* 긋기 칸 `createPlanDraw`(방식 칸 · 부채꼴 · +/− 색 · Shift · Tab · Enter · Esc · Backspace · 선 끝 이어 긋기) ·
* 고르기 겹 `createPlanEdit`(노드 → 선 → 구역 → 전체 · 끌어 고르기 · 노드 끌기 · 고른 토막 끌어 꺾임점 · Delete) ·
* 우클릭 메뉴 `createPlanMenu`(여기서 긋기 시작 · 이어 긋기 · 노드 지움 · 다시 긋기) · 속성 판 `createPlanProps`([다시 긋기] ·
* [노드 지움] · [선 지움] · [구역 지움] · [전체 지움]) — 숨김 = 기능 표 「b05」(`D03_PlanAnalysis_UI_Features` · 갈래 · 둘째 선 · 연도 · 지점).
* 캔버스 누름 · 맴돎 · 두 번 클릭 · 오른쪽 클릭을 먼저 받음(키트 `bindPlanInput` capture) = 계획선 · 구조물 · 등고선 고르기 · 재기 · 노드 넣기 막음 ·
* B05 평면 예정노선 수정(PLAN 626-3 · 635-3 · 요구 530 · 535-1 · 536 · 546) — 예정노선 키트(`createPlanKit` 「b05」)를 엮음.
* 왼쪽 칸 = 키트 「위치」(주소 · 장소 찾기 → 지도 이동만) · 「그리기」(머리 「예정노선 수정」 켬끔 · 받은 노선 켬끔 ·
* 길이 비교 · [받은 노선으로]) — 자리 = `B05_Profile_UI_Panel_Plan`. 숨김 = 기능 표 「b05」(`D03_PlanAnalysis_UI_Features`).
* 켬 = 지도 누름 · 키를 먼저 받음(키트 `bindPlanInput` capture) · 다른 계획노선 그림 어둡게(`--draw-map-dim` · 장면 `overAll.dim`) ·
* 가운데 · 오른쪽 끌기(지도 이동) · 휠은 그대로. 끔 = 지금 B05 평면 편집 그대로(예정노선은 보기만).
* 한 번 고침(긋기 끝 · 놓음 · 지움) → (켠 뒤 처음만) 확인창(직접 고친 평면 버림 · 예정노선 따라 통째 다시 추적) →
* `POST /route/follow`(저장 0) → `onFollow(평면, 예정노선)` = 편집기 한 걸음(되돌리기 됨) → 페이지 [저장] = 손 편집과
* 같은 길 + 초안 `expected`(`expected_route_edit.csv`). 선이 없으면(다 지움) 평면 그대로 · 새로 그을 때까지 기다림.
* 예정노선 세 벌 = 파일입력(`expected_route.csv` · 안 덮음 · 도구줄 「예정노선(원본)」 켬끔 = 옅은 점선) · 저장본
* (`expected_edited`) · 이번 고침(`edit` · 걸음 사진에 실림).
* 고칠 선 = 받은 원본 꼭짓점(`GET /route/plan/source` · 635-2) — 고친 뒤엔 고친 꼭짓점.
* 한 번 고침(긋기 끝 · 놓음 · 지움) → (켠 뒤 처음만) 확인창 → `POST /route/follow`(저장 0) → `onFollow(평면, 예정노선)` =
* 편집기 한 걸음(되돌리기 됨) → 페이지 [저장] = 손 편집과 같은 길 + 초안 `expected`(`expected_route_edit.csv`).
* [받은 노선으로] = 받은 꼭짓점으로 다시 추적 · 초안 `expected` = [](고친 예정노선 지움) — 한 걸음 · 페이지 [초기화] 는 그대로.
* 예정노선 세 벌 = 받은 노선(`expected_route.csv` · 안 덮음 · 고쳤으면 옅은 점선) · 저장본(`expected_edited`) · 이번 고침(`edit`).
* ========================================================================== */
import { keyIs } from "../A00_Common/key_registry";
import { listenWhileAlive } from "@ui/ui_template_window_watch";
import { showToast } from "@ui/ui_template_elements";
import { createFloatPanel } from "@ui/ui_template_float_panel";
import { createButton, showToast } from "@ui/ui_template_elements";
import { askConfirm } from "@ui/ui_template_modal";
import { createText } from "@ui/ui_template_text";
import { createToggle } from "@ui/ui_template_toggle";
import { themeColor } from "@ui/ui_template_palette";
import type { ViewState } from "../B04_PreProcess/B04_PreProcess_UI_MapRender";
import {
createTmNormalizer,
normalizedToScreen,
type ViewState,
} from "../B04_PreProcess/B04_PreProcess_UI_MapRender";
import type { VWorldMeta } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
import { normOfLonLat } from "../B04_PreProcess/logic/B04_PreProcess_UI_MapRender_Prepare";
import { heightAt as gridHeightAt } from "../common_util/common_util_surface_sampler";
import type { PlanNetwork } from "../D03_PlanAnalysis/logic/D03_PlanAnalysis_Plan";
import { createPlanDraw } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Draw";
import { createPlanEdit } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Edit";
import { bindPlanInput } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Kit";
import type { PlanMapLayer, ScreenMap } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Map";
import { createPlanMenu } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Menu";
import { createPlanProps } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Props";
import { bindPlanInput, createPlanKit } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Kit";
import type { ScreenMap } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Map";
import { createPlaceFind } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Site";
import { requestJson, type RoutePlanResponse } from "./B05_Profile_Api_Replan";
import { loadDesignTerrainGrid, type DesignTerrainGrid } from "./B05_Profile_Api_TerrainGrid";
import { fillPlanPanel } from "./B05_Profile_UI_Panel_Plan";
import { markShown } from "./B05_Profile_UI_RouteEdit_Marks";
import { polylineLengthM } from "./B05_Profile_UI_RouteEdit_Render";
import type { RouteEditChrome } from "./B05_Profile_UI_RouteEdit_Chrome";
import type { MapRotation } from "./B05_Profile_UI_RouteEdit_Rotate";
import { flattenServerPlan, type FlattenedPlan } from "./logic/B05_Profile_UI_RouteEdit_Edits";
@@ -43,6 +45,7 @@ type Vertex = [number, number];
const HINT =
"[긋기] · 우클릭 「여기서 긋기 시작」 · 선 끝에서 이어 긋기 · 고르고 Delete · 노드 끌기 — 한 번 고칠 때마다 평면선을 다시 추적합니다.";
const EMPTY_HINT = "예정노선이 없습니다 — 빈 땅 우클릭 「여기서 긋기 시작」으로 새로 긋습니다.";
const OFF_HINT = "「예정노선 수정」을 켜면 지도에서 예정노선을 고칩니다.";
/** `POST /route/follow` 응답 — GET `/route/plan` 칸 이름(626-2). */
type FollowResponse = Pick<RoutePlanResponse, "planned" | "nodes" | "curves" | "node_edits">;
@@ -50,40 +53,47 @@ type FollowResponse = Pick<RoutePlanResponse, "planned" | "nodes" | "curves" | "
export interface ExpectedDrawParams {
projectId: string;
canvas: HTMLCanvasElement;
/** 평면 뼈대 — 도구줄 칸(변경 켬끔 · 원본 켬끔) · 긋기 판 자리(도구 줄) · 속성 판 · 메뉴 자리(지도 칸). */
chrome: Pick<RouteEditChrome, "overlay" | "expectedDrawButton" | "expectedOriginalButton">;
/** 평면 뼈대 — 속성 판 · 메뉴 자리(지도 칸). */
chrome: Pick<RouteEditChrome, "overlay">;
toScreen: (vertex: Vertex) => [number, number];
toMetric: (px: number, py: number) => Vertex;
/** 지도 돌림 — 보이는 px ↔ 그리기 px · 글자 세울 각(만든 뒤에 서므로 함수로). */
rotation: () => Pick<MapRotation, "unrotate" | "rerotate" | "uprightRad">;
pxPerMeter: () => number;
view: () => ViewState;
/** 바탕 틀(경위도 ↔ 화면) · 보기 바꿈 — 위치 찾기 → 지도 이동. */
frame: () => { meta: VWorldMeta | null; setView: (next: ViewState) => void };
redraw: () => void;
/** 켬 — 편집기가 고른 노드 · 곡선 판을 풂. */
onOpen: () => void;
/** 다시 추적한 평면 · 고친 예정노선 — 편집기 한 걸음. */
/** 다시 추적한 평면 · 고친 예정노선(빈 = 받은 노선으로) — 편집기 한 걸음. */
onFollow: (plan: FlattenedPlan, expected: Vertex[]) => void;
}
export interface ExpectedDraw {
/** 노선을 읽음 — 파일입력 예정노선 · 저장본(없으면 빈). */
/** 노선을 읽음 — 받은 예정노선 · 저장본(없으면 빈). */
load: (original: Vertex[], saved: Vertex[]) => void;
/** 이번 고침(걸음 사진 칸) — null = 연 때 그대로. */
/** 이번 고침(걸음 사진 칸) — null = 연 때 그대로 · [] = 받은 노선으로. */
edit: () => Vertex[] | null;
setEdit: (edit: Vertex[] | null) => void;
/** 지금 보일 예정노선(같은 배열을 돌려줌 — 재기 높이 표가 배열로 낡음을 봄). */
line: () => Vertex[];
paint: (context: CanvasRenderingContext2D) => void;
/** 아래 그림 불투명(켠 동안 `--draw-map-dim`). */
dim: () => number;
}
const sameLine = (a: ReadonlyArray<Vertex>, b: ReadonlyArray<Vertex>): boolean =>
JSON.stringify(a) === JSON.stringify(b);
const meters = (line: ReadonlyArray<Vertex>): string =>
Math.round(polylineLengthM(line)).toLocaleString();
export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
const { canvas } = params;
const { expectedDrawButton: button, expectedOriginalButton: originalButton } = params.chrome;
const viewport = params.chrome.overlay.querySelector<HTMLElement>(".b05-routeedit__canvas-wrap")!;
let original: Vertex[] = [];
/** 받은 원본 꼭짓점(635-2 · 조밀화 전) — 못 받으면 빈(= 조밀 점 `original`). */
let source: Vertex[] = [];
let saved: Vertex[] = [];
let edit: Vertex[] | null = null;
let open = false;
@@ -92,86 +102,100 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
/** 켠 뒤 확인창을 물었나 — 다시 추적 뒤 평면은 손 고침이 없음(켠 동안 계획선 안 집힘). */
let asked = false;
let grid: DesignTerrainGrid | null = null;
/** 고치는 판(D03 꼴 · 한 선) · 긋기 시작 때 판(끝나면 바뀜을 봄). */
/** 고치는 판(예정노선 꼴 · 한 선) · 판을 지은 꼭짓점 · 긋기 시작 때 판(끝나면 바뀜을 봄). */
let network: PlanNetwork = { lines: [], pieces: [] };
let base: Vertex[] = [];
let strokeBase: PlanNetwork | null = null;
const line = (): Vertex[] =>
edit === null ? (saved.length > 1 ? saved : original) : edit.length > 1 ? edit : original;
const received = (): Vertex[] => (source.length > 1 ? source : original);
const changed = (): boolean => line() !== original && original.length > 1;
const z = (x: number, y: number): number => (grid ? gridHeightAt(grid.grid, x, y) : NaN);
const fromLine = (): PlanNetwork => {
const shown = line();
const vertices = shown.map(([x, y]): Vertex => [x, y]);
return { lines: shown.length > 1 ? [{ id: "main", vertices, branch: null }] : [], pieces: [] };
base = (shown === original ? received() : shown).map(([x, y]): Vertex => [x, y]);
const vertices = base.map(([x, y]): Vertex => [x, y]);
return { lines: base.length > 1 ? [{ id: "main", vertices, branch: null }] : [], pieces: [] };
};
// ── D03 긋기 · 고르기 · 메뉴 · 속성 판(같은 함수) ─────────
const draw = createPlanDraw({
page: "b05",
network: () => network,
heightAt: () => z,
commit,
onMode,
});
const show = (next: PlanNetwork): void => {
network = next;
// ── 위치 칸 — 찾기 → 지도 가운데로(지형 받기 · 기준점 없음 · 기능 표 「terrain」 숨김) ──
// ponytail: 경위도 → 화면 = 바탕 `tm_grid` 늘려 씀(50 km 28 m · 180 km 1.7 km 어긋남) — 먼 곳까지 맞출 땐 서버 변환.
const find = createPlaceFind((hit) => {
const { meta, setView } = params.frame();
const view = params.view();
if (!meta || view.width <= 0) return;
const [sx, sy] = normalizedToScreen(
view,
...normOfLonLat(createTmNormalizer(meta), hit.lon, hit.lat),
);
setView({
...view,
offsetX: view.offsetX + view.width / 2 - sx,
offsetY: view.offsetY + view.height / 2 - sy,
});
params.redraw();
};
// `heightAt` = 625-3(놓인 노드 부호 색) 덧 칸 — 합치기 앞뒤 둘 다 맞게 칸 묶음을 따로 둠
const editParams = {
network: () => network,
base: (): Vertex[] => [],
commit,
show,
commitPoints: (_points: Vertex[], next: PlanNetwork) => commit(next),
showPoints: (_points: Vertex[], next: PlanNetwork) => show(next),
enabled: () => open && draw.mode() === "idle",
editable: () => open && !busy,
heightAt: () => z,
onSelect: (): void => (props.render(), params.redraw()),
};
const editor = createPlanEdit(editParams);
const props = createPlanProps({
});
// ── 예정노선 키트(긋기 · 고르기 · 메뉴 · 속성 판 · 칸 = 예정노선 페이지와 같은 한 벌) ──
const kit = createPlanKit({
page: "b05",
network: () => network,
edit: editor,
editable: () => !busy,
commit,
show: (next) => {
network = next;
params.redraw();
},
heightAt: () => z,
commitPoints: (_points, next) => commit(next),
respan: draw.respan,
});
const menu = createPlanMenu({
page: "b05",
network: () => network,
edit: editor,
draw,
editable: () => open && !busy,
respan: draw.respan,
idle: () => open,
onMode,
onSelect: () => (kit.props.render(), params.redraw()),
cards: { site: { parts: [find.root] } },
});
const layers: PlanMapLayer[] = [draw.layer, editor.layer, menu.layer];
const { draw, edit: editor, props, menu } = kit;
const layers = kit.layers([], []);
props.panel.classList.add("b05-routeedit__expected-props");
viewport.append(props.panel);
menu.mount(viewport);
// ── 켬끔 판 ─────────────────────────────────────────────
const panel = createFloatPanel({ title: "예정노선 변경", onClose: () => setOpen(false) });
panel.root.classList.add("b05-routeedit__expected-draw");
panel.root.hidden = true;
const hint = createText(HINT, "muted");
panel.body.append(draw.panel, hint);
params.chrome.overlay.querySelector(".b05-routeedit__tools")!.append(panel.root);
button.addEventListener("click", () => setOpen(!open));
const paintOriginalButton = (): void => {
originalButton.setAttribute("aria-pressed", String(showOriginal));
originalButton.classList.toggle("is-active", showOriginal);
};
paintOriginalButton();
originalButton.addEventListener("click", () => {
showOriginal = !showOriginal;
paintOriginalButton();
params.redraw();
// ── 그리기 칸 — 머리 「예정노선 수정」 · 받은 노선 · 길이 비교 · [받은 노선으로] ──
const drawCard = kit.cards.find((card) => card.contains(draw.panel))!;
const toggle = createToggle({ label: "예정노선 수정", size: 20, onChange: (on) => setOpen(on) });
toggle.root.addEventListener("click", (event) => event.stopPropagation()); // 칸 접기로 안 올라감
drawCard.firstElementChild!.append(toggle.root);
const originalToggle = createToggle({
label: "받은 노선",
checked: true,
size: 20,
onChange: (on) => ((showOriginal = on), params.redraw()),
});
const resetButton = createButton({
label: "받은 노선으로",
variant: "ghost",
size: 24,
onClick: () => void follow(received(), []),
});
const compare = createText("", "muted");
const hint = createText(OFF_HINT, "muted");
const row = document.createElement("div");
row.className = "b05-routeedit__expected-row";
row.append(originalToggle.root, resetButton);
drawCard.append(row, compare, hint);
fillPlanPanel({
site: kit.cards.find((card) => card.contains(find.root)) ?? null,
draw: drawCard,
});
function syncCard(): void {
const edited = changed();
resetButton.disabled = !edited || busy;
compare.textContent = edited
? `받은 노선 ${meters(received())} m → 고친 노선 ${meters(line())} m`
: "";
compare.hidden = !edited;
hint.textContent = !open ? OFF_HINT : network.lines.length ? HINT : EMPTY_HINT;
}
function setOpen(next: boolean): void {
if (draw.mode() !== "idle") draw.setMode("idle");
@@ -180,21 +204,19 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
network = fromLine();
editor.select([]);
draw.setEditable(open);
button.setAttribute("aria-pressed", String(open));
button.classList.toggle("is-active", open);
panel.root.hidden = !open;
hint.textContent = network.lines.length ? HINT : EMPTY_HINT;
toggle.set(open);
if (open) {
params.onOpen();
void loadDesignTerrainGrid(params.projectId).then(
(loaded) => ((grid = loaded), params.redraw()),
);
}
syncCard();
props.render();
params.redraw();
}
/** 꼴 한 걸음(D03 `commit`) — 긋는 중이면 판만 · 쉴 때(놓음 · 지움) = 다시 추적. */
/** 꼴 한 걸음(예정노선 `commit`) — 긋는 중이면 판만 · 쉴 때(놓음 · 지움) = 다시 추적. */
function commit(next: PlanNetwork): void {
network = next;
props.render();
@@ -208,44 +230,49 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
strokeBase ??= network;
return params.redraw();
}
const base = strokeBase;
const before = strokeBase;
strokeBase = null;
if (base && JSON.stringify(base.lines) !== JSON.stringify(network.lines)) void apply();
if (before && JSON.stringify(before.lines) !== JSON.stringify(network.lines)) void apply();
params.redraw();
}
function revert(): void {
network = fromLine();
editor.sync();
draw.sync();
props.render();
kit.sync();
syncCard();
params.redraw();
}
/** 다시 추적 — 한 선 · 점 2 이상일 때만 · 켠 뒤 처음만 확인창 → `/route/follow`(저장 0) → 편집기 한 걸음. */
/** 고친 판 → 다시 추적(한 선 · 점 2 이상 · 판을 지은 꼭짓점과 다를 때만). */
async function apply(): Promise<void> {
if (busy || !open) return;
const drawn = network.lines.filter((entry) => entry.vertices.length > 1);
hint.textContent = drawn.length ? HINT : EMPTY_HINT;
syncCard();
if (!drawn.length) return; // 다 지움 — 새로 그을 때까지 평면 그대로
if (drawn.length > 1) {
showToast("예정노선은 한 선입니다.", "info");
return revert();
}
const points = drawn[0].vertices.map(([x, y]): Vertex => [x, y]);
if (sameLine(points, line())) return;
if (!sameLine(points, base)) await follow(points, points);
}
/** 켠 뒤 처음만 확인창 → `/route/follow`(저장 0) → 편집기 한 걸음(초안 `expected` = `expected`). */
async function follow(points: Vertex[], expected: Vertex[]): Promise<void> {
if (busy || points.length < 2) return;
if (!asked) {
const ok = await askConfirm({
title: "평면선 다시 추적",
message:
"고친 예정노선을 따라 평면선을 통째로 다시 추적합니다 — 직접 고친 평면(노드 · 곡선)은 버립니다. " +
"예정노선(원본)은 그대로 · [저장] 전까지는 되돌리기로 물릴 수 있습니다.",
"받은 노선(원본)은 그대로 · [저장] 전까지는 되돌리기로 물릴 수 있습니다.",
okLabel: "다시 추적",
});
if (!ok) return revert();
asked = true;
}
busy = true;
syncCard();
params.redraw();
try {
const plan = await requestJson<FollowResponse>(
@@ -255,7 +282,7 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
);
params.onFollow(
flattenServerPlan(plan.nodes ?? [], plan.curves ?? [], plan.node_edits),
points,
expected,
);
} catch (error) {
showToast(
@@ -265,11 +292,12 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
revert();
} finally {
busy = false;
syncCard();
params.redraw();
}
}
// ── 누름 · 키 — 켠 동안 먼저 받아 D03 겹에 넘김(계획선 · 구조물 · 재기로 안 샘) ──
// ── 누름 · 키 — 켠 동안 먼저 받아 예정노선 겹에 넘김(계획선 · 구조물 · 재기로 안 샘) ──
const map = (): ScreenMap => {
const rotation = params.rotation();
return {
@@ -294,10 +322,6 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
unrotate: rotation.unrotate,
};
};
const claim = (event: Event): void => {
event.stopImmediatePropagation();
event.preventDefault();
};
bindPlanInput({
canvas,
at: (event) => {
@@ -311,10 +335,11 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
});
const onKey = (event: KeyboardEvent): void => {
if (!open || (event.target as HTMLElement | null)?.closest?.("input, select, textarea")) return;
if (!open) return;
if (keyIs(event, "plan.undo") || keyIs(event, "plan.redo")) return; // 되돌리기 = 페이지 사진첩
if (!(draw.key(event) || editor.key(event))) return;
claim(event);
if (!kit.key(event)) return;
event.stopImmediatePropagation();
event.preventDefault();
params.redraw();
};
listenWhileAlive(window, "keydown", canvas, onKey, true);
@@ -332,7 +357,7 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
context.globalAlpha = 0.7;
context.setLineDash([2, 5]);
context.lineWidth = 2;
context.strokeStyle = themeColor("--draw-text-secondary", "#9ca3af"); // 원본 = 옅은 점선
context.strokeStyle = themeColor("--draw-text-secondary", "#9ca3af"); // 받은 노선 = 옅은 점선
context.stroke();
context.restore();
}
@@ -345,7 +370,7 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
}
}
// 검증 창구 — 켬 · 긋기 모드 · 고친 판 · 보이는 예정노선 · 사업지 좌표 → 보이는 화면 px
// 검증 창구 — 켬 · 긋기 모드 · 고친 판 · 보이는 예정노선 · 받은 꼭짓점 · 사업지 좌표 → 보이는 화면 px
Object.assign(canvas, {
expectedProbe: {
state: () => ({
@@ -355,9 +380,11 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
preview: draw.preview(),
selected: editor.selected()?.kind ?? null,
busy,
compare: compare.textContent,
}),
line,
original: () => original,
source: () => source,
edit: () => edit,
screen: (vertex: Vertex) => params.rotation().rerotate(...params.toScreen(vertex)),
},
@@ -368,16 +395,31 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
original = nextOriginal;
saved = nextSaved;
network = fromLine();
syncCard();
void requestJson<{ expected_source?: Vertex[] }>(
`/projects/${params.projectId}/route/plan/source`,
{},
30000,
)
.then((body) => {
source = body.expected_source ?? [];
if (!open) network = fromLine();
syncCard();
})
.catch(() => undefined); // 못 받음 = 조밀 점 그대로
},
edit: () => edit,
setEdit(next) {
edit = next;
syncCard();
if (draw.mode() !== "idle") return; // 긋는 중 되돌림 — 긋기가 끝나면 그 판으로 다시 추적
network = fromLine();
editor.sync();
props.render();
syncCard();
},
line,
paint,
dim: () => (open ? parseFloat(themeColor("--draw-map-dim", "0.35")) || 0.35 : 1),
};
}
+47
View File
@@ -0,0 +1,47 @@
/* =============================================================================
* B05_Profile_UI_Panel_Plan.ts
* 종단 보기 왼쪽 「위치」 · 「그리기」 칸 자리(PLAN 635-3 · 요구 546) — 예정노선 키트(`createPlanKit` 「b05」) 칸을 앉힘.
* 칸은 페이지가 먼저 세우고 편집기는 노선을 읽은 뒤 섬 → 칸 = 빈 자리(order 1 · 2) · 편집기가 `fillPlanPanel` 로 채움.
* 늦게 온 쪽이 붙임 · 새 칸 자리(페이지 다시 세움) = 떠난 편집기 칸(문서 밖) 버림 · 비면 자리 숨김.
* ========================================================================== */
import { registerPanelSection } from "../A00_Common/design/design_panel";
type Slot = "site" | "draw";
const holders: Partial<Record<Slot, HTMLElement>> = {};
const cards: Partial<Record<Slot, HTMLElement | null>> = {};
const attach = (slot: Slot): void => {
const holder = holders[slot];
if (!holder) return;
const card = cards[slot];
holder.replaceChildren(...(card ? [card] : []));
holder.hidden = !card;
};
for (const [slot, order, title] of [
["site", 1, "위치"],
["draw", 2, "그리기"],
] as const)
registerPanelSection({
id: `plan-${slot}`,
title,
views: ["profile"],
order,
build: () => {
const holder = document.createElement("div");
holder.className = "b05-route__plan-slot";
holders[slot] = holder;
if (!cards[slot]?.isConnected) cards[slot] = null; // 떠난 편집기 칸
attach(slot);
return holder;
},
});
/** 키트 칸을 앉힘(새 편집기가 갈음). */
export function fillPlanPanel(next: Record<Slot, HTMLElement | null>): void {
for (const slot of ["site", "draw"] as const) {
cards[slot] = next[slot];
attach(slot);
}
}
+2 -2
View File
@@ -119,7 +119,6 @@ export async function mountRouteEditor(
const { minorContourButton, stationSnapButton } = chrome;
chrome.onRedraw(() => draw()); // 겹 켬끔 · 추천 보기 = 장면 새로(624-3)
const context = canvas.getContext("2d")!;
/** 예정노선 그리기(626-3) — 고친 예정노선 따라 평면 다시 추적 = 편집기 한 걸음 · 배선은 `_ExpectedDraw` 몫. */
const expectedDraw = createExpectedDraw({
projectId,
canvas,
@@ -129,6 +128,7 @@ export async function mountRouteEditor(
rotation: () => rotation,
pxPerMeter: () => pxPerMeter(),
view: () => view,
frame: () => ({ meta, setView: (next: ViewState) => (view = next) }),
redraw: () => draw(),
onOpen: () => ((picked = -1), syncCurveBar()),
onFollow: (flat, line) =>
@@ -347,7 +347,7 @@ export async function mountRouteEditor(
measure: measure.marks(),
crossWidth: crossPreview.watched(),
expected: expectedDraw.line(),
overAll: (ctx) => expectedDraw.paint(ctx), // 예정노선 그리기 맨 위(626-3)
overAll: expectedDraw, // 예정노선 맨 위 · 켠 동안 아래 어둡게(626-3 · 635-3)
plannedLine,
planned,
nodeInfo,
@@ -23,7 +23,8 @@ import { bottomRowNarrow, infoLiftPx } from "./logic/B05_Profile_RouteEdit_InfoL
* 아래. 횡단 두 판은 지도 오른쪽을 덮는 판(아래 종단 판이 그 위).
* 한 행(634-3 · `createMapToolRow`) = 왼쪽 공용 겹 줄(`_Layers` · 끝 = 그래픽 카드 단추) · 오른쪽 도구줄 — 영역마다 가로 밀기.
* 도구줄 차례 = 공용 표(624-3 · `A00_Common/map/map_toolbar` 「b05」) — [?] | ↶ ↷ | 3D ⟲ ⟳ | 1m 등고선 · 측점 · 기울기 ·
* 절/성토선 · 거리 재기 | 측점 스냅 · 간격(고르기 메뉴) | 예정노선 변경 · 원본(626-3) | 추천 노선 보기(626-1) | 설계 지형 칩(626-1).
* 절/성토선 · 거리 재기 | 측점 스냅 · 간격(고르기 메뉴) | 추천 노선 보기(626-1) | 설계 지형 칩(626-1).
* 예정노선 수정 = 왼쪽 「그리기」 칸(635-3 · `_ExpectedDraw`).
* 범례 = 상태 글판 위. 마우스 글 = 공용 표 호버제목(634-2 · 페이지 덮어쓰기 없음).
*
* 자리(345-3) — 모달이 아니라 종단 보기 가운데 위 본문(옛 3D 자리)에 붙는다. 덮개 · 전체 화면 ·
@@ -68,10 +69,6 @@ export interface RouteEditChrome {
stationSnapButton: HTMLButtonElement;
/** 스냅 간격 고르기 메뉴(하나 · 634-3) — 누름 배선 = `onPick`(`_SnapStep`). */
snapStep: PickMenu & { onPick: (fn: (value: string | null) => void) => void };
/** 예정노선 그리기 켬끔(626-3 · 배선 = `B05_Profile_UI_ExpectedDraw`). */
expectedDrawButton: HTMLButtonElement;
/** 예정노선(원본 · 파일입력) 옅은 점선 켬끔(626-3 · 535-1). */
expectedOriginalButton: HTMLButtonElement;
/** 설계 지형 칩(626-1) — 지형 읽은 뒤 `load`. */
terrain: RouteTerrain;
/** 추천 노선 미리보기(626-1) — 그리기 안에서 `draw`. */
@@ -280,8 +277,6 @@ export function createRouteEditChrome(
minorContourButton,
stationSnapButton,
snapStep: Object.assign(snapStep, { onPick: (fn: typeof pickSnapStep) => (pickSnapStep = fn) }),
expectedDrawButton: mapTools.button("expected-draw")!,
expectedOriginalButton: mapTools.button("expected-original")!,
terrain,
preview,
layers,
@@ -127,8 +127,8 @@ export interface RouteEditScene {
overStations?: (context: CanvasRenderingContext2D) => void;
/** 예상노선 위 · 계획노선 아래에 얹을 것(추천 노선 보기 626-1). 회전 안에서 부른다. */
underRoute?: (context: CanvasRenderingContext2D) => void;
/** 그림 맨 위(예정노선 그리기 626-3 — 고치는 예정노선 · 그은 선 · 미리보기). 회전 안에서 부른다. */
overAll?: (context: CanvasRenderingContext2D) => void;
/** 그림 맨 위(예정노선 수정 626-3 — 고치는 예정노선 · 미리보기) · `dim` = 그 아래 노선 그림 불투명(635-3). 회전 안에서. */
overAll?: { paint: (context: CanvasRenderingContext2D) => void; dim: () => number };
}
/** 글자 자리는 그대로 두고 **글자만** 되돌려 세운 채로 그린다. */
@@ -253,6 +253,8 @@ export function drawRouteEditScene(context: CanvasRenderingContext2D, scene: Rou
context.restore();
// 평면 발자국(도로 끝 · 비탈 끝 · 346-2) — 등고선 위 · 노선 아래.
const dim = scene.overAll?.dim() ?? 1; // 예정노선 수정 켬 = 발자국 ~ 구조물 마커 어둡게(635-3)
context.globalAlpha = dim;
drawFootprint(context, toScreen, style, () => drawRouteEditScene(context, scene));
strokePolyline(
context,
@@ -309,7 +311,9 @@ export function drawRouteEditScene(context: CanvasRenderingContext2D, scene: Rou
drawMeasureMarks(context, scene, line);
// 등고 높이값은 **맨 나중에** 얹는다(2026-09-12 사용자 지시 ⑦) — 노선·측점 눈금보다 먼저
// 그리면 숫자가 그 아래 깔려 안 읽힌다. 띠(300m)는 다시 씌워 그리는 범위는 그대로 둔다.
context.globalAlpha = 1; // 등고 높이값 = 노선 그림 아님
drawContourLabels(context, scene, band, style);
context.globalAlpha = dim;
// 노드 · 손잡이는 라벨 뒤 맨 위(381-3) — 측점 · 기울기 · 높이 글자에 묻혀 안 집히던 자리.
context.save();
context.fillStyle = style.getPropertyValue("--map-route") || "#f97316";
@@ -351,7 +355,8 @@ export function drawRouteEditScene(context: CanvasRenderingContext2D, scene: Rou
drawPickedCurve(context, scene, style);
drawPickHover(context, scene, line, style, NODE_R);
drawStructureMarks(context, scene, line, style); // 구조물 측점 마커 — 맨 위(409 · 교각점 · 손잡이 · 맴돔 위)
scene.overAll?.(context); // 예정노선 그리기(626-3) — 구조물 마커보다도 위
context.globalAlpha = 1;
scene.overAll?.paint(context); // 예정노선 수정(626-3) — 구조물 마커보다도 위
context.restore(); // 회전 끝
}
+1 -15
View File
@@ -474,21 +474,7 @@ html.is-bm-route .b05-routeedit__tools {
display: none;
}
/* 626-3 예정노선 그리기 판 — 도구 줄 아래(떠 있는 판) · 단추 줄 */
/* 도구 줄 칸(세로 줄) 안 차례대로 — 판 기본 absolute · 높이 100 % 를 풂 */
.b05-routeedit__expected-draw {
position: relative;
max-height: none;
pointer-events: auto;
width: 300px;
}
.b05-routeedit__expected-draw .ui-float-panel__body {
display: flex;
flex-direction: column;
gap: var(--spacing-8);
}
/* 635-3 왼쪽 「그리기」 칸 덧 줄 — 받은 노선 켬끔 · [받은 노선으로] */
.b05-routeedit__expected-row {
display: flex;
flex-wrap: wrap;
@@ -55,6 +55,8 @@ DRAW_NAMES_READ = {m for _, n in DRAW_READS for m in re.findall(r"--draw-[a-z-]+
DECL = re.compile(r"^\s*(stroke|fill|stop-color|--b06-tag)\s*:(.*);", re.M)
VAR = re.compile(r"var\((--[a-z0-9-]+)(?:\s*,\s*([^()]*(?:\([^()]*\))?[^()]*))?\)")
HEX = re.compile(r"#[0-9a-f]{6}")
#: 색이 아닌 그림 값 — 불투명(테마 안 따름 · 밝음 블록에만)
DRAW_ALPHAS = {"--draw-map-dim"}
def _blocks(css: str) -> dict[str, dict[str, str]]:
@@ -125,6 +127,9 @@ def test_draw_values_are_literal_hex():
draws = {k: v for k, v in env.items() if k.startswith("--draw-")}
assert draws, mode
for name, value in draws.items():
if name in DRAW_ALPHAS: # 불투명 값(색 아님 · 635-3)
assert 0 < float(value) < 1, (mode, name, value)
continue
assert HEX.fullmatch(value), (mode, name, value)
# 밝음 블록이 전부 정의 · 어두움 · 시스템 어두움은 그 안의 것만 덮음(고정 개수 아님)
light = {k for k in blocks["light"] if k.startswith("--draw-")}
@@ -179,6 +184,8 @@ def test_map_drawing_colors_fixed_and_readable_on_dark_map():
if re.fullmatch(r"--draw-map-(sketch|measure|warn|route-\d|slope-(up|down))", k)
]
assert len(names) == 10, names
for name in DRAW_ALPHAS: # 635-3 어둡게 = 밝음 블록 하나(어두움 · 시스템 어두움 덮지 않음)
assert name in blocks["light"] and all(name not in blocks[m] for m in ("dark", "auto"))
ground = _lum("#1f2937")
for name in names:
for mode in ("dark", "auto"):
-4
View File
@@ -55,8 +55,6 @@ def test_단추_목록_아이콘_차례():
"footprint",
"measure",
"station-snap",
"expected-draw",
"expected-original",
]
names = _icon_names()
assert all(icon in names for _, icon, _ in rows)
@@ -68,8 +66,6 @@ def test_단추_목록_아이콘_차례():
"footprint",
"measure",
"station-snap",
"expected-draw",
"expected-original",
}
+19 -18
View File
@@ -1,11 +1,11 @@
"""626-3 B05 평면 예정노선 변경 → 다시 추적 → 편집기 한 걸음 → [저장](요구 530 · 531 · 535-1 · 536 · 결정 D3 · D4 · D9).
- 예정노선 페이지(D03) 긋기 · 고르기 · 메뉴 · 속성 판 함수를 그대로 부름(숨김 = 기능 표 「b05」 — 한 선 · 갈래 · 연도 ·
지점 · 635-1) · 지도 입력 = 키트 `bindPlanInput` capture.
- 예정노선 키트 `createPlanKit` 「b05」 한 벌(긋기 · 고르기 · 메뉴 · 속성 판 · 칸 · 숨김 = 기능 표 — 한 선 · 갈래 · 연도 ·
지점 · 635-1 · 635-3) · 지도 입력 = 키트 `bindPlanInput` capture.
- 지킴(글 대조) — 다시 추적 = `/route/follow` 만(저장 0) · `/route/replan` · `as_initial` · `expected_route.csv` 안 부름 ·
결과 = 편집기 `restoreSnapshot(…, true)` 한 걸음 · [저장] = 손 편집 같은 길(`bindRouteApply`) + 초안 `expected` ·
걸음 사진 · 노선 초안에 고친 예정노선 실림(되돌리기 · 세션 되살림) · 같은 선 판정에 예정노선 · 켠 동안 캔버스 누름을
먼저 받음(재기 · 계획선 고르기 막음) · 원본 켬끔 · RouteEdit 700 줄 안.
먼저 받음(재기 · 계획선 고르기 막음) · 받은 노선 켬끔(왼쪽 「그리기」 칸 · 635-3) · RouteEdit 700 줄 안.
./venv/Scripts/python.exe -m pytest resources/tester/test_626_3_expected_edit.py -q
"""
@@ -69,18 +69,18 @@ def test_걸음_사진_초안_같은_선에_예정노선():
def test_D03_긋기_고르기_메뉴_속성판_그대로_한_선():
ui = _src(B05 / "B05_Profile_UI_ExpectedDraw.ts")
for call in ("createPlanDraw({", "createPlanEdit(", "createPlanMenu({", "createPlanProps({"):
assert call in ui, call
assert ui.count('page: "b05",') == 3 # 긋기 · 메뉴 · 속성 판 = 기능 표(635-1)
assert "createPlanKit({" in ui and ui.count('page: "b05",') == 1 # 키트 한 벌(635-3)
kit = _src(D03 / "D03_PlanAnalysis_UI_Kit.ts")
for call in ("createPlanDraw({", "createPlanEdit({", "createPlanMenu({", "createPlanProps({"):
assert call in kit, call
for fake in ("NO_POINTS", "epsg: () => 0", "addPoint: () => undefined", "oneLine"):
assert fake not in ui, fake
assert "draw.key(event) || editor.key(event)" in ui # 같은 단축키
assert "if (!kit.key(event)) return;" in ui # 같은 단축키(키트)
assert (
'keyIs(event, "plan.undo") || keyIs(event, "plan.redo")' in ui
) # 되돌리기 = 페이지 사진첩
# 지도 입력 = 키트 한 함수 · 잡기 단계(capture) — 재기 · 계획선 고르기보다 먼저
assert "capture: { active: () => open, busy: () => busy }," in ui
kit = _src(D03 / "D03_PlanAnalysis_UI_Kit.ts")
assert "canvas.addEventListener(type, run, !!capture)" in kit
assert "event.stopImmediatePropagation()" in kit
assert "event.buttons & (capture ? 6 : 4)" in kit # 오른쪽 · 가운데 끌기 = 지도 이동 그대로
@@ -114,20 +114,21 @@ def test_D03_긋기_고르기_메뉴_속성판_그대로_한_선():
("draw", "use"),
):
assert re.search(rf'key: "{key}",[^}}]*b05: "{b05}"', table, re.S), key
tools = _src(ROOT / "A00_Common" / "map" / "map_toolbar.ts")
# 634-2 — 표 줄에 호버제목(title) · 걷음은 635-3
assert re.search(
r'key: "expected-draw",\s+title: "[^"]+",\s+does: "[^"]+",\s+pages: B05', tools
)
assert re.search(
r'key: "expected-original",\s+title: "[^"]+",\s+does: "[^"]+",\s+pages: B05', tools
)
# 635-3 — 도구줄 「예정노선 변경」 · 「원본」 걷음(왼쪽 「그리기」 칸 「예정노선 수정」)
for path in (
ROOT / "A00_Common" / "map" / "map_toolbar.ts",
ROOT / "ui_template" / "ui_template_map_tools.ts",
B05 / "B05_Profile_UI_RouteEdit_Chrome.ts",
):
assert "expected-draw" not in _src(path) and "expected-original" not in _src(path), path
render = _src(B05 / "B05_Profile_UI_RouteEdit_Render.ts")
# 맨 위 = 구조물 마커 뒤 · 회전 끝 앞
assert render.index("scene.overAll?.(context);") < render.index("context.restore(); // 회전 끝")
assert render.index("scene.overAll?.paint(context);") < render.index(
"context.restore(); // 회전 끝"
)
def test_원본_예정노선_안_덮음_옅은_점선_켬끔():
def test_받은_예정노선_안_덮음_옅은_점선_켬끔():
ui = _src(B05 / "B05_Profile_UI_ExpectedDraw.ts")
assert "showOriginal && original.length > 1 && line() !== original" in ui
assert "expected_route.csv" in ui.split("*/")[0] # 머리 = 원본 안 덮음 적음
+1 -3
View File
@@ -51,8 +51,6 @@ GROUP2 = [
("measure", "길이측정"),
("station-snap", "스냅"),
("snap-step", "스냅 간격"),
("expected-draw", "예정노선 변경"),
("expected-original", "예정노선 원본"),
("recommend", "추천노선"),
("terrain", "수치지형도/라이다"),
]
@@ -73,7 +71,7 @@ def test_그룹2_표_호버():
rows = re.findall(r'key: "([\w-]+)",\s+title: "([^"]+)"', src)
assert rows == GROUP2
tools = _read(ROOT / "ui_template" / "ui_template_map_tools.ts")
for key, title in GROUP2[1:-4]:
for key, title in GROUP2[1:-2]: # 추천 · 지형 = 페이지 요소
if key != "snap-step":
assert re.search(rf'"?{key}"?: \{{ icon: "\w+", title: "{title}"', tools), key
@@ -0,0 +1,77 @@
"""635-3 B05 왼쪽 「위치」 · 「그리기」 칸 · 「예정노선 수정」 · 받은 노선 비교 · [받은 노선으로] · 어둡게(요구 546).
- 칸 = 예정노선 키트(`createPlanKit` 「b05」) 칸 · 자리 = `registerPanelSection` order 1 · 2(종단 보기) · 편집기가 채움.
- 위치 = 찾기 → 지도 이동만(지형 받기 · 기준점 없음) · 고칠 선 = 받은 원본 꼭짓점(`GET /route/plan/source` · 635-2).
- [받은 노선으로] = 받은 꼭짓점 다시 추적 + 초안 `expected` [](고친 예정노선 지움) — 한 걸음 · 페이지 [초기화] 그대로.
- 켬 = 아래 계획노선 그림 불투명 `--draw-map-dim`(0.35) · 등고 높이값 · 예정노선 그림은 그대로.
- 큰 파일 안 늘림(RouteEdit 683 · Render 648 + 10 안).
./venv/Scripts/python.exe -m pytest resources/tester/test_635_3_b05_plan_panel.py -q
"""
from __future__ import annotations
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
B05 = ROOT / "B05_Profile"
def _src(path: Path) -> str:
return path.read_text(encoding="utf-8")
def test_왼쪽_칸_둘_종단_보기_위():
panel = _src(B05 / "B05_Profile_UI_Panel_Plan.ts")
assert '["site", 1, "위치"]' in panel and '["draw", 2, "그리기"]' in panel
assert 'views: ["profile"]' in panel and "id: `plan-${slot}`" in panel
assert "export function fillPlanPanel(" in panel
assert "holder.hidden = !card;" in panel # 비면 자리 숨김
# 다른 칸(구조물 배치 10 · 페이지 설정 20)보다 위
assert "order: 10" in _src(ROOT / "A00_Common" / "design" / "design_sections.ts")
assert "order: 20" in _src(B05 / "B05_Profile_UI_Panel_Sections.ts")
def test_키트_칸_위치_찾기_이동만_원본_꼭짓점():
ui = _src(B05 / "B05_Profile_UI_ExpectedDraw.ts")
body = ui.split("*/", 1)[1]
assert 'page: "b05",' in body and "cards: { site: { parts: [find.root] } }," in body
assert "fillPlanPanel({" in body and "createPlaceFind(" in body
assert "normOfLonLat(createTmNormalizer(meta), hit.lon, hit.lat)" in body # 지도 이동만
for banned in ("createPlanTerrain", "ensureCovered", "createPlanSite", "createFloatPanel"):
assert banned not in body, banned
assert "/route/plan/source" in body and "expected_source" in body
assert "shown === original ? received() : shown" in body # 안 고친 선 = 원본 꼭짓점
features = _src(ROOT / "D03_PlanAnalysis" / "D03_PlanAnalysis_UI_Features.ts")
assert re.search(r'key: "site",[^}]*b05: "use"', features)
assert re.search(r'key: "terrain",[^}]*b05: "hide"', features, re.S)
def test_수정_켬끔_받은_노선_비교_되돌림():
body = _src(B05 / "B05_Profile_UI_ExpectedDraw.ts").split("*/", 1)[1]
assert 'label: "예정노선 수정"' in body and 'label: "받은 노선"' in body
assert "drawCard.firstElementChild!.append(toggle.root);" in body # 그리기 칸 머리
assert "event.stopPropagation()" in body # 켬끔 누름 = 칸 접기 아님
assert "받은 노선 ${meters(received())} m → 고친 노선 ${meters(line())} m" in body
assert 'label: "받은 노선으로"' in body and "follow(received(), [])" in body
assert "resetButton.disabled = !edited || busy;" in body
assert "if (!sameLine(points, base)) await follow(points, points);" in body
main = _src(B05 / "B05_Profile_UI_RouteEdit.ts")
assert "frame: () => ({ meta, setView: (next: ViewState) => (view = next) })," in main
assert len(main.splitlines()) <= 683
def test_켬_때_아래_노선_어둡게():
body = _src(B05 / "B05_Profile_UI_ExpectedDraw.ts").split("*/", 1)[1]
assert 'dim: () => (open ? parseFloat(themeColor("--draw-map-dim", "0.35"))' in body
theme = _src(ROOT / "ui_template" / "ui_template_theme.css")
assert "--draw-map-dim: 0.35;" in theme
render = _src(B05 / "B05_Profile_UI_RouteEdit_Render.ts")
dim = render.index("context.globalAlpha = dim;\n drawFootprint(")
labels = render.index("context.globalAlpha = 1; // 등고 높이값")
top = render.index("context.globalAlpha = 1;\n scene.overAll?.paint(context);")
assert dim < render.index("scene.underRoute?.(context);") < labels < top # 추천 보기도 어둡게
assert render.index("drawStructureMarks(context, scene, line, style);") < top
assert len(render.splitlines()) <= 658
assert "overAll: expectedDraw," in _src(B05 / "B05_Profile_UI_RouteEdit.ts")
-2
View File
@@ -32,8 +32,6 @@ export const MAP_TOOLS = {
footprint: { icon: "cutFill", title: "절성토선", toggle: true },
measure: { icon: "ruler", title: "길이측정", toggle: true },
"station-snap": { icon: "magnet", title: "스냅", toggle: true },
"expected-draw": { icon: "pencil", title: "예정노선 변경", toggle: true },
"expected-original": { icon: "lineDashed", title: "예정노선 원본", toggle: true },
} satisfies Record<string, { icon: IconName; title: string; toggle?: boolean }>;
export type MapToolAct = keyof typeof MAP_TOOLS;
+1
View File
@@ -608,6 +608,7 @@ html.is-bm-route .m01lab-src__body * {
--draw-map-route-2: #cf9dff;
--draw-map-route-3: #ff8cbc;
--draw-map-route-4: #9dff4d;
--draw-map-dim: 0.35; /* 불투명 — B05 예정노선 수정 켬 때 아래 계획노선 그림(테마 안 따름) */
}
:root[data-theme="dark"] {