Files
Aislo/D03_PlanAnalysis/logic/D03_PlanAnalysis_Span.ts
T
eomsangdonandClaude Opus 5.5 9febb63e13 feat(D 예정노선): 노드마다 긋기 방식 · 값 기록 — 꼭짓점과 같이 움직임 · 옮긴 노드 기록 남기고 경고 고리 (PLAN 599-2 · 요구 463-11)
- 점 찍을 때 그때 방식 · 값을 nodes 칸에(594-1 꼴 · 붙은 점 = 자유 · 처음 기록 = 앞 노드 기록 없음)
- 끼움 · 더함 · 뺌 · 지움 · 뒤집기(토막 끝 노드 몫 한 칸 밀림) · 잇기 · 다시 긋기(뒤 노드 = 자유)가 기록 길이를 꼭짓점과 맞춤 — [저장] 400 막음
- nodeRecord · nodeOff(길이 1 cm · 기울기 0.1 % 넘게 어긋남) — Span.ts(Plan 700 줄 넘지 않게) · 598 노드 판이 씀
- 시험 599_3 에 기록 따라감 · 어긋남

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 14:49:21 +09:00

106 lines
4.8 KiB
TypeScript

/* =============================================================================
* 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<Vertex>,
records: ReadonlyArray<NodeRecord | null> = [],
): 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<Vertex>, 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,
),
};
}