Files
Aislo/B05_Profile/logic/B05_Profile_Grade_Seat.ts
T
eomsangdonandClaude Opus 5.5 cca08daa11 refactor(472-4 ① 꼭짓점 세움): 서버 꼭짓점 세움도 TS seatBornNodes 한 벌 — Node 입구 profile.seat
- 새 design_calc_node_follow.ts(따라감 몫 runner) · 입구는 두 줄만(import · 펼침) · 등록부 runner node:profile.seat · 실행기 NODE_IDS
- Router_Confirm._seat_born_nodes = run_calc 겉(세울 자리 없으면 Node 안 부름) · 파이썬 셈 · 상수 걷음
- 시험 test_472_4_follow_node(옛 코드 빨강 2) · 427_1 · 459 거울 통과 · 472-3 NODE_IDS 에 profile.seat(cross.design 빨강은 ② 몫)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 09:33:23 +09:00

104 lines
4.8 KiB
TypeScript

/* =============================================================================
* 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<AlignmentBase, "chainage" | "ground" | "policy" | "basePvi">,
nodes: AlignmentNode[],
born: ReadonlyArray<number>,
held: ReadonlyArray<number> = [],
): 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)),
},
);
}