/* ============================================================================= * B06_Section_UI_Page_Structure_Pick.ts * B05 3D에서 고른 구조물을 **B06 화면에 얹는다**(2026-09-04 사용자 확정) — 그 측점 * 카드를 고르고 화면에 드러낸 뒤, 부재키가 있으면 그 시설의 조정창까지 연다. * * 부재키는 **횡단도가 그 부재를 부르는 이름 그대로**다. 이름이 겹쳐도(배수관 유입 ↔ * 세월교 유입) 한 측점에는 시설이 하나뿐이라 헷갈리지 않는다 — 그 측점이 세월교면 * 세월교 조정창, BOX암거면 BOX 조정창, 아니면 기슭막이 조정창으로 보낸다. * 본체(`body` — 세월교 월류부 상판·BOX 구체)는 고르는 부재가 아니라 카드까지만 연다. * * 세월교·BOX는 조정창 하나가 그 측 부재 여럿(벽·바닥·날개벽·에이프런)을 함께 다루므로 * 부재키가 `측이름:부재`(예 `inlet:wall`) 꼴이다 — **앞칸이 조정창 이름**이다. 3D 강조는 * 부재 하나만 켜야 해서 뒤칸까지 나눠 두었다(2026-09-04 사용자 확정). * ========================================================================== */ import type { SectionDetailResponse } from "./B06_Section_Api_Fetch"; import type { RevetKey } from "./B06_Section_UI_Cross_Culvert"; import type { StructurePickHandoff } from "../B05_Profile/B05_Profile_UI_Structure_Pick_Session"; import { coversChainage, isNear, type SelectableStructure, } from "../A00_Common/design/design_selection"; /** 기슭막이 조정창이 받는 부재키 — 집수정·배관·본체는 여기서 빠진다. */ const REVET_KEYS = /^(inlet|outlet|own|extra\d+|bextra\d+)$/; interface PickTargets { focusStation: (stationId: string) => void; refreshCard: (chainageM: number) => void; revetSelect: (chainageM: number, key: RevetKey) => void; fordSelect: (chainageM: number, role: "inlet" | "outlet") => void; boxSelect: (chainageM: number, role: "left" | "right") => void; } /** * 넘김값을 화면에 적용한다. 값이 없거나 그 측점이 이 노선에 없으면 아무것도 안 한다. * 카드가 다 선 뒤에 고른다 — 스크롤이 자리를 잡아야 한다. 카드를 찾아 적용했으면 true — * 못 찾았으면(구조물 목록이 아직 안 실린 경우 등) false 라 부르는 쪽이 나중에 다시 부른다. */ export function applyStructurePick( handoff: StructurePickHandoff | null, detail: SectionDetailResponse | null, targets: PickTargets, /** 구조물 정본 — 카드 위치가 아닌 곳(구간형 옹벽 한가운데 등)을 골랐을 때 그 카드를 찾는 데 쓴다. */ structures: ReadonlyArray = [], ): boolean { if (!handoff) return false; // 허용오차는 관·구조물 찾는 거리 한 값이다 — 0.01m 로는 3D 가 준 누가거리가 // 소수점 아래에서 조금만 달라도 그 측점을 못 찾고 조용히 끝났다(2026-09-05 진단). const sections = detail?.cross_sections ?? []; // 구조물 id 가 있으면 그 구조물의 카드가 먼저다 — 같은 측점 근처에 다른 카드(관)가 있어도. const byId = handoff.structureId ? structures.find((entry) => entry.structure_id === handoff.structureId) : undefined; const covering = structures.find((entry) => coversChainage(entry, handoff.at)); const target = (byId && cardOfStructure(sections, byId, handoff.at)) || sections.find((section) => isNear(section.chainage_m, handoff.at)) || (covering && cardOfStructure(sections, covering, handoff.at)); if (!target) return false; // 조정창은 앞칸(측 이름)만 본다 — 뒤칸은 3D 강조를 부재 하나로 좁히는 몫이다. const key = handoff.key?.split(":")[0]; requestAnimationFrame(() => { targets.focusStation(target.station_id); if (!key || key === "body") return; if (target.ford) { if (key !== "inlet" && key !== "outlet") return; targets.fordSelect(target.chainage_m, key); } else if (target.box) { if (key !== "left" && key !== "right") return; targets.boxSelect(target.chainage_m, key); } else { if (!REVET_KEYS.test(key)) return; targets.revetSelect(target.chainage_m, key as RevetKey); } targets.refreshCard(target.chainage_m); }); return true; } type Sections = SectionDetailResponse["cross_sections"]; /** 구조물이 자리한 카드 — 구간형이면 그 안의 첫 카드, 점형이면 가장 가까운 카드. 옹벽 · 기슭막이는 * 횡단 제원이라 시작 측점 카드가 주인이다(관은 `isNear` 로 카드를 바로 찾는다). */ function cardOfStructure( sections: Sections, structure: SelectableStructure, at: number, ): Sections[number] | undefined { if (!sections.length) return undefined; const start = structure.start_m ?? structure.chainage_m; const end = structure.end_m ?? structure.chainage_m; if (start != null && end != null) { const inside = sections.find( (section) => section.chainage_m >= start - 0.01 && section.chainage_m <= end + 0.01, ); if (inside) return inside; } return sections.reduce((best, entry) => Math.abs(entry.chainage_m - at) < Math.abs(best.chainage_m - at) ? entry : best, ); }