- 새 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>
104 lines
4.8 KiB
TypeScript
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)),
|
|
},
|
|
);
|
|
}
|