- 노드 속성 판 · 우클릭 = 중간 노드에 [다시 긋기](선 토막 고름의 [다시 긋기] 뺌) · B05 긋기 중 중간 노드 누름 = 그대로 켬 - 끝 = 같은 선 다른 중간 노드만(rejoinMiss) · 시작 노드 · 끝점 = 못 찍음 + 안내(계속 긋기) · 빈 곳 = 점 찍고 계속 - 고리: 시작 노드 · 이을 중간 노드 · 마우스 올린 중간 노드(paintRejoin · --draw-map-*) · 옛 구간 흐림(dim) 뺌 - 시험: test_771_2 에 끝 판정 · 중간 노드 대조 더함 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
144 lines
6.8 KiB
TypeScript
144 lines
6.8 KiB
TypeScript
/* =============================================================================
|
|
* D03_PlanAnalysis_Span.ts
|
|
* D 예정노선 다시 긋기(PLAN 599-3 · 요구 463-12) — 선 하나의 두 꺾임점 사이를 새로 그은 점들로 바꿈.
|
|
* 중간 노드에서 긋기(771-2 · 요구 613) = `rejoinSpan` — 닿은 노드가 앞이면 거꾸로 · 같은 노드 = 까닭 글.
|
|
* 끝 판정(774 · 요구 616) = `rejoinMiss` — 다른 중간 노드만 끝 · 시작 노드 · 끝점 = 계속 긋기.
|
|
* 끝 두 점(앞 노드 · 뒤 노드) 그대로 · 속 꺾임점에 갈래가 붙었으면 막음(까닭 글) · 뒤 갈래 차례 밈 ·
|
|
* 마크(조각 경계) = 바뀐 구간 안은 길이 비율 · 뒤는 길이 차만큼 · 조각 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,
|
|
),
|
|
};
|
|
}
|
|
|
|
export const REJOIN_SAME =
|
|
"같은 노드에 닿아 반영하지 않았습니다 — 다른 노드에 닿으면 그 사이가 바뀝니다.";
|
|
|
|
/** 중간 노드에서 긋기(771-2) — `start` 노드에서 그은 `through` 가 `end` 노드에 닿음 = 두 노드 사이 갈음.
|
|
* 거꾸로 닿음(`end` < `start`) = 점 차례를 뒤집어 넣음(속 점 기록 = 선 차례 앞 토막 몫으로 밈) ·
|
|
* 같은 노드 = 까닭 글 · 그 밖 = `replaceSpan` 과 같음. */
|
|
export function rejoinSpan(
|
|
network: PlanNetwork,
|
|
line: number,
|
|
start: number,
|
|
end: number,
|
|
through: ReadonlyArray<Vertex>,
|
|
records: ReadonlyArray<NodeRecord | null> = [],
|
|
): PlanNetwork | string {
|
|
if (start === end) return REJOIN_SAME;
|
|
if (start < end) return replaceSpan(network, line, start, end, through, records);
|
|
const back = [...records].reverse();
|
|
return replaceSpan(network, line, end, start, [...through].reverse(), [
|
|
null,
|
|
...back.slice(0, -1),
|
|
]);
|
|
}
|
|
|
|
/** 다시 긋기 시작 · 끝 자리 = 중간 노드(774 · 요구 616) — 꼭짓점 `count` 개 선의 `vertex` 가 끝점 아님. */
|
|
export const isMiddleNode = (count: number, vertex: number): boolean =>
|
|
vertex > 0 && vertex < count - 1;
|
|
|
|
export const REJOIN_TIP = "끝점은 끝이 아닙니다 — 다른 중간 노드에 이어야 끝납니다.";
|
|
export const REJOIN_HOLD = "시작 노드는 끝이 아닙니다 — 다른 중간 노드에 이어야 끝납니다.";
|
|
|
|
/** 다시 긋기 끝 판정(774) — 닿은 `vertex` 가 다른 중간 노드 = null(끝) · 시작 노드 · 끝점 = 계속 긋기 까닭 글. */
|
|
export function rejoinMiss(count: number, start: number, vertex: number): string | null {
|
|
if (vertex === start) return REJOIN_HOLD;
|
|
return isMiddleNode(count, vertex) ? null : REJOIN_TIP;
|
|
}
|