feat(380-3): 노선 따라간 사토장 · 토취장 = 새 노선 일반 측점 맞춤 · test_322_2 마스터 판 독립

- follow_structures ↔ followStructures(intervalM): site 꼴 = 새 노선 일반 측점(격자 · 끝) 중 가장 가까운 곳 · 그 밖 정수 m 그대로
- 파이썬 snap_existing_station ↔ TS snapExistingStation · general_stations ↔ gridStations 짝
- 브라우저 노선 초안 따라감(Pipeline_Grade) = 측점 간격 넘김 · 서버 노선 저장 따라감 = 서버 간격
- test_322_2: B05 목록 고정 수 23 대신 both 목록 수와 같음(M02 켬끔 판마다 다름)
- 시험 test_380_3_site_follow(거울 · 가운데면 앞 측점 · 좌표 없음 그대로)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VmC34fsxxbweEG3RUjs3Mn
This commit is contained in:
2026-10-17 15:40:32 +09:00
co-authored by Claude Opus 5.5
parent 1f2d1cff56
commit 69ffd17317
8 changed files with 195 additions and 9 deletions
+13 -2
View File
@@ -19,7 +19,11 @@ from typing import Any, Iterable, Sequence
from shapely.geometry import LineString, Point
from common_util.common_util_structure_station import snap_structure_station
from common_util.common_util_structure_station import (
route_general_stations,
snap_existing_station,
snap_structure_station,
)
from config.config_system import STRUCTURE_ROUTE_DRIFT_MAX_M
_POSITION_KEYS = ("chainage_m", "start_m", "end_m")
@@ -79,6 +83,7 @@ def follow_structures(
기준 자리 = 투영 → 정수 m(노선 끝 안) · 구간 끝(`start_m` · `end_m`)은 같은 만큼 밀고 0 ~ 연장
안으로 자름. 좌표가 없는 옛 자료는 그대로(거리 None).
site 꼴(사토장 · 토취장 · 새 측점 안 심음) = 새 노선 일반 측점 중 가장 가까운 곳(380-3).
"""
items = list(items)
line = _line(xy)
@@ -93,7 +98,13 @@ def follow_structures(
result.append((None, False))
continue
point = Point(float(x), float(y))
shift = snap_structure_station(float(line.project(point)), [length]) - float(anchor)
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)
for key in _POSITION_KEYS:
value = getattr(item, key, None)
if value is not None:
+15 -3
View File
@@ -14,7 +14,12 @@
* 거울 시험 = resources/tester/test_344_2_route_pairs.py.
* ========================================================================== */
import { SAME_STATION_M, snapStructureStation } from "./common_util_structure_station";
import {
gridStations,
SAME_STATION_M,
snapExistingStation,
snapStructureStation,
} from "./common_util_structure_station";
/** 「노선에서 멀어짐」 거리(m) — 파이썬 `config_system.STRUCTURE_ROUTE_DRIFT_MAX_M` 짝(거울 시험이 대조). */
export const STRUCTURE_ROUTE_DRIFT_MAX_M = 20;
@@ -146,6 +151,7 @@ export function projectPipePoints<P extends FollowingPoint>(
}
export interface FollowingStructure {
placement?: string | null;
chainage_m?: number | null;
start_m?: number | null;
end_m?: number | null;
@@ -153,18 +159,24 @@ export interface FollowingStructure {
y?: number | null;
}
/** 구조물이 좌표를 따라감 — 사본 · 노선과 거리(좌표 없으면 null) · 멀어짐(차례는 그대로). */
/** 구조물이 좌표를 따라감 — 사본 · 노선과 거리(좌표 없으면 null) · 멀어짐(차례는 그대로) ·
* `intervalM` 주면 site 꼴(사토장 · 토취장) = 새 노선 일반 측점 중 가장 가까운 곳(380-3 · 파이썬은 서버 간격). */
export function followStructures<S extends FollowingStructure>(
items: ReadonlyArray<S>,
line: ReadonlyArray<XY>,
maxDriftM: number = STRUCTURE_ROUTE_DRIFT_MAX_M,
intervalM: number | null = null,
): Array<{ item: S; drift_m: number | null; far: boolean }> {
return items.map((item) => {
const hit =
item.x != null && item.y != null ? projectOnPolyline(line, { x: item.x, y: item.y }) : null;
if (hit === null || item.chainage_m == null)
return { item: { ...item }, drift_m: null, far: false };
const shift = snapStructureStation(hit.chainage_m, [hit.length_m]) - item.chainage_m;
const target =
intervalM && item.placement === "site"
? snapExistingStation(hit.chainage_m, gridStations(hit.length_m, intervalM))
: snapStructureStation(hit.chainage_m, [hit.length_m]);
const shift = target - item.chainage_m;
const clamp = (value: number): number => Math.min(Math.max(value + shift, 0), hit.length_m);
return {
item: {
@@ -45,6 +45,14 @@ def snap_structure_station(chainage_m: float, stations: Sequence[float]) -> floa
return max(0.0, min(whole, float(math.floor(end + _EPS))))
def snap_existing_station(chainage_m: float, stations: Sequence[float]) -> float:
"""가장 가까운 기존 측점(가운데면 앞) · 비면 정수 m — TS `snapExistingStation` 짝."""
value = float(chainage_m)
if not math.isfinite(value) or not stations:
return snap_structure_station(value, stations)
return float(min(stations, key=lambda at: (round(abs(float(at) - value), 6), float(at))))
def route_general_stations(total_m: float) -> list[float]:
"""노선 길이의 일반 측점 — 서버 측점 간격(`SECTION_STATION_INTERVAL_M`)."""
return general_stations(total_m, SECTION_STATION_INTERVAL_M)
@@ -115,6 +115,17 @@ export function snapExistingStation(
});
}
/** 격자 측점 + 끝 측점 — 파이썬 `general_stations` 짝(노선 따라감 site 맞춤 · 380-3). */
export function gridStations(totalM: number, intervalM: number): number[] {
if (!(totalM > 0 && intervalM > 0)) return [0];
const values = Array.from(
{ length: Math.floor(totalM / intervalM) + 1 },
(_, i) => i * intervalM,
);
if (totalM - values[values.length - 1] > EPS) values.push(totalM);
return values;
}
/** 놓는 꼴 따라 맞춤 — site = 가장 가까운 기존 측점 · 그 밖 = 정수 m. */
export const snapStationFor = (chainageM: number, placement: string | null | undefined): number =>
placement === "site" ? snapExistingStation(chainageM) : snapStructureStation(chainageM);