feat(D 예정노선): 고름 층 노드 → 선 → 구역 → 전체 · 판 하나 재활용 · 우클릭 메뉴 · 지점 ↔ 노드 붙음 (PLAN 598-1~3 · 요구 463)
- 새 logic/D03_PlanAnalysis_Select.ts — 토막 판정 · 끌어 고르기 토막 · 토막 지움(가운데 = 구역 둘 · 뒤쪽 새 구역) · 전체 지움 · 지점 ↔ 노드 같은 자리 - 새 UI_Select.ts — 고름 상태(다시 누름 = 한 단 넓힘 · 같은 해 = 왼쪽 해 줄로만) - UI_Edit.ts — 우클릭 즉시 지움 걷음 · 노드 끌면 지점 같이 · 지점 12 px 안 붙음 · 지움 창구(dropNode · dropRegion · dropAll · splitAt) - UI_Props.ts — 판 = 노드 · 선 · 구역 · 전체 · 구간 · 마커 몸 · [방향 바꾸기] · [갈래 시점 지정] 걷음 - 새 UI_Menu.ts — 우클릭 메뉴(Points 에서 옮김 · 노드 지움 · 지점으로 · 연도 구역 나눔) - UI_Points.ts — 핀 놓을 때 노드 12 px 안 붙음(nodeNear) - key_registry plan 줄 · 시험 585_2 갈래 시점 지정 줄 지움(요구 463-15 걷음) · 553_4 메뉴 자리 · 새 시험 598_1 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,21 +2,23 @@
|
||||
* D03_PlanAnalysis_UI_Points.ts
|
||||
* D03 지점(PLAN 591-2 · 요구 452 · 453) — 옛 기준 지정(D02) 지점 마킹을 노선분석으로.
|
||||
* 지점 = 긋기에 붙는 점 · 목록 하나(줄 왼 띠 = 지점 색 `pointColor` · 「지점 N」 · 🗑) · 핀 = 늘 맨 위(`pointLayer`).
|
||||
* 핀 끌기 = 지점 + 그 자리 노선 노드 같이(`moveNodesAt`) · 놓을 때 한 걸음(되돌리기 · 캐시 = 페이지).
|
||||
* 지도 우클릭 = 메뉴(부품 상자 `createMapContextMenu`) — 핀: 여기서 긋기 · 지움 / 그 밖: 긋기 시작(빈 땅 = 새 본선 ·
|
||||
* 선 위 = 갈래 · 선 끝 = 이어 긋기) · 지점 놓기(꺾임점 둘레면 그 점에) · 긋는 중 = 긋기 끝.
|
||||
* 겹 둘 — `input`(핀 누름 · 끌기 · 고르기 겹보다 앞) · `pins`(그림 · 우클릭 메뉴 · 겹 맨 끝 = 맨 위 ·
|
||||
* 꺾임점 · 마크 오른쪽 누름 = 고르기 겹이 먼저 지움).
|
||||
* 핀 끌기 = 지점 + 그 자리 노선 노드 같이(`moveNodesAt`) · 노드 12 px 안에 놓으면 그 노드에 붙음(598-3) ·
|
||||
* 놓을 때 한 걸음(되돌리기 · 캐시 = 페이지).
|
||||
* 지도 우클릭 메뉴 = `D03_PlanAnalysis_UI_Menu`(핀 집기 · 놓기 · 지우기를 여기서 빌림).
|
||||
* 겹 둘 — `input`(핀 누름 · 끌기 · 고르기 겹보다 앞) · `pins`(그림 · 겹 맨 끝 = 맨 위).
|
||||
* ========================================================================== */
|
||||
|
||||
import { createIconButton } from "@ui/ui_template_button";
|
||||
import { createMapContextMenu, type MapContextMenuItem } from "@ui/ui_template_context_menu";
|
||||
import { createListRow } from "@ui/ui_template_list_row";
|
||||
import { pointColor } from "@ui/ui_template_palette";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { moveNodesAt, type PlanNetwork, type Vertex } from "./logic/D03_PlanAnalysis_Plan";
|
||||
import {
|
||||
moveNodesAt,
|
||||
samePoint,
|
||||
type PlanNetwork,
|
||||
type Vertex,
|
||||
} from "./logic/D03_PlanAnalysis_Plan";
|
||||
import { tmParams, tmToLonLat } from "./logic/D03_PlanAnalysis_Tm";
|
||||
import type { PlanDraw } from "./D03_PlanAnalysis_UI_Draw";
|
||||
import { pointLayer, screenGap, SNAP_PX } from "./D03_PlanAnalysis_UI_DrawPaint";
|
||||
import type { PlanMapLayer, ScreenMap } from "./D03_PlanAnalysis_UI_Map";
|
||||
|
||||
@@ -31,7 +33,6 @@ export interface PlanPointsParams {
|
||||
names: () => string[];
|
||||
network: () => PlanNetwork;
|
||||
epsg: () => number;
|
||||
draw: PlanDraw;
|
||||
/** 한 걸음 — 지점 · 이름 · 노선 함께. */
|
||||
commit: (points: Vertex[], names: string[], network: PlanNetwork) => void;
|
||||
/** 지도 보기 가운데(작업 좌표 · 못 잼 = null) — [추가] 자리. */
|
||||
@@ -49,8 +50,12 @@ export interface PlanPoints {
|
||||
input: PlanMapLayer;
|
||||
pins: PlanMapLayer;
|
||||
render: () => void;
|
||||
/** 메뉴를 띄울 지도 그릇(우클릭 자리 = 그릇 기준). */
|
||||
mount: (viewport: HTMLElement) => void;
|
||||
/** 우클릭 메뉴가 빌림 — 핀 집기 · 지점 놓기(노드 둘레면 그 노드) · 지우기 · 이름 · 더 놓을 수 있나. */
|
||||
pinAt: (px: number, py: number, map: ScreenMap) => number | null;
|
||||
place: (px: number, py: number, map: ScreenMap) => void;
|
||||
remove: (i: number) => void;
|
||||
name: (i: number) => string;
|
||||
full: () => boolean;
|
||||
}
|
||||
|
||||
export function createPlanPoints(params: PlanPointsParams): PlanPoints {
|
||||
@@ -60,8 +65,6 @@ export function createPlanPoints(params: PlanPointsParams): PlanPoints {
|
||||
hint.classList.add("d03-plan__hint");
|
||||
const panel = document.createElement("div");
|
||||
panel.append(list, hint);
|
||||
const menu = createMapContextMenu("d03-plan");
|
||||
let menuAt: [number, number] = [0, 0];
|
||||
let drag: {
|
||||
index: number;
|
||||
points: Vertex[];
|
||||
@@ -95,47 +98,26 @@ export function createPlanPoints(params: PlanPointsParams): PlanPoints {
|
||||
);
|
||||
}
|
||||
|
||||
/** 지점 놓기 — 꺾임점 둘레(SNAP_PX)면 그 점 자리(노드에 붙음). */
|
||||
function place(px: number, py: number, map: ScreenMap): void {
|
||||
if (params.points().length >= MAX_POINTS) return;
|
||||
/** 붙음(598-3) — 화면 px 둘레(SNAP_PX) 가장 가까운 노드 자리 · `skip` 자리 노드(같이 가는 것) 빼고. */
|
||||
function nodeNear(px: number, py: number, map: ScreenMap, skip?: Vertex): Vertex | null {
|
||||
const node = params
|
||||
.network()
|
||||
.lines.flatMap((line) => line.vertices)
|
||||
.filter((point) => !skip || !samePoint(point, skip))
|
||||
.map((point) => ({ point, gap: screenGap(map, point, px, py) }))
|
||||
.filter((item) => item.gap <= SNAP_PX)
|
||||
.sort((a, b) => a.gap - b.gap)[0];
|
||||
const at = node?.point ?? map.toMetric(px, py);
|
||||
return node ? node.point : null;
|
||||
}
|
||||
|
||||
/** 지점 놓기 — 노드 둘레면 그 노드 자리(노드에 붙음). */
|
||||
function place(px: number, py: number, map: ScreenMap): void {
|
||||
if (params.points().length >= MAX_POINTS) return;
|
||||
const at = nodeNear(px, py, map) ?? map.toMetric(px, py);
|
||||
params.commit([...params.points(), [at[0], at[1]]], [...params.names(), ""], params.network());
|
||||
}
|
||||
|
||||
function openMenu(px: number, py: number, map: ScreenMap): boolean {
|
||||
if (!params.editable()) return false;
|
||||
const draw = params.draw;
|
||||
const pin = pinAt(px, py, map);
|
||||
const items: MapContextMenuItem[] = [];
|
||||
if (draw.mode() === "draw" && draw.stroke())
|
||||
items.push(["긋기 끝", () => draw.setMode("idle")]);
|
||||
else if (pin !== null) {
|
||||
const [sx, sy] = map.toScreen(params.points()[pin]); // 핀 머리를 눌러도 지점 자리에서
|
||||
items.push([`${name(pin)}에서 긋기 시작`, () => draw.startAt(sx, sy, map)]);
|
||||
items.push([`${name(pin)} 지우기`, () => remove(pin)]);
|
||||
} else {
|
||||
const label = {
|
||||
tip: "이어 긋기",
|
||||
point: "지점에서 긋기 시작",
|
||||
line: "갈래 시작",
|
||||
empty: "여기서 긋기 시작",
|
||||
}[draw.aim(px, py, map)];
|
||||
items.push([label, () => draw.startAt(px, py, map)]);
|
||||
}
|
||||
if (pin === null && params.points().length < MAX_POINTS)
|
||||
items.push(["지점 놓기", () => place(px, py, map)]);
|
||||
menu.open(menuAt[0], menuAt[1], items);
|
||||
return true;
|
||||
}
|
||||
|
||||
const pins = pointLayer(params.points);
|
||||
pins.press = (px, py, map, button) => (button === 2 ? openMenu(px, py, map) : false);
|
||||
|
||||
const input: PlanMapLayer = {
|
||||
draw: () => undefined,
|
||||
@@ -148,8 +130,8 @@ export function createPlanPoints(params: PlanPointsParams): PlanPoints {
|
||||
},
|
||||
hover(px, py, map) {
|
||||
if (!drag) return;
|
||||
const to = map.toMetric(px, py);
|
||||
const from = drag.points[drag.index];
|
||||
const to = nodeNear(px, py, map, from) ?? map.toMetric(px, py);
|
||||
const points = drag.points.map((p, k): Vertex => (k === drag!.index ? to : p));
|
||||
drag.last = [points, moveNodesAt(drag.network, from, to)];
|
||||
params.show(...drag.last);
|
||||
@@ -199,16 +181,10 @@ export function createPlanPoints(params: PlanPointsParams): PlanPoints {
|
||||
input,
|
||||
pins,
|
||||
render,
|
||||
mount(viewport) {
|
||||
viewport.append(menu.element);
|
||||
viewport.addEventListener(
|
||||
"contextmenu",
|
||||
(event) => {
|
||||
const rect = viewport.getBoundingClientRect();
|
||||
menuAt = [event.clientX - rect.left, event.clientY - rect.top];
|
||||
},
|
||||
true,
|
||||
);
|
||||
},
|
||||
pinAt,
|
||||
place,
|
||||
remove,
|
||||
name,
|
||||
full: () => params.points().length >= MAX_POINTS,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user