From 39adebdaf50e8bb10c4344f3b8f255b5a71e40e9 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 19 Oct 2026 09:37:00 +0900 Subject: [PATCH] =?UTF-8?q?refactor(472-4=20=E2=91=A0=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20=EC=B8=A1=EC=A0=90=20=EB=94=B0=EB=9D=BC=EA=B0=90):=20?= =?UTF-8?q?=EC=84=9C=EB=B2=84=20=EC=B6=94=EA=B0=80=20=EC=B8=A1=EC=A0=90=20?= =?UTF-8?q?=EB=94=B0=EB=9D=BC=EA=B0=90=EB=8F=84=20TS=20=ED=95=9C=20?= =?UTF-8?q?=EB=B2=8C=20=E2=80=94=20followPlusRows=20=C2=B7=20Node=20?= =?UTF-8?q?=EC=9E=85=EA=B5=AC=20facility.plus=5Ffollow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - common_util_route_follow.ts followPlusRows(땅 위 자리 → 새 노선 정수 m · 같은 m 하나) · followPlusStations 가 그것을 씀 - Plus_Stations.follow_plus_stations = run_calc 겉(옮긴 수 · 쓰기 그대로) · follow_structures 끌림 걷음 - 등록부 node:facility.plus_follow · NODE_IDS · 시험 test_472_4_follow_node(옛 코드 빨강) · 368_2 · 459 통과 Co-Authored-By: Claude Opus 5.5 --- .../design/logic/design_calc_node_follow.ts | 21 +++++++++ .../design/logic/design_calc_registry.ts | 11 +++-- B05_Profile/B05_Profile_Plus_Stations.py | 44 ++++++++++--------- common_util/common_util_calc_runner.py | 1 + common_util/common_util_route_follow.ts | 21 +++++++-- resources/tester/test_472_3_calc_registry.py | 1 + resources/tester/test_472_4_follow_node.py | 23 +++++++++- 7 files changed, 90 insertions(+), 32 deletions(-) diff --git a/A00_Common/design/logic/design_calc_node_follow.ts b/A00_Common/design/logic/design_calc_node_follow.ts index ebecc1f64..679460e79 100644 --- a/A00_Common/design/logic/design_calc_node_follow.ts +++ b/A00_Common/design/logic/design_calc_node_follow.ts @@ -5,15 +5,22 @@ * * profile.seat 입력 seat { ground_s · ground_z(저장 선형 표본) · policy(파이썬 `AlignmentPolicy.as_dict`) · * base_pvi(옮기기 전) · base(옮긴 꼭짓점) · born · held } → 꼭짓점 목록(`seatBornNodes`) + * facility.plus_follow 입력 plus { rows: [{ chainage_m · x · y }] · line: [[x, y]](새 노선) } → + * { rows: [{ chainage_m · x · y }](`followPlusRows` · 정수 m · 같은 m 하나) · moved: 옮긴 수 } * ========================================================================== */ import { seatBornNodes } from "../../../B05_Profile/logic/B05_Profile_Grade_Seat"; +import { followPlusRows, followStructures } from "@util/common_util_route_follow"; import type { AlignmentNode, AlignmentPolicy, } from "../../../B05_Profile/logic/B05_Profile_UI_Profile_Alignment"; +type Line = Array<[number, number]>; +const xyOf = (line: Line) => line.map(([x, y]) => ({ x, y })); + export interface FollowInputs { + plus?: { rows: Array<{ chainage_m: number; x?: number | null; y?: number | null }>; line: Line }; seat?: { ground_s: number[]; ground_z: number[]; @@ -39,5 +46,19 @@ export function followRunners(inputs: FollowInputs): Record unknow }; return seatBornNodes(base, seat.base, seat.born, seat.held ?? []); }, + "facility.plus_follow": () => { + const plus = inputs.plus; + if (!plus) return null; + const line = xyOf(plus.line); + const moved = followStructures(plus.rows, line).filter( + ({ item }, index) => item.chainage_m !== plus.rows[index].chainage_m, + ).length; + const rows = followPlusRows(plus.rows, line).map(({ chainage_m, x, y }) => ({ + chainage_m, + x: x ?? null, + y: y ?? null, + })); + return { rows, moved }; + }, }; } diff --git a/A00_Common/design/logic/design_calc_registry.ts b/A00_Common/design/logic/design_calc_registry.ts index 7bcccfe6a..386312393 100644 --- a/A00_Common/design/logic/design_calc_registry.ts +++ b/A00_Common/design/logic/design_calc_registry.ts @@ -479,12 +479,11 @@ export const CALC_REGISTRY: readonly CalcEntry[] = [ outputs: ["추가 측점(input.plusStations 고침)"], site: "browser_now", stage: "follow", - ts: `${B05}B05_Profile_Api_PlusStations.ts#followPlusDraft`, - runner: `py:${B05}B05_Profile_Plus_Stations.py#follow_plus_stations`, - until: "472-4", - pair: `${B05}B05_Profile_Plus_Stations.py#follow_plus_stations`, - mirror: ["test_459_route_seat_mirror"], - note: "TS 가 Api 파일 안 — logic 으로(472-4 ①)", + ts: `${U}common_util_route_follow.ts#followPlusRows`, + runner: "node:facility.plus_follow", + pair: null, + mirror: ["test_459_route_seat_mirror", "test_368_2_plus_stations", "test_472_4_follow_node"], + note: "파이썬 `Plus_Stations.follow_plus_stations` = Node 부름 겉(정본 파일 읽기 · 쓰기)", }, { id: "facility.stations", diff --git a/B05_Profile/B05_Profile_Plus_Stations.py b/B05_Profile/B05_Profile_Plus_Stations.py index 7865737e2..7f21321a2 100644 --- a/B05_Profile/B05_Profile_Plus_Stations.py +++ b/B05_Profile/B05_Profile_Plus_Stations.py @@ -1,7 +1,7 @@ """추가 측점 정본(`B05_Profile/route/plus_stations.json`) — 구조물 정본 곁(368-2 · 요구 240-21). 사용자가 종단 원지반선에서 넣은 일반 측점(정수 m) · 이름 없음. 자리 정본 = 좌표(x · y) — 노선이 -바뀌면 관 · 구조물처럼 새 노선에 투영해 누가거리를 다시 매김(`follow_structures` 재사용). +바뀌면 관 · 구조물처럼 새 노선에 투영해 누가거리를 다시 매김(TS `followPlusRows` · Node 입구). 측점 계획에는 `resolve_extra_stations` → `plan_stations(plus=)` 로 들어감(TS `planStations` 짝). """ @@ -10,11 +10,10 @@ from __future__ import annotations import json import os from pathlib import Path -from types import SimpleNamespace from typing import Any, Sequence +from common_util.common_util_calc_runner import run_calc from common_util.common_util_json import atomic_write_json -from common_util.common_util_route_follow import follow_structures PLUS_STATIONS_FILE_NAME = "plus_stations.json" @@ -71,23 +70,26 @@ def save_plus_stations( def follow_plus_stations(project_root: str | Path, new_xy: Sequence[Sequence[float]]) -> int: - """노선이 바뀔 때 좌표를 새 노선에 투영해 누가거리를 다시 매김 → 옮긴 수(같은 m 는 하나).""" + """노선이 바뀔 때 좌표를 새 노선에 투영해 누가거리를 다시 매김 → 옮긴 수(같은 m 는 하나). + 셈 = TS `followPlusRows`(Node 입구 `facility.plus_follow` · 472-4 ①).""" rows = load_plus_stations(project_root) - items = [ - SimpleNamespace(chainage_m=float(r["chainage_m"]), x=r.get("x"), y=r.get("y")) for r in rows - ] - if not items: + if not rows: return 0 - before = [item.chainage_m for item in items] - follow_structures(items, new_xy) - moved = sum(a != item.chainage_m for a, item in zip(before, items)) - if moved: - seen: set[float] = set() - kept = [] - for item in sorted(items, key=lambda i: i.chainage_m): - if item.chainage_m in seen: - continue - seen.add(item.chainage_m) - kept.append({"chainage_m": item.chainage_m, "x": item.x, "y": item.y}) - atomic_write_json(str(plus_stations_path(project_root)), {"stations": kept}) - return moved + results = run_calc( + ["facility.plus_follow"], + { + "plus": { + "rows": [ + {"chainage_m": float(r["chainage_m"]), "x": r.get("x"), "y": r.get("y")} + for r in rows + ], + "line": [[float(p[0]), float(p[1])] for p in new_xy], + } + }, + ) + if results is None: + raise RuntimeError("추가 측점 따라감 실패 — 계산 Node 입구(facility.plus_follow)") + followed = results["facility.plus_follow"] + if followed["moved"]: + atomic_write_json(str(plus_stations_path(project_root)), {"stations": followed["rows"]}) + return int(followed["moved"]) diff --git a/common_util/common_util_calc_runner.py b/common_util/common_util_calc_runner.py index d9eab93e0..b32643608 100644 --- a/common_util/common_util_calc_runner.py +++ b/common_util/common_util_calc_runner.py @@ -25,6 +25,7 @@ NODE_IDS = frozenset( "excavation.trench", "corridor.mesh", "profile.seat", + "facility.plus_follow", } ) diff --git a/common_util/common_util_route_follow.ts b/common_util/common_util_route_follow.ts index 9595495c9..069a94345 100644 --- a/common_util/common_util_route_follow.ts +++ b/common_util/common_util_route_follow.ts @@ -14,7 +14,8 @@ * - `placeOnLine` 의 좌표 없는 관 · `rebasePipeDraft` 는 TS 만 — 좌표 없는 세션 관 초안(정본 PUT 꼴)을 따라가기 * 전 자리로 · 노선을 저장한 [저장]이 서버가 따라 옮긴 관 위에 초안을 얹음(349-0u). * - `rebaseStructureDraft` 도 TS 만 — 구조물 초안을 서버가 따라 옮긴 정본 위에(459 · 관 짝). - * - `followPlusStations` ↔ 파이썬 `B05_Profile_Plus_Stations.follow_plus_stations`(459 · 노선 초안 동안). + * - `followPlusRows` = 추가 측점 따라감 한 벌 — 서버도(Node 입구 `facility.plus_follow` · 472-4 ①) · + * `followPlusStations` = 노선 초안 동안(옛 선 위 자리에서). * - `routeOutside` ↔ 파이썬 `route_outside`(459 · 노선 밖 = 측점 계획 · 알약에서 뺌 · 좌표 그대로) — 따라감이 * `outside: true` 를 붙임(파생값 · 저장 때 뺌 · 서버는 측점 세울 때 다시 셈). * 거울 시험 = resources/tester/test_344_2_route_pairs.py. @@ -221,8 +222,21 @@ export function rebaseStructureDraft( }); } +/** 땅 위 자리(x · y)가 있는 추가 측점이 새 선(`to`)을 따라감 — 정수 m · 누가거리 순 · 같은 m 하나(앞엣것) · + * 좌표 그대로. 서버 정본(`plus_stations.json`)도 이 셈(Node 입구 `facility.plus_follow` · 472-4 ①). */ +export function followPlusRows( + rows: ReadonlyArray, + to: ReadonlyArray, +): R[] { + const seen = new Set(); + return followStructures(rows, to) + .map(({ item }) => item) + .sort((a, b) => a.chainage_m! - b.chainage_m!) + .filter((item) => !seen.has(item.chainage_m!) && seen.add(item.chainage_m!)); +} + /** 추가 측점이 노선을 따라감(459) — 옛 선(`from`) 위 누가거리의 땅 위 자리를 새 선(`to`)에 투영한 정수 m - * (같은 m 하나 · 오름차순). 파이썬 `follow_plus_stations` 짝(`follow_structures` 같은 셈). */ + * (같은 m 하나 · 오름차순 · 노선 초안 동안 = 좌표가 아직 옛 선 위). */ export function followPlusStations( list: ReadonlyArray, from: ReadonlyArray, @@ -232,8 +246,7 @@ export function followPlusStations( list.map((chainage_m) => ({ chainage_m })), from, ); - const moved = followStructures(placed, to).map(({ item }) => item.chainage_m!); - return [...new Set(moved)].sort((a, b) => a - b); + return followPlusRows(placed, to).map((item) => item.chainage_m!); } /** 따라가기만 한 관 = 정본과 같은 땅 위 자리(m) — 서버 좌표가 mm 3자리라 그 반올림 폭. */ diff --git a/resources/tester/test_472_3_calc_registry.py b/resources/tester/test_472_3_calc_registry.py index 51dd773ef..c72649ac2 100644 --- a/resources/tester/test_472_3_calc_registry.py +++ b/resources/tester/test_472_3_calc_registry.py @@ -41,6 +41,7 @@ NODE_IDS = { "excavation.trench", "corridor.mesh", "profile.seat", + "facility.plus_follow", } pytestmark = pytest.mark.skipif( diff --git a/resources/tester/test_472_4_follow_node.py b/resources/tester/test_472_4_follow_node.py index 5e9f90751..223976d3a 100644 --- a/resources/tester/test_472_4_follow_node.py +++ b/resources/tester/test_472_4_follow_node.py @@ -1,7 +1,8 @@ -"""PLAN 472-4 ① — 측점 열쇠 · 시설 따라감 · 추가 측점 · 꼭짓점 세움을 서버도 같은 TS(Node 입구)로 셈. +"""PLAN 472-4 ① — 측점 열쇠 · 시설 따라감 · 추가 측점 · 꼭짓점 세움 = 서버도 같은 TS(Node 입구). 파이썬 짝을 걷고 서버 부름 자리는 `common_util_calc_runner.run_calc`(등록부 id) 한 길 — profile.seat `Router_Confirm._seat_born_nodes`(측점 병합 · 노선 저장 `_carry_profile`) + facility.plus_follow `Plus_Stations.follow_plus_stations`(노선 저장) 고정 자료 = `fixtures/golden_472/detail.json`(472-1 · 읽기만). ./venv/Scripts/python.exe -m pytest resources/tester/test_472_4_follow_node.py -q @@ -57,3 +58,23 @@ def test_세울_자리_없으면_Node_안_부름(calls) -> None: stored = DETAIL["longitudinal"]["profile_alignment"] assert confirm._seat_born_nodes(stored, stored["base_pvi"], [351.0]) == stored["base_pvi"] assert calls == [] + + +def test_추가_측점_따라감은_Node_입구(tmp_path, monkeypatch) -> None: + from B05_Profile import B05_Profile_Plus_Stations as plus + + seen: list[list[str]] = [] + + def spy(ids, inputs): + seen.append(list(ids)) + return runner.run_calc(ids, inputs) + + monkeypatch.setattr(plus, "run_calc", spy) + old = [(0.0, 0.0), (100.0, 0.0)] + plus.save_plus_stations(tmp_path, [30.0, 60.0], old) + new = [(0.0, 5.0), (40.0, 5.0), (100.0, 25.0)] # 땅 위 자리를 새 노선에 투영 + moved = plus.follow_plus_stations(tmp_path, new) + assert seen == [["facility.plus_follow"]] + rows = plus.load_plus_stations(tmp_path) + assert moved == 1 and [row["chainage_m"] for row in rows] == [30.0, 57.0] # 옛 파이썬 값 + assert [(row["x"], row["y"]) for row in rows] == [(30.0, 0.0), (60.0, 0.0)] # 좌표 그대로