From 162d32620e4cf9cbeab20df90eb7991b4d5ed25a Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 19 Oct 2026 09:39:54 +0900 Subject: [PATCH] =?UTF-8?q?refactor(472-4=20=E2=91=A0=20=EC=8B=9C=EC=84=A4?= =?UTF-8?q?=20=EB=94=B0=EB=9D=BC=EA=B0=90):=20=EC=84=9C=EB=B2=84=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=EB=AC=BC=20=EB=94=B0=EB=9D=BC=EA=B0=90?= =?UTF-8?q?=EB=8F=84=20TS=20followStructures=20=ED=95=9C=20=EB=B2=8C=20?= =?UTF-8?q?=E2=80=94=20Node=20=EC=9E=85=EA=B5=AC=20facility.follow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - common_util_route_follow.follow_structures = run_calc 겉(같은 계약 · 제자리 고침 · site 간격 = 서버 측점 간격) - 저장 때 좌표 맞춤 fill_structure_coordinates 는 파이썬 남음(옛 좌표 지킴 · 0.1 mm 반올림 — TS placeOnLine 과 규칙 달라 옮기면 값 바뀜) · 등록부 note - 등록부 node:facility.follow · NODE_IDS · 시험 test_472_4_follow_node(옛 코드 빨강) · 344_2 · 380_3 · 410 · 459 통과 Co-Authored-By: Claude Opus 5.5 --- .../design/logic/design_calc_node_follow.ts | 24 +++++++- .../design/logic/design_calc_registry.ts | 14 +++-- common_util/common_util_calc_runner.py | 1 + common_util/common_util_route_follow.py | 55 ++++++++++--------- resources/tester/test_472_3_calc_registry.py | 1 + resources/tester/test_472_4_follow_node.py | 23 ++++++++ 6 files changed, 86 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 679460e79..f6ce41a40 100644 --- a/A00_Common/design/logic/design_calc_node_follow.ts +++ b/A00_Common/design/logic/design_calc_node_follow.ts @@ -5,12 +5,18 @@ * * profile.seat 입력 seat { ground_s · ground_z(저장 선형 표본) · policy(파이썬 `AlignmentPolicy.as_dict`) · * base_pvi(옮기기 전) · base(옮긴 꼭짓점) · born · held } → 꼭짓점 목록(`seatBornNodes`) + * facility.follow 입력 follow { items: [{ placement · chainage_m · start_m · end_m · x · y }] · line · + * max_drift_m · interval_m(site 맞춤 간격) } → [{ item · drift_m · far }](`followStructures`) * 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 { + followPlusRows, + followStructures, + type FollowingStructure, +} from "@util/common_util_route_follow"; import type { AlignmentNode, AlignmentPolicy, @@ -20,6 +26,12 @@ type Line = Array<[number, number]>; const xyOf = (line: Line) => line.map(([x, y]) => ({ x, y })); export interface FollowInputs { + follow?: { + items: FollowingStructure[]; + line: Line; + max_drift_m?: number; + interval_m?: number | null; + }; plus?: { rows: Array<{ chainage_m: number; x?: number | null; y?: number | null }>; line: Line }; seat?: { ground_s: number[]; @@ -46,6 +58,16 @@ export function followRunners(inputs: FollowInputs): Record unknow }; return seatBornNodes(base, seat.base, seat.born, seat.held ?? []); }, + "facility.follow": () => { + const follow = inputs.follow; + if (!follow) return null; + return followStructures( + follow.items, + xyOf(follow.line), + follow.max_drift_m, + follow.interval_m ?? null, + ); + }, "facility.plus_follow": () => { const plus = inputs.plus; if (!plus) return null; diff --git a/A00_Common/design/logic/design_calc_registry.ts b/A00_Common/design/logic/design_calc_registry.ts index 386312393..9263a1d9d 100644 --- a/A00_Common/design/logic/design_calc_registry.ts +++ b/A00_Common/design/logic/design_calc_registry.ts @@ -466,10 +466,16 @@ export const CALC_REGISTRY: readonly CalcEntry[] = [ site: "browser_now", stage: "follow", ts: `${U}common_util_route_follow.ts#followStructures`, - runner: `py:${U}common_util_route_follow.py#follow_structures`, - until: "472-4", - pair: `${U}common_util_route_follow.py#follow_structures`, - mirror: ["test_410_follow_xy", "test_344_2_route_pairs", "test_459_route_seat_mirror"], + runner: "node:facility.follow", + pair: null, + mirror: [ + "test_410_follow_xy", + "test_344_2_route_pairs", + "test_459_route_seat_mirror", + "test_380_3_site_follow", + "test_472_4_follow_node", + ], + note: "파이썬 `common_util_route_follow.follow_structures` = Node 부름 겉 · 저장 때 좌표 맞춤 `fill_structure_coordinates` 는 파이썬 남음(옛 좌표 지킴 · 0.1 mm 반올림 — TS `placeOnLine` 과 규칙 다름)", }, { id: "facility.plus_follow", diff --git a/common_util/common_util_calc_runner.py b/common_util/common_util_calc_runner.py index b32643608..f15a44e30 100644 --- a/common_util/common_util_calc_runner.py +++ b/common_util/common_util_calc_runner.py @@ -26,6 +26,7 @@ NODE_IDS = frozenset( "corridor.mesh", "profile.seat", "facility.plus_follow", + "facility.follow", } ) diff --git a/common_util/common_util_route_follow.py b/common_util/common_util_route_follow.py index e6a3bf8c0..cf56c50bf 100644 --- a/common_util/common_util_route_follow.py +++ b/common_util/common_util_route_follow.py @@ -6,7 +6,8 @@ - `fill_structure_coordinates` — 저장 때 노선 위 기준 자리로 x · y 채움(자리가 그대로면 옛 좌표 지킴 · 보낸 좌표가 그 누가거리를 낳으면 살림 · 410) · 옛 자료는 읽을 때 없는 것만 채움. - - `follow_structures` — 좌표 → 새 노선 누가거리 · 구간 끝은 같은 만큼 밂 · 노선과의 거리. + - `follow_structures` — 좌표 → 새 노선 누가거리 · 구간 끝은 같은 만큼 밂 · 노선과의 거리 + (셈 = TS `followStructures` · Node 입구 `facility.follow` · 472-4 ①). - `route_outside` — 노선 밖(시점 앞 · 종점 뒤 0.51 m 넘음) · 측점 세울 때 셈(459 · 정본에 안 씀). @@ -22,15 +23,17 @@ from typing import Any, Iterable, Sequence from shapely.geometry import LineString, Point +from common_util.common_util_calc_runner import run_calc from common_util.common_util_structure_station import ( SAME_STATION_M, route_general_stations, snap_existing_station, snap_structure_station, ) -from config.config_system import STRUCTURE_ROUTE_DRIFT_MAX_M +from config.config_system import SECTION_STATION_INTERVAL_M, STRUCTURE_ROUTE_DRIFT_MAX_M _POSITION_KEYS = ("chainage_m", "start_m", "end_m") +_FOLLOW_KEYS = ("placement", *_POSITION_KEYS, "x", "y") #: 노선 밖 판정(m) — 투영이 시점 앞 · 종점 뒤로 이만큼 넘으면 측점 안 세움(459 · TS 짝). ROUTE_OUTSIDE_M = 0.51 @@ -125,33 +128,31 @@ def follow_structures( """좌표를 새 노선에 투영해 누가거리를 다시 매긴다(제자리) → 항목마다 (노선과 거리, 멀어짐). 기준 자리 = 투영 → 정수 m(노선 끝 안) · 구간 끝(`start_m` · `end_m`)은 같은 만큼 밀고 0 ~ 연장 - 안으로 자름. 좌표가 없는 옛 자료는 그대로(거리 None). - site 꼴(사토장 · 토취장 · 새 측점 안 심음) = 새 노선 일반 측점 중 가장 가까운 곳(380-3). + 안으로 자름. 좌표가 없는 옛 자료는 그대로(거리 None) · site 꼴(사토장 · 토취장) = 새 노선 일반 + 측점 중 가장 가까운 곳(380-3). 셈 = TS `followStructures`(Node 입구 `facility.follow` · 472-4). """ items = list(items) line = _line(xy) - if line is None: + if line is None or not items: return [(None, False)] * len(items) - length = float(line.length) - result: list[tuple[float | None, bool]] = [] - for item in items: - x, y = getattr(item, "x", None), getattr(item, "y", None) - anchor = getattr(item, "chainage_m", None) - if x is None or y is None or anchor is None: - result.append((None, False)) - continue - point = Point(float(x), float(y)) - at = float(line.project(point)) - target = ( - snap_existing_station(at, route_general_stations(length)) - if getattr(item, "placement", None) == "site" - else snap_structure_station(at, [length]) - ) - shift = target - float(anchor) + rows = [{key: getattr(item, key, None) for key in _FOLLOW_KEYS} for item in items] + results = run_calc( + ["facility.follow"], + { + "follow": { + "items": rows, + "line": [[x, y] for x, y in line.coords], + "max_drift_m": max_drift_m, + "interval_m": SECTION_STATION_INTERVAL_M, + } + }, + ) + if results is None: + raise RuntimeError("구조물 따라감 실패 — 계산 Node 입구(facility.follow)") + out: list[tuple[float | None, bool]] = [] + for item, row in zip(items, results["facility.follow"], strict=True): for key in _POSITION_KEYS: - value = getattr(item, key, None) - if value is not None: - setattr(item, key, min(max(float(value) + shift, 0.0), length)) - drift = float(line.distance(point)) - result.append((drift, drift > max_drift_m)) - return result + if getattr(item, key, None) is not None: + setattr(item, key, float(row["item"][key])) + out.append((row["drift_m"], bool(row["far"]))) + return out diff --git a/resources/tester/test_472_3_calc_registry.py b/resources/tester/test_472_3_calc_registry.py index c72649ac2..ad4bdd7d3 100644 --- a/resources/tester/test_472_3_calc_registry.py +++ b/resources/tester/test_472_3_calc_registry.py @@ -42,6 +42,7 @@ NODE_IDS = { "corridor.mesh", "profile.seat", "facility.plus_follow", + "facility.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 223976d3a..5162bde76 100644 --- a/resources/tester/test_472_4_follow_node.py +++ b/resources/tester/test_472_4_follow_node.py @@ -78,3 +78,26 @@ def test_추가_측점_따라감은_Node_입구(tmp_path, monkeypatch) -> None: 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)] # 좌표 그대로 + + +def test_구조물_따라감은_Node_입구(monkeypatch) -> None: + from types import SimpleNamespace + + from common_util import common_util_route_follow as follow + + seen: list[list[str]] = [] + + def spy(ids, inputs): + seen.append(list(ids)) + return runner.run_calc(ids, inputs) + + monkeypatch.setattr(follow, "run_calc", spy) + items = [ + SimpleNamespace(chainage_m=60.0, start_m=55.0, end_m=65.0, x=60.0, y=0.0, placement=None), + SimpleNamespace(chainage_m=30.0, start_m=None, end_m=None, x=None, y=None, placement=None), + ] + result = follow.follow_structures(items, [(0.0, 5.0), (40.0, 5.0), (100.0, 25.0)]) + assert seen == [["facility.follow"]] + assert (items[0].chainage_m, items[0].start_m, items[0].end_m) == (57.0, 52.0, 62.0) + assert items[1].chainage_m == 30.0 and result[1] == (None, False) # 좌표 없는 옛 자료 그대로 + assert result[0][0] == pytest.approx(11.068, abs=1e-3) and result[0][1] is False # 옛 파이썬 값