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:
@@ -0,0 +1,236 @@
|
||||
/* =============================================================================
|
||||
* D03_PlanAnalysis_UI_Select.ts
|
||||
* D03 고름 상태(PLAN 598-1 · 요구 463 9 · 13) — 노드 → 선(토막) → 구역 → 전체 · 연도 구간(왼쪽 해 줄) · 마커.
|
||||
* 넓힘 = 같은 것을 다시 누름 — 씨(처음 누른 노드 · 토막)에서 한 단 위 · 전체 다음 = 씨로 돌아옴.
|
||||
* 「같은 해」 = 왼쪽 목록 해 줄로만(`year`).
|
||||
* 상태 = 화면 기억(저장 안 함) · 꼴이 바뀌면 `sync` 가 사라진 것을 뺌.
|
||||
* ========================================================================== */
|
||||
|
||||
import { lineLength, sliceLine } from "./logic/D03_PlanAnalysis_Draw";
|
||||
import type { PlanNetwork, Vertex } from "./logic/D03_PlanAnalysis_Plan";
|
||||
import {
|
||||
lineSegments,
|
||||
nodeSegment,
|
||||
sameSegment,
|
||||
segmentPath,
|
||||
segmentPieces,
|
||||
type PlanNode,
|
||||
type PlanSegment,
|
||||
} from "./logic/D03_PlanAnalysis_Select";
|
||||
|
||||
export type PlanSelectKind = "node" | "segment" | "line" | "all" | "pieces" | "mark";
|
||||
/** 왼쪽 목록 밝힘 꼴(구간 · 선 · 같은 해). */
|
||||
export type PlanScope = "piece" | "line" | "year";
|
||||
|
||||
export interface PlanSelection {
|
||||
kind: PlanSelectKind;
|
||||
/** 노드 고름 · 노드 씨. */
|
||||
node: PlanNode | null;
|
||||
/** 토막 고름(Shift · 끌어 고르기 = 여럿). */
|
||||
segments: PlanSegment[];
|
||||
/** 구역 고름 · 씨가 든 구역. */
|
||||
line: string | null;
|
||||
/** 고름에 든 연도 구간 id(구간 · 마커 고름 = 그대로 · 그 밖 = 걸친 구간). */
|
||||
pieces: string[];
|
||||
/** 고른 마커(그 마커가 여는 구간 id). */
|
||||
mark: string | null;
|
||||
scope: PlanScope;
|
||||
/** 넓힘 씨 — 처음 누른 노드 · 토막. */
|
||||
seed: { node: PlanNode } | { segment: PlanSegment } | null;
|
||||
}
|
||||
|
||||
const NEXT: Partial<Record<PlanSelectKind, PlanSelectKind>> = {
|
||||
node: "segment",
|
||||
segment: "line",
|
||||
line: "all",
|
||||
};
|
||||
|
||||
export interface PlanSelect {
|
||||
get: () => PlanSelection | null;
|
||||
clear: () => void;
|
||||
node: (node: PlanNode) => void;
|
||||
/** 토막 — `toggle` = 지금 토막 고름에 더함 · 뺌. */
|
||||
segments: (segments: PlanSegment[], toggle?: boolean) => void;
|
||||
region: (line: string) => void;
|
||||
all: () => void;
|
||||
/** 연도 구간 — 마커 고름 = 그 마커가 여는 구간. */
|
||||
pieces: (ids: string[], mark?: string | null, scope?: PlanScope) => void;
|
||||
/** 넓힘 한 단(씨 기준). */
|
||||
grow: () => void;
|
||||
/** 이 노드 · 토막이 지금 고름 안인가(다시 누름 = 넓힘). */
|
||||
holds: (hit: { node?: PlanNode; segment?: PlanSegment }) => boolean;
|
||||
/** 밝힐 꺾은선. */
|
||||
paths: () => Vertex[][];
|
||||
sync: () => void;
|
||||
}
|
||||
|
||||
export function createPlanSelect(network: () => PlanNetwork, onChange: () => void): PlanSelect {
|
||||
let selected: PlanSelection | null = null;
|
||||
|
||||
function piecesOf(next: Omit<PlanSelection, "pieces" | "scope">): string[] {
|
||||
const net = network();
|
||||
if (next.kind === "segment") return segmentPieces(net, next.segments);
|
||||
if (next.kind === "line")
|
||||
return net.pieces.filter((p) => p.line === next.line).map((p) => p.id);
|
||||
if (next.kind === "all") return net.pieces.map((p) => p.id);
|
||||
return [];
|
||||
}
|
||||
|
||||
function choose(
|
||||
next: Omit<PlanSelection, "pieces" | "scope"> & Partial<PlanSelection>,
|
||||
quiet = false,
|
||||
): void {
|
||||
selected = {
|
||||
...next,
|
||||
pieces: next.pieces ?? piecesOf(next),
|
||||
scope: next.scope ?? (next.kind === "line" ? "line" : "piece"),
|
||||
};
|
||||
if (!quiet) onChange();
|
||||
}
|
||||
|
||||
const empty = { node: null, segments: [], line: null, mark: null, seed: null };
|
||||
|
||||
function node(at: PlanNode): void {
|
||||
choose({ ...empty, kind: "node", node: at, line: at.line, seed: { node: at } });
|
||||
}
|
||||
|
||||
function segments(list: PlanSegment[], toggle = false): void {
|
||||
let next = list;
|
||||
if (toggle && selected?.kind === "segment") {
|
||||
const has = (s: PlanSegment): boolean => selected!.segments.some((t) => sameSegment(s, t));
|
||||
next = [
|
||||
...selected.segments.filter((s) => !list.some((t) => sameSegment(s, t))),
|
||||
...list.filter((s) => !has(s)),
|
||||
];
|
||||
}
|
||||
if (!next.length) return clear();
|
||||
choose({
|
||||
...empty,
|
||||
kind: "segment",
|
||||
segments: next,
|
||||
line: next[0].line,
|
||||
seed: { segment: next[0] },
|
||||
});
|
||||
}
|
||||
|
||||
function clear(): void {
|
||||
selected = null;
|
||||
onChange();
|
||||
}
|
||||
|
||||
/** 씨 기준 한 단 — 노드 → 토막 → 구역 → 전체 → 씨. */
|
||||
function grow(): void {
|
||||
if (!selected?.seed) return;
|
||||
const seed = selected.seed;
|
||||
const next = NEXT[selected.kind];
|
||||
const line = "node" in seed ? seed.node.line : seed.segment.line;
|
||||
if (next === "segment") {
|
||||
const seg = "node" in seed ? nodeSegment(network(), seed.node) : seed.segment;
|
||||
if (seg) choose({ ...empty, kind: "segment", segments: [seg], line, seed });
|
||||
} else if (next === "line") choose({ ...empty, kind: "line", line, seed });
|
||||
else if (next === "all") choose({ ...empty, kind: "all", line, seed });
|
||||
else if ("node" in seed) node(seed.node);
|
||||
else choose({ ...empty, kind: "segment", segments: [seed.segment], line, seed });
|
||||
}
|
||||
|
||||
function holds(hit: { node?: PlanNode; segment?: PlanSegment }): boolean {
|
||||
const s = selected;
|
||||
if (!s || s.kind === "pieces" || s.kind === "mark") return false;
|
||||
if (s.kind === "all") return true;
|
||||
const line = hit.node?.line ?? hit.segment?.line;
|
||||
if (s.kind === "line") return line === s.line;
|
||||
if (s.kind === "node")
|
||||
return !!hit.node && hit.node.line === s.node!.line && hit.node.vertex === s.node!.vertex;
|
||||
if (hit.segment) return s.segments.some((t) => sameSegment(t, hit.segment!));
|
||||
return s.segments.some(
|
||||
(t) =>
|
||||
t.line === hit.node!.line &&
|
||||
(t.index === hit.node!.vertex || t.index + 1 === hit.node!.vertex),
|
||||
);
|
||||
}
|
||||
|
||||
function paths(): Vertex[][] {
|
||||
const s = selected;
|
||||
const net = network();
|
||||
if (!s || s.kind === "node") return [];
|
||||
if (s.kind === "segment")
|
||||
return s.segments
|
||||
.map((seg) => segmentPath(net, seg))
|
||||
.filter((p): p is [Vertex, Vertex] => !!p);
|
||||
if (s.kind === "line") return net.lines.filter((l) => l.id === s.line).map((l) => l.vertices);
|
||||
if (s.kind === "all") return net.lines.map((l) => l.vertices);
|
||||
return s.pieces.flatMap((id) => {
|
||||
const piece = net.pieces.find((p) => p.id === id);
|
||||
const v = net.lines.find((l) => l.id === piece?.line)?.vertices;
|
||||
return piece && v ? [sliceLine(v, piece.from_m, piece.to_m)] : [];
|
||||
});
|
||||
}
|
||||
|
||||
/** 사라진 것 뺌 — 부르는 쪽(페이지 `refresh`)이 다시 그림(여기서 onChange 를 부르면 돌고 돎). */
|
||||
function sync(): void {
|
||||
const s = selected;
|
||||
if (!s) return;
|
||||
const net = network();
|
||||
const vertexCount = (line: string | null): number =>
|
||||
net.lines.find((l) => l.id === line)?.vertices.length ?? 0;
|
||||
const okNode = (n: PlanNode): boolean => n.vertex < vertexCount(n.line);
|
||||
const okSeg = (g: PlanSegment): boolean => g.index + 1 < vertexCount(g.line);
|
||||
if (s.kind === "pieces" || s.kind === "mark") {
|
||||
const ids = new Set(net.pieces.map((p) => p.id));
|
||||
const pieces = s.pieces.filter((id) => ids.has(id));
|
||||
selected = pieces.length
|
||||
? { ...s, pieces, mark: s.mark && ids.has(s.mark) ? s.mark : null }
|
||||
: null;
|
||||
if (selected?.kind === "mark" && !selected.mark) selected = { ...selected, kind: "pieces" };
|
||||
return;
|
||||
}
|
||||
const seed =
|
||||
s.seed && ("node" in s.seed ? okNode(s.seed.node) : okSeg(s.seed.segment)) ? s.seed : null;
|
||||
const segments = s.segments.filter(okSeg);
|
||||
const alive = {
|
||||
all: net.lines.length > 0,
|
||||
line: vertexCount(s.line) > 0,
|
||||
node: !!s.node && okNode(s.node),
|
||||
segment: segments.length > 0,
|
||||
}[s.kind];
|
||||
if (alive) choose({ ...s, segments, seed, pieces: undefined }, true);
|
||||
else selected = null;
|
||||
}
|
||||
|
||||
return {
|
||||
get: () => selected,
|
||||
clear,
|
||||
node,
|
||||
segments,
|
||||
region: (line) => choose({ ...empty, kind: "line", line, seed: lineSeed(line) }),
|
||||
all: () => choose({ ...empty, kind: "all", seed: null }),
|
||||
pieces(ids, mark = null, scope = "piece") {
|
||||
const list = [...new Set(ids)];
|
||||
if (!list.length) return clear();
|
||||
const net = network();
|
||||
choose({
|
||||
...empty,
|
||||
kind: mark ? "mark" : "pieces",
|
||||
line: net.pieces.find((p) => p.id === list[0])?.line ?? null,
|
||||
pieces: list,
|
||||
mark,
|
||||
scope,
|
||||
});
|
||||
},
|
||||
grow,
|
||||
holds,
|
||||
paths,
|
||||
sync,
|
||||
};
|
||||
|
||||
/** 구역 씨 = 첫 토막(구역에서 넓힘 = 전체 → 첫 토막). */
|
||||
function lineSeed(line: string): { segment: PlanSegment } | null {
|
||||
const first = lineSegments(network(), line)[0];
|
||||
return first ? { segment: first } : null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 고름 길이(m) — 토막 · 구역 · 전체 · 구간. */
|
||||
export function selectionLength(paths: Vertex[][]): number {
|
||||
return paths.reduce((sum, path) => sum + lineLength(path), 0);
|
||||
}
|
||||
Reference in New Issue
Block a user