/* ============================================================================= * B05_Profile_UI_Profile_Guard.ts * 종단도가 편집 · 경고에 쓰는 **셈만** — 계획선 샘플 보간, 평면 최소곡선반지름 경고, * 구조물 측점을 종단 정본 측점으로 맞추기. * * 종단도 한 벌(`A00_Common/design/design_profile.ts`)이 700줄을 넘겨 떼어낸 조각이다 * (계획서 24-2c). 본문 로직과 수치는 그대로이고 화면 상태를 인자로 받는다. * ========================================================================== */ import type { SectionDetailResponse } from "../B06_Section/B06_Section_Api_Fetch"; import type { ProfileAlignment } from "./B05_Profile_UI_Profile_Alignment"; import type { IrregularStation } from "./B05_Profile_UI_IrregularStations"; /** 구조물 측점을 종단 정본 측점으로 맞춰 주는 허용 오차(m). 층마다 반올림이 달라 생기는 * mm~cm 어긋남만 흡수할 크기다 — 이보다 멀면 서로 다른 측점이다. */ const STATION_SNAP_TOLERANCE_M = 0.1; /** 계획선 샘플에서 chainage로 계획고·지반고를 되짚는 보간기(최소고 판정 입력). */ export function sampleAt( alignment: ProfileAlignment | null, chainageM: number, field: "elevation_m" | "ground_elevation_m", ): number | null { const samples = alignment?.samples; if (!samples?.length) return null; if (chainageM <= samples[0].chainage_m) return samples[0][field]; for (let i = 1; i < samples.length; i += 1) { if (chainageM <= samples[i].chainage_m) { const span = samples[i].chainage_m - samples[i - 1].chainage_m; const t = span <= 1e-12 ? 0 : (chainageM - samples[i - 1].chainage_m) / span; return samples[i - 1][field] + (samples[i][field] - samples[i - 1][field]) * t; } } return samples[samples.length - 1][field]; } /** * 법정 평면 최소곡선반지름을 밑도는 측점을 센다(별표2 Ⅰ.2.다.(1), 2026-09-06). * 반경은 횡단 측점마다 실려 오고(`plan_radius_m`), 하한은 계획선 정책이 들고 있다. * **경고만** 낸다 — 노선을 고칠지는 사용자 판단이다. */ export function planCurveWarning( alignment: ProfileAlignment | null, detail: SectionDetailResponse | null, ): { count: number; worstRadiusM: number; limitM: number; hairpin: number; } | null { const limit = alignment?.policy.min_plan_radius_m ?? 0; const sections = detail?.cross_sections ?? []; if (!limit || !sections.length) return null; const hairpinLimit = alignment?.policy.hairpin_min_radius_m ?? 0; let count = 0; let hairpin = 0; let worst = Number.POSITIVE_INFINITY; sections.forEach((section) => { const radius = section.plan_radius_m; if (typeof radius !== "number" || !Number.isFinite(radius) || radius >= limit) return; count += 1; worst = Math.min(worst, radius); if (hairpinLimit > 0 && radius < hairpinLimit) hairpin += 1; }); return count ? { count, worstRadiusM: worst, limitM: limit, hairpin } : null; } /** * 구조물 측점의 누가거리를 **종단 정본 값으로 갈아 끼운다**(2026-09-04). * * 같은 구조물이 층마다 다른 누가거리를 들고 있었다 — 횡단 측점 `264.054626`, * 종단 정본 변화점 `264.055`, 배수 정본 파일 `264.06`(cm 반올림). 사이드 목록이 든 * 값으로 계획고를 편집하면 정본 옆 mm 자리에 **가짜 변화점**이 하나 더 서고, 둘 사이 * 종단곡선이 mm 로 쭈그러들어 기울기가 155,791% 로 튀었다(2026-09-04 사용자 보고). * * 2026-09-03 에 ▲▼ 버튼 경로만 막았는데 측점 테이블·도구 경로가 남아 다시 터졌다. * 값이 화면에 들어오는 **이 한 자리**에서 정본으로 맞춰 모든 경로를 함께 막는다. */ export function snapToMasterStations( alignment: ProfileAlignment | null, stations: IrregularStation[], ): IrregularStation[] { const master = alignment?.stations; if (!master?.length) return stations; return stations.map((station) => { let best: number | null = null; let gap = STATION_SNAP_TOLERANCE_M; for (const node of master) { const distance = Math.abs(node.chainage_m - station.chainage_m); if (distance <= gap) { gap = distance; best = node.chainage_m; } } return best === null || best === station.chainage_m ? station : { ...station, chainage_m: best }; }); }