From 30d99acbdd35fede297409f049463f5ef72f005b Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 18 Oct 2026 16:17:24 +0900 Subject: [PATCH] =?UTF-8?q?fix(427=20=EA=B3=B5=EC=9A=A9=20=EC=B8=A1?= =?UTF-8?q?=EC=A0=90):=20=EA=BC=AD=EC=A7=93=EC=A0=90=20=EC=84=B8=EC=9B=80?= =?UTF-8?q?=20=EC=85=88=EC=9D=84=20=EA=B3=B5=EC=9A=A9=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=20=EB=B0=96=20B05=20logic=20=EC=83=88=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=EB=A1=9C=20=E2=80=94=20=EA=B3=B5=EC=9A=A9=EC=9D=B4=20B05=20?= =?UTF-8?q?=EC=A2=85=EB=8B=A8=20=EC=84=A0=ED=98=95=EC=9D=84=20=EC=95=88=20?= =?UTF-8?q?=EB=81=8E=20=C2=B7=20410=20=EC=8B=9C=ED=97=98=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=203=20=ED=92=82=20(427-2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL --- A00_Common/design/design_profile_base.ts | 16 ++-- B05_Profile/B05_Profile_Router_Confirm.py | 4 +- B05_Profile/logic/B05_Profile_Grade_Seat.ts | 59 +++++++++++++++ common_util/common_util_structure_station.ts | 73 +++---------------- .../tester/helper_272_1_station_list.cjs | 4 +- resources/tester/helper_427_1_follow_seat.cjs | 13 ++-- 6 files changed, 90 insertions(+), 79 deletions(-) create mode 100644 B05_Profile/logic/B05_Profile_Grade_Seat.ts diff --git a/A00_Common/design/design_profile_base.ts b/A00_Common/design/design_profile_base.ts index d9d3ea049..e7410f0c6 100644 --- a/A00_Common/design/design_profile_base.ts +++ b/A00_Common/design/design_profile_base.ts @@ -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, diff --git a/B05_Profile/B05_Profile_Router_Confirm.py b/B05_Profile/B05_Profile_Router_Confirm.py index fb6e58d74..752271427 100644 --- a/B05_Profile/B05_Profile_Router_Confirm.py +++ b/B05_Profile/B05_Profile_Router_Confirm.py @@ -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: diff --git a/B05_Profile/logic/B05_Profile_Grade_Seat.ts b/B05_Profile/logic/B05_Profile_Grade_Seat.ts new file mode 100644 index 000000000..7108f3dee --- /dev/null +++ b/B05_Profile/logic/B05_Profile_Grade_Seat.ts @@ -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, + nodes: AlignmentNode[], + born: 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 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)), + }, + ); +} diff --git a/common_util/common_util_structure_station.ts b/common_util/common_util_structure_station.ts index 64bd2f778..b6eda5730 100644 --- a/common_util/common_util_structure_station.ts +++ b/common_util/common_util_structure_station.ts @@ -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, +export function followMovedNodes( + nodes: ReadonlyArray, gone: ReadonlyArray, born: ReadonlyArray, edited: ReadonlyArray = [], -): 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, - nodes: AlignmentNode[], - born: 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 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 }; } diff --git a/resources/tester/helper_272_1_station_list.cjs b/resources/tester/helper_272_1_station_list.cjs index 75cdb0cfe..11aa038b2 100644 --- a/resources/tester/helper_272_1_station_list.cjs +++ b/resources/tester/helper_272_1_station_list.cjs @@ -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); diff --git a/resources/tester/helper_427_1_follow_seat.cjs b/resources/tester/helper_427_1_follow_seat.cjs index 8abf14dca..0d915ff69 100644 --- a/resources/tester/helper_427_1_follow_seat.cjs +++ b/resources/tester/helper_427_1_follow_seat.cjs @@ -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));