/* ============================================================================= * 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; } 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)) ); }