Files
Aislo/B05_Profile/logic/B05_Profile_RouteShift.ts
T
eomsangdonandClaude Opus 5.5 c67a39c266 feat(B06): 356-3 횡단 중심점 고름 — ▲▼ 계획고 · ◀▶ · 가로 끌기 = 그 측점 노선 좌우
- 중심점 = 보라 선 고름 끝 조각(판정 원 · 보라 선 위 · 다른 고름과 하나만) · 십자는 누름 통과
- 좌우 = 노선 편집기가 등록한 host(B05_Profile_UI_RouteEdit_Shift) — 0.5 m 안 노드 옮김 · 곡선 안 = 교각점 · 아니면 끼움 · 고정 노드 안 옮김
- 끄는 동안 · 키를 잇는 동안 십자만 옮겨 보임 · 놓을 때 한 번 커밋
- 뒤 측점은 계산 묶음이 조용히 · 그 측점 계획고 = 옮기기 전 값(계획고 편집) · 노선 + 계획고 = 되돌리기 한 걸음
- B06 을 바로 열어 편집기가 없으면 종단 보기를 숨긴 채 세움(design_page 등록)
- 시험 test_356 · helper_356(노드 고름 · 뒤 측점 범위 · 높이 그대로 · 되돌리기 한 걸음 · 바로 열기)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TteK8mfYZetk5P3bEZkyVZ
2026-10-16 20:53:10 +09:00

98 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* B05_Profile_RouteShift.ts
* 횡단 중심점 좌우 이동(356-3) — 측점 자리 노선을 좌측 축으로 Δ m 옮길 노드 편집을 고르고 · 그 측점
* 계획고를 옮기기 전 값으로 되돌릴 계획고 편집을 냄.
*
* · P = 측점 중심 · L = 좌측 단위 벡터(`stationFrameAt` · +offset = 좌)
* · P 에서 0.5 m 안 노드 = 그 노드 + Δ·L(고정 노드면 안 옮김)
* · 곡선(BC ~ EC) 안 측점 = 그 교각점 + Δ·L(고정이면 안 옮김)
* · 둘 다 아니면 P + Δ·L 을 그 자리 노드 토막에 끼움
* · 높이 = 옮기기 전 계획고 − 옮긴 뒤 계획고 만큼 그 측점 계획고 편집(`adjustStation`)
*
* 브라우저가 고르는 편집값일 뿐 — 서버는 [저장] 때 노드 · 계획고 편집을 받아 정본으로 다시 셈(짝 없음).
* ========================================================================== */
import {
adjustStation,
buildAlignment,
planElevationAt,
type AlignmentBase,
type AlignmentEdits,
} from "./B05_Profile_UI_Profile_Alignment";
import { curveSpans, stationFrameAt, type CurveEnds } from "./B05_Profile_UI_RouteEdit_Frame";
type Vertex = [number, number];
/** 측점 중심에서 이 안의 노드는 끼우지 않고 옮김(m). */
export const NODE_SNAP_M = 0.5;
/** 이보다 작은 높이 차는 보정 안 함(m). */
export const PIN_TOLERANCE_M = 5e-4;
export type RouteShiftPlan =
| { kind: "move"; index: number; to: Vertex }
| { kind: "insert"; index: number; at: Vertex }
| { kind: "locked"; index: number };
export interface RouteShiftInput {
/** 노드(꺾임점). */
planned: ReadonlyArray<Vertex>;
/** 그려진 계획노선(원호 펴진 폴리라인) — 측점 누가거리의 기준. */
line: ReadonlyArray<Vertex>;
/** 곡선표 — `node_first` = 그 곡선 교각점 노드 자리. */
curves: ReadonlyArray<CurveEnds & { node_first: number }>;
/** 노드마다 고정. */
locked: ReadonlyArray<boolean>;
chainageM: number;
/** 좌(+) · 우(−) 이동(m). */
deltaM: number;
}
/** 점에서 가장 가까운 노드 토막 차례(앞 노드 자리). */
function nearestSegment(nodes: ReadonlyArray<Vertex>, point: Vertex): number {
let best = 0;
let bestGap = Infinity;
for (let i = 0; i < nodes.length - 1; i += 1) {
const [ax, ay] = nodes[i];
const [vx, vy] = [nodes[i + 1][0] - ax, nodes[i + 1][1] - ay];
const length2 = vx * vx + vy * vy || 1;
const t = Math.min(1, Math.max(0, ((point[0] - ax) * vx + (point[1] - ay) * vy) / length2));
const gap = Math.hypot(ax + vx * t - point[0], ay + vy * t - point[1]);
if (gap < bestGap) [best, bestGap] = [i, gap];
}
return best;
}
/** 노드 편집 고름 — 노선이 없으면 null. */
export function planRouteShift(input: RouteShiftInput): RouteShiftPlan | null {
const { planned, line, curves, locked, chainageM, deltaM } = input;
const frame = planned.length >= 2 ? stationFrameAt(line, chainageM) : null;
if (!frame) return null;
const { origin, left } = frame;
const shifted = ([x, y]: Vertex): Vertex => [x + left[0] * deltaM, y + left[1] * deltaM];
const move = (index: number): RouteShiftPlan =>
locked[index]
? { kind: "locked", index }
: { kind: "move", index, to: shifted(planned[index]) };
const near = planned.findIndex(
([x, y]) => Math.hypot(x - origin[0], y - origin[1]) <= NODE_SNAP_M,
);
if (near >= 0) return move(near);
const curve = curves.find((one) => {
const [span] = curveSpans(line, [one]);
return span && chainageM > span[0] && chainageM < span[1];
});
if (curve && planned[curve.node_first]) return move(curve.node_first);
return { kind: "insert", index: nearestSegment(planned, origin) + 1, at: shifted(origin) };
}
/** 그 측점 계획고를 `zBefore` 로 되돌린 계획고 편집 — 차가 없으면 null. */
export function pinStationHeight(
base: AlignmentBase,
edits: AlignmentEdits,
chainageM: number,
zBefore: number,
): AlignmentEdits | null {
const drift = zBefore - planElevationAt(buildAlignment(base, edits), chainageM);
return Math.abs(drift) < PIN_TOLERANCE_M ? null : adjustStation(base, edits, chainageM, drift);
}