Files
Aislo/D03_PlanAnalysis/logic/D03_PlanAnalysis_Edit.ts
T

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))
);
}