/* ============================================================================= * D03_PlanAnalysis_Span.ts * D 예정노선 다시 긋기(PLAN 599-3 · 요구 463-12) — 선 하나의 두 꺾임점 사이를 새로 그은 점들로 바꿈. * 끝 두 점(앞 노드 · 뒤 노드) 그대로 · 속 꺾임점에 갈래가 붙었으면 막음(까닭 글) · 뒤 갈래 차례 밈 · * 마크(조각 경계) = 바뀐 구간 안은 길이 비율 · 뒤는 길이 차만큼 · 조각 id 그대로(업체 열쇠). * 노드 기록(599-2) — 그리기 칸 값 → 기록 · 옮긴 노드 어긋남(⚠) · 다시 그은 속 노드 = 그때 기록 · 뒤 노드 = 자유. * DOM · fetch · sessionStorage 없음 · 길이 셈 = 562-1 짝(`D03_PlanAnalysis_Draw`). * ========================================================================== */ import { groundSlopePct, lineLength, segmentLength, type HeightAt } from "./D03_PlanAnalysis_Draw"; import type { NodeKind, NodeRecord, PlanLine, PlanNetwork, Vertex } from "./D03_PlanAnalysis_Plan"; const round3 = (value: number): number => Math.round(value * 1000) / 1000; /** 그리기 칸 방식 · 값 → 노드 기록(안 쓴 값 = null · 599-2). */ export function nodeRecord(kind: NodeKind, lengthM: number, slopePct: number): NodeRecord { return { kind, length_m: kind === "length" || kind === "both" ? lengthM : null, slope_pct: kind === "slope" || kind === "both" ? slopePct : null, }; } /** 노드 기록과 지금 앞 토막이 어긋남(옮긴 노드 ⚠ · 599-2) — 길이 1 cm · |기울기| 0.1 % 넘게 · 지형 밖 = 안 봄. */ export function nodeOff(line: PlanLine, vertex: number, z: HeightAt): boolean { const record = line.nodes?.[vertex]; if (!record || vertex < 1) return false; const [a, b] = [line.vertices[vertex - 1], line.vertices[vertex]]; if (record.length_m !== null && Math.abs(segmentLength(a, b) - record.length_m) > 0.01) return true; const slope = record.slope_pct === null ? null : groundSlopePct(a, b, z); return slope !== null && Math.abs(Math.abs(slope) - Math.abs(record.slope_pct!)) > 0.1; } /** `line` 째 선 `from`~`to` 꺾임점 사이를 다시 그을 수 있나 — 못 하면 까닭 글. */ export function respanBlocked( network: PlanNetwork, line: number, from: number, to: number, ): string | null { const entry = network.lines[line]; if (!entry || !(from >= 0 && to > from && to < entry.vertices.length)) return "다시 그을 구간을 고르세요."; if ( network.lines.some( (o) => o.branch?.line === entry.id && o.branch.vertex > from && o.branch.vertex < to, ) ) return "구간 속 점에 갈래가 붙어 있습니다 — 갈래를 먼저 지우세요."; return null; } /** `line` 째 선 `from`~`to` 꺾임점 사이 속 점을 `through` 로 바꿈(끝 두 점 그대로) — 막히면 까닭 글. * `records` = 속 점 기록(`through` 와 같은 길이) · 뒤 노드 기록 = 자유 · 기록 없는 선 + 기록 없음 = 그대로 없음. */ export function replaceSpan( network: PlanNetwork, line: number, from: number, to: number, through: ReadonlyArray, records: ReadonlyArray = [], ): PlanNetwork | string { const blocked = respanBlocked(network, line, from, to); if (blocked) return blocked; const entry = network.lines[line]; const old = entry.vertices; const vertices: Vertex[] = [ ...old.slice(0, from + 1), ...through.map(([x, y]): Vertex => [x, y]), ...old.slice(to), ]; const shift = through.length - (to - from - 1); const at = (list: ReadonlyArray, i: number): number => lineLength(list.slice(0, i + 1)); const [startM, oldEndM, newEndM] = [at(old, from), at(old, to), at(vertices, to + shift)]; const carry = (m: number): number => m <= startM ? m : m >= oldEndM ? m - oldEndM + newEndM : startM + ((m - startM) / (oldEndM - startM)) * (newEndM - startM); const branched = (other: PlanLine): PlanLine => other.branch?.line === entry.id && other.branch.vertex >= to ? { ...other, branch: { ...other.branch, vertex: other.branch.vertex + shift } } : other; return { lines: network.lines.map((other, i) => { if (i !== line) return branched(other); if (!other.nodes && !records.some(Boolean)) return { ...other, vertices }; const nodes = other.nodes ?? old.map(() => null); const inner = through.map((_, k) => records[k] ?? null); const end = nodeRecord("free", 0, 0); return { ...other, vertices, nodes: [...nodes.slice(0, from + 1), ...inner, end, ...nodes.slice(to + 1)], }; }), pieces: network.pieces.map((piece) => piece.line === entry.id ? { ...piece, from_m: round3(carry(piece.from_m)), to_m: round3(carry(piece.to_m)) } : piece, ), }; }