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