feat(755-1·755-2): B05 예정노선 수정 — 켠 동안 판만 고침 · 끌 때 한 번 추적 · 재기 · 받은 노선 색 · 설명 · 위치 칸 걷음
- 755-1 켠 동안 끊김 허용(가운데 묶음 지움) · 고칠 때마다 다시 추적 걷음 · 끌 때 끊김 = 알림하고 안 끔 · 바뀜 = 확인창 → /route/follow 한 걸음 · 켠 동안 되돌리기 = 예정노선 판 사진첩(Ctrl+Z · 도구줄 단추 · bindHistoryControls local) · [잇기] 기능 표 b05 사용 - 755-2 켠 동안 재기 = D03 재기 겹(createPlanMeasure · 선 따라 · 두 점) · 받은 노선 = 분홍 점선 + 검은 테(기존 --draw-map-route-3 · theme.css 안 고침) · 켬 설명 걷음 · 위치 칸 걷음(기능 표 site b05 숨김 · 칸 자리 그리기 하나 · 찾기 이동 · toPlan 걷음) - 낡은 시험 고침: test_626_3(되돌리기 = 판 사진첩 · join b05 사용) · test_635_3(칸 하나 · 위치 숨김 · 끌 때 한 번 · RouteEdit 줄 한도 683 → 690) · test_635_5(찾기 이동 걷음) · 새 지킴 시험 test_755_expected_flow Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,15 @@
|
|||||||
/* =============================================================================
|
/* =============================================================================
|
||||||
* B05_Profile_UI_ExpectedDraw.ts
|
* B05_Profile_UI_ExpectedDraw.ts
|
||||||
* B05 평면 예정노선 수정(PLAN 626-3 · 635-3 · 요구 530 · 535-1 · 536 · 546) — 예정노선 키트(`createPlanKit` 「b05」)를 엮음.
|
* B05 평면 예정노선 수정(PLAN 626-3 · 635-3 · 755 · 요구 530 · 535-1 · 536 · 546 · 589) — 예정노선 키트(`createPlanKit` 「b05」)를 엮음.
|
||||||
* 왼쪽 칸 = 키트 「위치」(주소 · 장소 찾기 → 지도 이동만) · 「그리기」(머리 「예정노선 수정」 켬끔 · 받은 노선 켬끔 ·
|
* 왼쪽 칸 = 키트 「그리기」 하나(머리 「예정노선 수정」 켬끔 · 받은 노선 켬끔 · 길이 비교 · [받은 노선으로]) —
|
||||||
* 길이 비교 · [받은 노선으로]) — 자리 = `B05_Profile_UI_Panel_Plan`. 숨김 = 기능 표 「b05」(`D03_PlanAnalysis_UI_Features`).
|
* 자리 = `B05_Profile_UI_Panel_Plan`. 숨김 = 기능 표 「b05」(`D03_PlanAnalysis_UI_Features` · 위치 칸 숨김).
|
||||||
* 켬 = 지도 누름 · 키를 먼저 받음(키트 `bindPlanInput` capture) · 다른 계획노선 그림 어둡게(`--draw-map-dim` · 장면 `overAll.dim`) ·
|
* 켬 = 지도 누름 · 키를 먼저 받음(키트 `bindPlanInput` capture) · 다른 계획노선 그림 어둡게(`--draw-map-dim` · 장면 `overAll.dim`) ·
|
||||||
* 가운데 · 오른쪽 끌기(지도 이동) · 휠은 그대로. 끔 = 지금 B05 평면 편집 그대로(예정노선은 보기만).
|
* 가운데 · 오른쪽 끌기(지도 이동) · 휠은 그대로. 끔 = 지금 B05 평면 편집 그대로(예정노선은 보기만).
|
||||||
* 고칠 선 = 받은 원본 꼭짓점(`GET /route/plan/source` · 635-2) — 고친 뒤엔 고친 꼭짓점.
|
* 고칠 선 = 받은 원본 꼭짓점(`GET /route/plan/source` · 635-2) — 고친 뒤엔 고친 꼭짓점.
|
||||||
* 한 번 고침(긋기 끝 · 놓음 · 지움) → (켠 뒤 처음만) 확인창 → `POST /route/follow`(저장 0) → `onFollow(평면, 예정노선)` =
|
* 켠 동안(755) = 판만 고침 · 끊김 허용(가운데 묶음 지움) · 되돌리기 = 이 판 사진첩(Ctrl+Z · 도구줄 단추) ·
|
||||||
* 편집기 한 걸음(되돌리기 됨) → 페이지 [저장] = 손 편집과 같은 길 + 초안 `expected`(`expected_route_edit.csv`).
|
* 재기 켬 = D03 재기 겹(고치는 판 따라 · 두 점 곧은 거리 · `createPlanMeasure`).
|
||||||
|
* 끌 때 한 번 = 끊겨 있으면 알림하고 안 끔 · 바뀌었으면 확인창 → `POST /route/follow`(저장 0 · 폴리라인 두 단계) →
|
||||||
|
* `onFollow(평면, 예정노선)` = 편집기 한 걸음 → 페이지 [저장] = 손 편집과 같은 길 + 초안 `expected`(`expected_route_edit.csv`).
|
||||||
* [받은 노선으로] = 받은 꼭짓점으로 다시 추적 · 초안 `expected` = [](고친 예정노선 지움) — 한 걸음 · 페이지 [초기화] 는 그대로.
|
* [받은 노선으로] = 받은 꼭짓점으로 다시 추적 · 초안 `expected` = [](고친 예정노선 지움) — 한 걸음 · 페이지 [초기화] 는 그대로.
|
||||||
* 예정노선 세 벌 = 받은 노선(`expected_route.csv` · 안 덮음 · 고쳤으면 옅은 점선) · 저장본(`expected_edited`) · 이번 고침(`edit`).
|
* 예정노선 세 벌 = 받은 노선(`expected_route.csv` · 안 덮음 · 고쳤으면 옅은 점선) · 저장본(`expected_edited`) · 이번 고침(`edit`).
|
||||||
* 같은 예정노선 다른 세트(635-4) = 흐린 겹 보기만(`B05_Profile_UI_PlanContext`) · 그 끝 = 긋기 · 끌기 붙을 자리 · 벗어나면 안내 한 줄.
|
* 같은 예정노선 다른 세트(635-4) = 흐린 겹 보기만(`B05_Profile_UI_PlanContext`) · 그 끝 = 긋기 · 끌기 붙을 자리 · 벗어나면 안내 한 줄.
|
||||||
@@ -21,12 +23,12 @@ import { createText } from "@ui/ui_template_text";
|
|||||||
import { createToggle } from "@ui/ui_template_toggle";
|
import { createToggle } from "@ui/ui_template_toggle";
|
||||||
import { themeColor } from "@ui/ui_template_palette";
|
import { themeColor } from "@ui/ui_template_palette";
|
||||||
import type { ViewState } from "../B04_PreProcess/B04_PreProcess_UI_MapRender";
|
import type { ViewState } from "../B04_PreProcess/B04_PreProcess_UI_MapRender";
|
||||||
import type { VWorldMeta } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
|
||||||
import { heightAt as gridHeightAt } from "../common_util/common_util_surface_sampler";
|
import { heightAt as gridHeightAt } from "../common_util/common_util_surface_sampler";
|
||||||
import type { PlanNetwork } from "../D03_PlanAnalysis/logic/D03_PlanAnalysis_Plan";
|
import type { PlanNetwork } from "../D03_PlanAnalysis/logic/D03_PlanAnalysis_Plan";
|
||||||
|
import { strokeCased } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_DrawPaint";
|
||||||
import { bindPlanInput, createPlanKit } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Kit";
|
import { bindPlanInput, createPlanKit } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Kit";
|
||||||
import type { ScreenMap } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Map";
|
import type { PlanMapLayer, ScreenMap } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Map";
|
||||||
import { createPlaceFind } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Site";
|
import { createPlanMeasure } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Measure";
|
||||||
import { requestJson, type RoutePlanResponse } from "./B05_Profile_Api_Replan";
|
import { requestJson, type RoutePlanResponse } from "./B05_Profile_Api_Replan";
|
||||||
import { loadDesignTerrainGrid, type DesignTerrainGrid } from "./B05_Profile_Api_TerrainGrid";
|
import { loadDesignTerrainGrid, type DesignTerrainGrid } from "./B05_Profile_Api_TerrainGrid";
|
||||||
import { createPlanCoords, fillPlanPanel } from "./B05_Profile_UI_Panel_Plan";
|
import { createPlanCoords, fillPlanPanel } from "./B05_Profile_UI_Panel_Plan";
|
||||||
@@ -39,9 +41,6 @@ import { flattenServerPlan, type FlattenedPlan } from "./logic/B05_Profile_UI_Ro
|
|||||||
|
|
||||||
type Vertex = [number, number];
|
type Vertex = [number, number];
|
||||||
|
|
||||||
const HINT =
|
|
||||||
"[긋기] · 우클릭 「여기서 긋기 시작」 · 선 끝에서 이어 긋기 · 고르고 Delete · 노드 끌기 — 한 번 고칠 때마다 평면선을 다시 추적합니다.";
|
|
||||||
const EMPTY_HINT = "예정노선이 없습니다 — 빈 땅 우클릭 「여기서 긋기 시작」으로 새로 긋습니다.";
|
|
||||||
const OFF_HINT = "「예정노선 수정」을 켜면 지도에서 예정노선을 고칩니다.";
|
const OFF_HINT = "「예정노선 수정」을 켜면 지도에서 예정노선을 고칩니다.";
|
||||||
|
|
||||||
/** `POST /route/follow` 응답 — GET `/route/plan` 칸 이름(626-2). */
|
/** `POST /route/follow` 응답 — GET `/route/plan` 칸 이름(626-2). */
|
||||||
@@ -58,11 +57,13 @@ export interface ExpectedDrawParams {
|
|||||||
rotation: () => Pick<MapRotation, "unrotate" | "rerotate" | "uprightRad">;
|
rotation: () => Pick<MapRotation, "unrotate" | "rerotate" | "uprightRad">;
|
||||||
pxPerMeter: () => number;
|
pxPerMeter: () => number;
|
||||||
view: () => ViewState;
|
view: () => ViewState;
|
||||||
/** 바탕 틀(경위도 ↔ 화면) · 보기 바꿈 — 위치 찾기 → 지도 이동. */
|
|
||||||
frame: () => { meta: VWorldMeta | null; setView: (next: ViewState) => void };
|
|
||||||
redraw: () => void;
|
redraw: () => void;
|
||||||
/** 켬 — 편집기가 고른 노드 · 곡선 판을 풂. */
|
/** 켬 — 편집기가 고른 노드 · 곡선 판을 풂. */
|
||||||
onOpen: () => void;
|
onOpen: () => void;
|
||||||
|
/** 도구줄 재기 단추 켬 — 켠 동안 D03 재기 겹이 누름을 받음(755-2). */
|
||||||
|
measuring: () => boolean;
|
||||||
|
/** 켠 동안 사진첩이 바뀜 — 도구줄 되돌리기 단추 맞춤. */
|
||||||
|
onHistory: () => void;
|
||||||
/** 다시 추적한 평면 · 고친 예정노선(빈 = 받은 노선으로) — 편집기 한 걸음. */
|
/** 다시 추적한 평면 · 고친 예정노선(빈 = 받은 노선으로) — 편집기 한 걸음. */
|
||||||
onFollow: (plan: FlattenedPlan, expected: Vertex[]) => void;
|
onFollow: (plan: FlattenedPlan, expected: Vertex[]) => void;
|
||||||
}
|
}
|
||||||
@@ -78,6 +79,15 @@ export interface ExpectedDraw {
|
|||||||
paint: (context: CanvasRenderingContext2D) => void;
|
paint: (context: CanvasRenderingContext2D) => void;
|
||||||
/** 아래 그림 불투명(켠 동안 `--draw-map-dim`). */
|
/** 아래 그림 불투명(켠 동안 `--draw-map-dim`). */
|
||||||
dim: () => number;
|
dim: () => number;
|
||||||
|
/** 켠 동안 되돌리기 — 이 판 사진첩(끔 = null · 페이지 사진첩). */
|
||||||
|
history: () => ExpectedHistory | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExpectedHistory {
|
||||||
|
undo: () => void;
|
||||||
|
redo: () => void;
|
||||||
|
canUndo: () => boolean;
|
||||||
|
canRedo: () => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sameLine = (a: ReadonlyArray<Vertex>, b: ReadonlyArray<Vertex>): boolean =>
|
const sameLine = (a: ReadonlyArray<Vertex>, b: ReadonlyArray<Vertex>): boolean =>
|
||||||
@@ -99,10 +109,14 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
|
|||||||
/** 켠 뒤 확인창을 물었나 — 다시 추적 뒤 평면은 손 고침이 없음(켠 동안 계획선 안 집힘). */
|
/** 켠 뒤 확인창을 물었나 — 다시 추적 뒤 평면은 손 고침이 없음(켠 동안 계획선 안 집힘). */
|
||||||
let asked = false;
|
let asked = false;
|
||||||
let grid: DesignTerrainGrid | null = null;
|
let grid: DesignTerrainGrid | null = null;
|
||||||
/** 고치는 판(예정노선 꼴 · 한 선) · 판을 지은 꼭짓점 · 긋기 시작 때 판(끝나면 바뀜을 봄). */
|
/** 고치는 판(예정노선 꼴 · 켠 동안 끊김 허용) · 판을 지은 꼭짓점. */
|
||||||
let network: PlanNetwork = { lines: [], pieces: [] };
|
let network: PlanNetwork = { lines: [], pieces: [] };
|
||||||
let base: Vertex[] = [];
|
let base: Vertex[] = [];
|
||||||
let strokeBase: PlanNetwork | null = null;
|
/** 켠 동안 사진첩(755-1) — `commit` 한 번 = 한 걸음 · 판을 다시 지으면 비움. */
|
||||||
|
let undoStack: PlanNetwork[] = [];
|
||||||
|
let redoStack: PlanNetwork[] = [];
|
||||||
|
/** 재기 겹이 받던 때(켬끔 바뀜 = 잰 것 지움). */
|
||||||
|
let measured = false;
|
||||||
|
|
||||||
const line = (): Vertex[] =>
|
const line = (): Vertex[] =>
|
||||||
edit === null ? (saved.length > 1 ? saved : original) : edit.length > 1 ? edit : original;
|
edit === null ? (saved.length > 1 ? saved : original) : edit.length > 1 ? edit : original;
|
||||||
@@ -113,26 +127,20 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
|
|||||||
const shown = line();
|
const shown = line();
|
||||||
base = (shown === original ? received() : shown).map(([x, y]): Vertex => [x, y]);
|
base = (shown === original ? received() : shown).map(([x, y]): Vertex => [x, y]);
|
||||||
const vertices = base.map(([x, y]): Vertex => [x, y]);
|
const vertices = base.map(([x, y]): Vertex => [x, y]);
|
||||||
|
undoStack = [];
|
||||||
|
redoStack = [];
|
||||||
return { lines: base.length > 1 ? [{ id: "main", vertices, branch: null }] : [], pieces: [] };
|
return { lines: base.length > 1 ? [{ id: "main", vertices, branch: null }] : [], pieces: [] };
|
||||||
};
|
};
|
||||||
|
const measuring = (): boolean => open && params.measuring();
|
||||||
|
|
||||||
// ── 위치 칸 — 찾기 → 지도 가운데로(지형 받기 · 기준점 없음 · 기능 표 「terrain」 숨김) · 좌표 = 서버 변환(635-5) ──
|
// 속성 판 경위도 = 서버 변환(635-5)
|
||||||
const coords = createPlanCoords(params.projectId, () => kit.props.render());
|
const coords = createPlanCoords(params.projectId, () => kit.props.render());
|
||||||
const find = createPlaceFind((hit) => {
|
|
||||||
void coords.toPlan(hit.lon, hit.lat).then((at) => {
|
|
||||||
const view = params.view();
|
|
||||||
if (!at || view.width <= 0) return;
|
|
||||||
const [sx, sy] = params.toScreen(at);
|
|
||||||
params.frame().setView({
|
|
||||||
...view,
|
|
||||||
offsetX: view.offsetX + view.width / 2 - sx,
|
|
||||||
offsetY: view.offsetY + view.height / 2 - sy,
|
|
||||||
});
|
|
||||||
params.redraw();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const others = createPlanContextLayer(params.projectId, params.redraw);
|
const others = createPlanContextLayer(params.projectId, params.redraw);
|
||||||
|
// 재기 = 예정노선 페이지 재기 한 벌(첫 누름 선 위 = 선 따라 · 밖 = 두 점 곧은 거리 · 높이 = 설계 지형 격자)
|
||||||
|
const measure = createPlanMeasure(
|
||||||
|
() => z,
|
||||||
|
() => network,
|
||||||
|
);
|
||||||
|
|
||||||
// ── 예정노선 키트(긋기 · 고르기 · 메뉴 · 속성 판 · 칸 = 예정노선 페이지와 같은 한 벌) ──
|
// ── 예정노선 키트(긋기 · 고르기 · 메뉴 · 속성 판 · 칸 = 예정노선 페이지와 같은 한 벌) ──
|
||||||
const kit = createPlanKit({
|
const kit = createPlanKit({
|
||||||
@@ -146,21 +154,44 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
|
|||||||
heightAt: () => z,
|
heightAt: () => z,
|
||||||
lonLat: coords.lonLat,
|
lonLat: coords.lonLat,
|
||||||
editable: () => open && !busy,
|
editable: () => open && !busy,
|
||||||
idle: () => open,
|
idle: () => open && !measuring(),
|
||||||
onMode,
|
onMode: () => params.redraw(),
|
||||||
onSelect: () => (kit.props.render(), params.redraw()),
|
onSelect: () => (kit.props.render(), params.redraw()),
|
||||||
snaps: others.joints,
|
snaps: others.joints,
|
||||||
cards: { site: { parts: [find.root] } },
|
onKey: (event) =>
|
||||||
|
keyIs(event, "plan.undo")
|
||||||
|
? (step("undo"), true)
|
||||||
|
: keyIs(event, "plan.redo")
|
||||||
|
? (step("redo"), true)
|
||||||
|
: measuring() && event.key === "Escape"
|
||||||
|
? (measure.clear(), true)
|
||||||
|
: false,
|
||||||
|
cards: {},
|
||||||
});
|
});
|
||||||
const { draw, edit: editor, props, menu } = kit;
|
const { draw, edit: editor, props, menu } = kit;
|
||||||
const layers = kit.layers([], []);
|
const layers = kit.layers([], []);
|
||||||
|
const measureLayers = kit.layers([], [measure.layer]);
|
||||||
|
/** 지금 겹 — 재기 켬이면 재기 겹(고르기 · 노드 끌기 쉼) · 켬끔이 바뀌면 잰 것 지움 · 긋기 끝. */
|
||||||
|
const liveLayers = (): PlanMapLayer[] => {
|
||||||
|
const on = measuring();
|
||||||
|
if (on !== measured) {
|
||||||
|
measured = on;
|
||||||
|
measure.clear();
|
||||||
|
if (on && draw.mode() !== "idle") draw.setMode("idle");
|
||||||
|
}
|
||||||
|
return on ? measureLayers : layers;
|
||||||
|
};
|
||||||
props.panel.classList.add("b05-routeedit__expected-props");
|
props.panel.classList.add("b05-routeedit__expected-props");
|
||||||
viewport.append(props.panel);
|
viewport.append(props.panel);
|
||||||
menu.mount(viewport);
|
menu.mount(viewport);
|
||||||
|
|
||||||
// ── 그리기 칸 — 머리 「예정노선 수정」 · 받은 노선 · 길이 비교 · [받은 노선으로] ──
|
// ── 그리기 칸 — 머리 「예정노선 수정」 · 받은 노선 · 길이 비교 · [받은 노선으로] ──
|
||||||
const drawCard = kit.cards.find((card) => card.contains(draw.panel))!;
|
const drawCard = kit.cards.find((card) => card.contains(draw.panel))!;
|
||||||
const toggle = createToggle({ label: "예정노선 수정", size: 20, onChange: (on) => setOpen(on) });
|
const toggle = createToggle({
|
||||||
|
label: "예정노선 수정",
|
||||||
|
size: 20,
|
||||||
|
onChange: (on) => void setOpen(on),
|
||||||
|
});
|
||||||
toggle.root.addEventListener("click", (event) => event.stopPropagation()); // 칸 접기로 안 올라감
|
toggle.root.addEventListener("click", (event) => event.stopPropagation()); // 칸 접기로 안 올라감
|
||||||
drawCard.firstElementChild!.append(toggle.root);
|
drawCard.firstElementChild!.append(toggle.root);
|
||||||
const originalToggle = createToggle({
|
const originalToggle = createToggle({
|
||||||
@@ -182,10 +213,7 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
|
|||||||
row.className = "b05-routeedit__expected-row";
|
row.className = "b05-routeedit__expected-row";
|
||||||
row.append(originalToggle.root, resetButton);
|
row.append(originalToggle.root, resetButton);
|
||||||
drawCard.append(row, compare, jointHint, hint);
|
drawCard.append(row, compare, jointHint, hint);
|
||||||
fillPlanPanel({
|
fillPlanPanel(drawCard);
|
||||||
site: kit.cards.find((card) => card.contains(find.root)) ?? null,
|
|
||||||
draw: drawCard,
|
|
||||||
});
|
|
||||||
|
|
||||||
function syncCard(): void {
|
function syncCard(): void {
|
||||||
const edited = changed();
|
const edited = changed();
|
||||||
@@ -196,15 +224,17 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
|
|||||||
compare.hidden = !edited;
|
compare.hidden = !edited;
|
||||||
jointHint.textContent = others.note(line()) ?? "";
|
jointHint.textContent = others.note(line()) ?? "";
|
||||||
jointHint.hidden = !jointHint.textContent;
|
jointHint.hidden = !jointHint.textContent;
|
||||||
hint.textContent = !open ? OFF_HINT : network.lines.length ? HINT : EMPTY_HINT;
|
hint.hidden = open; // 켠 동안 설명 걷음(589-7)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setOpen(next: boolean): void {
|
async function setOpen(next: boolean): Promise<void> {
|
||||||
if (draw.mode() !== "idle") draw.setMode("idle");
|
if (draw.mode() !== "idle") draw.setMode("idle");
|
||||||
|
if (!next && open && !(await finish())) return toggle.set(true);
|
||||||
open = next;
|
open = next;
|
||||||
asked = false;
|
asked = false;
|
||||||
network = fromLine();
|
network = fromLine();
|
||||||
editor.select([]);
|
editor.select([]);
|
||||||
|
measure.clear();
|
||||||
draw.setEditable(open);
|
draw.setEditable(open);
|
||||||
toggle.set(open);
|
toggle.set(open);
|
||||||
if (open) {
|
if (open) {
|
||||||
@@ -215,53 +245,56 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
|
|||||||
}
|
}
|
||||||
syncCard();
|
syncCard();
|
||||||
props.render();
|
props.render();
|
||||||
|
params.onHistory();
|
||||||
params.redraw();
|
params.redraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 꼴 한 걸음(예정노선 `commit`) — 긋는 중이면 판만 · 쉴 때(놓음 · 지움) = 다시 추적. */
|
/** 꼴 한 걸음(예정노선 `commit`) — 켠 동안 판만 · 이 판 사진첩에 쌓음(평면선 = 끌 때 한 번). */
|
||||||
function commit(next: PlanNetwork): void {
|
function commit(next: PlanNetwork): void {
|
||||||
|
undoStack.push(network);
|
||||||
|
redoStack = [];
|
||||||
network = next;
|
network = next;
|
||||||
props.render();
|
kit.sync(); // 지운 고름 · 판 맞춤(예정노선 페이지 `refresh` 와 같음)
|
||||||
params.redraw();
|
params.onHistory();
|
||||||
if (draw.mode() === "idle") void apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 긋기 들어감 = 판 기억 · 긋기 끝 = 바뀌었으면 다시 추적. */
|
|
||||||
function onMode(): void {
|
|
||||||
if (draw.mode() === "draw") {
|
|
||||||
strokeBase ??= network;
|
|
||||||
return params.redraw();
|
|
||||||
}
|
|
||||||
const before = strokeBase;
|
|
||||||
strokeBase = null;
|
|
||||||
if (before && JSON.stringify(before.lines) !== JSON.stringify(network.lines)) void apply();
|
|
||||||
params.redraw();
|
params.redraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
function revert(): void {
|
function step(which: "undo" | "redo"): void {
|
||||||
network = fromLine();
|
const [from, to] = which === "undo" ? [undoStack, redoStack] : [redoStack, undoStack];
|
||||||
|
const last = from.pop();
|
||||||
|
if (!last) return;
|
||||||
|
if (draw.mode() !== "idle") draw.setMode("idle");
|
||||||
|
to.push(network);
|
||||||
|
network = last;
|
||||||
|
editor.select([]);
|
||||||
kit.sync();
|
kit.sync();
|
||||||
syncCard();
|
params.onHistory();
|
||||||
params.redraw();
|
params.redraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 고친 판 → 다시 추적(한 선 · 점 2 이상 · 판을 지은 꼭짓점과 다를 때만). */
|
/** 끌 때 한 번 — 끊겼으면 알림 · 안 끔 · 바뀌었으면 다시 추적(실패 · 물음 취소 = 안 끔). 참 = 꺼도 됨. */
|
||||||
async function apply(): Promise<void> {
|
async function finish(): Promise<boolean> {
|
||||||
if (busy || !open) return;
|
if (busy) return false;
|
||||||
const drawn = network.lines.filter((entry) => entry.vertices.length > 1);
|
const drawn = network.lines.filter((entry) => entry.vertices.length > 1);
|
||||||
syncCard();
|
|
||||||
if (!drawn.length) return; // 다 지움 — 새로 그을 때까지 평면 그대로
|
|
||||||
if (drawn.length > 1) {
|
if (drawn.length > 1) {
|
||||||
showToast("예정노선은 한 선입니다.", "info");
|
showToast(
|
||||||
return revert();
|
`예정노선이 ${drawn.length} 토막으로 끊겨 있습니다 — 선 끝에서 이어 긋거나 [잇기]로 이은 뒤 끕니다.`,
|
||||||
|
"info",
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!drawn.length) {
|
||||||
|
if (base.length > 1)
|
||||||
|
showToast("예정노선이 비어 고친 것을 버렸습니다 — 평면선 그대로.", "info");
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
const points = drawn[0].vertices.map(([x, y]): Vertex => [x, y]);
|
const points = drawn[0].vertices.map(([x, y]): Vertex => [x, y]);
|
||||||
if (!sameLine(points, base)) await follow(points, points);
|
return sameLine(points, base) || (await follow(points, points));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 켠 뒤 처음만 확인창 → `/route/follow`(저장 0) → 편집기 한 걸음(초안 `expected` = `expected`). */
|
/** 확인창(켠 뒤 처음만) → `/route/follow`(저장 0) → 편집기 한 걸음(초안 `expected` = `expected`). 참 = 됨. */
|
||||||
async function follow(points: Vertex[], expected: Vertex[]): Promise<void> {
|
async function follow(points: Vertex[], expected: Vertex[]): Promise<boolean> {
|
||||||
if (busy || points.length < 2) return;
|
if (busy || points.length < 2) return false;
|
||||||
if (!asked) {
|
if (!asked) {
|
||||||
const ok = await askConfirm({
|
const ok = await askConfirm({
|
||||||
title: "평면선 다시 추적",
|
title: "평면선 다시 추적",
|
||||||
@@ -270,7 +303,7 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
|
|||||||
"받은 노선(원본)은 그대로 · [저장] 전까지는 되돌리기로 물릴 수 있습니다.",
|
"받은 노선(원본)은 그대로 · [저장] 전까지는 되돌리기로 물릴 수 있습니다.",
|
||||||
okLabel: "다시 추적",
|
okLabel: "다시 추적",
|
||||||
});
|
});
|
||||||
if (!ok) return revert();
|
if (!ok) return false;
|
||||||
asked = true;
|
asked = true;
|
||||||
}
|
}
|
||||||
busy = true;
|
busy = true;
|
||||||
@@ -286,12 +319,13 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
|
|||||||
flattenServerPlan(plan.nodes ?? [], plan.curves ?? [], plan.node_edits),
|
flattenServerPlan(plan.nodes ?? [], plan.curves ?? [], plan.node_edits),
|
||||||
expected,
|
expected,
|
||||||
);
|
);
|
||||||
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast(
|
showToast(
|
||||||
`평면선을 다시 추적하지 못했습니다.${error instanceof Error ? ` ${error.message}` : ""}`,
|
`평면선을 다시 추적하지 못했습니다.${error instanceof Error ? ` ${error.message}` : ""}`,
|
||||||
"error",
|
"error",
|
||||||
);
|
);
|
||||||
revert();
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
busy = false;
|
busy = false;
|
||||||
syncCard();
|
syncCard();
|
||||||
@@ -331,14 +365,13 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
|
|||||||
return params.rotation().unrotate(event.clientX - rect.left, event.clientY - rect.top);
|
return params.rotation().unrotate(event.clientX - rect.left, event.clientY - rect.top);
|
||||||
},
|
},
|
||||||
screen: map,
|
screen: map,
|
||||||
layers: () => layers,
|
layers: liveLayers,
|
||||||
redraw: params.redraw,
|
redraw: params.redraw,
|
||||||
capture: { active: () => open, busy: () => busy },
|
capture: { active: () => open, busy: () => busy },
|
||||||
});
|
});
|
||||||
|
|
||||||
const onKey = (event: KeyboardEvent): void => {
|
const onKey = (event: KeyboardEvent): void => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
if (keyIs(event, "plan.undo") || keyIs(event, "plan.redo")) return; // 되돌리기 = 페이지 사진첩
|
|
||||||
if (!kit.key(event)) return;
|
if (!kit.key(event)) return;
|
||||||
event.stopImmediatePropagation();
|
event.stopImmediatePropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -357,16 +390,15 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
|
|||||||
if (i) context.lineTo(sx, sy);
|
if (i) context.lineTo(sx, sy);
|
||||||
else context.moveTo(sx, sy);
|
else context.moveTo(sx, sy);
|
||||||
});
|
});
|
||||||
context.globalAlpha = 0.7;
|
context.lineCap = "round";
|
||||||
context.setLineDash([2, 5]);
|
context.lineJoin = "round";
|
||||||
context.lineWidth = 2;
|
// 받은 노선 = 분홍 점선 + 검은 테(589-6 · 계획 주황 · 예정 민트 · 추천 노랑 · 등고선과 가름 · 테마 안 따름)
|
||||||
context.strokeStyle = themeColor("--draw-text-secondary", "#9ca3af"); // 받은 노선 = 옅은 점선
|
strokeCased(context, themeColor("--draw-map-route-3", "#ff8cbc"), 2.5, [8, 5]);
|
||||||
context.stroke();
|
|
||||||
context.restore();
|
context.restore();
|
||||||
}
|
}
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
const screen = map();
|
const screen = map();
|
||||||
for (const layer of layers) {
|
for (const layer of liveLayers()) {
|
||||||
context.save();
|
context.save();
|
||||||
layer.draw(context, screen); // 고치는 예정노선 = 맨 위(D9) · 고름 · 미리보기
|
layer.draw(context, screen); // 고치는 예정노선 = 맨 위(D9) · 고름 · 미리보기
|
||||||
context.restore();
|
context.restore();
|
||||||
@@ -385,9 +417,14 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
|
|||||||
busy,
|
busy,
|
||||||
compare: compare.textContent,
|
compare: compare.textContent,
|
||||||
joint: jointHint.textContent,
|
joint: jointHint.textContent,
|
||||||
|
undo: undoStack.length,
|
||||||
|
redo: redoStack.length,
|
||||||
|
measuring: measuring(),
|
||||||
}),
|
}),
|
||||||
|
measure: () => measure.result(),
|
||||||
others: others.probe,
|
others: others.probe,
|
||||||
line,
|
line,
|
||||||
|
network: () => network,
|
||||||
original: () => original,
|
original: () => original,
|
||||||
source: () => source,
|
source: () => source,
|
||||||
edit: () => edit,
|
edit: () => edit,
|
||||||
@@ -418,7 +455,7 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
|
|||||||
setEdit(next) {
|
setEdit(next) {
|
||||||
edit = next;
|
edit = next;
|
||||||
syncCard();
|
syncCard();
|
||||||
if (draw.mode() !== "idle") return; // 긋는 중 되돌림 — 긋기가 끝나면 그 판으로 다시 추적
|
if (draw.mode() !== "idle") return; // 긋는 중 = 긋기 판 그대로
|
||||||
network = fromLine();
|
network = fromLine();
|
||||||
editor.sync();
|
editor.sync();
|
||||||
props.render();
|
props.render();
|
||||||
@@ -427,5 +464,14 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
|
|||||||
line,
|
line,
|
||||||
paint,
|
paint,
|
||||||
dim: () => (open ? parseFloat(themeColor("--draw-map-dim", "0.35")) || 0.35 : 1),
|
dim: () => (open ? parseFloat(themeColor("--draw-map-dim", "0.35")) || 0.35 : 1),
|
||||||
|
history: () =>
|
||||||
|
open
|
||||||
|
? {
|
||||||
|
undo: () => step("undo"),
|
||||||
|
redo: () => step("redo"),
|
||||||
|
canUndo: () => undoStack.length > 0,
|
||||||
|
canRedo: () => redoStack.length > 0,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
/* =============================================================================
|
/* =============================================================================
|
||||||
* B05_Profile_UI_Panel_Plan.ts
|
* B05_Profile_UI_Panel_Plan.ts
|
||||||
* 종단 보기 왼쪽 「위치」 · 「그리기」 칸 자리(PLAN 635-3 · 요구 546) — 예정노선 키트(`createPlanKit` 「b05」) 칸을 앉힘.
|
* 종단 보기 왼쪽 「그리기」 칸 자리(PLAN 635-3 · 755-2 · 요구 546 · 589-8 위치 칸 걷음) — 예정노선 키트(`createPlanKit` 「b05」) 칸을 앉힘.
|
||||||
* 칸은 페이지가 먼저 세우고 편집기는 노선을 읽은 뒤 섬 → 칸 = 빈 자리(order 1 · 2) · 편집기가 `fillPlanPanel` 로 채움.
|
* 칸은 페이지가 먼저 세우고 편집기는 노선을 읽은 뒤 섬 → 칸 = 빈 자리(order 1) · 편집기가 `fillPlanPanel` 로 채움.
|
||||||
* 늦게 온 쪽이 붙임 · 새 칸 자리(페이지 다시 세움) = 떠난 편집기 칸(문서 밖) 버림 · 비면 자리 숨김.
|
* 늦게 온 쪽이 붙임 · 새 칸 자리(페이지 다시 세움) = 떠난 편집기 칸(문서 밖) 버림 · 비면 자리 숨김.
|
||||||
* 좌표 변환(635-5) = 서버(`POST /route/coords` · 사업지 좌표계 pyproj) — 찾기 이동 · 속성 판 경위도 때만 부름.
|
* 좌표 변환(635-5) = 서버(`POST /route/coords` · 사업지 좌표계 pyproj) — 속성 판 경위도 때만 부름.
|
||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
|
|
||||||
import { registerPanelSection } from "../A00_Common/design/design_panel";
|
import { registerPanelSection } from "../A00_Common/design/design_panel";
|
||||||
@@ -11,55 +11,43 @@ import { requestJson } from "./B05_Profile_Api_Replan";
|
|||||||
|
|
||||||
type Vertex = [number, number];
|
type Vertex = [number, number];
|
||||||
|
|
||||||
type Slot = "site" | "draw";
|
let holder: HTMLElement | null = null;
|
||||||
const holders: Partial<Record<Slot, HTMLElement>> = {};
|
let card: HTMLElement | null = null;
|
||||||
const cards: Partial<Record<Slot, HTMLElement | null>> = {};
|
|
||||||
|
|
||||||
const attach = (slot: Slot): void => {
|
const attach = (): void => {
|
||||||
const holder = holders[slot];
|
|
||||||
if (!holder) return;
|
if (!holder) return;
|
||||||
const card = cards[slot];
|
|
||||||
holder.replaceChildren(...(card ? [card] : []));
|
holder.replaceChildren(...(card ? [card] : []));
|
||||||
holder.hidden = !card;
|
holder.hidden = !card;
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const [slot, order, title] of [
|
registerPanelSection({
|
||||||
["site", 1, "위치"],
|
id: "plan-draw",
|
||||||
["draw", 2, "그리기"],
|
title: "그리기",
|
||||||
] as const)
|
views: ["profile"],
|
||||||
registerPanelSection({
|
order: 1,
|
||||||
id: `plan-${slot}`,
|
build: () => {
|
||||||
title,
|
holder = document.createElement("div");
|
||||||
views: ["profile"],
|
holder.className = "b05-route__plan-slot";
|
||||||
order,
|
if (!card?.isConnected) card = null; // 떠난 편집기 칸
|
||||||
build: () => {
|
attach();
|
||||||
const holder = document.createElement("div");
|
return holder;
|
||||||
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 {
|
export function fillPlanPanel(next: HTMLElement | null): void {
|
||||||
for (const slot of ["site", "draw"] as const) {
|
card = next;
|
||||||
cards[slot] = next[slot];
|
attach();
|
||||||
attach(slot);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlanCoords {
|
export interface PlanCoords {
|
||||||
/** 경위도 → 사업지 [x, y] m(못 받으면 null). */
|
|
||||||
toPlan: (lon: number, lat: number) => Promise<Vertex | null>;
|
|
||||||
/** 사업지 → 경위도 — 받은 점이면 바로 · 처음이면 null 주고 한 번 물음(오면 `onLonLat`). */
|
/** 사업지 → 경위도 — 받은 점이면 바로 · 처음이면 null 주고 한 번 물음(오면 `onLonLat`). */
|
||||||
lonLat: (x: number, y: number) => Vertex | null;
|
lonLat: (x: number, y: number) => Vertex | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 경위도 ↔ 사업지 좌표 서버 변환 · 같은 점은 캐시(끌기 중엔 안 부름 — 판은 고름 · 놓음 때만 그림). */
|
/** 사업지 → 경위도 서버 변환 · 같은 점은 캐시(끌기 중엔 안 부름 — 판은 고름 · 놓음 때만 그림). */
|
||||||
export function createPlanCoords(projectId: string, onLonLat: () => void): PlanCoords {
|
export function createPlanCoords(projectId: string, onLonLat: () => void): PlanCoords {
|
||||||
const ask = (to: "plan" | "lonlat", point: Vertex): Promise<Vertex | null> =>
|
const ask = (to: "lonlat", point: Vertex): Promise<Vertex | null> =>
|
||||||
requestJson<{ points: Vertex[] }>(
|
requestJson<{ points: Vertex[] }>(
|
||||||
`/projects/${projectId}/route/coords`,
|
`/projects/${projectId}/route/coords`,
|
||||||
{ method: "POST", body: JSON.stringify({ to, points: [point] }) },
|
{ method: "POST", body: JSON.stringify({ to, points: [point] }) },
|
||||||
@@ -70,7 +58,6 @@ export function createPlanCoords(projectId: string, onLonLat: () => void): PlanC
|
|||||||
);
|
);
|
||||||
const known = new Map<string, Vertex | null>();
|
const known = new Map<string, Vertex | null>();
|
||||||
return {
|
return {
|
||||||
toPlan: (lon, lat) => ask("plan", [lon, lat]),
|
|
||||||
lonLat(x, y) {
|
lonLat(x, y) {
|
||||||
const key = `${x},${y}`;
|
const key = `${x},${y}`;
|
||||||
if (known.has(key)) return known.get(key) ?? null;
|
if (known.has(key)) return known.get(key) ?? null;
|
||||||
|
|||||||
@@ -128,9 +128,10 @@ export async function mountRouteEditor(
|
|||||||
rotation: () => rotation,
|
rotation: () => rotation,
|
||||||
pxPerMeter: () => pxPerMeter(),
|
pxPerMeter: () => pxPerMeter(),
|
||||||
view: () => view,
|
view: () => view,
|
||||||
frame: () => ({ meta, setView: (next: ViewState) => (view = next) }),
|
|
||||||
redraw: () => draw(),
|
redraw: () => draw(),
|
||||||
onOpen: () => ((picked = -1), syncCurveBar()),
|
onOpen: () => ((picked = -1), syncCurveBar()),
|
||||||
|
measuring: () => measureControls.mode(), // 켠 동안 재기 = D03 재기 겹(755-2)
|
||||||
|
onHistory: () => historyControls.sync(), // 켠 동안 되돌리기 = 예정노선 판(755-1)
|
||||||
onFollow: (flat, line) =>
|
onFollow: (flat, line) =>
|
||||||
restoreSnapshot(
|
restoreSnapshot(
|
||||||
{ ...flat, apexLock: [], picked: -1, rec: initial.tag(), expected: line },
|
{ ...flat, apexLock: [], picked: -1, rec: initial.tag(), expected: line },
|
||||||
@@ -500,6 +501,7 @@ export async function mountRouteEditor(
|
|||||||
const historyControls = bindHistoryControls({
|
const historyControls = bindHistoryControls({
|
||||||
overlay,
|
overlay,
|
||||||
getHistory: () => history,
|
getHistory: () => history,
|
||||||
|
local: () => expectedDraw.history(),
|
||||||
restore: (snapshot, message) => {
|
restore: (snapshot, message) => {
|
||||||
initial.restoreTag(snapshot.rec); // 「현재 추천」도 그 걸음 것으로 — 노선과 짝.
|
initial.restoreTag(snapshot.rec); // 「현재 추천」도 그 걸음 것으로 — 노선과 짝.
|
||||||
restoreSnapshot(snapshot, message, false);
|
restoreSnapshot(snapshot, message, false);
|
||||||
|
|||||||
@@ -143,6 +143,13 @@ export interface HistoryControlsParams {
|
|||||||
overlay: HTMLElement;
|
overlay: HTMLElement;
|
||||||
/** 아직 노선을 못 읽었으면 null 이다(단추는 꺼진 채로 둔다). */
|
/** 아직 노선을 못 읽었으면 null 이다(단추는 꺼진 채로 둔다). */
|
||||||
getHistory: () => RouteEditHistory | null;
|
getHistory: () => RouteEditHistory | null;
|
||||||
|
/** 예정노선 수정 켠 동안 사진첩(755-1) — 있으면 단추가 이쪽을 씀. */
|
||||||
|
local?: () => {
|
||||||
|
undo: () => void;
|
||||||
|
redo: () => void;
|
||||||
|
canUndo: () => boolean;
|
||||||
|
canRedo: () => boolean;
|
||||||
|
} | null;
|
||||||
/** 사진 한 벌을 화면에 되살린다. */
|
/** 사진 한 벌을 화면에 되살린다. */
|
||||||
restore: (snapshot: RouteEditSnapshot, message: string) => void;
|
restore: (snapshot: RouteEditSnapshot, message: string) => void;
|
||||||
}
|
}
|
||||||
@@ -162,12 +169,14 @@ export function bindHistoryControls(params: HistoryControlsParams): {
|
|||||||
const redoBtn = overlay.querySelector<HTMLButtonElement>('[data-act="redo"]')!;
|
const redoBtn = overlay.querySelector<HTMLButtonElement>('[data-act="redo"]')!;
|
||||||
|
|
||||||
const sync = (): void => {
|
const sync = (): void => {
|
||||||
const history = getHistory();
|
const history = params.local?.() ?? getHistory();
|
||||||
undoBtn.disabled = !history?.canUndo();
|
undoBtn.disabled = !history?.canUndo();
|
||||||
redoBtn.disabled = !history?.canRedo();
|
redoBtn.disabled = !history?.canRedo();
|
||||||
};
|
};
|
||||||
|
|
||||||
const step = (which: "undo" | "redo"): void => {
|
const step = (which: "undo" | "redo"): void => {
|
||||||
|
const local = params.local?.();
|
||||||
|
if (local) return (local[which](), sync());
|
||||||
const history = getHistory();
|
const history = getHistory();
|
||||||
if (!history) return;
|
if (!history) return;
|
||||||
const snapshot = history[which]();
|
const snapshot = history[which]();
|
||||||
|
|||||||
@@ -34,7 +34,12 @@ export interface PlanFeatureRow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const PLAN_FEATURES: readonly PlanFeatureRow[] = [
|
export const PLAN_FEATURES: readonly PlanFeatureRow[] = [
|
||||||
{ key: "site", name: "주소 · 장소 찾기(B05 = 지도 이동만)", pages: { d03: "use", b05: "use" } },
|
{
|
||||||
|
key: "site",
|
||||||
|
name: "주소 · 장소 찾기",
|
||||||
|
pages: { d03: "use", b05: "hide" },
|
||||||
|
why: "B 사업지 = 전처리 확정",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "draw",
|
key: "draw",
|
||||||
name: "긋기 네 방식 · 길이 · 기울기 · Tab · Shift · 부채꼴 · ± 색",
|
name: "긋기 네 방식 · 길이 · 기울기 · Tab · Shift · 부채꼴 · ± 색",
|
||||||
@@ -92,9 +97,8 @@ export const PLAN_FEATURES: readonly PlanFeatureRow[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "join",
|
key: "join",
|
||||||
name: "[잇기] 두 선 끝 이음",
|
name: "[잇기] 두 선 끝 이음(B05 = 켠 동안 끊긴 곳 이음)",
|
||||||
pages: { d03: "use", b05: "hide" },
|
pages: { d03: "use", b05: "use" },
|
||||||
why: "B 계획선 한 줄",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "others",
|
key: "others",
|
||||||
|
|||||||
@@ -76,9 +76,7 @@ def test_D03_긋기_고르기_메뉴_속성판_그대로_한_선():
|
|||||||
for fake in ("NO_POINTS", "epsg: () => 0", "addPoint: () => undefined", "oneLine"):
|
for fake in ("NO_POINTS", "epsg: () => 0", "addPoint: () => undefined", "oneLine"):
|
||||||
assert fake not in ui, fake
|
assert fake not in ui, fake
|
||||||
assert "if (!kit.key(event)) return;" in ui # 같은 단축키(키트)
|
assert "if (!kit.key(event)) return;" in ui # 같은 단축키(키트)
|
||||||
assert (
|
assert '? (step("undo"), true)' in ui # 켠 동안 Ctrl+Z = 이 판 사진첩(755-1)
|
||||||
'keyIs(event, "plan.undo") || keyIs(event, "plan.redo")' in ui
|
|
||||||
) # 되돌리기 = 페이지 사진첩
|
|
||||||
# 지도 입력 = 키트 한 함수 · 잡기 단계(capture) — 재기 · 계획선 고르기보다 먼저
|
# 지도 입력 = 키트 한 함수 · 잡기 단계(capture) — 재기 · 계획선 고르기보다 먼저
|
||||||
assert "capture: { active: () => open, busy: () => busy }," in ui
|
assert "capture: { active: () => open, busy: () => busy }," in ui
|
||||||
assert "canvas.addEventListener(type, run, !!capture)" in kit
|
assert "canvas.addEventListener(type, run, !!capture)" in kit
|
||||||
@@ -99,7 +97,7 @@ def test_D03_긋기_고르기_메뉴_속성판_그대로_한_선():
|
|||||||
for src in (draw, menu, props):
|
for src in (draw, menu, props):
|
||||||
assert "oneLine" not in src
|
assert "oneLine" not in src
|
||||||
table = _src(D03 / "D03_PlanAnalysis_UI_Features.ts")
|
table = _src(D03 / "D03_PlanAnalysis_UI_Features.ts")
|
||||||
# 638-3 — 세트 칸 · 세트 줄 = B05 보기만 · [잇기] = B05 숨김 · 세트 고침 = assignSet 한 길
|
# 638-3 — 세트 칸 · 세트 줄 = B05 보기만 · 세트 고침 = assignSet 한 길 · 755-1 [잇기] = B05 사용(끊긴 곳 이음)
|
||||||
assert 'disabled: !params.editable() || !featureOn(page, "set")' in props
|
assert 'disabled: !params.editable() || !featureOn(page, "set")' in props
|
||||||
assert 'featureOn(page, "join") && edit.isEnd(' in props + menu
|
assert 'featureOn(page, "join") && edit.isEnd(' in props + menu
|
||||||
assert "setPiecesYear" not in props and "setLineYear" not in _src(
|
assert "setPiecesYear" not in props and "setLineYear" not in _src(
|
||||||
@@ -109,7 +107,7 @@ def test_D03_긋기_고르기_메뉴_속성판_그대로_한_선():
|
|||||||
("branch", "hide"),
|
("branch", "hide"),
|
||||||
("years", "view"),
|
("years", "view"),
|
||||||
("set", "view"),
|
("set", "view"),
|
||||||
("join", "hide"),
|
("join", "use"),
|
||||||
("points", "hide"),
|
("points", "hide"),
|
||||||
("draw", "use"),
|
("draw", "use"),
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"""635-3 B05 왼쪽 「위치」 · 「그리기」 칸 · 「예정노선 수정」 · 받은 노선 비교 · [받은 노선으로] · 어둡게(요구 546).
|
"""635-3 B05 왼쪽 「그리기」 칸(755-2 위치 칸 걷음) · 「예정노선 수정」 · 받은 노선 비교 · [받은 노선으로] · 어둡게(요구 546).
|
||||||
|
|
||||||
- 칸 = 예정노선 키트(`createPlanKit` 「b05」) 칸 · 자리 = `registerPanelSection` order 1 · 2(종단 보기) · 편집기가 채움.
|
- 칸 = 예정노선 키트(`createPlanKit` 「b05」) 칸 · 자리 = `registerPanelSection` order 1(종단 보기) · 편집기가 채움.
|
||||||
- 위치 = 찾기 → 지도 이동만(지형 받기 · 기준점 없음) · 고칠 선 = 받은 원본 꼭짓점(`GET /route/plan/source` · 635-2).
|
- 위치 칸 = 기능 표 「site」 b05 숨김(589-8) · 고칠 선 = 받은 원본 꼭짓점(`GET /route/plan/source` · 635-2).
|
||||||
- [받은 노선으로] = 받은 꼭짓점 다시 추적 + 초안 `expected` [](고친 예정노선 지움) — 한 걸음 · 페이지 [초기화] 그대로.
|
- [받은 노선으로] = 받은 꼭짓점 다시 추적 + 초안 `expected` [](고친 예정노선 지움) — 한 걸음 · 페이지 [초기화] 그대로.
|
||||||
- 켬 = 아래 계획노선 그림 불투명 `--draw-map-dim`(0.35) · 등고 높이값 · 예정노선 그림은 그대로.
|
- 켬 = 아래 계획노선 그림 불투명 `--draw-map-dim`(0.35) · 등고 높이값 · 예정노선 그림은 그대로.
|
||||||
- 큰 파일 안 늘림(RouteEdit 683 · Render 648 + 10 안).
|
- 큰 파일 안 늘림(RouteEdit 690 · Render 648 + 10 안).
|
||||||
|
|
||||||
./venv/Scripts/python.exe -m pytest resources/tester/test_635_3_b05_plan_panel.py -q
|
./venv/Scripts/python.exe -m pytest resources/tester/test_635_3_b05_plan_panel.py -q
|
||||||
"""
|
"""
|
||||||
@@ -22,10 +22,10 @@ def _src(path: Path) -> str:
|
|||||||
return path.read_text(encoding="utf-8")
|
return path.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
def test_왼쪽_칸_둘_종단_보기_위():
|
def test_왼쪽_칸_하나_종단_보기_위():
|
||||||
panel = _src(B05 / "B05_Profile_UI_Panel_Plan.ts")
|
panel = _src(B05 / "B05_Profile_UI_Panel_Plan.ts")
|
||||||
assert '["site", 1, "위치"]' in panel and '["draw", 2, "그리기"]' in panel
|
assert 'id: "plan-draw",' in panel and 'title: "그리기",' in panel and "order: 1," in panel
|
||||||
assert 'views: ["profile"]' in panel and "id: `plan-${slot}`" in panel
|
assert 'views: ["profile"]' in panel and '"site"' not in panel and '위치"' not in panel
|
||||||
assert "export function fillPlanPanel(" in panel
|
assert "export function fillPlanPanel(" in panel
|
||||||
assert "holder.hidden = !card;" in panel # 비면 자리 숨김
|
assert "holder.hidden = !card;" in panel # 비면 자리 숨김
|
||||||
# 다른 칸(구조물 배치 10 · 페이지 설정 20)보다 위
|
# 다른 칸(구조물 배치 10 · 페이지 설정 20)보다 위
|
||||||
@@ -33,18 +33,18 @@ def test_왼쪽_칸_둘_종단_보기_위():
|
|||||||
assert "order: 20" in _src(B05 / "B05_Profile_UI_Panel_Sections.ts")
|
assert "order: 20" in _src(B05 / "B05_Profile_UI_Panel_Sections.ts")
|
||||||
|
|
||||||
|
|
||||||
def test_키트_칸_위치_찾기_이동만_원본_꼭짓점():
|
def test_키트_칸_위치_걷음_원본_꼭짓점():
|
||||||
ui = _src(B05 / "B05_Profile_UI_ExpectedDraw.ts")
|
ui = _src(B05 / "B05_Profile_UI_ExpectedDraw.ts")
|
||||||
body = ui.split("*/", 1)[1]
|
body = ui.split("*/", 1)[1]
|
||||||
assert 'page: "b05",' in body and "cards: { site: { parts: [find.root] } }," in body
|
assert 'page: "b05",' in body and "cards: {}," in body
|
||||||
assert "fillPlanPanel({" in body and "createPlaceFind(" in body
|
assert "fillPlanPanel(drawCard);" in body and "createPlaceFind" not in body
|
||||||
assert "coords.toPlan(hit.lon, hit.lat)" in body # 지도 이동만(서버 변환 · 635-5)
|
assert "toPlan" not in body and "frame" not in body # 찾기 이동 걷음(589-8)
|
||||||
for banned in ("createPlanTerrain", "ensureCovered", "createPlanSite", "createFloatPanel"):
|
for banned in ("createPlanTerrain", "ensureCovered", "createPlanSite", "createFloatPanel"):
|
||||||
assert banned not in body, banned
|
assert banned not in body, banned
|
||||||
assert "/route/plan/source" in body and "expected_source" in body
|
assert "/route/plan/source" in body and "expected_source" in body
|
||||||
assert "shown === original ? received() : shown" in body # 안 고친 선 = 원본 꼭짓점
|
assert "shown === original ? received() : shown" in body # 안 고친 선 = 원본 꼭짓점
|
||||||
features = _src(ROOT / "D03_PlanAnalysis" / "D03_PlanAnalysis_UI_Features.ts")
|
features = _src(ROOT / "D03_PlanAnalysis" / "D03_PlanAnalysis_UI_Features.ts")
|
||||||
assert re.search(r'key: "site",[^}]*b05: "use"', features)
|
assert re.search(r'key: "site",[^}]*b05: "hide"', features, re.S)
|
||||||
assert re.search(r'key: "terrain",[^}]*b05: "hide"', features, re.S)
|
assert re.search(r'key: "terrain",[^}]*b05: "hide"', features, re.S)
|
||||||
|
|
||||||
|
|
||||||
@@ -56,10 +56,10 @@ def test_수정_켬끔_받은_노선_비교_되돌림():
|
|||||||
assert "받은 노선 ${meters(received())} m → 고친 노선 ${meters(line())} m" in body
|
assert "받은 노선 ${meters(received())} m → 고친 노선 ${meters(line())} m" in body
|
||||||
assert 'label: "받은 노선으로"' in body and "follow(received(), [])" in body
|
assert 'label: "받은 노선으로"' in body and "follow(received(), [])" in body
|
||||||
assert "resetButton.disabled = !edited || busy;" in body
|
assert "resetButton.disabled = !edited || busy;" in body
|
||||||
assert "if (!sameLine(points, base)) await follow(points, points);" in body
|
assert "return sameLine(points, base) || (await follow(points, points));" in body # 끌 때 한 번
|
||||||
main = _src(B05 / "B05_Profile_UI_RouteEdit.ts")
|
main = _src(B05 / "B05_Profile_UI_RouteEdit.ts")
|
||||||
assert "frame: () => ({ meta, setView: (next: ViewState) => (view = next) })," in main
|
assert "frame:" not in main
|
||||||
assert len(main.splitlines()) <= 683
|
assert len(main.splitlines()) <= 690
|
||||||
|
|
||||||
|
|
||||||
def test_켬_때_아래_노선_어둡게():
|
def test_켬_때_아래_노선_어둡게():
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""635-5 B05 경위도 ↔ 사업지 좌표 서버 변환(요구 546) — 찾기 이동 · 속성 판 경위도.
|
"""635-5 B05 경위도 ↔ 사업지 좌표 서버 변환(요구 546) — 속성 판 경위도(찾기 이동 = 755-2 걷음).
|
||||||
|
|
||||||
- 서버 `POST /route/coords` = 작업 좌표계(`resolve_project_crs` · WKT 그대로) pyproj 묶음 변환.
|
- 서버 `POST /route/coords` = 작업 좌표계(`resolve_project_crs` · WKT 그대로) pyproj 묶음 변환.
|
||||||
- 화면 = 찾기 이동 · 노드 고름 때만 부름 · 같은 점 캐시 · 바탕 `tm_grid` 늘려 쓰기 걷음.
|
- 화면 = 노드 고름 때만 부름 · 같은 점 캐시 · 바탕 `tm_grid` 늘려 쓰기 걷음.
|
||||||
|
|
||||||
./venv/Scripts/python.exe -m pytest resources/tester/test_635_5_b05_coords.py -q
|
./venv/Scripts/python.exe -m pytest resources/tester/test_635_5_b05_coords.py -q
|
||||||
"""
|
"""
|
||||||
@@ -50,12 +50,12 @@ def test_라우터_등록():
|
|||||||
assert "resolve_project_crs(root, route_crs_input=" in router
|
assert "resolve_project_crs(root, route_crs_input=" in router
|
||||||
|
|
||||||
|
|
||||||
def test_화면_서버_변환_찾기_고름만():
|
def test_화면_서버_변환_고름만():
|
||||||
panel = (B05 / "B05_Profile_UI_Panel_Plan.ts").read_text(encoding="utf-8")
|
panel = (B05 / "B05_Profile_UI_Panel_Plan.ts").read_text(encoding="utf-8")
|
||||||
assert "`/projects/${projectId}/route/coords`" in panel # requestJson 이 /api 를 붙임
|
assert "`/projects/${projectId}/route/coords`" in panel # requestJson 이 /api 를 붙임
|
||||||
assert "known.has(key)" in panel # 같은 점 캐시
|
assert "known.has(key)" in panel # 같은 점 캐시
|
||||||
ui = (B05 / "B05_Profile_UI_ExpectedDraw.ts").read_text(encoding="utf-8")
|
ui = (B05 / "B05_Profile_UI_ExpectedDraw.ts").read_text(encoding="utf-8")
|
||||||
assert "coords.toPlan(hit.lon, hit.lat)" in ui and "lonLat: coords.lonLat," in ui
|
assert "lonLat: coords.lonLat," in ui and "toPlan" not in ui + panel
|
||||||
assert "normOfLonLat" not in ui and "tm_grid` 늘려" not in ui
|
assert "normOfLonLat" not in ui and "tm_grid` 늘려" not in ui
|
||||||
# 끌기 중엔 판을 안 그림 — 속성 판은 고름 · 놓음(commit) 때만
|
# 끌기 중엔 판을 안 그림 — 속성 판은 고름 · 놓음(commit) 때만
|
||||||
kit = (ROOT / "D03_PlanAnalysis" / "D03_PlanAnalysis_UI_Kit.ts").read_text(encoding="utf-8")
|
kit = (ROOT / "D03_PlanAnalysis" / "D03_PlanAnalysis_UI_Kit.ts").read_text(encoding="utf-8")
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
"""755 B05 평면 예정노선 수정 흐름(요구 589-1 · 3 · 5 · 6 · 7 · 8).
|
||||||
|
|
||||||
|
- 755-1 켠 동안 = 판만 고침 · 끊김 허용(가운데 묶음 지움) · 이 판 사진첩(Ctrl+Z · 도구줄 단추) ·
|
||||||
|
끌 때 한 번 = 끊겼으면 알림하고 안 끔 · 바뀌었으면 `/route/follow` 한 걸음 · [잇기] = B05 사용.
|
||||||
|
- 755-2 켠 동안 재기 = D03 재기 겹 · 받은 노선 = 분홍 점선 + 검은 테 · 켬 설명 걷음 · 위치 칸 걷음.
|
||||||
|
|
||||||
|
./venv/Scripts/python.exe -m pytest resources/tester/test_755_expected_flow.py -q
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
B05 = ROOT / "B05_Profile"
|
||||||
|
D03 = ROOT / "D03_PlanAnalysis"
|
||||||
|
|
||||||
|
|
||||||
|
def _body() -> str:
|
||||||
|
return (B05 / "B05_Profile_UI_ExpectedDraw.ts").read_text("utf-8").split("*/", 1)[1]
|
||||||
|
|
||||||
|
|
||||||
|
def _fn(body: str, head: str) -> str:
|
||||||
|
start = body.index(head)
|
||||||
|
end = body.index("\n }\n", start)
|
||||||
|
return body[start:end]
|
||||||
|
|
||||||
|
|
||||||
|
def test_켠_동안_판만_한_걸음_사진첩():
|
||||||
|
body = _body().replace("\r\n", "\n")
|
||||||
|
commit = _fn(body, "function commit(")
|
||||||
|
assert "undoStack.push(network);" in commit and "redoStack = [];" in commit
|
||||||
|
assert "follow(" not in commit and "apply(" not in commit # 켠 동안 평면선 안 바꿈
|
||||||
|
assert "function apply(" not in body and "strokeBase" not in body
|
||||||
|
assert "onMode: () => params.redraw()," in body # 긋기 끝 = 다시 추적 아님
|
||||||
|
assert 'keyIs(event, "plan.undo")\n ? (step("undo"), true)' in body
|
||||||
|
assert "history: () =>\n open" in body # 끔 = 페이지 사진첩
|
||||||
|
history = (B05 / "B05_Profile_UI_RouteEdit_History.ts").read_text("utf-8")
|
||||||
|
assert "const local = params.local?.();" in history
|
||||||
|
assert "params.local?.() ?? getHistory()" in history
|
||||||
|
main = (B05 / "B05_Profile_UI_RouteEdit.ts").read_text("utf-8")
|
||||||
|
assert "local: () => expectedDraw.history()," in main
|
||||||
|
assert "onHistory: () => historyControls.sync()," in main
|
||||||
|
|
||||||
|
|
||||||
|
def test_끌_때_끊김_알림_안_끔_한_번_추적():
|
||||||
|
body = _body().replace("\r\n", "\n")
|
||||||
|
open_ = _fn(body, "async function setOpen(")
|
||||||
|
assert "if (!next && open && !(await finish())) return toggle.set(true);" in open_
|
||||||
|
finish = _fn(body, "async function finish(")
|
||||||
|
assert "drawn.length > 1" in finish and "return false;" in finish and "showToast(" in finish
|
||||||
|
assert "return sameLine(points, base) || (await follow(points, points));" in finish
|
||||||
|
follow = _fn(body, "async function follow(")
|
||||||
|
assert (
|
||||||
|
"/route/follow" in follow
|
||||||
|
and "return true;" in follow
|
||||||
|
and "if (!ok) return false;" in follow
|
||||||
|
)
|
||||||
|
assert "revert" not in body # 실패 · 취소 = 판 그대로(켠 채)
|
||||||
|
table = (D03 / "D03_PlanAnalysis_UI_Features.ts").read_text("utf-8")
|
||||||
|
assert re.search(r'key: "join",[^}]*b05: "use"', table, re.S)
|
||||||
|
assert re.search(r'key: "branch",[^}]*b05: "hide"', table, re.S) # 갈래 · 둘째 선 그대로 막음
|
||||||
|
|
||||||
|
|
||||||
|
def test_재기_D03_겹_켠_동안():
|
||||||
|
body = _body()
|
||||||
|
assert "createPlanMeasure(" in body and "params.measuring()" in body
|
||||||
|
assert "kit.layers([], [measure.layer])" in body and "layers: liveLayers," in body
|
||||||
|
assert "idle: () => open && !measuring()," in body # 재는 동안 고르기 · 끌기 쉼
|
||||||
|
main = (B05 / "B05_Profile_UI_RouteEdit.ts").read_text("utf-8")
|
||||||
|
assert "measuring: () => measureControls.mode()," in main
|
||||||
|
|
||||||
|
|
||||||
|
def test_받은_노선_색_설명_위치_걷음():
|
||||||
|
body = _body()
|
||||||
|
assert 'strokeCased(context, themeColor("--draw-map-route-3", "#ff8cbc"), 2.5, [8, 5]);' in body
|
||||||
|
assert "--draw-text-secondary" not in body
|
||||||
|
assert "HINT =" not in body.replace("OFF_HINT =", "") and "hint.hidden = open;" in body
|
||||||
|
table = (D03 / "D03_PlanAnalysis_UI_Features.ts").read_text("utf-8")
|
||||||
|
assert re.search(r'key: "site",[^}]*b05: "hide"', table, re.S)
|
||||||
|
assert re.search(r'key: "site",[^}]*d03: "use"', table, re.S) # D03 그대로
|
||||||
|
theme = (ROOT / "ui_template" / "ui_template_theme.css").read_text("utf-8")
|
||||||
|
assert "--draw-map-route-3:" in theme
|
||||||
Reference in New Issue
Block a user