- 옮김: B04 2 · B05 23 · B06 11 · A00_Common/design 13 (9,299 줄) · 이미 logic/ 에 있던 16 파일 그대로 - 안 옮김: 묶음 입구(package.json build:*) · 파이썬 · 순수지만 계산 아닌 8 (보기 결선 · 콜백 · API 타입 · 갈아끼움 알림판) - import 266 줄(142 파일) · 시험 · 주석 · 짝 표시 문자열 경로 고침 · B05 · B06 CLAUDE.md 짝 줄 logic/ - 시험 도우미 새 경로: 가짜 require 열쇠 · B05()/B06() 부름 · 형제 import 자리 · tsc 묶어 돌리는 시험 · 정규식 · rglob - design_view_cross 줄 한도 시험 685 → 688 (긴 import 경로 줄바꿈 3줄) - 확인: typecheck 0 · 서버 묶음 넷(corridor · server-calc · b07-extent · b07-note) 다시 지어 코드 같음(//#region 경로 주석만 다름) · 관련 시험 270 파일 — 남은 실패 19 는 옮기기 전과 같음 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL
130 lines
4.8 KiB
TypeScript
130 lines
4.8 KiB
TypeScript
/* =============================================================================
|
|
* B05_Profile/logic/B05_Profile_Grade_Carry.ts
|
|
* 노선이 바뀔 때 계획고 옮김 — 바뀐 구간만 지반에 다시 맞추고 그 밖 편집은 길이 차만큼 밂(344-2).
|
|
*
|
|
* 짝 = 파이썬 `B05_Profile_Engine_Grade_Carry.py`(서버 정본 · 같은 규칙) · 거울 시험 =
|
|
* resources/tester/test_344_2_route_pairs.py.
|
|
* - 바뀐 구간 = 옛 · 새 노선 정점열의 같은 앞머리 · 꼬리를 뺀 가운데(바뀐 노드 앞 · 뒤 접선점 사이).
|
|
* - 앞 = 그대로 · 뒤 = 누가거리만 길이 차만큼 밂 · 안 = 새 지반 첫 계획선 꼭짓점(`groundFollowingBasePvi`)
|
|
* · 안의 편집(계획고 · 종단곡선 R)은 버림.
|
|
* - 옮기는 것 = base_pvi · station_offsets · curve_radii(누가거리 글자 키).
|
|
* ========================================================================== */
|
|
|
|
import {
|
|
chainageKey,
|
|
type AlignmentEdits,
|
|
type AlignmentNode,
|
|
} from "./B05_Profile_UI_Profile_Alignment";
|
|
|
|
/** 같은 정점으로 볼 거리(m) — 파이썬 `SAME_VERTEX_M` 짝. */
|
|
export const SAME_VERTEX_M = 1e-3;
|
|
const EPS = 1e-6;
|
|
|
|
export interface XY {
|
|
x: number;
|
|
y: number;
|
|
}
|
|
|
|
export interface ChangedSpan {
|
|
start_m: number;
|
|
old_end_m: number;
|
|
new_end_m: number;
|
|
delta_m: number;
|
|
start_fixed: boolean;
|
|
end_fixed: boolean;
|
|
}
|
|
|
|
const roundTo = (value: number, digits: number): number => Number(value.toFixed(digits));
|
|
|
|
function cumulative(xy: ReadonlyArray<XY>): number[] {
|
|
const total = [0];
|
|
for (let index = 1; index < xy.length; index += 1) {
|
|
const prev = xy[index - 1];
|
|
total.push(total[index - 1] + Math.hypot(xy[index].x - prev.x, xy[index].y - prev.y));
|
|
}
|
|
return total;
|
|
}
|
|
|
|
const same = (a: XY, b: XY): boolean =>
|
|
Math.abs(a.x - b.x) <= SAME_VERTEX_M && Math.abs(a.y - b.y) <= SAME_VERTEX_M;
|
|
|
|
/** 옛 · 새 노선 정점열 → 바뀐 누가거리 구간 · 같으면 null(파이썬 `changed_span` 짝). */
|
|
export function changedSpan(
|
|
oldXy: ReadonlyArray<XY>,
|
|
newXy: ReadonlyArray<XY>,
|
|
): ChangedSpan | null {
|
|
const limit = Math.min(oldXy.length, newXy.length);
|
|
let prefix = 0;
|
|
while (prefix < limit && same(oldXy[prefix], newXy[prefix])) prefix += 1;
|
|
if (prefix === oldXy.length && prefix === newXy.length) return null;
|
|
let suffix = 0;
|
|
while (
|
|
suffix < limit - prefix &&
|
|
same(oldXy[oldXy.length - 1 - suffix], newXy[newXy.length - 1 - suffix])
|
|
)
|
|
suffix += 1;
|
|
const oldCum = cumulative(oldXy);
|
|
const newCum = cumulative(newXy);
|
|
return {
|
|
start_m: prefix ? oldCum[prefix - 1] : 0,
|
|
old_end_m: suffix ? oldCum[oldXy.length - suffix] : oldCum[oldCum.length - 1],
|
|
new_end_m: suffix ? newCum[newXy.length - suffix] : newCum[newCum.length - 1],
|
|
delta_m: newCum[newCum.length - 1] - oldCum[oldCum.length - 1],
|
|
start_fixed: prefix > 0,
|
|
end_fixed: suffix > 0,
|
|
};
|
|
}
|
|
|
|
const before = (span: ChangedSpan, s: number): boolean =>
|
|
s < span.start_m - EPS || (span.start_fixed && s <= span.start_m + EPS);
|
|
|
|
const after = (span: ChangedSpan, s: number, end: number): boolean =>
|
|
s > end + EPS || (span.end_fixed && s >= end - EPS);
|
|
|
|
/** 옛 꼭짓점 · 편집을 새 노선으로 옮김 — `groundPvi` = 새 노선 지반 첫 계획선(전 구간). */
|
|
export function carryProfileEdits(
|
|
basePvi: ReadonlyArray<AlignmentNode>,
|
|
edits: Partial<AlignmentEdits> | null | undefined,
|
|
span: ChangedSpan,
|
|
groundPvi: ReadonlyArray<AlignmentNode>,
|
|
): { basePvi: AlignmentNode[]; edits: AlignmentEdits } {
|
|
const moved = (s: number): number | null => {
|
|
if (before(span, s)) return s;
|
|
if (after(span, s, span.old_end_m)) return s + span.delta_m;
|
|
return null; // 바뀐 구간 안 — 버림
|
|
};
|
|
|
|
const nodes = new Map<number, AlignmentNode>();
|
|
basePvi.forEach((node) => {
|
|
const at = moved(node.chainage_m);
|
|
if (at !== null)
|
|
nodes.set(roundTo(at, 3), { chainage_m: roundTo(at, 6), elevation_m: node.elevation_m });
|
|
});
|
|
groundPvi.forEach((node) => {
|
|
const s = node.chainage_m;
|
|
if (before(span, s) || after(span, s, span.new_end_m)) return;
|
|
const slot = roundTo(s, 3);
|
|
if (!nodes.has(slot))
|
|
nodes.set(slot, { chainage_m: roundTo(s, 6), elevation_m: node.elevation_m });
|
|
});
|
|
|
|
const carry = (values: Record<string, number> | undefined): Record<string, number> => {
|
|
const out: Record<string, number> = {};
|
|
Object.entries(values ?? {}).forEach(([key, value]) => {
|
|
const chainage = Number(key);
|
|
if (!Number.isFinite(chainage) || value == null) return;
|
|
const at = moved(chainage);
|
|
if (at !== null) out[chainageKey(at)] = Number(value);
|
|
});
|
|
return out;
|
|
};
|
|
|
|
return {
|
|
basePvi: [...nodes.keys()].sort((a, b) => a - b).map((slot) => nodes.get(slot)!),
|
|
edits: {
|
|
station_offsets: carry(edits?.station_offsets),
|
|
curve_radii: carry(edits?.curve_radii),
|
|
},
|
|
};
|
|
}
|