Merge remote-tracking branch 'origin/sub_laptop_6' into main_laptop_1
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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(); // 회전 끝
|
||||
}
|
||||
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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] # 머리 = 원본 안 덮음 적음
|
||||
|
||||
@@ -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")
|
||||
@@ -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;
|
||||
|
||||
@@ -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"] {
|
||||
|
||||
Reference in New Issue
Block a user