From df0240eb2f01ffb71c1540378160f90d80d7a94d Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 29 Sep 2026 20:17:39 +0900 Subject: [PATCH] =?UTF-8?q?refactor(B05=C2=B7B06):=20=EC=B8=A1=EC=A0=90=20?= =?UTF-8?q?=C2=B7=20=EA=B5=AC=EC=A1=B0=EB=AC=BC=20=EA=B3=A0=EB=A6=84=20?= =?UTF-8?q?=ED=95=9C=20=EA=B3=B3=20=EB=BC=88=EB=8C=80=20=E2=80=94=20design?= =?UTF-8?q?=5Fselection(PIPE=5FMATCH=5FM=20=C2=B7=20selection=20=EC=9D=BD?= =?UTF-8?q?=EA=B8=B0=20=C2=B7=20=EC=93=B0=EA=B8=B0=20=C2=B7=20=EA=B5=AC?= =?UTF-8?q?=EB=8F=85)=20(24-2d)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Ly2oHAaTDYRjcniAmHR81z --- A00_Common/design/design_selection.ts | 155 ++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 A00_Common/design/design_selection.ts diff --git a/A00_Common/design/design_selection.ts b/A00_Common/design/design_selection.ts new file mode 100644 index 00000000..43f23a77 --- /dev/null +++ b/A00_Common/design/design_selection.ts @@ -0,0 +1,155 @@ +/* ============================================================================= + * A00_Common/design/design_selection.ts + * 측점 · 구조물 고름 **한 곳** (PLAN 24-2d). + * + * 24-1 약속의 `design_store` 「selection({chainageM, structureId})」 키 자리다. 그 모듈이 + * 서면 이 파일의 `readSelection` · `selectDesign` · `subscribeSelection` 세 함수를 그쪽 + * `get` · `set` · `subscribe("selection")` 으로 잇는다 — 부르는 쪽 코드는 바뀌지 않는다. + * + * 값은 등록표 키 `structure-pick`(② 설계 초안 · 페이지를 오가도 남음)에 실린다. + * 어느 화면(3D · 종단 알약 · 목록 · 횡단 카드)에서 골라도 같은 값이 적히고, 다른 쪽은 + * 이 값을 읽어 따라간다. 관 · 구조물을 찾는 거리 `PIPE_MATCH_M` 도 여기 한 값뿐이다. + * ========================================================================== */ + +import { readState, writeState } from "../b_page_state"; + +/** 관 · 구조물 찾는 거리(m) — 관은 계획선에 스냅돼 측점과 살짝 어긋난다. 이 값이 유일하다. */ +export const PIPE_MATCH_M = 0.51; + +export interface DesignSelection { + chainageM: number | null; + structureId: string | null; +} + +export const NO_SELECTION: DesignSelection = { chainageM: null, structureId: null }; + +/** 두 누가거리가 같은 자리인가. */ +export function isNear(a: number, b: number): boolean { + return Math.abs(a - b) < PIPE_MATCH_M; +} + +/** 선택을 풀 때 필요한 구조물의 최소 모양 — B05 `StructureInstance` 가 그대로 맞는다. */ +export interface SelectableStructure { + structure_id: string; + chainage_m?: number | null; + start_m?: number | null; + end_m?: number | null; +} + +/** 풀린 선택 — 관이면 그 관의 누가거리, 아니면 구조물 id. 둘 다 없으면 고를 항목이 없다. */ +export interface ResolvedSelection { + pipeChainageM: number | null; + structureId: string | null; +} + +/** 이 구조물이 그 측점을 덮는가 — 점형은 기준점 근처, 구간형은 시작~종료 안(양끝 여유 포함). */ +export function coversChainage(structure: SelectableStructure, chainageM: number): boolean { + const anchor = structure.chainage_m ?? structure.start_m; + const start = structure.start_m ?? anchor; + const end = structure.end_m ?? anchor; + if (start == null || end == null) return false; + return ( + chainageM >= Math.min(start, end) - PIPE_MATCH_M && + chainageM <= Math.max(start, end) + PIPE_MATCH_M + ); +} + +/** + * 선택 값을 폼이 열 항목으로 푼다 — 어느 화면에서 골랐든 **같은 순서 · 같은 거리**다. + * ① 구조물 id 가 있고 목록에 있으면 그 구조물 ② 측점 근처 관(계곡 통과 시설이 먼저 — + * 같은 측점에 구조물이 겹쳐도 횡단도에 그려지는 것은 관이다) ③ 측점을 덮는 구조물. + */ +export function resolveSelection( + selection: DesignSelection, + pipes: ReadonlyArray<{ chainage_m: number }>, + structures: ReadonlyArray, +): ResolvedSelection { + if (selection.structureId) { + const byId = structures.find((entry) => entry.structure_id === selection.structureId); + if (byId) return { pipeChainageM: null, structureId: byId.structure_id }; + } + const chainageM = selection.chainageM; + if (chainageM === null) return { pipeChainageM: null, structureId: null }; + const pipe = pipes.find((entry) => isNear(entry.chainage_m, chainageM)); + if (pipe) return { pipeChainageM: pipe.chainage_m, structureId: null }; + const hit = structures.find((entry) => coversChainage(entry, chainageM)); + return { pipeChainageM: null, structureId: hit?.structure_id ?? null }; +} + +/* ── 세션 값 + 알림 ─────────────────────────────────────────────────────── */ + +interface Stored { + at?: unknown; + key?: unknown; + sid?: unknown; +} + +/** 세션에 남은 고름 — 없으면 null. `key` 는 횡단도가 부재를 부르는 이름(조정창 여는 데 씀). */ +export function readSelection( + projectId: string | null, +): (DesignSelection & { partKey?: string }) | null { + if (!projectId) return null; + try { + const value = readState("structure-pick", projectId); + if (!value || typeof value.at !== "number" || !Number.isFinite(value.at)) return null; + return { + chainageM: value.at, + structureId: typeof value.sid === "string" ? value.sid : null, + partKey: typeof value.key === "string" ? value.key : undefined, + }; + } catch { + return null; + } +} + +export type SelectionListener = (selection: DesignSelection, from: string | undefined) => void; + +const listeners = new Map>(); +let dispatching = false; + +/** 고름이 바뀔 때 부른다. 돌려주는 함수로 구독을 끊는다. */ +export function subscribeSelection(projectId: string | null, fn: SelectionListener): () => void { + if (!projectId) return () => undefined; + let set = listeners.get(projectId); + if (!set) listeners.set(projectId, (set = new Set())); + set.add(fn); + return () => set.delete(fn); +} + +/** + * 고름을 적고 구독자에게 알린다. `partKey` 를 안 주면 이미 적힌 부재키를 지킨다 — 3D 로 + * 부재를 집은 직후 선택 동기화가 측점만 다시 적어도 조정창이 그대로 열려야 한다. + * `from` 은 낸 쪽 이름 — 구독자가 제 것을 되받아 다시 그리지 않게 거른다. + * 값이 그대로면 알리지 않고, 알림 도중 다시 낸 값은 적기만 한다(되울림 방지). + */ +export function selectDesign( + projectId: string | null, + selection: DesignSelection, + options: { partKey?: string; from?: string } = {}, +): void { + if (!projectId) return; + if (selection.chainageM === null) { + writeState("structure-pick", null, projectId); + } else { + const before = readSelection(projectId); + const same = + before !== null && + before.chainageM === selection.chainageM && + before.structureId === selection.structureId && + (options.partKey === undefined || options.partKey === before.partKey); + if (same) return; + const key = options.partKey ?? before?.partKey; + writeState( + "structure-pick", + { at: selection.chainageM, key, sid: selection.structureId ?? undefined }, + projectId, + ); + } + if (dispatching) return; + dispatching = true; + try { + for (const fn of [...(listeners.get(projectId) ?? [])]) fn(selection, options.from); + } finally { + dispatching = false; + } +}