/* ============================================================================= * A00_Common/design/design_pick_profile.ts * 종단 보기 고름 강조 = 고름 한 칸에서 파생 — 종단도(측점 세로선 · 알약 · 구간 띠 · 233-1) · * 3D(측점 띠 · 핀 · 부재 · 233-2) · 배수유역도(관 마커 · 유역 · 측점 마름모 · 233-3) (조사 AF (나) ② ③ ⑤). * * 고름 한 칸(`structure-pick`)이 유일 원천이다. 값이 바뀌면(구독) 그 값으로 종단도를 다시 세우고, * 관 · 구조물 목록이 바뀐 뒤에도 같은 값으로 다시 얹는다(`paint`) — 화면끼리 손으로 부르던 길이 * 하나 빠지거나 다른 길이 강조를 끄더라도 종단도는 값을 따라간다. 관 알약 id(`pipe-`+측점)는 * 그릴 때마다 지금 관 목록에서 다시 찾으므로 관이 옮겨져도 끊기지 않는다. * ========================================================================== */ import { nearestWithin, readSelection, subscribeSelection, type DesignSelection, } from "./design_selection"; import { bindEscapeRelease, ESCAPE_FROM } from "./design_pick_escape"; import { graphTargetAt } from "../../B05_Profile/B05_Profile_UI_Selection"; import { irregularStationId } from "../../B05_Profile/B05_Profile_UI_IrregularStations"; /** 종단도가 켤 것 — 측점선(알약은 그 측점 짝을 종단도가 맞춤) 또는 알약(구조물 · 관 `pipe-*`). */ export type ProfilePick = { stationId: string | null } | { structureId: string }; export interface ProfilePickSource { /** 구조물 측점(수동 + 관 투영) — id 는 `irregularStationId` 를 씌우기 전 값. */ irregular: () => ReadonlyArray<{ id: string; chainage_m: number; origin?: string }>; /** 관 지점 정본(배수유역도). */ pipes: () => ReadonlyArray<{ chainage_m: number }>; /** 일반 측점 카드. */ sections: () => ReadonlyArray<{ station_id: string; chainage_m: number }>; } export interface ProfilePickTarget { setSelectedStation: (stationId: string | null) => void; setSelectedStructure: (structureId: string | null) => void; /** 배수유역도 — 관 마커 · 유역(알림 없이) · 측점 마름모. */ drainage?: { selectPipeAtChainage: (chainageM: number | null, notify?: boolean) => void; markStation: (chainageM: number | null) => void; } | null; } /** 같은 값으로 적힌 측점인가 — 고름 한 칸은 고른 측점의 누가거리를 그대로 적음(찾는 폭 아님). */ const sameAt = (a: number, b: number): boolean => Math.abs(a - b) < 1e-6; /** * 고름 값 → 종단도가 켤 것(B05 규칙). ① 구조물 id = 그 알약(측점선은 제 측점이 있을 때만 · 189-1) * ② 그 자리 구조물 측점(같은 m 면 측점 하나 · 구조물이 위 · 272 — 일반 카드 id 는 종단도가 숨긴 측점) * ③ 고른 측점 그대로(일반 카드) ④ 그 자리 관 측점 · 관 알약 · 카드(`graphTargetAt` · 188-1). */ export function profilePickOf( selection: DesignSelection | null, source: ProfilePickSource, ): ProfilePick { if (!selection || selection.chainageM === null) return { stationId: null }; if (selection.structureId) return { structureId: selection.structureId }; const at = selection.chainageM; const irregular = source.irregular(); const exact = irregular.find((entry) => sameAt(entry.chainage_m, at)); if (exact) return { stationId: irregularStationId(exact.id) }; const card = source.sections().find((entry) => sameAt(entry.chainage_m, at)); if (card) return { stationId: card.station_id }; const own = nearestWithin( irregular.filter((entry) => entry.origin === "pipe"), at, (entry) => entry.chainage_m, ); if (own) return { stationId: irregularStationId(own.id) }; const target = graphTargetAt(null, at, source.pipes(), source.sections()); return "pipeChainageM" in target ? { structureId: `pipe-${target.pipeChainageM.toFixed(2)}` } : target; } export function paintProfilePick(target: ProfilePickTarget, pick: ProfilePick): void { if ("structureId" in pick) target.setSelectedStructure(pick.structureId); else target.setSelectedStation(pick.stationId); } /** 배수유역도 = 그 자리 관 마커 · 유역(없으면 풂) + 측점 마름모 — 재계산이 지운 뒤에도 다시 얹음(a3 · a6). */ export function paintMapPick( map: NonNullable, selection: DesignSelection | null, ): void { const at = selection?.chainageM ?? null; map.selectPipeAtChainage(at, false); map.markStation(at); } export interface ViewerPickTarget { markers: { selectStation: (stationId: string | null, notify?: boolean) => void }; structurePick: { select: (pick: { chainageM: number; kind: string; key?: string } | null) => void; }; } /** 고름 값 → 3D 가 켤 것 — 띠(+핀) = 종단 측점선과 같은 측점(구조물 고름은 그 자리 구조물 측점만) · * 부재 = 고른 측점의 부재 전부(부재키가 있으면 그 부재 하나). */ export function viewerPickOf( selection: (DesignSelection & { partKey?: string }) | null, source: ProfilePickSource, ): { stationId: string | null; member: { chainageM: number; kind: string; key?: string } | null } { if (!selection || selection.chainageM === null) return { stationId: null, member: null }; const at = selection.chainageM; const pick = profilePickOf(selection, source); const own = selection.structureId ? nearestWithin(source.irregular(), at, (entry) => entry.chainage_m) : undefined; const stationId = "stationId" in pick ? pick.stationId : own ? irregularStationId(own.id) : null; return { stationId, member: { chainageM: at, kind: "", key: selection.partKey } }; } export function paintViewerPick( target: ViewerPickTarget, pick: ReturnType, ): void { target.markers.selectStation(pick.stationId, false); // 칠하기만 — 알림이 사슬을 다시 돌리지 않게 target.structurePick.select(pick.member); } /** * 종단 보기(종단도 · 3D · 배수유역도)를 고름 한 칸에 묶음 — 값이 바뀔 때마다 그림 · `paint()` = 목록이 * 바뀐 뒤 · 재계산 뒤 · 폼이 닫힌 뒤 같은 값으로 다시 얹음. 보기가 숨어 있으면(횡단 보기가 종단도를 쥠) * 안 그림 — 그 보기가 제 규칙으로 그림 · 보기를 떠나면 구독 끊음. Esc(풀기 한 길)는 좌측 폼까지 닫음. */ export function bindProfilePick( projectId: string | null, target: { profile: ProfilePickTarget; viewer?: ViewerPickTarget; form?: { hasSelection: () => boolean; selectById: (id: null) => void }; }, host: { visible: () => boolean; onDispose: (fn: () => void) => void }, source: ProfilePickSource, ): { paint: () => void } { const paint = (): void => { if (!host.visible()) return; const selection = readSelection(projectId); paintProfilePick(target.profile, profilePickOf(selection, source)); if (target.viewer) paintViewerPick(target.viewer, viewerPickOf(selection, source)); if (target.profile.drainage) paintMapPick(target.profile.drainage, selection); }; host.onDispose( subscribeSelection(projectId, (_selection, from) => { paint(); if (from === ESCAPE_FROM && host.visible() && target.form?.hasSelection()) target.form.selectById(null); }), ); bindEscapeRelease(projectId, host); return { paint }; }