- 중심점 = 보라 선 고름 끝 조각(판정 원 · 보라 선 위 · 다른 고름과 하나만) · 십자는 누름 통과 - 좌우 = 노선 편집기가 등록한 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
98 lines
4.2 KiB
TypeScript
98 lines
4.2 KiB
TypeScript
/* =============================================================================
|
||
* 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);
|
||
}
|