feat(D 예정노선): 노드 판 — 방식 · 값 고침(앞 노드 기준 다시 놓음 · 뒤 노드 그대로 · 어긋남 ⚠) · [지점으로] · [다시 긋기] 이음 (PLAN 598-4 · 요구 463-11 · 464)
- logic/Select placeNode — 지점 자리 노드면 지점 같이 · 첫 노드 = 까닭 글 - UI_Props 노드 판 = 599-1 방식 칸(createDrawKindFields) · 방식 고름 = 바로 · 수 칸 = Enter · 칸 떠날 때 - 판 · 메뉴 [다시 긋기] = draw.respan(599-3) - 시험 598_1 placeNode 줄 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,8 +22,8 @@ export interface PlanMenuParams {
|
||||
edit: PlanEdit;
|
||||
draw: PlanDraw;
|
||||
editable: () => boolean;
|
||||
/** [다시 긋기](599 · 없으면 항목 없음) — 구역 id · 앞 노드 · 뒤 노드. */
|
||||
respan?: (line: string, from: number, to: number) => boolean;
|
||||
/** [다시 긋기](599-3) — 구역 id · 앞 노드 · 뒤 노드. */
|
||||
respan: (line: string, from: number, to: number) => boolean;
|
||||
}
|
||||
|
||||
export interface PlanMenu {
|
||||
@@ -80,8 +80,7 @@ export function createPlanMenu(params: PlanMenuParams): PlanMenu {
|
||||
const seg = at.segment;
|
||||
start(px, py);
|
||||
items.push(["여기서 연도 구역 나눔", () => edit.splitAt(px, py, map)]);
|
||||
if (params.respan)
|
||||
items.push(["다시 긋기", () => params.respan!(seg.line, seg.index, seg.index + 1)]);
|
||||
items.push(["다시 긋기", () => params.respan(seg.line, seg.index, seg.index + 1)]);
|
||||
} else start(px, py);
|
||||
if (pin === null && !at?.node && !at?.mark && !marks.full())
|
||||
items.push("-", ["지점 놓기", () => marks.place(px, py, map)]);
|
||||
|
||||
@@ -155,6 +155,8 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
||||
epsg: () => epsg,
|
||||
points: () => points,
|
||||
addPoint: (at) => step({ network, points: [...points, at], pointNames: [...pointNames, ""] }),
|
||||
commitPoints: (nextPoints, next) => step({ network: next, points: nextPoints, pointNames }),
|
||||
respan: draw.respan,
|
||||
});
|
||||
const pieces = createPlanPieces({ network: () => network, edit });
|
||||
const measure = createPlanMeasure(
|
||||
@@ -183,6 +185,7 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
|
||||
edit,
|
||||
draw,
|
||||
editable: () => editable,
|
||||
respan: draw.respan,
|
||||
});
|
||||
// 지도 보기 가운데 — 그릴 때마다 작업 좌표로(지점 [추가] 자리 · 보기만 옮김 = 안 받음 596-4)
|
||||
const viewWatch: PlanMapLayer = {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* 좌측 「구역 · 연도」 목록 = `UI_Pieces.ts`(페이지가 붙임).
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton } from "@ui/ui_template_elements";
|
||||
import { createButton, showToast } from "@ui/ui_template_elements";
|
||||
import { createFloatPanel } from "@ui/ui_template_float_panel";
|
||||
import { createSelectField } from "@ui/ui_template_input";
|
||||
import { createKv, type KvRow } from "@ui/ui_template_kv";
|
||||
@@ -26,8 +26,20 @@ import {
|
||||
type PlanPiece,
|
||||
type Vertex,
|
||||
} from "./logic/D03_PlanAnalysis_Plan";
|
||||
import { segmentPath, segmentPieces, segmentRange } from "./logic/D03_PlanAnalysis_Select";
|
||||
import {
|
||||
placeNode,
|
||||
segmentPath,
|
||||
segmentPieces,
|
||||
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,
|
||||
DRAW_DEFAULT_SLOPE_PCT,
|
||||
type DrawKindValue,
|
||||
} from "./D03_PlanAnalysis_UI_DrawPanel";
|
||||
import type { PlanEdit, PlanSelection } from "./D03_PlanAnalysis_UI_Edit";
|
||||
import { PLAN_STATION_INTERVAL_M } from "./D03_PlanAnalysis_UI_Map";
|
||||
import { selectionLength } from "./D03_PlanAnalysis_UI_Select";
|
||||
@@ -50,8 +62,10 @@ export interface PlanPropsParams {
|
||||
/** 지점(노드 판 [지점으로] — 이미 있으면 숨김). */
|
||||
points: () => Vertex[];
|
||||
addPoint: (point: Vertex) => void;
|
||||
/** [다시 긋기](599 · 없으면 단추 없음) — 구역 id · 앞 노드 · 뒤 노드. */
|
||||
respan?: (line: string, from: number, to: number) => boolean;
|
||||
/** 노드 판 방식 · 값 바꿈 — 노드 · 그 자리 지점 한 걸음. */
|
||||
commitPoints: (points: Vertex[], next: PlanNetwork) => void;
|
||||
/** [다시 긋기](599-3) — 구역 id · 앞 노드 · 뒤 노드. */
|
||||
respan: (line: string, from: number, to: number) => boolean;
|
||||
}
|
||||
|
||||
export interface PlanProps {
|
||||
@@ -125,6 +139,36 @@ export function createPlanProps(params: PlanPropsParams): PlanProps {
|
||||
return rows;
|
||||
}
|
||||
|
||||
/** 노드 방식 · 값(598-4 · 599-1 부품) — 방식 고름 = 바로 · 수 칸 = Enter · 칸 떠날 때 · 앞 노드 기준 다시 놓음. */
|
||||
function nodeKind(line: number, vertex: number): HTMLElement {
|
||||
const record = params.network().lines[line].nodes?.[vertex] ?? null;
|
||||
let value: DrawKindValue = {
|
||||
kind: record?.kind ?? "free",
|
||||
lengthM: record?.length_m ?? DRAW_DEFAULT_LENGTH_M,
|
||||
slopePct: record?.slope_pct ?? DRAW_DEFAULT_SLOPE_PCT,
|
||||
};
|
||||
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());
|
||||
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);
|
||||
};
|
||||
const fields = createDrawKindFields({
|
||||
value,
|
||||
onChange: (next) => {
|
||||
const kindChanged = next.kind !== value.kind;
|
||||
value = next;
|
||||
if (kindChanged) apply();
|
||||
},
|
||||
});
|
||||
fields.root.addEventListener("change", apply);
|
||||
fields.root.dataset.role = "props-node-kind";
|
||||
fields.setDisabled(!params.editable());
|
||||
return fields.root;
|
||||
}
|
||||
|
||||
/** 고름 꼴마다 — 머리 글 · 몸 · 단추. */
|
||||
function body(
|
||||
picked: PlanSelection,
|
||||
@@ -159,13 +203,17 @@ export function createPlanProps(params: PlanPropsParams): PlanProps {
|
||||
{ label: "앞 선 기울기", value: percent(groundSlopePct(a, b, z)) },
|
||||
);
|
||||
}
|
||||
const entry = network.lines.find((l) => l.id === line)!;
|
||||
if (nodeOff(entry, vertex, z)) rows.push({ label: "기록", value: "⚠ 지금 자리와 어긋남" });
|
||||
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]]),
|
||||
);
|
||||
toPoint.hidden = params.points().some((p) => samePoint(p, point));
|
||||
return {
|
||||
title: `${nameOf(line)} · 노드 ${vertex + 1}`,
|
||||
parts: [createKv(rows)],
|
||||
parts,
|
||||
actions: [
|
||||
button("노드 지움", "props-drop-node", () => edit.dropNode(picked.node!)),
|
||||
toPoint,
|
||||
@@ -191,10 +239,10 @@ export function createPlanProps(params: PlanPropsParams): PlanProps {
|
||||
}
|
||||
rows.push({ label: "연도", value: years.map((y) => `${y}년`).join(" · ") });
|
||||
const actions = [button("선 지움", "props-drop-segment", edit.dropSelected)];
|
||||
if (single && params.respan)
|
||||
if (single)
|
||||
actions.unshift(
|
||||
button("다시 긋기", "props-respan", () =>
|
||||
params.respan!(single.line, single.index, single.index + 1),
|
||||
params.respan(single.line, single.index, single.index + 1),
|
||||
),
|
||||
);
|
||||
return {
|
||||
|
||||
@@ -4,15 +4,26 @@
|
||||
* 저장 꼴은 그대로(`PlanNetwork`) — 선 = 「구역 id + 토막 차례」로 가리킴 · 낱 자료 없음.
|
||||
* 토막 지움 = 끝 토막은 끝 점 뺌 · 가운데 토막은 구역이 둘로(뒤쪽 = 새 구역 · 갈래 고리 · 조각이 따라감).
|
||||
* 지점 ↔ 노드 = 같은 자리(1 mm)로 묶음 — 묶인 노드 · 지점 찾기.
|
||||
* 노드 판(598-4) 방식 · 값 바꿈 = 앞 노드 기준 같은 방향으로 다시 놓음 · 뒤 노드 그대로(어긋남 ⚠ = 599-2 `nodeOff`).
|
||||
* DOM · fetch · sessionStorage 없음 · 조각 맞춤은 부르는 쪽(`normalizePieces`).
|
||||
* ========================================================================== */
|
||||
|
||||
import { lineLength, segmentLength } from "./D03_PlanAnalysis_Draw";
|
||||
import {
|
||||
compassPoint,
|
||||
fixedLengthPoint,
|
||||
fixedSlopePoint,
|
||||
lineLength,
|
||||
segmentLength,
|
||||
type HeightAt,
|
||||
} from "./D03_PlanAnalysis_Draw";
|
||||
import {
|
||||
moveNodesAt,
|
||||
moveVertex,
|
||||
piecesInBox,
|
||||
removeLine,
|
||||
samePoint,
|
||||
orderLines,
|
||||
type NodeRecord,
|
||||
type PlanLine,
|
||||
type PlanNetwork,
|
||||
type PlanPiece,
|
||||
@@ -237,3 +248,40 @@ export function nodesAt(network: PlanNetwork, point: Vertex): PlanNode[] {
|
||||
export function movePointsAt(points: ReadonlyArray<Vertex>, from: Vertex, to: Vertex): Vertex[] {
|
||||
return points.map((p): Vertex => (samePoint(p, from) ? [to[0], to[1]] : p));
|
||||
}
|
||||
|
||||
/** 노드 판(598-4) — `vertex` 노드 기록을 `record` 로 · 앞 노드 기준 지금 방향으로 다시 놓음(자유 = 자리 그대로) ·
|
||||
* 뒤 노드 그대로 · 그 자리 지점 · 같은 자리 노드도 같이 · 첫 노드 · 자리 못 찾음 = 까닭 글. */
|
||||
export function placeNode(
|
||||
network: PlanNetwork,
|
||||
points: ReadonlyArray<Vertex>,
|
||||
line: number,
|
||||
vertex: number,
|
||||
record: NodeRecord,
|
||||
heightAt: HeightAt,
|
||||
): { network: PlanNetwork; points: Vertex[] } | string {
|
||||
const entry = network.lines[line];
|
||||
if (!entry || vertex < 1 || vertex >= entry.vertices.length) return "첫 노드는 방식이 없습니다.";
|
||||
const [prev, here] = [entry.vertices[vertex - 1], entry.vertices[vertex]];
|
||||
const { kind, length_m: lengthM, slope_pct: slopePct } = record;
|
||||
const to =
|
||||
kind === "free"
|
||||
? here
|
||||
: kind === "length"
|
||||
? fixedLengthPoint(prev, here, lengthM ?? 0)
|
||||
: kind === "slope"
|
||||
? fixedSlopePoint(prev, here, slopePct ?? 0, heightAt)
|
||||
: compassPoint(prev, here, lengthM ?? 0, slopePct ?? 0, heightAt);
|
||||
if (!to) return "그 값으로 놓을 자리가 없습니다(지형 · 길이를 보세요).";
|
||||
const bound = points.some((p) => samePoint(p, here));
|
||||
let next = bound ? moveNodesAt(network, here, to) : moveVertex(network, line, vertex, to);
|
||||
next = {
|
||||
...next,
|
||||
lines: next.lines.map((l) => {
|
||||
if (l.id !== entry.id) return l;
|
||||
const nodes = l.nodes ? [...l.nodes] : l.vertices.map(() => null);
|
||||
nodes[vertex] = record;
|
||||
return { ...l, nodes };
|
||||
}),
|
||||
};
|
||||
return { network: next, points: bound ? movePointsAt(points, here, to) : [...points] };
|
||||
}
|
||||
|
||||
@@ -52,6 +52,11 @@ writeFileSync(process.argv[2], JSON.stringify({
|
||||
all: show(sel.removeAll(n)),
|
||||
nodes: sel.nodesAt(n, [200, 0]),
|
||||
moved: sel.movePointsAt([[200, 0], [5, 5]], [200, 0], [210, 5]),
|
||||
placed: (() => {
|
||||
const out = sel.placeNode(n, [[100, 0], [9, 9]], 0, 1, { kind: "length", length_m: 50, slope_pct: null }, (x) => x * 0.1);
|
||||
return [out.network.lines[0].vertices, out.network.lines[0].nodes, out.points];
|
||||
})(),
|
||||
placeFirst: sel.placeNode(n, [], 0, 0, { kind: "free", length_m: null, slope_pct: null }, () => 0),
|
||||
nodes594: (() => {
|
||||
const r = { kind: "length", length_m: 100, slope_pct: null };
|
||||
const withNodes = { ...n, lines: n.lines.map((l, i) => (i ? l : { ...l, nodes: [null, r, r, r] })) };
|
||||
@@ -133,6 +138,11 @@ def test_끌어_고르기_전체_지점_노드(tmp_path: Path) -> None:
|
||||
assert got["all"] == {"lines": [], "pieces": []}
|
||||
assert got["nodes"] == [{"line": "main", "vertex": 2}] # 갈래 첫 점 뺌
|
||||
assert got["moved"] == [[210, 5], [5, 5]]
|
||||
vertices, nodes, points = got["placed"]
|
||||
assert vertices == [[0, 0], [50, 0], [200, 0], [300, 0]] # 앞 노드 기준 50 m · 뒤 노드 그대로
|
||||
assert nodes[1] == {"kind": "length", "length_m": 50, "slope_pct": None} and nodes[0] is None
|
||||
assert points == [[50, 0], [9, 9]] # 그 자리 지점 같이
|
||||
assert got["placeFirst"] == "첫 노드는 방식이 없습니다."
|
||||
rec = {"kind": "length", "length_m": 100, "slope_pct": None}
|
||||
assert got["nodes594"] == [
|
||||
["main", [None, rec]],
|
||||
|
||||
Reference in New Issue
Block a user