/* ============================================================================= * B05_Profile_UI_Page_Stations.ts * 종단 보기 3D **측점 바** 목록 — 규칙 측점 + 구조물(비정규) 측점 · 계획고 높이 · 상단측. * 종단 보기 본체(`design_view_profile`)가 700줄을 넘어 여기로 뗐다(계획서 24-3b). * ========================================================================== */ import { dropStationsNear } from "./B05_Profile_Util_Station"; import { interpolateIrregularStations } from "./B05_Profile_UI_Page_Helpers"; import type { IrregularStation } from "./B05_Profile_UI_IrregularStations"; import type { SectionDetailResponse } from "../B06_Section/B06_Section_Api_Fetch"; export interface StationLinesInput { detail: SectionDetailResponse; irregular: IrregularStation[]; routePoints: ReadonlyArray<{ x: number; y: number; z?: number | null }>; /** 편집 중 라이브 계획선 샘플(없으면 정본 계획선). */ liveSamples: ReadonlyArray<{ chainage_m: number; elevation_m: number }> | null; /** [예상형상] 켜짐 — 측점 바를 계획고 위로 올린다. */ corridorVisible: boolean; /** 상단측(측구 방향) 사용자 변경분 — 키는 누가거리 3자리. */ uphillOverrides: Map; } export function stationLinesOf(input: StationLinesInput) { const { detail } = input; // 확정 때 종단 정본에 병합해 둔 비정규 측점은 그리지 않는다. 화면의 정본은 사이드바 목록이고 // 관을 옮기면 그 목록만 따라오므로, 둘을 겹쳐 그리면 옮기기 전 자리가 그대로 남는다 // (2026-08-02 사용자 보고). 저장분은 진입 시 `restoreSections`가 목록으로 되살린다. const regular = detail.longitudinal.stations.filter((station) => station.kind !== "irregular"); const injected = interpolateIrregularStations( regular, input.irregular, detail.longitudinal.length_m, // 중심 좌표는 노선 폴리라인 기준 — 3D 마커가 노선 위에 정확히 얹힌다(2026-08-19). input.routePoints, ); // 예상형상이 켜져 있으면 측점 바를 **계획고** 기준으로 올린다 — 원지반 위에 // 얹힌 예상형상에 묻혀 측점 기준선이 안 보이던 문제(2026-08-23 사용자 지시). const designAt = (chainageM: number): number | null => { if (!input.corridorVisible) return null; // 편집 중에는 라이브 alignment 샘플이 정본보다 새것이다(2026-08-23 지적 ③). const samples = input.liveSamples ?? detail.longitudinal.design_profiles?.[0]?.samples; if (!samples?.length) return null; let best = samples[0]; for (const sample of samples) { if (Math.abs(sample.chainage_m - chainageM) < Math.abs(best.chainage_m - chainageM)) { best = sample; } } return best.elevation_m; }; // 측점 바 양 끝 램프용 상단측: 사용자 변경분 → solve 자동 판정 순으로 적용. // 구조물 측점과 0.1m 안에서 겹치는 규칙 측점은 지운다 — 3D 측점 띠도 종단 그래프와 // 같은 규칙을 써야 두 화면의 측점이 어긋나지 않는다(2026-09-06). return [...dropStationsNear(regular, injected), ...injected].map((station) => { const design = designAt(station.chainage_m); return { ...station, // 절토 구간에서는 계획고가 지반보다 **아래**다(2026-09-04 사용자 지적). // max 로 잡으면 코리도가 절취해 내려간 노면을 두고 막대만 원지반에 떠 있다. // 코리도가 켜져 있으면(designAt 이 값을 줌) 계획고를 그대로 쓴다. center_z: design !== null && station.center_z !== null ? design : station.center_z, uphill_side: input.uphillOverrides.get(station.chainage_m.toFixed(3)) ?? station.uphill_side ?? null, }; }); }