diff --git a/A00_Common/map/map_toolbar.ts b/A00_Common/map/map_toolbar.ts index 1138d27f1..031d60e95 100644 --- a/A00_Common/map/map_toolbar.ts +++ b/A00_Common/map/map_toolbar.ts @@ -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", diff --git a/B05_Profile/B05_Profile_UI_ExpectedDraw.ts b/B05_Profile/B05_Profile_UI_ExpectedDraw.ts index 56bc22eb4..b0b04c79b 100644 --- a/B05_Profile/B05_Profile_UI_ExpectedDraw.ts +++ b/B05_Profile/B05_Profile_UI_ExpectedDraw.ts @@ -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; @@ -50,40 +53,47 @@ type FollowResponse = Pick; + /** 평면 뼈대 — 속성 판 · 메뉴 자리(지도 칸). */ + chrome: Pick; toScreen: (vertex: Vertex) => [number, number]; toMetric: (px: number, py: number) => Vertex; /** 지도 돌림 — 보이는 px ↔ 그리기 px · 글자 세울 각(만든 뒤에 서므로 함수로). */ rotation: () => Pick; 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, b: ReadonlyArray): boolean => JSON.stringify(a) === JSON.stringify(b); +const meters = (line: ReadonlyArray): 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(".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 { 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 { + 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( @@ -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), }; } diff --git a/B05_Profile/B05_Profile_UI_Panel_Plan.ts b/B05_Profile/B05_Profile_UI_Panel_Plan.ts new file mode 100644 index 000000000..70374a5d0 --- /dev/null +++ b/B05_Profile/B05_Profile_UI_Panel_Plan.ts @@ -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> = {}; +const cards: Partial> = {}; + +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): void { + for (const slot of ["site", "draw"] as const) { + cards[slot] = next[slot]; + attach(slot); + } +} diff --git a/B05_Profile/B05_Profile_UI_RouteEdit.ts b/B05_Profile/B05_Profile_UI_RouteEdit.ts index e1b7d29bd..829023d60 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit.ts @@ -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, diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts index 792fb709c..ec60d45b6 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts @@ -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, diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Render.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Render.ts index dd107d6b9..0264527ed 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Render.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Render.ts @@ -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(); // 회전 끝 } diff --git a/B05_Profile/B05_Profile_UI_Style_RouteEdit.css b/B05_Profile/B05_Profile_UI_Style_RouteEdit.css index 106d40063..ae3ae3715 100644 --- a/B05_Profile/B05_Profile_UI_Style_RouteEdit.css +++ b/B05_Profile/B05_Profile_UI_Style_RouteEdit.css @@ -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; diff --git a/resources/tester/test_551_2_draw_tokens.py b/resources/tester/test_551_2_draw_tokens.py index f0efc0787..b9a26b965 100644 --- a/resources/tester/test_551_2_draw_tokens.py +++ b/resources/tester/test_551_2_draw_tokens.py @@ -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"): diff --git a/resources/tester/test_568_1_map_tools.py b/resources/tester/test_568_1_map_tools.py index 2fbc11742..9cfd9653f 100644 --- a/resources/tester/test_568_1_map_tools.py +++ b/resources/tester/test_568_1_map_tools.py @@ -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", } diff --git a/resources/tester/test_626_3_expected_edit.py b/resources/tester/test_626_3_expected_edit.py index d384c9279..225d281b9 100644 --- a/resources/tester/test_626_3_expected_edit.py +++ b/resources/tester/test_626_3_expected_edit.py @@ -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] # 머리 = 원본 안 덮음 적음 diff --git a/resources/tester/test_634_2_hover_table.py b/resources/tester/test_634_2_hover_table.py index 14281f9a2..ee57a94e4 100644 --- a/resources/tester/test_634_2_hover_table.py +++ b/resources/tester/test_634_2_hover_table.py @@ -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 diff --git a/resources/tester/test_635_3_b05_plan_panel.py b/resources/tester/test_635_3_b05_plan_panel.py new file mode 100644 index 000000000..6d6fa454a --- /dev/null +++ b/resources/tester/test_635_3_b05_plan_panel.py @@ -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") diff --git a/ui_template/ui_template_map_tools.ts b/ui_template/ui_template_map_tools.ts index 4fa983ccf..a4a22155f 100644 --- a/ui_template/ui_template_map_tools.ts +++ b/ui_template/ui_template_map_tools.ts @@ -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; export type MapToolAct = keyof typeof MAP_TOOLS; diff --git a/ui_template/ui_template_theme.css b/ui_template/ui_template_theme.css index 295039dd4..83f90d0ea 100644 --- a/ui_template/ui_template_theme.css +++ b/ui_template/ui_template_theme.css @@ -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"] {