174 lines
6.6 KiB
TypeScript
174 lines
6.6 KiB
TypeScript
/* =============================================================================
|
|
* D03_PlanAnalysis_Edit.ts
|
|
* D 예정노선 꺾임점 고침 계산(PLAN 543-1) — 고른 갈래 노선의 마디(IP) 끌기 · 넣기 · 지우기 · 곡선 손잡이
|
|
* = R. DOM · fetch · sessionStorage 없음.
|
|
*
|
|
* 셈은 B05 편집기 순수 부품 그대로(import 만) — 서버 노드 펴기 `flattenServerPlan` · 다시 그음
|
|
* `rebuildLine`(R 하한 끌어올림 + `buildEditedPolyline`) · 마디 넣기 · 빼기 `spliceNode` · 손잡이 →
|
|
* 교각점 · R `dragHandleTo` · 하한 새로 깸 막기 `floorStop`.
|
|
* 처음 · 끝 마디 = 연도 경계 지점 → 잠금(옮기면 연도 사이가 끊김 · 지점은 지점 끌기로).
|
|
*
|
|
* ⚠ 서버 짝 — [저장] 때 `D03_PlanAnalysis_Edit.py` `edited_line` 이 같은 마디 · 켬끔 · R 로 선을 다시
|
|
* 그음(Node `route.polyline` = 같은 `buildEditedPolyline` · 폴백 `build_planned_polyline`) · 거울 시험
|
|
* `resources/tester/test_543_plan_edit.py`.
|
|
* ========================================================================== */
|
|
|
|
import {
|
|
flattenServerPlan,
|
|
floorStop,
|
|
rebuildLine,
|
|
spliceNode,
|
|
type CurveLimits,
|
|
type NodeColumns,
|
|
} from "../../B05_Profile/logic/B05_Profile_UI_RouteEdit_Edits";
|
|
import {
|
|
dragHandleTo,
|
|
type EditedCurve,
|
|
type EditedPolyline,
|
|
type Vertex,
|
|
} from "../../B05_Profile/logic/B05_Profile_UI_RouteEdit_Curve";
|
|
import type { PlanRoute } from "./D03_PlanAnalysis_Plan";
|
|
|
|
/** 연도 하나의 꺾임점 고침(캐시 · [저장] 입력) — 고친 갈래 · 그 분석 지문 · 마디 · 곡선 켬끔 · R. */
|
|
export interface PlanEdit {
|
|
base_mode: string;
|
|
base_fingerprint: string;
|
|
nodes: Vertex[];
|
|
curve_flags: boolean[];
|
|
/** 넣은 R(하한까지 끌어올린 값) · null = 법정 최소 R. */
|
|
radii: Array<number | null>;
|
|
}
|
|
|
|
export type EditStep = { edit: PlanEdit; line: EditedPolyline } | { blocked: string };
|
|
|
|
const limitsOf = (minRadiusM: number): CurveLimits => ({
|
|
minRadiusM,
|
|
limitRadiusM: minRadiusM,
|
|
limitArcM: 0,
|
|
});
|
|
|
|
const columnsOf = (edit: PlanEdit): NodeColumns => ({
|
|
planned: edit.nodes.map(([x, y]) => [x, y] as Vertex),
|
|
curveOn: [...edit.curve_flags],
|
|
curveRadius: [...edit.radii],
|
|
curveLock: edit.nodes.map(() => null),
|
|
curveArc: edit.nodes.map(() => null),
|
|
apexLock: edit.nodes.map(() => false),
|
|
});
|
|
|
|
/** 엔진 노선 → 고침 시작 마디(묶인 곡선은 교각점 하나로 · 서버가 고른 R 그대로). */
|
|
export function editFromRoute(route: PlanRoute, mode: string, fingerprint: string): PlanEdit {
|
|
const flat = flattenServerPlan(route.nodes ?? [], (route.curves ?? []) as EditedCurve[]);
|
|
return {
|
|
base_mode: mode,
|
|
base_fingerprint: fingerprint,
|
|
nodes: flat.planned,
|
|
curve_flags: flat.curveOn,
|
|
radii: flat.curveRadius,
|
|
};
|
|
}
|
|
|
|
/** 마디 → 선 — R 은 하한까지 끌어올린 값으로 고침에 되적음(서버에 같은 값을 보냄). */
|
|
export function buildEdit(
|
|
edit: PlanEdit,
|
|
minRadiusM: number,
|
|
): { edit: PlanEdit; line: EditedPolyline } {
|
|
const columns = columnsOf(edit);
|
|
const line = rebuildLine(columns, limitsOf(minRadiusM));
|
|
return { edit: { ...edit, radii: columns.curveRadius }, line };
|
|
}
|
|
|
|
/** 고친 뒤 — 지키던 하한을 새로 깨면 막음(그 걸음 버림). */
|
|
function settle(before: PlanEdit, after: PlanEdit, minRadiusM: number): EditStep {
|
|
const built = buildEdit(after, minRadiusM);
|
|
const stop = floorStop(
|
|
columnsOf(before),
|
|
built.edit.nodes,
|
|
built.line.nodes,
|
|
limitsOf(minRadiusM),
|
|
);
|
|
return stop ? { blocked: stop } : built;
|
|
}
|
|
|
|
const isEnd = (edit: PlanEdit, index: number): boolean =>
|
|
index <= 0 || index >= edit.nodes.length - 1;
|
|
const ENDS_LOCKED = "처음 · 끝 마디는 연도 경계 지점이라 여기서 못 고칩니다.";
|
|
|
|
/** 마디 끌기 — 처음 · 끝 잠금. */
|
|
export function moveNode(edit: PlanEdit, index: number, to: Vertex, minRadiusM: number): EditStep {
|
|
if (isEnd(edit, index)) return { blocked: ENDS_LOCKED };
|
|
const nodes = edit.nodes.map((node, i) => (i === index ? ([to[0], to[1]] as Vertex) : node));
|
|
return settle(edit, { ...edit, nodes }, minRadiusM);
|
|
}
|
|
|
|
/** 곡선 손잡이(접선점) 끌기 → 그쪽 직선이 돌고 교각점 · R 이 바뀜 · R 은 법정 최소 밑으로 안 내려감. */
|
|
export function moveHandle(
|
|
edit: PlanEdit,
|
|
index: number,
|
|
which: "start" | "end",
|
|
to: Vertex,
|
|
minRadiusM: number,
|
|
): EditStep {
|
|
if (isEnd(edit, index)) return { blocked: ENDS_LOCKED };
|
|
const got = dragHandleTo(
|
|
edit.nodes[index - 1],
|
|
edit.nodes[index],
|
|
edit.nodes[index + 1],
|
|
which,
|
|
to,
|
|
);
|
|
if (!got) return { blocked: "곡선을 그 자리로 못 옮깁니다." };
|
|
const nodes = edit.nodes.map((node, i) => (i === index ? got.apex : node));
|
|
const radii = edit.radii.map((r, i) => (i === index ? Math.max(got.radius, minRadiusM) : r));
|
|
const curve_flags = edit.curve_flags.map((on, i) => (i === index ? true : on));
|
|
return settle(edit, { ...edit, nodes, radii, curve_flags }, minRadiusM);
|
|
}
|
|
|
|
/** 마디 넣기 — `segment` 번째 마디와 다음 마디 사이. */
|
|
export function insertNode(
|
|
edit: PlanEdit,
|
|
segment: number,
|
|
at: Vertex,
|
|
minRadiusM: number,
|
|
): EditStep {
|
|
if (segment < 0 || segment >= edit.nodes.length - 1) return { blocked: "선 위를 누르세요." };
|
|
const columns = columnsOf(edit);
|
|
spliceNode(columns, segment + 1, [at[0], at[1]]);
|
|
const after = {
|
|
...edit,
|
|
nodes: columns.planned,
|
|
curve_flags: columns.curveOn,
|
|
radii: columns.curveRadius,
|
|
};
|
|
return settle(edit, after, minRadiusM);
|
|
}
|
|
|
|
/** 마디 지우기 — 처음 · 끝 잠금. */
|
|
export function removeNode(edit: PlanEdit, index: number, minRadiusM: number): EditStep {
|
|
if (isEnd(edit, index)) return { blocked: ENDS_LOCKED };
|
|
const columns = columnsOf(edit);
|
|
spliceNode(columns, index);
|
|
const after = {
|
|
...edit,
|
|
nodes: columns.planned,
|
|
curve_flags: columns.curveOn,
|
|
radii: columns.curveRadius,
|
|
};
|
|
return settle(edit, after, minRadiusM);
|
|
}
|
|
|
|
/** 같은 고침인가(서버 저장값과 견줌 · 1 mm · R 1 mm). */
|
|
export function sameEdit(a: PlanEdit | null | undefined, b: PlanEdit | null | undefined): boolean {
|
|
if (!a || !b) return false;
|
|
const near = (x: number | null, y: number | null): boolean =>
|
|
x === null || y === null ? x === y : Math.abs(x - y) < 1e-3;
|
|
return (
|
|
a.base_mode === b.base_mode &&
|
|
a.base_fingerprint === b.base_fingerprint &&
|
|
a.nodes.length === b.nodes.length &&
|
|
a.nodes.every((p, i) => near(p[0], b.nodes[i][0]) && near(p[1], b.nodes[i][1])) &&
|
|
a.curve_flags.every((on, i) => on === b.curve_flags[i]) &&
|
|
a.radii.every((r, i) => near(r, b.radii[i] ?? null))
|
|
);
|
|
}
|