fix(427 공용 측점): 꼭짓점 세움 셈을 공용 파일 밖 B05 logic 새 파일로 — 공용이 B05 종단 선형을 안 끎 · 410 시험 오류 3 풂 (427-2)

- common_util_structure_station `followMovedNodes` = 옮김 · 지움 · 세울 측점(`seat`)만 돌려줌
- 새 B05_Profile/logic/B05_Profile_Grade_Seat.ts `seatBornNodes` = 파이썬 `_seat_born_nodes` 짝
- design_profile_base 가 둘을 차례로 부름 · 272-1 · 427-1 도우미 맞춤

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL
This commit is contained in:
2026-10-18 16:17:24 +09:00
co-authored by Claude Opus 5.5
parent 18ef18e72a
commit 30d99acbdd
6 changed files with 90 additions and 79 deletions
+9 -7
View File
@@ -13,9 +13,10 @@ import {
} from "../../B05_Profile/B05_Profile_UI_Profile_Layout";
import { followMovedNodes, oneStationList } from "../../common_util/common_util_structure_station";
import { readAlignment } from "../../B05_Profile/logic/B05_Profile_UI_Profile_Data";
import { seatBornNodes } from "../../B05_Profile/logic/B05_Profile_Grade_Seat";
/** 계획선 측점 줄 = 측점 한 벌(서버 일반 + 지금 구조물 측점 · 272) — 표 · ▲▼ · 그래프 · 3D · 횡단 다시 셈(342-1)이
* 같은 목록 · 사라진 구조물 측점의 꼭짓점은 서버와 같게 옮기거나 지우고 새 측점엔 세움(`followMovedNodes` · 427) ·
* 같은 목록 · 사라진 구조물 측점의 꼭짓점은 서버와 같게 옮기거나 지우고 새 측점엔 세움(`followMovedNodes` · `seatBornNodes` · 427) ·
* 늘 저장분에서 셈. */
export function listedBase(
storedBase: AlignmentBase | null,
@@ -35,14 +36,15 @@ export function listedBase(
const born = list
.map((station) => station.chainage_m)
.filter((chainage) => !before.has(chainage.toFixed(3)));
const moved = followMovedNodes(
storedBase.basePvi,
gone,
born,
Object.keys(readAlignment(detail.longitudinal)?.edits.station_offsets ?? {}),
);
return {
...storedBase,
basePvi: followMovedNodes(
storedBase,
gone,
born,
Object.keys(readAlignment(detail.longitudinal)?.edits.station_offsets ?? {}),
),
basePvi: seatBornNodes(storedBase, moved.nodes, moved.seat),
stations: list.map((station) => ({
station_id: station.station_id,
chainage_m: station.chainage_m,
+2 -2
View File
@@ -219,8 +219,8 @@ def _seat_born_nodes(
"""새 측점(`born`)에 자동 계획선 규칙(원지반 지남 · `pass_through`)으로 꼭짓점을 세움(427).
다른 꼭짓점 높이는 그대로 · 양 끝 밖 · 이미 꼭짓점 있는(0.006m 안) 자리는 건너뜀. 지반 = 저장
선형 표본(`samples`) · 곡선 = 편집 없이(`pass_through` 와 같음). TS 짝 = `followMovedNodes`
(common_util_structure_station.ts) · 거울 시험 = `resources/tester/test_427_1_follow_seat.py`.
선형 표본(`samples`) · 곡선 = 편집 없이(`pass_through` 와 같음). TS 짝 = `seatBornNodes`
(B05_Profile/logic/B05_Profile_Grade_Seat.ts) · 거울 시험 = `test_427_1_follow_seat.py`.
"""
samples = stored.get("samples") or []
if not base or len(samples) < 2:
@@ -0,0 +1,59 @@
/* =============================================================================
* B05_Profile/logic/B05_Profile_Grade_Seat.ts
* 옮긴 관 · 구조물의 새 측점에 계획선 꼭짓점 세움(427) — 원지반 지남(`passThrough` 규칙) · 다른 꼭짓점 그대로.
*
* 짝 = 파이썬 `B05_Profile_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";
/** 새 측점(`born`)에 원지반을 지나는 꼭짓점 — 양 끝 밖 · 이미 꼭짓점 있는(0.006m 안) 자리는 건너뜀. */
export function seatBornNodes(
base: Pick<AlignmentBase, "chainage" | "ground" | "policy">,
nodes: AlignmentNode[],
born: 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 rows = [
...nodes.map((node) => ({
s: node.chainage_m,
z: node.elevation_m,
node: node as AlignmentNode | null,
})),
...fresh.map((s) => ({ s, z: evaluateAt(base.chainage, base.ground, [], s), node: null })),
].sort((a, b) => a.s - b.s);
const baseS = rows.map((row) => row.s);
const target = rows.map((row) => row.z);
let baseZ = [...target];
for (let round = 0; round < 20; round += 1) {
const pvi = baseS.map((s, index) => ({ chainage_m: s, elevation_m: baseZ[index] }));
const curves = buildCurves(pvi, base.policy, {}, []);
const error = rows.map((row, index) =>
row.node ? 0 : target[index] - evaluateAt(baseS, baseZ, curves, baseS[index]),
);
if (Math.max(...error.map(Math.abs)) < 1e-6) break;
baseZ = baseZ.map((z, index) => z + error[index]);
}
return rows.map(
(row, index) =>
row.node ?? {
chainage_m: Number(row.s.toFixed(6)),
elevation_m: Number(baseZ[index].toFixed(6)),
},
);
}
+11 -62
View File
@@ -28,12 +28,6 @@
* ========================================================================== */
import { cardTitle, catalogRow, shapeNeeds } from "./common_util_structure_catalog";
import {
buildCurves,
evaluateAt,
type AlignmentBase,
type AlignmentNode,
} from "../B05_Profile/logic/B05_Profile_UI_Profile_Alignment";
const EPS = 1e-6;
@@ -345,19 +339,19 @@ export function oneStationList<S extends ListedStation, P extends { chainage_m:
}
/**
* 사라진 구조물 측점(`gone`)의 계획선 꼭짓점을 새 측점(`born`)으로 옮기거나 지우고 새로 세운다 — 서버
* `_follow_moved_stations` · `_seat_born_nodes`(B05_Profile_Router_Confirm.py) 짝: 0.006m 안 반올림 차이 ·
* 옛 소수 측점의 정수 m 맞춤 자리(272)면 높이 그대로 옮김. 옮긴 관 · 구조물(427) — 짝 못 찾은 옛 측점과
* 새 측점이 같은 수면 차례대로 짝 · 높이 고친 꼭짓점(`edited` = 저장 `station_offsets` 키)은 높이째 ·
* 나머지 새 측점은 자동 계획선 규칙(원지반 지남 · 다른 꼭짓점 그대로)으로 세움 · 짝 없는 옛 꼭짓점은 지움.
* 거울 시험 = `resources/tester/test_427_1_follow_seat.py`.
* 사라진 구조물 측점(`gone`)의 계획선 꼭짓점을 새 측점(`born`)으로 옮기거나 지우고, 새로 세울 측점을 고른다 —
* 서버 `_follow_moved_stations`(B05_Profile_Router_Confirm.py) 짝: 0.006m 안 반올림 차이 · 옛 소수 측점의
* 정수 m 맞춤 자리(272)면 높이 그대로 옮김. 옮긴 관 · 구조물(427) — 짝 못 찾은 옛 측점과 새 측점이 같은
* 수면 차례대로 짝 · 높이 고친 꼭짓점(`edited` = 저장 `station_offsets` 키)은 높이째 · 나머지 새 측점은
* `seat`(세움은 부르는 쪽이 `seatBornNodes` · B05_Profile/logic/B05_Profile_Grade_Seat.ts) · 짝 없는 옛
* 꼭짓점은 지움. 거울 시험 = `resources/tester/test_427_1_follow_seat.py`.
*/
export function followMovedNodes(
base: Pick<AlignmentBase, "basePvi" | "chainage" | "ground" | "policy">,
export function followMovedNodes<N extends { chainage_m: number }>(
nodes: ReadonlyArray<N>,
gone: ReadonlyArray<number>,
born: ReadonlyArray<number>,
edited: ReadonlyArray<string> = [],
): AlignmentNode[] {
): { nodes: N[]; seat: number[] } {
const target = (old: number): number | null => {
const near = born.find((value) => Math.abs(value - old) < 0.006);
if (near !== undefined) return near;
@@ -379,56 +373,11 @@ export function followMovedNodes(
if (!edited.some((key) => Math.abs(Number(key) - old) < 0.006)) seat.push(left[index]);
});
}
const kept = base.basePvi.flatMap((node) => {
const kept = nodes.flatMap((node) => {
const old = gone.find((value) => Math.abs(value - node.chainage_m) < 0.006);
if (old === undefined) return [node];
const to = moves.get(old) ?? null;
return to === null || seat.includes(to) ? [] : [{ ...node, chainage_m: to }];
});
return seatBornNodes(base, kept, seat);
}
/** 새 측점에 원지반을 지나는 꼭짓점(서버 `_seat_born_nodes` 짝 · `pass_through` 규칙 · 다른 꼭짓점 그대로). */
function seatBornNodes(
base: Pick<AlignmentBase, "chainage" | "ground" | "policy">,
nodes: AlignmentNode[],
born: 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 rows = [
...nodes.map((node) => ({
s: node.chainage_m,
z: node.elevation_m,
node: node as AlignmentNode | null,
})),
...fresh.map((s) => ({ s, z: evaluateAt(base.chainage, base.ground, [], s), node: null })),
].sort((a, b) => a.s - b.s);
const baseS = rows.map((row) => row.s);
const target = rows.map((row) => row.z);
let baseZ = [...target];
for (let round = 0; round < 20; round += 1) {
const pvi = baseS.map((s, index) => ({ chainage_m: s, elevation_m: baseZ[index] }));
const curves = buildCurves(pvi, base.policy, {}, []);
const error = rows.map((row, index) =>
row.node ? 0 : target[index] - evaluateAt(baseS, baseZ, curves, baseS[index]),
);
if (Math.max(...error.map(Math.abs)) < 1e-6) break;
baseZ = baseZ.map((z, index) => z + error[index]);
}
return rows.map(
(row, index) =>
row.node ?? {
chainage_m: Number(row.s.toFixed(6)),
elevation_m: Number(baseZ[index].toFixed(6)),
},
);
return { nodes: kept, seat };
}
@@ -15,8 +15,6 @@ function loadTs(file) {
const box = { exports: {} };
const localRequire = (request) => {
if (CATALOG.isCatalogRequest(request)) return CATALOG;
// 427 — 꼭짓점 세움이 종단 선형(B05 logic)을 부름 · 상대 경로 TS 는 그대로 읽음
if (request.startsWith(".")) return loadTs(path.join(path.dirname(file), `${request}.ts`));
throw new Error(`stub 없음: ${request} (${file})`);
};
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
@@ -28,7 +26,7 @@ const station = loadTs("common_util/common_util_structure_station.ts");
const out = {
planted: station.plantedChainages(input.pipes, input.structures, input.types),
list: station.oneStationList(input.server, input.fresh),
nodes: station.followMovedNodes(input.base, input.gone, input.born, input.edited),
nodes: station.followMovedNodes(input.base.basePvi, input.gone, input.born, input.edited).nodes,
};
// 368-2 추가 측점 — 화면이 둔 읽는 곳(setPlusStations)으로 심는 자리에 듦
station.setPlusStations(() => input.plus);
@@ -1,5 +1,6 @@
/* 427-1 관 · 구조물 옮김 때 꼭짓점 따라감 — 진짜 `common_util_structure_station.ts` 의 `followMovedNodes` 를
* Node 로 돌림. 입력 argv[2] JSON [{ base, gone, born, edited }, …] · 표준출력 = 꼭짓점 목록 한 벌씩.
/* 427-1 관 · 구조물 옮김 때 꼭짓점 따라감 — 진짜 `common_util_structure_station.ts` 의 `followMovedNodes` +
* `B05_Profile_Grade_Seat.ts` 의 `seatBornNodes`(화면 `listedBase` 차례)를 Node 로 돌림.
* 입력 argv[2] JSON [{ base, gone, born, edited }, …] · 표준출력 = 꼭짓점 목록 한 벌씩.
*/
const fs = require("fs");
const path = require("path");
@@ -24,7 +25,9 @@ function loadTs(file) {
const cases = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
const station = loadTs("common_util/common_util_structure_station.ts");
const out = cases.map((item) =>
station.followMovedNodes(item.base, item.gone, item.born, item.edited),
);
const seat = loadTs("B05_Profile/logic/B05_Profile_Grade_Seat.ts");
const out = cases.map((item) => {
const moved = station.followMovedNodes(item.base.basePvi, item.gone, item.born, item.edited);
return seat.seatBornNodes(item.base, moved.nodes, moved.seat);
});
process.stdout.write(JSON.stringify(out));