/* ============================================================================= * B05_Profile/logic/B05_Profile_Grade_Seat.ts * 옮긴 관 · 구조물의 새 측점에 계획선 꼭짓점 세움(427) — 원지반 지남(`passThrough` 규칙) · 이웃 꼭짓점은 곡선 모양이 * 바뀌어 옮기기 전 계획고에서 1mm 넘게 뜨면 그 높이로 맞춤(433-5 · 바로 앞뒤부터 번지는 만큼) · 높이 고친 꼭짓점은 그대로. * 옮기기 전 계획선이 꼭짓점 대부분에서 지반을 안 지나면(균형 종단) 지반 대신 옮기기 전 계획선 높이(438). * * 서버도 이 셈 — Node 입구 `profile.seat`(472-4 ① · 파이썬 `Router_Confirm._seat_born_nodes` 가 부름) · 세울 측점은 공용 `followMovedNodes` * (common_util_structure_station.ts)가 고름 · 거울 시험 = resources/tester/test_427_1_follow_seat.py. * ========================================================================== */ import { buildCurves, evaluateAt, type AlignmentBase, type AlignmentNode, } from "./B05_Profile_UI_Profile_Alignment"; /** 지반 종단으로 볼 꼭짓점 자리 계획선 · 지반 어긋남 가운데값(m) (438). */ const SEAT_GROUND_M = 0.01; /** 새 꼭짓점 이웃이 옮기기 전 계획고에서 뜨면 맞출 한도(m · 433-5). */ const SEAT_KEEP_M = 0.001; /** `np.median` 짝. */ function median(values: number[]): number { const sorted = [...values].sort((a, b) => a - b); const mid = sorted.length >> 1; return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2; } /** 새 측점(`born`)에 원지반을 지나는 꼭짓점 — 양 끝 밖 · 이미 꼭짓점 있는(0.006m 안) 자리는 건너뜀 · * `held` = 옮긴 뒤 높이 고친 꼭짓점 측점(`followMovedNodes` 의 `held`). */ export function seatBornNodes( base: Pick, nodes: AlignmentNode[], born: ReadonlyArray, held: ReadonlyArray = [], ): AlignmentNode[] { if (!nodes.length || base.chainage.length < 2) return nodes; const first = nodes[0].chainage_m; const last = nodes[nodes.length - 1].chainage_m; const fresh = born.filter( (value) => first < value && value < last && nodes.every((node) => Math.abs(value - node.chainage_m) >= 0.006), ); if (!fresh.length) return nodes; const groundAt = (s: number): number => evaluateAt(base.chainage, base.ground, [], s); // 옮기기 전 계획선(편집 없이) — 꼭짓점 대부분에서 지반을 지나면 지반 종단 const preS = base.basePvi.map((node) => node.chainage_m); const preZ = base.basePvi.map((node) => node.elevation_m); const preCurves = buildCurves(base.basePvi, base.policy, {}, []); const onGround = median(preS.map((s) => Math.abs(evaluateAt(preS, preZ, preCurves, s) - groundAt(s)))) < SEAT_GROUND_M; const seatZ = (s: number): number => onGround ? groundAt(s) : evaluateAt(preS, preZ, preCurves, s); const rows = [ ...nodes.map((node) => ({ s: node.chainage_m, z: node.elevation_m, node: node as AlignmentNode | null, })), ...fresh.map((s) => ({ s, z: seatZ(s), node: null })), ].sort((a, b) => a.s - b.s); const baseS = rows.map((row) => row.s); // 다른 꼭짓점 목표 = 옮기기 전 계획선 그 자리 높이(433-5) · 맞출 수 있는 = 높이 안 고친 것 const target = rows.map((row) => (row.node ? evaluateAt(preS, preZ, preCurves, row.s) : row.z)); const can = rows.map( (row) => row.node !== null && held.every((value) => Math.abs(row.s - value) >= 0.006), ); const free = rows.map((row) => row.node === null); let baseZ = rows.map((row) => row.z); const errorOf = (): number[] => { const pvi = baseS.map((s, index) => ({ chainage_m: s, elevation_m: baseZ[index] })); const curves = buildCurves(pvi, base.policy, {}, []); return baseS.map((s, index) => target[index] - evaluateAt(baseS, baseZ, curves, s)); }; for (let grow = 0; grow < rows.length; grow += 1) { for (let round = 0; round < 20; round += 1) { const error = errorOf().map((value, index) => (free[index] ? value : 0)); if (Math.max(...error.map(Math.abs)) < 1e-6) break; baseZ = baseZ.map((z, index) => z + error[index]); } // 이웃 곡선 모양이 바뀌어 뜬 꼭짓점 — 그 꼭짓점도 맞춤(번지는 만큼) const drift = errorOf().map( (value, index) => can[index] && !free[index] && Math.abs(value) > SEAT_KEEP_M, ); if (!drift.includes(true)) break; drift.forEach((hit, index) => { if (hit) free[index] = true; }); } return rows.map((row, index) => row.node && !free[index] ? row.node : { ...row.node, chainage_m: Number(row.s.toFixed(6)), elevation_m: Number(baseZ[index].toFixed(6)), }, ); }