/* ============================================================================= * 측점 선택 동기화 (B05) * * 같은 측점을 네 곳이 각자 그린다 — 3D 마커 · 종단 그래프 세로선 · 좌측 구조물 폼 · * 배수유역도의 세부유역. 어느 하나에서 고르면 나머지 셋도 같은 것을 가리켜야 한다 * (2026-08-02 사용자 지시). * * 서로가 서로를 갱신하므로 `isSyncing` 가드로 재진입을 막는다. 배수유역 강조는 "배관" * 구조물(`origin: "pipe"`)일 때만 붙는다 — 다른 구조물에는 대응하는 유역이 없다. * ========================================================================== */ import { PIPE_MATCH_M, nearestWithin } from "../A00_Common/design/design_selection"; import { irregularStationId, isPipeStation, type IrregularStation, } from "./B05_Profile_UI_IrregularStations"; export interface SelectionSyncPorts { /** 현재 구조물 측점 목록(수동 + 배관 투영분). */ stations: () => ReadonlyArray; isSyncing: () => boolean; setSyncing: (value: boolean) => void; /** 3D 뷰어 마커 선택. */ selectMarker: (stationId: string | null) => void; /** 종단 그래프 세로선 선택 — 누가거리는 세로선 없는 시설(물넘이포장)의 알약을 켜는 데 씀. */ selectGraph: (stationId: string | null, chainageM?: number | null) => void; /** 좌측 구조물 폼 선택(누가거리 기준). */ selectSidebar: (chainageM: number | null) => void; /** 배수유역도 세부유역 강조(누가거리 기준). */ selectBasin: (chainageM: number | null) => void; /** 배수유역도 계획선 위 측점 마킹(누가거리 기준). 유역 없는 구조물도 위치를 보여 준다. */ markStation?: (chainageM: number | null) => void; /** 배수유역도 관 마커 선택(누가거리 기준) — 어느 화면에서 배관을 골라도 지도 * 마커·유역 강조가 따라오게 한다(2026-08-17 전역 선택 동기화). 부속 옵션 폼은 * 사이드 「구조물 배치」가 selectSidebar 경로에서 연다. */ selectPipeForm?: (chainageM: number | null) => void; /** 3D 코리도 구조물 강조(누가거리 기준) — 좌측 폼·종단에서 골라도 3D가 따라온다 * (2026-09-04). 3D에서 부재를 집어 이미 그 측점을 고른 상태면 그대로 둔다. */ selectStructure3D?: (chainageM: number | null) => void; /** 일반 측점 id → 누가거리(없으면 undefined — 고름 한 칸을 안 건드림) — 일반 측점 고름도 고름 한 칸에 남기는 데 씀(188-1). */ regularChainage?: (stationId: string) => number | undefined; /** 사용자 고름을 고름 한 칸(`structure-pick`)에 적음 — 마지막 고름 하나만 이김(188-1). */ remember?: (chainageM: number | null) => void; } export interface SelectionSync { /** 그래프·3D에서 측점을 골랐을 때 — 좌측 폼과 배수유역도를 맞춘다. */ syncIrregularSelection: (stationId: string | null) => void; /** 고른 측점이 배관이면 그 유역을 강조한다(아니면 강조 해제). */ syncBasinHighlight: (stationId: string | null) => void; /** 배수유역도에서 유역을 골랐을 때 — 그래프·3D·좌측 폼을 맞춘다. */ selectStationOfPipe: (chainageM: number | null) => void; } export function createSelectionSync(ports: SelectionSyncPorts): SelectionSync { const prefix = irregularStationId(""); /** 측점 id로 구조물 측점을 찾는다(비정규 id가 아니면 undefined). */ function irregularOf(stationId: string | null): IrregularStation | undefined { if (!stationId?.startsWith(prefix)) return undefined; const id = stationId.slice(prefix.length); return ports.stations().find((entry) => entry.id === id); } function syncBasinHighlight(stationId: string | null): void { const station = irregularOf(stationId); ports.selectBasin(station && isPipeStation(station) ? station.chainage_m : null); // 배수유역도 측점 마킹 — 유역이 없는 구조물(대피로 등)도 위치는 표시한다(2026-08-05). ports.markStation?.(station ? station.chainage_m : null); } return { syncBasinHighlight, syncIrregularSelection(stationId) { if (ports.isSyncing()) return; syncBasinHighlight(stationId); const station = irregularOf(stationId); // 측점선 · 3D 띠 · 표 고름도 고름 한 칸에 — 일반 측점을 안 적어 옛 배관 고름이 이겼음(188-1) const at = stationId === null ? null : (station?.chainage_m ?? ports.regularChainage?.(stationId)); if (at !== undefined) ports.remember?.(at); // 규칙 측점을 고른 것이면 구조물 폼은 건드리지 않는다(고를 항목이 없다). if (stationId !== null && !stationId.startsWith(prefix)) return; ports.setSyncing(true); try { ports.selectSidebar(station ? station.chainage_m : null); ports.selectPipeForm?.(station && isPipeStation(station) ? station.chainage_m : null); ports.selectStructure3D?.(station ? station.chainage_m : null); } finally { ports.setSyncing(false); } }, selectStationOfPipe(chainageM) { if (ports.isSyncing()) return; const matched = chainageM === null ? undefined : ports .stations() .find( (entry) => isPipeStation(entry) && Math.abs(entry.chainage_m - chainageM) < PIPE_MATCH_M, ); const id = matched ? irregularStationId(matched.id) : null; ports.setSyncing(true); try { ports.selectMarker(id); ports.selectGraph(id, chainageM); // 투영 측점이 아직 없어도(방금 넣은 임시 관 — 재계산 전) 누가거리는 그대로 // 넘긴다. null을 넘기면 사이드가 "해제"로 받아 임시 배치를 스스로 취소한다 // (2026-08-18 임시 배치 자기 삭제 버그). ports.selectSidebar(matched ? matched.chainage_m : chainageM); // 유역·마커 어느 쪽에서 왔든 시설 폼도 그 관을 연다(투영 측점이 없어도 // 누가거리로 근사 매칭 — Drainage 쪽이 못 찾으면 해제된다). ports.selectPipeForm?.(matched ? matched.chainage_m : chainageM); ports.selectStructure3D?.(matched ? matched.chainage_m : chainageM); } finally { ports.setSyncing(false); } }, }; } /** 종단 그래프가 켤 것 — 측점 id 가 있으면 그 선 · 없으면 그 자리 관 알약(53-2) · 관도 없으면 * 그 자리 측점선(188-1 · 횡단에서 고른 일반 측점이 종단으로 이어짐). */ export function graphTargetAt( stationId: string | null, chainageM: number | null, pipes: ReadonlyArray<{ chainage_m: number }>, sections: ReadonlyArray<{ station_id: string; chainage_m: number }>, ): { pipeChainageM: number } | { stationId: string | null } { if (stationId !== null || chainageM === null) return { stationId }; const pipe = nearestWithin(pipes, chainageM, (entry) => entry.chainage_m); if (pipe) return { pipeChainageM: pipe.chainage_m }; const card = nearestWithin(sections, chainageM, (entry) => entry.chainage_m); return { stationId: card?.station_id ?? null }; }