feat(380-2): 사토장 · 토취장 자리 = 가장 가까운 기존 측점 · 새 측점 안 심음

- common_util_structure_station: 기존 측점(일반 · 구조물 · 추가) · snapExistingStation(가운데면 앞 측점) · snapStationFor(site 만)
- 폼 [추가] · 끌기(movedStructure) · 우클릭 추가 = 같은 맞춤 · 폼 칸에 맞춘 측점 다시 씀
- 옛 소수 자동 맞춤 = 기존 측점(끝 소수 · 옛 구조물 측점)에 선 site 안 옮김
- 상세 모달 CAD 글 = 맞춘 측점에서 종류 이름 + 「상세 내용: 사토장 부피」 줄(기존 길 그대로)
- 시험 test_380_2_site_station(아무 m 입력 = 가까운 측점 · CAD 글 줄)

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:03:16 +09:00
co-authored by Claude Opus 5.5
parent 3898cb0019
commit 8563da1c76
6 changed files with 235 additions and 12 deletions
+26 -1
View File
@@ -40,14 +40,16 @@ const EPS = 1e-6;
export const SAME_STATION_M = 0.005;
let general: number[] = [];
let listed: number[] = [];
/** 일반 측점 목록을 둔다 — 횡단 상세를 읽을 때마다(B05 · B06 한 노선). */
/** 일반 측점 목록을 둔다 — 횡단 상세를 읽을 때마다(B05 · B06 한 노선). 구조물 측점까지 든 목록도 둠(380-2). */
export function setGeneralStations(
stations: ReadonlyArray<{ chainage_m: number; kind?: string }> | null | undefined,
): void {
general = (stations ?? [])
.filter((station) => station.kind !== "irregular")
.map((station) => station.chainage_m);
listed = (stations ?? []).map((station) => station.chainage_m);
}
export const generalStations = (): readonly number[] => general;
@@ -94,6 +96,29 @@ export const isSnappedStation = (
stations: readonly number[] = general,
): boolean => Math.abs(snapStructureStation(chainageM, stations) - chainageM) < EPS;
/* ── 기존 측점 맞춤(380-2 · 요구 241-8) — 새 측점을 안 심는 자리 꼴(site · 사토장 · 토취장) ── */
/** 기존 측점(일반 · 구조물 · 추가) — 오름차순 · 같은 m 하나.
* ponytail: 구조물 측점 = 마지막으로 읽은 횡단 상세 것 — 세션에서 새로 놓은 관 측점은 다시 읽을 때까지 빠짐. */
export const existingStations = (): number[] =>
[...new Set([...listed, ...plusSource()])].sort((a, b) => a - b);
/** 가장 가까운 기존 측점(가운데면 앞 측점) — 목록이 비면 정수 m 맞춤. 두 번 맞춰도 같다. */
export function snapExistingStation(
chainageM: number,
stations: readonly number[] = existingStations(),
): number {
if (!Number.isFinite(chainageM) || !stations.length) return snapStructureStation(chainageM);
return stations.reduce((best, at) => {
const [gap, bestGap] = [Math.abs(at - chainageM), Math.abs(best - chainageM)];
return gap < bestGap - EPS || (gap < bestGap + EPS && at < best) ? at : best;
});
}
/** 놓는 꼴 따라 맞춤 — site = 가장 가까운 기존 측점 · 그 밖 = 정수 m. */
export const snapStationFor = (chainageM: number, placement: string | null | undefined): number =>
placement === "site" ? snapExistingStation(chainageM) : snapStructureStation(chainageM);
/* ── 측점을 심는 자리(272 · 서버 `resolve_extra_stations` · `is_station_planting_type` 짝) ── */
export interface PlantingPipe {