Merge remote-tracking branch 'origin/sub_laptop_2' into main_laptop_1
This commit is contained in:
@@ -5,8 +5,8 @@
|
||||
* 긋기 칸 `createPlanDraw`(방식 칸 · 부채꼴 · +/− 색 · Shift · Tab · Enter · Esc · Backspace · 선 끝 이어 긋기) ·
|
||||
* 고르기 겹 `createPlanEdit`(노드 → 선 → 구역 → 전체 · 끌어 고르기 · 노드 끌기 · 고른 토막 끌어 꺾임점 · Delete) ·
|
||||
* 우클릭 메뉴 `createPlanMenu`(여기서 긋기 시작 · 이어 긋기 · 노드 지움 · 다시 긋기) · 속성 판 `createPlanProps`([다시 긋기] ·
|
||||
* [노드 지움] · [선 지움] · [구역 지움] · [전체 지움]) — 한 선(`oneLine`) = 갈래 · 둘째 선 · 연도 · 지점 막음/숨김.
|
||||
* 캔버스 누름 · 맴돎 · 두 번 클릭 · 오른쪽 클릭을 먼저 받음 = 계획선 · 구조물 · 등고선 고르기 · 재기 · 노드 넣기 막음 ·
|
||||
* [노드 지움] · [선 지움] · [구역 지움] · [전체 지움]) — 숨김 = 기능 표 「b05」(`D03_PlanAnalysis_UI_Features` · 갈래 · 둘째 선 · 연도 · 지점).
|
||||
* 캔버스 누름 · 맴돎 · 두 번 클릭 · 오른쪽 클릭을 먼저 받음(키트 `bindPlanInput` capture) = 계획선 · 구조물 · 등고선 고르기 · 재기 · 노드 넣기 막음 ·
|
||||
* 가운데 · 오른쪽 끌기(지도 이동) · 휠은 그대로. 끔 = 지금 B05 평면 편집 그대로(예정노선은 보기만).
|
||||
* 한 번 고침(긋기 끝 · 놓음 · 지움) → (켠 뒤 처음만) 확인창(직접 고친 평면 버림 · 예정노선 따라 통째 다시 추적) →
|
||||
* `POST /route/follow`(저장 0) → `onFollow(평면, 예정노선)` = 편집기 한 걸음(되돌리기 됨) → 페이지 [저장] = 손 편집과
|
||||
@@ -27,9 +27,9 @@ import { heightAt as gridHeightAt } from "../common_util/common_util_surface_sam
|
||||
import type { PlanNetwork } from "../D03_PlanAnalysis/logic/D03_PlanAnalysis_Plan";
|
||||
import { createPlanDraw } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Draw";
|
||||
import { createPlanEdit } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Edit";
|
||||
import { bindPlanInput } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Kit";
|
||||
import type { PlanMapLayer, ScreenMap } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Map";
|
||||
import { createPlanMenu } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Menu";
|
||||
import type { PlanPoints } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Points";
|
||||
import { createPlanProps } from "../D03_PlanAnalysis/D03_PlanAnalysis_UI_Props";
|
||||
import { requestJson, type RoutePlanResponse } from "./B05_Profile_Api_Replan";
|
||||
import { loadDesignTerrainGrid, type DesignTerrainGrid } from "./B05_Profile_Api_TerrainGrid";
|
||||
@@ -47,19 +47,6 @@ const EMPTY_HINT = "예정노선이 없습니다 — 빈 땅 우클릭 「여기
|
||||
/** `POST /route/follow` 응답 — GET `/route/plan` 칸 이름(626-2). */
|
||||
type FollowResponse = Pick<RoutePlanResponse, "planned" | "nodes" | "curves" | "node_edits">;
|
||||
|
||||
/** 지점 없음(B05 예정노선 = 한 선 · 메뉴의 지점 항목 숨김). */
|
||||
const NO_POINTS: PlanPoints = {
|
||||
panel: document.createElement("div"),
|
||||
input: { draw: () => undefined },
|
||||
pins: { draw: () => undefined },
|
||||
render: () => undefined,
|
||||
pinAt: () => null,
|
||||
place: () => undefined,
|
||||
remove: () => undefined,
|
||||
name: () => "",
|
||||
full: () => true,
|
||||
};
|
||||
|
||||
export interface ExpectedDrawParams {
|
||||
projectId: string;
|
||||
canvas: HTMLCanvasElement;
|
||||
@@ -120,12 +107,11 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
|
||||
|
||||
// ── D03 긋기 · 고르기 · 메뉴 · 속성 판(같은 함수) ─────────
|
||||
const draw = createPlanDraw({
|
||||
base: () => [],
|
||||
page: "b05",
|
||||
network: () => network,
|
||||
heightAt: () => z,
|
||||
commit,
|
||||
onMode,
|
||||
oneLine: true,
|
||||
});
|
||||
const show = (next: PlanNetwork): void => {
|
||||
network = next;
|
||||
@@ -146,28 +132,22 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
|
||||
};
|
||||
const editor = createPlanEdit(editParams);
|
||||
const props = createPlanProps({
|
||||
page: "b05",
|
||||
network: () => network,
|
||||
edit: editor,
|
||||
editable: () => !busy,
|
||||
yearStart: () => new Date().getFullYear(),
|
||||
commit,
|
||||
heightAt: () => z,
|
||||
epsg: () => 0,
|
||||
points: () => [],
|
||||
addPoint: () => undefined,
|
||||
commitPoints: (_points, next) => commit(next),
|
||||
respan: draw.respan,
|
||||
oneLine: true,
|
||||
});
|
||||
const menu = createPlanMenu({
|
||||
page: "b05",
|
||||
network: () => network,
|
||||
points: () => [],
|
||||
marks: NO_POINTS,
|
||||
edit: editor,
|
||||
draw,
|
||||
editable: () => open && !busy,
|
||||
respan: draw.respan,
|
||||
oneLine: true,
|
||||
});
|
||||
const layers: PlanMapLayer[] = [draw.layer, editor.layer, menu.layer];
|
||||
props.panel.classList.add("b05-routeedit__expected-props");
|
||||
@@ -314,65 +294,20 @@ export function createExpectedDraw(params: ExpectedDrawParams): ExpectedDraw {
|
||||
unrotate: rotation.unrotate,
|
||||
};
|
||||
};
|
||||
const at = (event: MouseEvent): [number, number] => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
return params.rotation().unrotate(event.clientX - rect.left, event.clientY - rect.top);
|
||||
};
|
||||
const claim = (event: Event): void => {
|
||||
event.stopImmediatePropagation();
|
||||
event.preventDefault();
|
||||
};
|
||||
canvas.addEventListener(
|
||||
"pointerdown",
|
||||
(event) => {
|
||||
if (!open || event.button !== 0) return;
|
||||
claim(event);
|
||||
const [px, py] = at(event);
|
||||
if (!busy && layers.some((layer) => layer.press?.(px, py, map(), 0, event.shiftKey)))
|
||||
canvas.setPointerCapture(event.pointerId); // 끌기가 밖으로 나가도 놓음을 받음
|
||||
params.redraw();
|
||||
bindPlanInput({
|
||||
canvas,
|
||||
at: (event) => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
return params.rotation().unrotate(event.clientX - rect.left, event.clientY - rect.top);
|
||||
},
|
||||
true,
|
||||
);
|
||||
canvas.addEventListener(
|
||||
"pointermove",
|
||||
(event) => {
|
||||
if (!open || event.buttons & 6) return; // 오른쪽 · 가운데 끌기 = 지도 이동 그대로
|
||||
event.stopImmediatePropagation();
|
||||
const [px, py] = at(event);
|
||||
const screen = map();
|
||||
for (const layer of layers) layer.hover?.(px, py, screen, event.shiftKey);
|
||||
params.redraw();
|
||||
},
|
||||
true,
|
||||
);
|
||||
canvas.addEventListener(
|
||||
"pointerup",
|
||||
(event) => {
|
||||
if (!open || event.button !== 0) return;
|
||||
claim(event);
|
||||
const screen = map();
|
||||
for (const layer of layers) layer.release?.(screen);
|
||||
params.redraw();
|
||||
},
|
||||
true,
|
||||
);
|
||||
canvas.addEventListener(
|
||||
"contextmenu",
|
||||
(event) => {
|
||||
if (!open) return;
|
||||
claim(event);
|
||||
const [px, py] = at(event);
|
||||
if (!busy) layers.some((layer) => layer.press?.(px, py, map(), 2, event.shiftKey));
|
||||
params.redraw();
|
||||
},
|
||||
true,
|
||||
);
|
||||
for (const type of ["click", "dblclick"])
|
||||
canvas.addEventListener(type, (event) => open && claim(event), true);
|
||||
canvas.addEventListener("pointerleave", () => {
|
||||
for (const layer of layers) layer.leave?.();
|
||||
if (open) params.redraw();
|
||||
screen: map,
|
||||
layers: () => layers,
|
||||
redraw: params.redraw,
|
||||
capture: { active: () => open, busy: () => busy },
|
||||
});
|
||||
|
||||
const onKey = (event: KeyboardEvent): void => {
|
||||
|
||||
@@ -62,14 +62,15 @@ import {
|
||||
DRAW_DEFAULT_SLOPE_PCT,
|
||||
type DrawConstraint,
|
||||
} from "./D03_PlanAnalysis_UI_DrawPanel";
|
||||
import { featureOn, type PlanPage } from "./D03_PlanAnalysis_UI_Features";
|
||||
import type { PlanMapLayer, ScreenMap } from "./D03_PlanAnalysis_UI_Map";
|
||||
|
||||
const START_HINT = "지점 · 선 끝 · 선 위를 눌러 시작합니다 — 빈 땅은 우클릭 「여기서 긋기 시작」.";
|
||||
|
||||
export type DrawMode = "idle" | "draw";
|
||||
|
||||
/** 한 선(626-3) 막힘 까닭 — 선 위 · 지점 = 갈래 · 선이 있는데 빈 땅 = 둘째 선 · 됨 = null. */
|
||||
export function oneLineBlock(
|
||||
/** 갈래 숨긴 페이지(기능 표 「branch」 · B05 = 한 선) 막힘 까닭 — 선 위 · 지점 = 갈래 · 선이 있는데 빈 땅 = 둘째 선 · 됨 = null. */
|
||||
export function lineLimitBlock(
|
||||
kind: "tip" | "point" | "line" | "empty",
|
||||
network: PlanNetwork,
|
||||
): string | null {
|
||||
@@ -95,15 +96,15 @@ export interface DrawPreview {
|
||||
}
|
||||
|
||||
export interface PlanDrawParams {
|
||||
/** 지점(긋기에 붙는 점 · 591-2). */
|
||||
base: () => Vertex[];
|
||||
/** 기능 표 페이지(`D03_PlanAnalysis_UI_Features` — 갈래 · 연도 마커 숨김). */
|
||||
page: PlanPage;
|
||||
/** 지점(긋기에 붙는 점 · 591-2) — 없음 = 지점 없는 페이지. */
|
||||
base?: () => Vertex[];
|
||||
network: () => PlanNetwork;
|
||||
heightAt: () => (x: number, y: number) => number;
|
||||
commit: (next: PlanNetwork) => void;
|
||||
/** 모드 · 그리는 선이 바뀜 — 페이지가 단추 · 안내를 다시. */
|
||||
onMode: () => void;
|
||||
/** 한 선만(B05 평면 예정노선 · 626-3) — 갈래 · 둘째 선 막음 · 연도 구역 마커 숨김. 없음 = D03 그대로. */
|
||||
oneLine?: boolean;
|
||||
}
|
||||
|
||||
export interface PlanDraw {
|
||||
@@ -149,6 +150,7 @@ export function createPlanDraw(params: PlanDrawParams): PlanDraw {
|
||||
through: Vertex[];
|
||||
records: NodeRecord[];
|
||||
} | null = null;
|
||||
const basePoints = (): Vertex[] => params.base?.() ?? [];
|
||||
const spanLine = (): number =>
|
||||
span ? params.network().lines.findIndex((line) => line.id === span!.line) : -1;
|
||||
|
||||
@@ -159,7 +161,7 @@ export function createPlanDraw(params: PlanDrawParams): PlanDraw {
|
||||
});
|
||||
const drawPanel = createDrawPanel({
|
||||
kind: kindFields,
|
||||
marks: !params.oneLine,
|
||||
page: params.page,
|
||||
onDraw: () => setMode(mode === "draw" ? "idle" : "draw"),
|
||||
});
|
||||
const panel = drawPanel.root;
|
||||
@@ -186,8 +188,7 @@ export function createPlanDraw(params: PlanDrawParams): PlanDraw {
|
||||
): { point: Vertex; note: string; onLine: DrawPreview["onLine"] } | null {
|
||||
const network = params.network();
|
||||
const head = active === null ? undefined : network.lines[active]?.vertices[0];
|
||||
const offers = params
|
||||
.base()
|
||||
const offers = basePoints()
|
||||
.filter((point) => !head || !samePoint(point, head))
|
||||
.map((point) => ({ point, note: "지점에 붙음" }));
|
||||
network.lines.forEach((line, i) => {
|
||||
@@ -351,7 +352,7 @@ export function createPlanDraw(params: PlanDrawParams): PlanDraw {
|
||||
| { kind: "line"; hit: NonNullable<ReturnType<typeof lineAt>> }
|
||||
| { kind: "empty"; point: Vertex } {
|
||||
const network = params.network();
|
||||
const points = params.base();
|
||||
const points = basePoints();
|
||||
const gap = (point: Vertex): number => screenGap(map, point, px, py);
|
||||
const nearest = <T extends { gap: number }>(items: T[]): T | undefined =>
|
||||
items.filter((item) => item.gap <= SNAP_PX).sort((a, b) => a.gap - b.gap)[0];
|
||||
@@ -381,7 +382,7 @@ export function createPlanDraw(params: PlanDrawParams): PlanDraw {
|
||||
function begin(px: number, py: number, map: ScreenMap, free = false): boolean {
|
||||
const network = params.network();
|
||||
const target = aim(px, py, map);
|
||||
const single = params.oneLine ? oneLineBlock(target.kind, network) : null;
|
||||
const single = featureOn(params.page, "branch") ? null : lineLimitBlock(target.kind, network);
|
||||
if (single) {
|
||||
showToast(single, "info");
|
||||
return false;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import { createButton } from "@ui/ui_template_elements";
|
||||
import { YEAR_MARK_DRAG } from "./D03_PlanAnalysis_UI_Map";
|
||||
import type { DrawKindFields } from "../A00_Common/map/map_draw_kind";
|
||||
import { featureOn, type PlanPage } from "./D03_PlanAnalysis_UI_Features";
|
||||
import "./D03_PlanAnalysis_UI_DrawParts.css";
|
||||
|
||||
// 방식 칸 = 공용(`A00_Common/map/map_draw_kind` · 626-3 · B05 평면 예정노선 그리기도 씀)
|
||||
@@ -34,8 +35,8 @@ export interface DrawPanel {
|
||||
export function createDrawPanel(opts: {
|
||||
kind: DrawKindFields;
|
||||
onDraw: () => void;
|
||||
/** 연도 구역 마커 단추(없음 = 보임 · B05 평면 예정노선 = 숨김 626-3). */
|
||||
marks?: boolean;
|
||||
/** 기능 표 페이지 — 연도 구역 마커 단추 = 「years」 사용일 때만. */
|
||||
page: PlanPage;
|
||||
}): DrawPanel {
|
||||
const drawButton = createButton({
|
||||
label: "긋기",
|
||||
@@ -51,7 +52,7 @@ export function createDrawPanel(opts: {
|
||||
});
|
||||
markButton.title = "선 위로 끌어 놓으면 구역을 나눕니다";
|
||||
markButton.dataset.role = "draw-year-mark";
|
||||
markButton.hidden = opts.marks === false;
|
||||
markButton.hidden = !featureOn(opts.page, "years");
|
||||
markButton.draggable = true;
|
||||
markButton.addEventListener("dragstart", (event) => {
|
||||
event.dataTransfer?.setData(YEAR_MARK_DRAG, "1");
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/* =============================================================================
|
||||
* D03_PlanAnalysis_UI_Features.ts
|
||||
* 노선 그리기 기능 표(PLAN 635-1 · 요구서 546 4장) — 예정노선(d03) · B05 평면이 같은 그리기 부품을 쓰고 이 표로만 가름.
|
||||
* 한 줄 = 기능 열쇠 · 이름 · 페이지마다 쓰임(use 사용 · view 보기만 · hide 숨김) · 까닭.
|
||||
* 숨김을 바꿀 때 = 이 표만 고침(부품 · 키트가 `featureUse` 로 읽음) · 꼴 = 지도 도구줄 표(`map_toolbar`).
|
||||
* ========================================================================== */
|
||||
|
||||
import type { MapPage } from "../A00_Common/map/map_layer_list";
|
||||
|
||||
export type PlanPage = MapPage;
|
||||
export type FeatureUse = "use" | "view" | "hide";
|
||||
export type PlanFeature =
|
||||
| "site"
|
||||
| "draw"
|
||||
| "extend"
|
||||
| "select"
|
||||
| "props"
|
||||
| "history"
|
||||
| "terrain"
|
||||
| "road-upload"
|
||||
| "branch"
|
||||
| "years"
|
||||
| "points"
|
||||
| "plan-work";
|
||||
|
||||
export interface PlanFeatureRow {
|
||||
key: PlanFeature;
|
||||
name: string;
|
||||
pages: Record<PlanPage, FeatureUse>;
|
||||
why?: string;
|
||||
}
|
||||
|
||||
export const PLAN_FEATURES: readonly PlanFeatureRow[] = [
|
||||
{ key: "site", name: "주소 · 장소 찾기(B05 = 지도 이동만)", pages: { d03: "use", b05: "use" } },
|
||||
{
|
||||
key: "draw",
|
||||
name: "긋기 네 방식 · 길이 · 기울기 · Tab · Shift · 부채꼴 · ± 색",
|
||||
pages: { d03: "use", b05: "use" },
|
||||
},
|
||||
{
|
||||
key: "extend",
|
||||
name: "이어 긋기 · 다시 긋기 · 빈 땅 새 선(선 없을 때)",
|
||||
pages: { d03: "use", b05: "use" },
|
||||
},
|
||||
{
|
||||
key: "select",
|
||||
name: "고름 노드 → 선 → 전체 · 끌어 고르기 · 노드 끌기 · 꺾임점 · 지움",
|
||||
pages: { d03: "use", b05: "use" },
|
||||
},
|
||||
{
|
||||
key: "props",
|
||||
name: "속성 판 지반고 · 측점 · 길이 · 기울기 · 경위도(B05 = 서버 변환)",
|
||||
pages: { d03: "use", b05: "use" },
|
||||
},
|
||||
{
|
||||
key: "history",
|
||||
name: "되돌리기 · 다시하기 · 길이측정 · 3D(B05 = B05 것)",
|
||||
pages: { d03: "use", b05: "use" },
|
||||
},
|
||||
{
|
||||
key: "terrain",
|
||||
name: "기준점 · 지형 받기 · 다시 받기 · 10 km 알림",
|
||||
pages: { d03: "use", b05: "hide" },
|
||||
why: "B 지형 = 전처리 확정",
|
||||
},
|
||||
{
|
||||
key: "road-upload",
|
||||
name: "임도(업로드) 올리기 · 묶음 목록",
|
||||
pages: { d03: "use", b05: "hide" },
|
||||
why: "서버 B 400 · 받은 자료로 보임",
|
||||
},
|
||||
{
|
||||
key: "branch",
|
||||
name: "갈래 · 둘째 선 · 다른 선과 잇기 · 가운데 토막 지움(끝 토막 지움은 사용)",
|
||||
pages: { d03: "use", b05: "hide" },
|
||||
why: "B 계획선 한 줄",
|
||||
},
|
||||
{
|
||||
key: "years",
|
||||
name: "연도 구역 마커 · 구역 · 연도 고침 · 선 이름",
|
||||
pages: { d03: "use", b05: "view" },
|
||||
why: "받은 몫 고정",
|
||||
},
|
||||
{
|
||||
key: "points",
|
||||
name: "지점 칸 · 핀 · 지점에서 긋기",
|
||||
pages: { d03: "use", b05: "hide" },
|
||||
why: "B 저장 자리 없음",
|
||||
},
|
||||
{
|
||||
key: "plan-work",
|
||||
name: "등록 기준점 · 확정 · 업체 지정 · 배포 · 산림청 [저장]",
|
||||
pages: { d03: "use", b05: "hide" },
|
||||
why: "예정노선 업무",
|
||||
},
|
||||
];
|
||||
|
||||
/** 그 페이지 쓰임 — 표에 없는 열쇠 = 숨김. */
|
||||
export function featureUse(page: PlanPage, key: PlanFeature): FeatureUse {
|
||||
return PLAN_FEATURES.find((row) => row.key === key)?.pages[page] ?? "hide";
|
||||
}
|
||||
|
||||
/** 고칠 수 있게 씀(보기만 · 숨김 = 거짓). */
|
||||
export const featureOn = (page: PlanPage, key: PlanFeature): boolean =>
|
||||
featureUse(page, key) === "use";
|
||||
|
||||
/** 보임(사용 · 보기만). */
|
||||
export const featureShown = (page: PlanPage, key: PlanFeature): boolean =>
|
||||
featureUse(page, key) !== "hide";
|
||||
@@ -0,0 +1,293 @@
|
||||
/* =============================================================================
|
||||
* D03_PlanAnalysis_UI_Kit.ts
|
||||
* 노선 그리기 키트(PLAN 635-1 · 요구서 546 3장 「노선 그리기」) — 예정노선(d03) · B05 평면이 같은 한 벌을 엮음.
|
||||
*
|
||||
* 부품 = 긋기 `createPlanDraw` · 고르기 `createPlanEdit` · 속성 판 `createPlanProps` · 구역·연도 `createPlanPieces` ·
|
||||
* 지점 `createPlanPoints` · 우클릭 메뉴 `createPlanMenu` — 숨김 = 기능 표(`D03_PlanAnalysis_UI_Features`)만 읽음.
|
||||
* 내놓음 = 왼쪽 칸(위치 · 지점 · 그리기 · 구역·연도 중 표가 켠 것) · 겹 차례(고정) · 키 · `sync()`.
|
||||
* 저장 · 초기화 · 되돌리기 = 페이지 몫(`commit` 한 번 = 한 걸음 · 캐시 = 페이지).
|
||||
* 지도 입력 나눠 주기 `bindPlanInput` 하나 — 예정노선 지도(겹 몫 그대로) · B05 평면(`capture` = 켠 동안 먼저 받음).
|
||||
* ========================================================================== */
|
||||
|
||||
import { perfTime } from "@ui/ui_template_gl_map";
|
||||
import { createPanelCard } from "@ui/ui_template_collapsible";
|
||||
import type { HeightAt } from "./logic/D03_PlanAnalysis_Draw";
|
||||
import type { PlanNetwork, Vertex } from "./logic/D03_PlanAnalysis_Plan";
|
||||
import { createPlanDraw, type PlanDraw } from "./D03_PlanAnalysis_UI_Draw";
|
||||
import { createPlanEdit, type PlanEdit } from "./D03_PlanAnalysis_UI_Edit";
|
||||
import { featureOn, featureShown, type PlanPage } from "./D03_PlanAnalysis_UI_Features";
|
||||
import { YEAR_MARK_DRAG, type PlanMapLayer, type ScreenMap } from "./D03_PlanAnalysis_UI_Map";
|
||||
import { createPlanMenu, type PlanMenu } from "./D03_PlanAnalysis_UI_Menu";
|
||||
import { createPlanPieces, type PlanPieces } from "./D03_PlanAnalysis_UI_Pieces";
|
||||
import { createPlanPoints, type PlanPoints } from "./D03_PlanAnalysis_UI_Points";
|
||||
import { createPlanProps, type PlanProps } from "./D03_PlanAnalysis_UI_Props";
|
||||
|
||||
/** 지점 통로(표 「points」 켠 페이지) — 지점 · 이름 · 좌표계 · 보기 가운데 · 한 걸음 · 보여 주기만. */
|
||||
export interface PlanKitPoints {
|
||||
list: () => Vertex[];
|
||||
names: () => string[];
|
||||
epsg: () => number;
|
||||
center: () => Vertex | null;
|
||||
commit: (points: Vertex[], next: PlanNetwork, names?: string[]) => void;
|
||||
show: (points: Vertex[], next: PlanNetwork) => void;
|
||||
}
|
||||
|
||||
export interface PlanKitParams {
|
||||
page: PlanPage;
|
||||
network: () => PlanNetwork;
|
||||
/** 한 걸음(되돌리기 · 캐시 = 페이지). */
|
||||
commit: (next: PlanNetwork) => void;
|
||||
/** 끄는 중 보여 주기만(걸음 아님). */
|
||||
show: (next: PlanNetwork) => void;
|
||||
heightAt: () => HeightAt;
|
||||
/** 작업 좌표 → 경위도(없음 = 속성 판 경위도 칸 숨김). */
|
||||
lonLat?: (x: number, y: number) => [number, number] | null;
|
||||
editable: () => boolean;
|
||||
/** 고르기 · 핀이 누름을 받을 때(긋기 쉼 말고 더 — 예정노선 = 재기 끔). */
|
||||
idle?: () => boolean;
|
||||
onMode: () => void;
|
||||
onSelect: () => void;
|
||||
/** 사업연도 시작(연도 고름). */
|
||||
yearStart?: () => number;
|
||||
points?: PlanKitPoints;
|
||||
/** 페이지 키(되돌리기 · 다시하기 · 재기 끔) — 긋기 · 고르기보다 먼저. */
|
||||
onKey?: (event: KeyboardEvent) => boolean;
|
||||
cards: {
|
||||
/** 위치 칸 몸 · 제목 곁(진행 서클) — 찾기 → 이동은 페이지 몫. */
|
||||
site?: { parts: HTMLElement[]; title?: HTMLElement };
|
||||
};
|
||||
}
|
||||
|
||||
export interface PlanKit {
|
||||
/** 왼쪽 칸 — 표가 켠 차례(위치 · 지점 · 그리기 · 구역·연도). */
|
||||
cards: HTMLElement[];
|
||||
/** 겹 차례 고정 — 페이지 아래 겹 · 긋기 · 핀 누름 · 고르기 · 페이지 위 겹 · 핀 그림 · 우클릭 메뉴(맨 위). */
|
||||
layers: (under: PlanMapLayer[], over: PlanMapLayer[]) => PlanMapLayer[];
|
||||
draw: PlanDraw;
|
||||
edit: PlanEdit;
|
||||
props: PlanProps;
|
||||
menu: PlanMenu;
|
||||
marks: PlanPoints | null;
|
||||
pieces: PlanPieces | null;
|
||||
/** 키 — 받았으면 참(글 칸 · 보기만 = 안 받음). */
|
||||
key: (event: KeyboardEvent) => boolean;
|
||||
/** 꼴 · 고름이 바뀜 — 긋기 · 고르기 · 판 · 칸 다시. */
|
||||
sync: () => void;
|
||||
}
|
||||
|
||||
export function createPlanKit(params: PlanKitParams): PlanKit {
|
||||
const { page, network, commit, heightAt, editable } = params;
|
||||
const points = featureOn(page, "points") ? params.points : undefined;
|
||||
const idle = (): boolean => draw.mode() === "idle" && (params.idle?.() ?? true);
|
||||
const commitPoints = (list: Vertex[], next: PlanNetwork): void =>
|
||||
points ? points.commit(list, next) : commit(next);
|
||||
|
||||
const draw = createPlanDraw({
|
||||
page,
|
||||
base: points?.list,
|
||||
network,
|
||||
heightAt,
|
||||
commit,
|
||||
onMode: params.onMode,
|
||||
});
|
||||
const edit = createPlanEdit({
|
||||
network,
|
||||
base: () => points?.list() ?? [],
|
||||
commit,
|
||||
heightAt,
|
||||
show: params.show,
|
||||
commitPoints,
|
||||
showPoints: (list, next) => (points ? points.show(list, next) : params.show(next)),
|
||||
enabled: idle,
|
||||
editable,
|
||||
onSelect: params.onSelect,
|
||||
});
|
||||
const props = createPlanProps({
|
||||
page,
|
||||
network,
|
||||
edit,
|
||||
editable,
|
||||
yearStart: params.yearStart,
|
||||
commit,
|
||||
heightAt,
|
||||
lonLat: params.lonLat,
|
||||
points: points?.list,
|
||||
addPoint: points
|
||||
? (at) => points.commit([...points.list(), at], network(), [...points.names(), ""])
|
||||
: undefined,
|
||||
commitPoints,
|
||||
respan: draw.respan,
|
||||
});
|
||||
const pieces = featureShown(page, "years")
|
||||
? createPlanPieces({ network, edit, editable, yearOptions: props.yearOptions, commit })
|
||||
: null;
|
||||
const marks = points
|
||||
? createPlanPoints({
|
||||
points: points.list,
|
||||
network,
|
||||
names: points.names,
|
||||
epsg: points.epsg,
|
||||
commit: (list, names, next) => points.commit(list, next, names),
|
||||
center: points.center,
|
||||
show: points.show,
|
||||
idle,
|
||||
editable,
|
||||
})
|
||||
: null;
|
||||
const menu = createPlanMenu({
|
||||
page,
|
||||
network,
|
||||
points: points?.list,
|
||||
marks: marks ?? undefined,
|
||||
edit,
|
||||
draw,
|
||||
editable,
|
||||
respan: draw.respan,
|
||||
});
|
||||
|
||||
const card = (title: string, ...body: HTMLElement[]): HTMLElement => {
|
||||
const box = createPanelCard(title, false);
|
||||
box.append(...body);
|
||||
return box;
|
||||
};
|
||||
const cards: HTMLElement[] = [];
|
||||
const site = params.cards.site;
|
||||
if (site && featureShown(page, "site")) {
|
||||
const siteCard = card("위치", ...site.parts);
|
||||
if (site.title) siteCard.firstElementChild!.append(site.title);
|
||||
cards.push(siteCard);
|
||||
}
|
||||
if (marks) cards.push(card("지점", marks.panel));
|
||||
cards.push(card("그리기", draw.panel));
|
||||
if (pieces) cards.push(card("구역 · 연도", pieces.list));
|
||||
|
||||
return {
|
||||
cards,
|
||||
layers: (under, over) => [
|
||||
...under,
|
||||
draw.layer,
|
||||
...(marks ? [marks.input] : []),
|
||||
edit.layer,
|
||||
...over,
|
||||
...(marks ? [marks.pins] : []),
|
||||
menu.layer,
|
||||
],
|
||||
draw,
|
||||
edit,
|
||||
props,
|
||||
menu,
|
||||
marks,
|
||||
pieces,
|
||||
key(event) {
|
||||
const typing = (event.target as HTMLElement | null)?.closest?.("input, textarea, select");
|
||||
if (typing || !editable()) return false;
|
||||
return !!params.onKey?.(event) || draw.key(event) || edit.key(event);
|
||||
},
|
||||
sync() {
|
||||
draw.sync();
|
||||
edit.sync();
|
||||
props.render();
|
||||
pieces?.render();
|
||||
marks?.render();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** 지도 입력 — 그림 px · 겹 · 그릴 틀(없음 = 아직 못 그림) · 다시 그림. */
|
||||
export interface PlanInput {
|
||||
canvas: HTMLCanvasElement;
|
||||
/** 누른 자리 → 그림 px(돌리기 전). */
|
||||
at: (event: MouseEvent) => [number, number];
|
||||
screen: () => ScreenMap | null;
|
||||
layers: () => PlanMapLayer[];
|
||||
redraw: () => void;
|
||||
/** 남의 지도 위에 얹음(B05 평면) — 켠 동안 잡기 단계에서 먼저 받고 막음 · 오른쪽 · 가운데 끌기 · 휠은 그대로. */
|
||||
capture?: { active: () => boolean; busy: () => boolean };
|
||||
}
|
||||
|
||||
/** 누름 · 맴돎 · 놓음 · 우클릭 · 연도 마커 끌어 놓기를 겹에 나눠 줌(한 함수 · 두 페이지). */
|
||||
export function bindPlanInput(input: PlanInput): void {
|
||||
const { canvas, at, layers, redraw, capture } = input;
|
||||
const on = <K extends keyof HTMLElementEventMap>(
|
||||
type: K,
|
||||
run: (event: HTMLElementEventMap[K]) => void,
|
||||
): void => canvas.addEventListener(type, run, !!capture);
|
||||
const claim = (event: Event): void => {
|
||||
event.stopImmediatePropagation();
|
||||
event.preventDefault();
|
||||
};
|
||||
/** 지금 받을 틀 — 얹은 지도는 켠 동안만. */
|
||||
const live = (): ScreenMap | null => (capture && !capture.active() ? null : input.screen());
|
||||
const free = (): boolean => !capture?.busy();
|
||||
|
||||
on("pointerdown", (event) => {
|
||||
if (event.button !== 0) return;
|
||||
const map = live();
|
||||
if (!map) return;
|
||||
if (capture) claim(event);
|
||||
const [px, py] = at(event);
|
||||
if (free() && layers().some((layer) => layer.press?.(px, py, map, 0, event.shiftKey))) {
|
||||
canvas.setPointerCapture(event.pointerId); // 끌기가 캔버스 밖으로 나가도 놓음을 받음
|
||||
redraw();
|
||||
} else if (capture) redraw();
|
||||
});
|
||||
on("pointerup", (event) => {
|
||||
if (event.button !== 0) return;
|
||||
const map = live();
|
||||
if (!map) return;
|
||||
if (capture) claim(event);
|
||||
for (const layer of layers()) layer.release?.(map);
|
||||
redraw();
|
||||
});
|
||||
on("pointermove", (event) => {
|
||||
const map = live();
|
||||
if (!map || event.buttons & (capture ? 6 : 4)) return; // 가운데(얹음 = 오른쪽도) 끌기 = 지도 이동
|
||||
if (capture) event.stopImmediatePropagation();
|
||||
perfTime("plan:pointermove", () => {
|
||||
const [px, py] = at(event);
|
||||
for (const layer of layers()) layer.hover?.(px, py, map, event.shiftKey);
|
||||
redraw();
|
||||
});
|
||||
});
|
||||
canvas.addEventListener("pointerleave", () => {
|
||||
for (const layer of layers()) layer.leave?.();
|
||||
if (!capture || capture.active()) redraw();
|
||||
});
|
||||
on("contextmenu", (event) => {
|
||||
if (capture ? capture.active() : true) event.preventDefault();
|
||||
const map = live();
|
||||
if (!map) return;
|
||||
if (capture) claim(event);
|
||||
const [px, py] = at(event);
|
||||
const took = free() && layers().some((layer) => layer.press?.(px, py, map, 2, event.shiftKey));
|
||||
if (took || capture) redraw();
|
||||
});
|
||||
if (capture)
|
||||
for (const type of ["click", "dblclick"] as const)
|
||||
on(type, (event) => capture.active() && claim(event));
|
||||
const markDrag = (event: DragEvent): ScreenMap | null =>
|
||||
event.dataTransfer?.types.includes(YEAR_MARK_DRAG) ? live() : null;
|
||||
on("dragover", (event) => {
|
||||
const map = markDrag(event);
|
||||
if (!map) return;
|
||||
const [px, py] = at(event);
|
||||
if (layers().some((layer) => layer.dragOver?.(px, py, map))) {
|
||||
event.preventDefault();
|
||||
event.dataTransfer!.dropEffect = "copy";
|
||||
}
|
||||
redraw();
|
||||
});
|
||||
on("drop", (event) => {
|
||||
const map = markDrag(event);
|
||||
if (!map) return;
|
||||
event.preventDefault();
|
||||
const [px, py] = at(event);
|
||||
layers().some((layer) => layer.drop?.(px, py, map));
|
||||
redraw();
|
||||
});
|
||||
canvas.addEventListener("dragleave", () => {
|
||||
if (capture && !capture.active()) return;
|
||||
for (const layer of layers()) layer.leave?.();
|
||||
redraw();
|
||||
});
|
||||
}
|
||||
@@ -38,7 +38,7 @@ import type { GlBase } from "./D03_PlanAnalysis_UI_MapGl";
|
||||
import type { Vertex } from "./logic/D03_PlanAnalysis_Plan";
|
||||
import { localTmFrame, tmParams } from "./logic/D03_PlanAnalysis_Tm";
|
||||
|
||||
/** 「연도 구역 마커」 끌어 놓기 자료 꼴(585-3 · 단추 = 그리기 칸 599-4). */
|
||||
/** 「연도 구역 마커」 끌어 놓기 자료 꼴(585-3 · 단추 = 그리기 칸 599-4 · 받음 = 키트 `bindPlanInput`). */
|
||||
export const YEAR_MARK_DRAG = "application/x-aislo-year-mark";
|
||||
|
||||
/** 측점 간격(m) — B 규칙 측점과 같음. */
|
||||
@@ -157,6 +157,14 @@ export interface PlanMap {
|
||||
screenOfLonLat: (lon: number, lat: number) => [number, number] | null;
|
||||
/** 검증 창구 — 지금 돌린 각(도). */
|
||||
rotationDeg: () => number;
|
||||
/** 지도 입력 — 누름 · 맴돎 · 놓음 · 우클릭을 겹에 나눠 줄 재료(`bindPlanInput`). */
|
||||
input: {
|
||||
canvas: HTMLCanvasElement;
|
||||
at: (event: MouseEvent) => [number, number];
|
||||
screen: () => ScreenMap | null;
|
||||
layers: () => PlanMapLayer[];
|
||||
redraw: () => void;
|
||||
};
|
||||
dispose: () => void;
|
||||
}
|
||||
|
||||
@@ -444,71 +452,11 @@ export function createPlanMap(events: PlanMapEvents): PlanMap {
|
||||
flush();
|
||||
}
|
||||
|
||||
/** 누른 화면 자리 → 그림 px(돌리기 전). */
|
||||
const at = (event: PointerEvent | MouseEvent): [number, number] => {
|
||||
/** 누른 화면 자리 → 그림 px(돌리기 전) · 나눠 주기 = `bindPlanInput`(키트 · 페이지가 묶음). */
|
||||
const at = (event: MouseEvent): [number, number] => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
return rotation.unrotate(event.clientX - rect.left, event.clientY - rect.top);
|
||||
};
|
||||
|
||||
canvas.addEventListener("pointerdown", (event) => {
|
||||
if (!meta || event.button !== 0) return;
|
||||
const [px, py] = at(event);
|
||||
const map = screenMap();
|
||||
if (layers.some((layer) => layer.press?.(px, py, map, 0, event.shiftKey))) {
|
||||
canvas.setPointerCapture(event.pointerId); // 끌기가 캔버스 밖으로 나가도 놓음을 받음
|
||||
drawTop();
|
||||
}
|
||||
});
|
||||
canvas.addEventListener("pointerup", (event) => {
|
||||
if (event.button !== 0 || !meta) return;
|
||||
const map = screenMap();
|
||||
for (const layer of layers) layer.release?.(map);
|
||||
drawTop();
|
||||
});
|
||||
canvas.addEventListener("pointermove", (event) => {
|
||||
if (!meta || event.buttons & 4) return; // 가운데 단추 옮기는 중
|
||||
perfTime("plan:pointermove", () => {
|
||||
const [px, py] = at(event);
|
||||
const map = screenMap();
|
||||
for (const layer of layers) layer.hover?.(px, py, map, event.shiftKey);
|
||||
drawTop();
|
||||
});
|
||||
});
|
||||
canvas.addEventListener("pointerleave", () => {
|
||||
for (const layer of layers) layer.leave?.();
|
||||
drawTop();
|
||||
});
|
||||
const markDrag = (event: DragEvent): boolean =>
|
||||
!!meta && !!event.dataTransfer?.types.includes(YEAR_MARK_DRAG);
|
||||
canvas.addEventListener("dragover", (event) => {
|
||||
if (!markDrag(event)) return;
|
||||
const [px, py] = at(event);
|
||||
const map = screenMap();
|
||||
if (layers.some((layer) => layer.dragOver?.(px, py, map))) {
|
||||
event.preventDefault();
|
||||
event.dataTransfer!.dropEffect = "copy";
|
||||
}
|
||||
drawTop();
|
||||
});
|
||||
canvas.addEventListener("drop", (event) => {
|
||||
if (!markDrag(event)) return;
|
||||
event.preventDefault();
|
||||
const [px, py] = at(event);
|
||||
const map = screenMap();
|
||||
layers.some((layer) => layer.drop?.(px, py, map));
|
||||
drawTop();
|
||||
});
|
||||
canvas.addEventListener("dragleave", () => {
|
||||
for (const layer of layers) layer.leave?.();
|
||||
drawTop();
|
||||
});
|
||||
canvas.addEventListener("contextmenu", (event) => {
|
||||
event.preventDefault();
|
||||
if (!meta) return;
|
||||
const [px, py] = at(event);
|
||||
const map = screenMap();
|
||||
if (layers.some((layer) => layer.press?.(px, py, map, 2, event.shiftKey))) drawTop();
|
||||
});
|
||||
bindRouteEditNavigation({
|
||||
canvas,
|
||||
getView: () => view,
|
||||
@@ -572,6 +520,13 @@ export function createPlanMap(events: PlanMapEvents): PlanMap {
|
||||
screenOfLonLat: (lon, lat) =>
|
||||
scene.meta ? rotation.rerotate(...lonLat(scene.meta).project(lon, lat)) : null,
|
||||
rotationDeg: () => (rotation.radians() * 180) / Math.PI,
|
||||
input: {
|
||||
canvas,
|
||||
at,
|
||||
screen: () => (meta ? screenMap() : null),
|
||||
layers: () => layers,
|
||||
redraw: drawTop,
|
||||
},
|
||||
dispose: () => {
|
||||
gl?.enable(null);
|
||||
observer.disconnect();
|
||||
|
||||
@@ -10,22 +10,24 @@
|
||||
|
||||
import { createMapContextMenu, type MapContextMenuItem } from "@ui/ui_template_context_menu";
|
||||
import { samePoint, type PlanNetwork, type Vertex } from "./logic/D03_PlanAnalysis_Plan";
|
||||
import { oneLineBlock, type PlanDraw } from "./D03_PlanAnalysis_UI_Draw";
|
||||
import { lineLimitBlock, type PlanDraw } from "./D03_PlanAnalysis_UI_Draw";
|
||||
import type { PlanEdit } from "./D03_PlanAnalysis_UI_Edit";
|
||||
import { featureOn, type PlanPage } from "./D03_PlanAnalysis_UI_Features";
|
||||
import type { PlanMapLayer } from "./D03_PlanAnalysis_UI_Map";
|
||||
import type { PlanPoints } from "./D03_PlanAnalysis_UI_Points";
|
||||
|
||||
export interface PlanMenuParams {
|
||||
/** 기능 표 페이지 — 갈래 · 연도 구역 나눔 · 지점 항목. */
|
||||
page: PlanPage;
|
||||
network: () => PlanNetwork;
|
||||
points: () => Vertex[];
|
||||
marks: PlanPoints;
|
||||
/** 지점 · 핀(표 「points」 켠 페이지만). */
|
||||
points?: () => Vertex[];
|
||||
marks?: PlanPoints;
|
||||
edit: PlanEdit;
|
||||
draw: PlanDraw;
|
||||
editable: () => boolean;
|
||||
/** [다시 긋기](599-3) — 구역 id · 앞 노드 · 뒤 노드. */
|
||||
respan: (line: string, from: number, to: number) => boolean;
|
||||
/** 한 선만(B05 평면 예정노선 · 626-3) — 갈래 시작 · 둘째 선 · 연도 구역 나눔 항목 뺌. 없음 = D03 그대로. */
|
||||
oneLine?: boolean;
|
||||
}
|
||||
|
||||
export interface PlanMenu {
|
||||
@@ -35,7 +37,9 @@ export interface PlanMenu {
|
||||
}
|
||||
|
||||
export function createPlanMenu(params: PlanMenuParams): PlanMenu {
|
||||
const { marks, edit, draw } = params;
|
||||
const { edit, draw, page } = params;
|
||||
const marks = featureOn(page, "points") ? params.marks : undefined;
|
||||
const points = (): Vertex[] => params.points?.() ?? [];
|
||||
const menu = createMapContextMenu("d03-plan");
|
||||
let menuAt: [number, number] = [0, 0];
|
||||
|
||||
@@ -44,11 +48,11 @@ export function createPlanMenu(params: PlanMenuParams): PlanMenu {
|
||||
press(px, py, map, button) {
|
||||
if (button !== 2 || !params.editable()) return false;
|
||||
const items: MapContextMenuItem[] = [];
|
||||
const pin = marks.pinAt(px, py, map);
|
||||
const pin = marks?.pinAt(px, py, map) ?? null;
|
||||
const at = pin === null ? edit.hit(px, py, map) : null;
|
||||
const start = (x: number, y: number): void => {
|
||||
const aim = draw.aim(x, y, map);
|
||||
if (params.oneLine && oneLineBlock(aim, params.network())) return;
|
||||
if (!featureOn(page, "branch") && lineLimitBlock(aim, params.network())) return;
|
||||
const label = {
|
||||
tip: "이어 긋기",
|
||||
point: "지점에서 긋기 시작",
|
||||
@@ -59,8 +63,8 @@ export function createPlanMenu(params: PlanMenuParams): PlanMenu {
|
||||
};
|
||||
if (draw.mode() === "draw" && draw.stroke())
|
||||
items.push(["긋기 끝", () => draw.setMode("idle")]);
|
||||
else if (pin !== null) {
|
||||
const [sx, sy] = map.toScreen(params.points()[pin]); // 핀 머리를 눌러도 지점 자리에서
|
||||
else if (marks && pin !== null) {
|
||||
const [sx, sy] = map.toScreen(points()[pin]); // 핀 머리를 눌러도 지점 자리에서
|
||||
items.push([`${marks.name(pin)}에서 긋기 시작`, () => draw.startAt(sx, sy, map)]);
|
||||
items.push([`${marks.name(pin)} 지우기`, () => marks.remove(pin)]);
|
||||
} else if (at?.mark) {
|
||||
@@ -78,15 +82,16 @@ export function createPlanMenu(params: PlanMenuParams): PlanMenu {
|
||||
const [sx, sy] = map.toScreen(point);
|
||||
start(sx, sy);
|
||||
items.push("-", ["노드 지움", () => edit.dropNode(node)]);
|
||||
if (!marks.full() && !params.points().some((p) => samePoint(p, point)))
|
||||
if (marks && !marks.full() && !points().some((p) => samePoint(p, point)))
|
||||
items.push(["지점으로", () => marks.place(sx, sy, map)]);
|
||||
} else if (at?.segment) {
|
||||
const seg = at.segment;
|
||||
start(px, py);
|
||||
if (!params.oneLine) items.push(["여기서 연도 구역 나눔", () => edit.splitAt(px, py, map)]);
|
||||
if (featureOn(page, "years"))
|
||||
items.push(["여기서 연도 구역 나눔", () => edit.splitAt(px, py, map)]);
|
||||
items.push(["다시 긋기", () => params.respan(seg.line, seg.index, seg.index + 1)]);
|
||||
} else start(px, py);
|
||||
if (pin === null && !at?.node && !at?.mark && !marks.full())
|
||||
if (marks && pin === null && !at?.node && !at?.mark && !marks.full())
|
||||
items.push("-", ["지점 놓기", () => marks.place(px, py, map)]);
|
||||
menu.open(menuAt[0], menuAt[1], items);
|
||||
return true;
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
import "./D03_PlanAnalysis_UI_Style.css";
|
||||
import { ROUTES } from "@config/config_frontend";
|
||||
import { attachCollapsible, createPanelCard } from "@ui/ui_template_collapsible";
|
||||
import { attachCollapsible } from "@ui/ui_template_collapsible";
|
||||
import { askConfirm } from "@ui/ui_template_modal";
|
||||
import { createProgressCircle } from "@ui/ui_template_progress";
|
||||
import {
|
||||
@@ -50,8 +50,7 @@ import {
|
||||
type TerrainStart,
|
||||
} from "./D03_PlanAnalysis_Api";
|
||||
import { openPlan3d } from "./D03_PlanAnalysis_UI_3D";
|
||||
import { createPlanDraw } from "./D03_PlanAnalysis_UI_Draw";
|
||||
import { createPlanEdit } from "./D03_PlanAnalysis_UI_Edit";
|
||||
import { bindPlanInput, createPlanKit } from "./D03_PlanAnalysis_UI_Kit";
|
||||
import { createPlanLayers } from "./D03_PlanAnalysis_UI_Layers";
|
||||
import { createPlanTerrain } from "./D03_PlanAnalysis_UI_LidarUpload";
|
||||
import {
|
||||
@@ -60,10 +59,6 @@ import {
|
||||
type PlanMapLayer,
|
||||
} from "./D03_PlanAnalysis_UI_Map";
|
||||
import { createPlanMeasure } from "./D03_PlanAnalysis_UI_Measure";
|
||||
import { createPlanMenu } from "./D03_PlanAnalysis_UI_Menu";
|
||||
import { createPlanPoints } from "./D03_PlanAnalysis_UI_Points";
|
||||
import { createPlanPieces } from "./D03_PlanAnalysis_UI_Pieces";
|
||||
import { createPlanProps } from "./D03_PlanAnalysis_UI_Props";
|
||||
import { createPlanSite } from "./D03_PlanAnalysis_UI_Site";
|
||||
import {
|
||||
aroundSite,
|
||||
@@ -84,6 +79,7 @@ import {
|
||||
type PlanNetwork,
|
||||
type Vertex,
|
||||
} from "./logic/D03_PlanAnalysis_Plan";
|
||||
import { tmParams, tmToLonLat } from "./logic/D03_PlanAnalysis_Tm";
|
||||
|
||||
/** 되돌리기 쌓는 수. */
|
||||
const UNDO_MAX = 100;
|
||||
@@ -130,83 +126,75 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
||||
const heights = terrain.surface(surface); // 지형 칩 원천(624-4)
|
||||
return heights ? gridHeightAt(heights.grid, x, y) : NaN;
|
||||
};
|
||||
const draw = createPlanDraw({
|
||||
base: () => points,
|
||||
network: () => network,
|
||||
heightAt: () => z,
|
||||
commit,
|
||||
onMode: () => {
|
||||
if (draw.mode() !== "idle") setMeasure(false);
|
||||
refresh();
|
||||
},
|
||||
});
|
||||
const stepPoints = (nextPoints: Vertex[], next: PlanNetwork, nextNames = pointNames): void =>
|
||||
step({ network: next, points: nextPoints, pointNames: nextNames, site }); // 지점 한 걸음 — 위치 그대로
|
||||
const edit = createPlanEdit({
|
||||
const showPoints = (nextPoints: Vertex[], next: PlanNetwork): void => {
|
||||
[points, network] = [nextPoints, next];
|
||||
map.redraw();
|
||||
};
|
||||
// 긋기 · 고르기 · 속성 판 · 지점 · 구역 · 메뉴 · 왼쪽 칸 · 키 = 노선 그리기 키트(635-1 · 숨김 = 기능 표 「d03」)
|
||||
const circle = createProgressCircle({ size: 16, inline: true }); // 받기 진행 = 위치 칸 제목 오른쪽 서클(604-3)
|
||||
const siteBox = createPlanSite({
|
||||
onPick: (hit) => void pickSite(hit),
|
||||
onRetry: () => {
|
||||
[failed, lastTried, ready] = [null, "", true];
|
||||
void ensureCovered(true);
|
||||
},
|
||||
onUpload: (files) => uploadRoads(files),
|
||||
roads: {
|
||||
projectId,
|
||||
onPick: (pick, bounds) => {
|
||||
backdrop.pickRoad(pick);
|
||||
if (pick) fitTo(boxOfLonLat(epsg, bounds));
|
||||
},
|
||||
onDelete: (bundle) => backdrop.deleteRoad(bundle),
|
||||
},
|
||||
});
|
||||
const kit = createPlanKit({
|
||||
page: "d03",
|
||||
network: () => network,
|
||||
base: () => points,
|
||||
commit,
|
||||
heightAt: () => z,
|
||||
show: (next) => {
|
||||
network = normalizePieces(next, yearStart);
|
||||
map.redraw();
|
||||
},
|
||||
commitPoints: stepPoints,
|
||||
showPoints: (nextPoints, next) => {
|
||||
[points, network] = [nextPoints, next];
|
||||
map.redraw();
|
||||
},
|
||||
enabled: () => draw.mode() === "idle" && !measuring,
|
||||
editable: () => editable,
|
||||
onSelect: () => refresh(),
|
||||
});
|
||||
const props = createPlanProps({
|
||||
network: () => network,
|
||||
edit,
|
||||
editable: () => editable,
|
||||
yearStart: () => yearStart,
|
||||
commit,
|
||||
heightAt: () => z,
|
||||
epsg: () => epsg,
|
||||
points: () => points,
|
||||
addPoint: (at) => stepPoints([...points, at], network, [...pointNames, ""]),
|
||||
commitPoints: stepPoints,
|
||||
respan: draw.respan,
|
||||
});
|
||||
const pieces = createPlanPieces({
|
||||
network: () => network,
|
||||
edit,
|
||||
lonLat: (x, y) => {
|
||||
const tm = tmParams(epsg);
|
||||
return tm ? tmToLonLat(tm, x, y) : null;
|
||||
},
|
||||
editable: () => editable,
|
||||
yearOptions: props.yearOptions,
|
||||
commit,
|
||||
idle: () => !measuring,
|
||||
onMode: () => {
|
||||
if (draw.mode() !== "idle") setMeasure(false);
|
||||
refresh();
|
||||
},
|
||||
onSelect: () => refresh(),
|
||||
yearStart: () => yearStart,
|
||||
points: {
|
||||
list: () => points,
|
||||
names: () => pointNames,
|
||||
epsg: () => epsg,
|
||||
center: () => viewMid,
|
||||
commit: stepPoints,
|
||||
show: showPoints,
|
||||
},
|
||||
onKey: (event) =>
|
||||
keyIs(event, "plan.undo")
|
||||
? (undo(), true)
|
||||
: keyIs(event, "plan.redo")
|
||||
? (redo(), true)
|
||||
: measuring && event.key === "Escape"
|
||||
? (setMeasure(false), true)
|
||||
: false,
|
||||
cards: { site: { parts: siteBox.parts, title: circle.root } },
|
||||
});
|
||||
const { draw, edit, props } = kit;
|
||||
const marks = kit.marks!;
|
||||
const measure = createPlanMeasure(
|
||||
() => z,
|
||||
() => network,
|
||||
);
|
||||
const marks = createPlanPoints({
|
||||
points: () => points,
|
||||
network: () => network,
|
||||
names: () => pointNames,
|
||||
epsg: () => epsg,
|
||||
commit: (nextPoints, nextNames, next) => stepPoints(nextPoints, next, nextNames),
|
||||
center: () => viewMid,
|
||||
show: (nextPoints, next) => {
|
||||
[points, network] = [nextPoints, next];
|
||||
map.redraw();
|
||||
},
|
||||
idle: () => draw.mode() === "idle" && !measuring,
|
||||
editable: () => editable,
|
||||
});
|
||||
const menu = createPlanMenu({
|
||||
network: () => network,
|
||||
points: () => points,
|
||||
marks,
|
||||
edit,
|
||||
draw,
|
||||
editable: () => editable,
|
||||
respan: draw.respan,
|
||||
});
|
||||
// 지도 보기 가운데 — 그릴 때마다 작업 좌표로(지점 [추가] 자리 · 보기만 옮김 = 안 받음 596-4)
|
||||
const viewWatch: PlanMapLayer = {
|
||||
draw(context, screen) {
|
||||
@@ -216,8 +204,6 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
||||
if (!had && viewMid) marks.render(); // 첫 그림 뒤 가운데가 생김 = 지점 칸 [추가] 켬
|
||||
},
|
||||
};
|
||||
// 받기 진행 = 위치 칸 제목 오른쪽 서클 하나(받는 일 수 · 0 = 숨김 · 지도 조작 안 막음 604-3)
|
||||
const circle = createProgressCircle({ size: 16, inline: true });
|
||||
const work = createWorkCircle(circle);
|
||||
const terrain = createPlanTerrain({
|
||||
projectId,
|
||||
@@ -237,31 +223,6 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
||||
main.className = "d03-plan";
|
||||
main.append(map.root);
|
||||
|
||||
const siteBox = createPlanSite({
|
||||
onPick: (hit) => void pickSite(hit),
|
||||
onRetry: () => {
|
||||
[failed, lastTried, ready] = [null, "", true];
|
||||
void ensureCovered(true);
|
||||
},
|
||||
onUpload: (files) => uploadRoads(files),
|
||||
roads: {
|
||||
projectId,
|
||||
onPick: (pick, bounds) => {
|
||||
backdrop.pickRoad(pick);
|
||||
if (pick) fitTo(boxOfLonLat(epsg, bounds));
|
||||
},
|
||||
onDelete: (bundle) => backdrop.deleteRoad(bundle),
|
||||
},
|
||||
});
|
||||
const siteCard = createPanelCard("위치", false);
|
||||
siteCard.firstElementChild!.append(circle.root);
|
||||
siteCard.append(...siteBox.parts);
|
||||
const pointCard = createPanelCard("지점", false);
|
||||
pointCard.append(marks.panel);
|
||||
const drawCard = createPanelCard("그리기", false);
|
||||
drawCard.append(draw.panel);
|
||||
const pieceCard = createPanelCard("구역 · 연도", false);
|
||||
pieceCard.append(pieces.list);
|
||||
const saveButton = createButton({ label: "저장", variant: "filled", onClick: () => void save() });
|
||||
const resetButton = createButton({
|
||||
label: "초기화",
|
||||
@@ -273,7 +234,7 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
||||
actions.append(saveButton, resetButton);
|
||||
const panel = document.createElement("div");
|
||||
panel.className = "ui-sidebar-panel d03-plan__panel";
|
||||
panel.append(siteCard, pointCard, drawCard, pieceCard, actions);
|
||||
panel.append(...kit.cards, actions);
|
||||
attachCollapsible(panel);
|
||||
// 떠날 때(단계 아이콘 · [<] [>] · 머리 메뉴 · 뒤로) 고친 선이 있으면 공용 물음 [취소][폐기][저장](572-3)
|
||||
mountGroupStep(root, ROUTES.D03_PLAN_ANALYSIS, {
|
||||
@@ -351,17 +312,9 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
||||
refresh();
|
||||
}
|
||||
|
||||
/** 겹 차례 — 핀 누름(`marks.input`)은 고르기보다 앞 · 핀 그림(`marks.pins`) · 우클릭 메뉴(`menu.layer`)는 맨 끝(맨 위). */
|
||||
const layerList = (): PlanMapLayer[] => [
|
||||
...backdrop.layers,
|
||||
draw.layer,
|
||||
marks.input,
|
||||
edit.layer,
|
||||
...(measuring ? [measure.layer] : []),
|
||||
viewWatch,
|
||||
marks.pins,
|
||||
menu.layer,
|
||||
];
|
||||
/** 겹 차례 = 키트(바탕 겹 아래 · 재기 · 보기 가운데 = 고르기 위 · 핀 그림 · 우클릭 메뉴 맨 위). */
|
||||
const layerList = (): PlanMapLayer[] =>
|
||||
kit.layers(backdrop.layers, [...(measuring ? [measure.layer] : []), viewWatch]);
|
||||
|
||||
/** 점들이 받은 범위 안인가. */
|
||||
const inside = (list: Vertex[]): boolean =>
|
||||
@@ -381,8 +334,7 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
||||
}
|
||||
|
||||
function refresh(): void {
|
||||
draw.sync();
|
||||
edit.sync();
|
||||
kit.sync();
|
||||
map.setTools({
|
||||
undo: editable && undoStack.length > 0,
|
||||
redo: editable && redoStack.length > 0,
|
||||
@@ -390,9 +342,6 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
||||
measure: measuring,
|
||||
});
|
||||
map.redraw();
|
||||
props.render();
|
||||
pieces.render();
|
||||
marks.render();
|
||||
const far = epsg ? farFromSite(epsg, site, [...points, ...allVertices()]) : 0;
|
||||
const farNote = far ? `기준점 10 km 밖 점 ${far}곳 — 지형 안 받음 · 위치를 옮기세요` : "";
|
||||
siteBox.render({
|
||||
@@ -620,26 +569,17 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
||||
layers: backdrop.state,
|
||||
terrain: terrain.state,
|
||||
};
|
||||
// 키 = 등록부(588-3 · 한글 자판도 자판 자리로) · 되돌리기 · 다시하기 · 재기 끔 = 키트 `onKey` 먼저
|
||||
const onKey = (event: KeyboardEvent): void => {
|
||||
if (!root.isConnected) return window.removeEventListener("keydown", onKey);
|
||||
const typing = (event.target as HTMLElement | null)?.closest?.("input, textarea, select");
|
||||
if (typing || !editable) return;
|
||||
// 키 = 등록부(588-3 · 한글 자판도 자판 자리로)
|
||||
const handled = keyIs(event, "plan.undo")
|
||||
? (undo(), true)
|
||||
: keyIs(event, "plan.redo")
|
||||
? (redo(), true)
|
||||
: measuring && event.key === "Escape"
|
||||
? (setMeasure(false), true)
|
||||
: draw.key(event) || edit.key(event);
|
||||
if (handled) {
|
||||
event.preventDefault();
|
||||
map.redraw();
|
||||
}
|
||||
if (!kit.key(event)) return;
|
||||
event.preventDefault();
|
||||
map.redraw();
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
map.setLayers(layerList());
|
||||
menu.mount(map.root);
|
||||
kit.menu.mount(map.root);
|
||||
bindPlanInput(map.input);
|
||||
|
||||
try {
|
||||
editable = !!(await fetchSessionUser())?.is_forest_service;
|
||||
|
||||
@@ -33,7 +33,6 @@ import {
|
||||
segmentRange,
|
||||
} from "./logic/D03_PlanAnalysis_Select";
|
||||
import { nodeOff, nodeRecord } from "./logic/D03_PlanAnalysis_Span";
|
||||
import { tmParams, tmToLonLat } from "./logic/D03_PlanAnalysis_Tm";
|
||||
import {
|
||||
createDrawKindFields,
|
||||
DRAW_DEFAULT_LENGTH_M,
|
||||
@@ -41,6 +40,7 @@ import {
|
||||
type DrawKindValue,
|
||||
} from "./D03_PlanAnalysis_UI_DrawPanel";
|
||||
import type { PlanEdit, PlanSelection } from "./D03_PlanAnalysis_UI_Edit";
|
||||
import { featureOn, featureShown, type PlanPage } from "./D03_PlanAnalysis_UI_Features";
|
||||
import { PLAN_STATION_INTERVAL_M } from "./D03_PlanAnalysis_UI_Map";
|
||||
import { selectionLength } from "./D03_PlanAnalysis_UI_Select";
|
||||
import "./D03_PlanAnalysis_UI_DrawParts.css";
|
||||
@@ -53,22 +53,24 @@ const meters = (m: number): string => `${m.toFixed(1)} m`;
|
||||
const percent = (pct: number | null): string => (pct === null ? "-" : `${pct.toFixed(1)} %`);
|
||||
|
||||
export interface PlanPropsParams {
|
||||
/** 기능 표 페이지 — 연도 · 구간 줄 · [지점으로]. */
|
||||
page: PlanPage;
|
||||
network: () => PlanNetwork;
|
||||
edit: PlanEdit;
|
||||
editable: () => boolean;
|
||||
yearStart: () => number;
|
||||
/** 사업연도 시작(연도 고름 · 표 「years」 사용 페이지). */
|
||||
yearStart?: () => number;
|
||||
commit: (next: PlanNetwork) => void;
|
||||
heightAt: () => HeightAt;
|
||||
epsg: () => number;
|
||||
/** 지점(노드 판 [지점으로] — 이미 있으면 숨김). */
|
||||
points: () => Vertex[];
|
||||
addPoint: (point: Vertex) => void;
|
||||
/** 작업 좌표 → 경위도(없음 = 경위도 칸 숨김). */
|
||||
lonLat?: (x: number, y: number) => [number, number] | null;
|
||||
/** 지점(노드 판 [지점으로] — 이미 있으면 숨김 · 표 「points」 켠 페이지만). */
|
||||
points?: () => Vertex[];
|
||||
addPoint?: (point: Vertex) => void;
|
||||
/** 노드 판 방식 · 값 바꿈 — 노드 · 그 자리 지점 한 걸음. */
|
||||
commitPoints: (points: Vertex[], next: PlanNetwork) => void;
|
||||
/** [다시 긋기](599-3) — 구역 id · 앞 노드 · 뒤 노드. */
|
||||
respan: (line: string, from: number, to: number) => boolean;
|
||||
/** 한 선만(B05 평면 예정노선 · 626-3) — 연도 · 구간 줄 · [지점으로] 뺌. 없음 = D03 그대로. */
|
||||
oneLine?: boolean;
|
||||
}
|
||||
|
||||
export interface PlanProps {
|
||||
@@ -81,7 +83,11 @@ export interface PlanProps {
|
||||
}
|
||||
|
||||
export function createPlanProps(params: PlanPropsParams): PlanProps {
|
||||
const { edit } = params;
|
||||
const { edit, page } = params;
|
||||
const points = (): Vertex[] => params.points?.() ?? [];
|
||||
/** 연도 줄 보임 — 표 「years」 사용 · 보기만 + 조각 있음. */
|
||||
const yearsShown = (network: PlanNetwork): boolean =>
|
||||
featureShown(page, "years") && network.pieces.length > 0;
|
||||
const float = createFloatPanel({
|
||||
title: "고름",
|
||||
draggable: true,
|
||||
@@ -92,7 +98,7 @@ export function createPlanProps(params: PlanPropsParams): PlanProps {
|
||||
float.root.hidden = true;
|
||||
|
||||
function yearOptions(): Array<{ value: string; text: string }> {
|
||||
const start = params.yearStart();
|
||||
const start = params.yearStart?.() ?? new Date().getFullYear();
|
||||
const years = new Set(Array.from({ length: YEAR_SPAN }, (_, i) => start + i));
|
||||
params.network().pieces.forEach((piece) => years.add(piece.year));
|
||||
return [...years].sort().map((year) => ({ value: String(year), text: `${year}년` }));
|
||||
@@ -115,7 +121,7 @@ export function createPlanProps(params: PlanPropsParams): PlanProps {
|
||||
value: years.size > 1 ? "" : String(pieces[0]?.year ?? ""),
|
||||
size: 24,
|
||||
ariaLabel: "고른 구간 연도",
|
||||
disabled: !params.editable(),
|
||||
disabled: !params.editable() || !featureOn(page, "years"),
|
||||
onChange: (value) =>
|
||||
value && params.commit(setPiecesYear(params.network(), ids, Number(value))),
|
||||
});
|
||||
@@ -153,7 +159,7 @@ export function createPlanProps(params: PlanPropsParams): PlanProps {
|
||||
const apply = (): void => {
|
||||
const network = params.network();
|
||||
const next = nodeRecord(value.kind, value.lengthM, value.slopePct);
|
||||
const out = placeNode(network, params.points(), line, vertex, next, params.heightAt());
|
||||
const out = placeNode(network, points(), line, vertex, next, params.heightAt());
|
||||
if (typeof out === "string") return void showToast(out, "info");
|
||||
if (JSON.stringify(out.network.lines) !== JSON.stringify(network.lines))
|
||||
params.commitPoints(out.points, out.network);
|
||||
@@ -188,17 +194,17 @@ export function createPlanProps(params: PlanPropsParams): PlanProps {
|
||||
const { line, vertex } = picked.node!;
|
||||
const vertices = network.lines.find((l) => l.id === line)!.vertices;
|
||||
const point = vertices[vertex];
|
||||
const tm = tmParams(params.epsg());
|
||||
const lonLat = tm ? tmToLonLat(tm, point[0], point[1]) : null;
|
||||
const lonLat = params.lonLat?.(point[0], point[1]) ?? null;
|
||||
const height = z(point[0], point[1]);
|
||||
const rows: KvRow[] = [
|
||||
{
|
||||
label: "경위도",
|
||||
value: lonLat ? `${lonLat[0].toFixed(6)}, ${lonLat[1].toFixed(6)}` : null,
|
||||
},
|
||||
{ label: "지반고", value: Number.isFinite(height) ? `${height.toFixed(2)} m` : null },
|
||||
{ label: "측점", value: station(lineLength(vertices.slice(0, vertex + 1))) },
|
||||
];
|
||||
if (params.lonLat)
|
||||
rows.unshift({
|
||||
label: "경위도",
|
||||
value: lonLat ? `${lonLat[0].toFixed(6)}, ${lonLat[1].toFixed(6)}` : null,
|
||||
});
|
||||
if (vertex > 0) {
|
||||
const [a, b] = [vertices[vertex - 1], vertices[vertex]];
|
||||
rows.push(
|
||||
@@ -211,9 +217,10 @@ export function createPlanProps(params: PlanPropsParams): PlanProps {
|
||||
const parts: HTMLElement[] = [createKv(rows)];
|
||||
if (vertex > 0) parts.push(nodeKind(network.lines.indexOf(entry), vertex));
|
||||
const toPoint = button("지점으로", "props-to-point", () =>
|
||||
params.addPoint([point[0], point[1]]),
|
||||
params.addPoint?.([point[0], point[1]]),
|
||||
);
|
||||
toPoint.hidden = !!params.oneLine || params.points().some((p) => samePoint(p, point));
|
||||
toPoint.hidden =
|
||||
!featureOn(page, "points") || !params.addPoint || points().some((p) => samePoint(p, point));
|
||||
return {
|
||||
title: `${nameOf(line)} · 노드 ${vertex + 1}`,
|
||||
parts,
|
||||
@@ -240,7 +247,7 @@ export function createPlanProps(params: PlanPropsParams): PlanProps {
|
||||
rows.unshift({ label: "측점", value: `${station(from)} ~ ${station(to)}` });
|
||||
rows.push({ label: "지반 기울기", value: percent(groundSlopePct(a, b, z)) });
|
||||
}
|
||||
if (!params.oneLine)
|
||||
if (yearsShown(network))
|
||||
rows.push({ label: "연도", value: years.map((y) => `${y}년`).join(" · ") });
|
||||
const actions = [button("선 지움", "props-drop-segment", edit.dropSelected)];
|
||||
if (single)
|
||||
@@ -267,7 +274,7 @@ export function createPlanProps(params: PlanPropsParams): PlanProps {
|
||||
{ label: "길이", value: meters(length) },
|
||||
{ label: "노드", value: `${vertices.length}개` },
|
||||
]),
|
||||
...(params.oneLine ? [] : [yearField(pieces), pieceRows(network, pieces)]),
|
||||
...(yearsShown(network) ? [yearField(pieces), pieceRows(network, pieces)] : []),
|
||||
],
|
||||
actions: [button("구역 지움", "props-drop-line", () => edit.dropRegion(picked.line!))],
|
||||
};
|
||||
|
||||
@@ -107,7 +107,9 @@ def test_layers_turn_drops_old_answers() -> None:
|
||||
def test_one_circle_in_site_title() -> None:
|
||||
page = _src("D03_PlanAnalysis_UI_Page.ts")
|
||||
assert "createProgressCircle({ size: 16, inline: true })" in page
|
||||
assert "siteCard.firstElementChild!.append(circle.root);" in page
|
||||
assert "cards: { site: { parts: siteBox.parts, title: circle.root } }" in page # 635-1 키트
|
||||
kit = _src("D03_PlanAnalysis_UI_Kit.ts")
|
||||
assert "if (site.title) siteCard.firstElementChild!.append(site.title);" in kit
|
||||
assert (
|
||||
"corner: true" not in page
|
||||
and "circle.root);\n" not in page.split("map.root.append(")[1][:80]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""626-3 B05 평면 예정노선 변경 → 다시 추적 → 편집기 한 걸음 → [저장](요구 530 · 531 · 535-1 · 536 · 결정 D3 · D4 · D9).
|
||||
|
||||
- 예정노선 페이지(D03) 긋기 · 고르기 · 메뉴 · 속성 판 함수를 그대로 부름(`oneLine` = 한 선 · 갈래 · 연도 · 지점 막음/숨김) ·
|
||||
D03 덧 인자는 기본값 = 지금 그대로.
|
||||
- 예정노선 페이지(D03) 긋기 · 고르기 · 메뉴 · 속성 판 함수를 그대로 부름(숨김 = 기능 표 「b05」 — 한 선 · 갈래 · 연도 ·
|
||||
지점 · 635-1) · 지도 입력 = 키트 `bindPlanInput` capture.
|
||||
- 지킴(글 대조) — 다시 추적 = `/route/follow` 만(저장 0) · `/route/replan` · `as_initial` · `expected_route.csv` 안 부름 ·
|
||||
결과 = 편집기 `restoreSnapshot(…, true)` 한 걸음 · [저장] = 손 편집 같은 길(`bindRouteApply`) + 초안 `expected` ·
|
||||
걸음 사진 · 노선 초안에 고친 예정노선 실림(되돌리기 · 세션 되살림) · 같은 선 판정에 예정노선 · 켠 동안 캔버스 누름을
|
||||
@@ -71,32 +71,36 @@ def test_D03_긋기_고르기_메뉴_속성판_그대로_한_선():
|
||||
ui = _src(B05 / "B05_Profile_UI_ExpectedDraw.ts")
|
||||
for call in ("createPlanDraw({", "createPlanEdit(", "createPlanMenu({", "createPlanProps({"):
|
||||
assert call in ui, call
|
||||
assert ui.count("oneLine: true,") == 3 # 긋기 · 메뉴 · 속성 판
|
||||
assert ui.count('page: "b05",') == 3 # 긋기 · 메뉴 · 속성 판 = 기능 표(635-1)
|
||||
for fake in ("NO_POINTS", "epsg: () => 0", "addPoint: () => undefined", "oneLine"):
|
||||
assert fake not in ui, fake
|
||||
assert "draw.key(event) || editor.key(event)" in ui # 같은 단축키
|
||||
assert (
|
||||
'keyIs(event, "plan.undo") || keyIs(event, "plan.redo")' in ui
|
||||
) # 되돌리기 = 페이지 사진첩
|
||||
assert '"pointerdown",' in ui and "event.stopImmediatePropagation()" in ui
|
||||
assert re.search(
|
||||
r"\n true,\n \);", ui
|
||||
) # 잡기 단계(capture) — 재기 · 계획선 고르기보다 먼저
|
||||
assert "event.buttons & 6" in ui # 오른쪽 · 가운데 끌기 = 지도 이동 그대로
|
||||
# 지도 입력 = 키트 한 함수 · 잡기 단계(capture) — 재기 · 계획선 고르기보다 먼저
|
||||
assert "capture: { active: () => open, busy: () => busy }," in ui
|
||||
kit = _src(D03 / "D03_PlanAnalysis_UI_Kit.ts")
|
||||
assert "canvas.addEventListener(type, run, !!capture)" in kit
|
||||
assert "event.stopImmediatePropagation()" in kit
|
||||
assert "event.buttons & (capture ? 6 : 4)" in kit # 오른쪽 · 가운데 끌기 = 지도 이동 그대로
|
||||
assert 'createElement("button")' not in ui
|
||||
# D03 덧 인자 — 기본값 = 지금 그대로
|
||||
# 숨김 = 기능 표 읽기(635-1 · 플래그 없음)
|
||||
draw = _src(D03 / "D03_PlanAnalysis_UI_Draw.ts")
|
||||
assert (
|
||||
"oneLine?: boolean;" in draw
|
||||
and "params.oneLine ? oneLineBlock(target.kind, network) : null" in draw
|
||||
)
|
||||
assert "marks: !params.oneLine," in draw
|
||||
assert "markButton.hidden = opts.marks === false;" in _src(
|
||||
assert 'featureOn(params.page, "branch") ? null : lineLimitBlock(target.kind, network)' in draw
|
||||
assert 'markButton.hidden = !featureOn(opts.page, "years");' in _src(
|
||||
D03 / "D03_PlanAnalysis_UI_DrawPanel.ts"
|
||||
)
|
||||
menu = _src(D03 / "D03_PlanAnalysis_UI_Menu.ts")
|
||||
assert "if (params.oneLine && oneLineBlock(aim, params.network())) return;" in menu
|
||||
assert 'if (!params.oneLine) items.push(["여기서 연도 구역 나눔"' in menu
|
||||
assert '!featureOn(page, "branch") && lineLimitBlock(aim, params.network())' in menu
|
||||
assert 'if (featureOn(page, "years"))' in menu
|
||||
props = _src(D03 / "D03_PlanAnalysis_UI_Props.ts")
|
||||
assert "!!params.oneLine ||" in props and "...(params.oneLine ? [] :" in props
|
||||
assert '!featureOn(page, "points") ||' in props and "yearsShown(network)" in props
|
||||
for src in (draw, menu, props):
|
||||
assert "oneLine" not in src
|
||||
table = _src(D03 / "D03_PlanAnalysis_UI_Features.ts")
|
||||
for key, b05 in (("branch", "hide"), ("years", "view"), ("points", "hide"), ("draw", "use")):
|
||||
assert re.search(rf'key: "{key}",[^}}]*b05: "{b05}"', table, re.S), key
|
||||
tools = _src(ROOT / "A00_Common" / "map" / "map_toolbar.ts")
|
||||
# 634-2 — 표 줄에 호버제목(title) · 걷음은 635-3
|
||||
assert re.search(
|
||||
|
||||
Reference in New Issue
Block a user