/* ============================================================================= * A00_Common/design/design_pipe_ids.ts * 관 세션 id(323-5) — 관을 처음 볼 때 자리마다 붙이는 id · 옮겨도 그대로 · 저장 안 함(파일 모양 그대로). * * id = `pipe-s` + 차례(`pipe-s3`) — 머리 `pipe-` 로 관임을 가름(옛 자리 이름 `pipe-{누가거리}` 와 같은 머리) · * 누가거리는 id 에서 읽지 않음(`pipeChainageOf`). 장부 = 세션 `pipe-ids`(고름 `structure-pick` 과 같은 수명 · * 초안 비우기에 함께 비움). 자리 → id = 찾는 거리(`PIPE_MATCH_M`) 안 가장 가까운 것 · 없으면 새 차례 · * 옮김 = `movePipeId`(고름 따라가기 `followMovedPipe` 가 부름). * 프로젝트 = 보기가 열 때 정함(`usePipeIds`) — 그리는 함수(카드 · 알약)가 프로젝트를 몰라도 같은 id. * 프로젝트가 없으면(시험 · 도움 파일) 옛 자리 이름. * ========================================================================== */ import { readState, writeState } from "../b_page_state"; import { isNear, nearestWithin, readSelection, selectDesign } from "./design_selection"; /** 관 찾는 거리 — 정의는 `design_selection` 한 곳(배수 패널이 관 옮김과 함께 가져감). */ export { PIPE_MATCH_M } from "./design_selection"; interface Book { next: number; ids: Array<{ id: string; at: number }>; } let active: string | null = null; /** 지금 프로젝트 — 종단 · 횡단 보기가 열 때. */ export function usePipeIds(projectId: string | null): void { active = projectId; } const bookOf = (projectId: string): Book => readState("pipe-ids", projectId) ?? { next: 1, ids: [] }; /** 그 자리 관의 id — 장부에 없으면 새 차례로 적음. */ export function pipeIdAt(chainageM: number, projectId: string | null = active): string { if (!projectId) return `pipe-${chainageM.toFixed(2)}`; const book = bookOf(projectId); const hit = nearestWithin(book.ids, chainageM, (entry) => entry.at); if (hit) return hit.id; const id = `pipe-s${book.next}`; writeState( "pipe-ids", { next: book.next + 1, ids: [...book.ids, { id, at: chainageM }] }, projectId, ); return id; } /** id 의 관 자리 — 장부 · 옛 자리 이름 순 · 관 id 가 아니면 null. */ export function pipeChainageOf(id: string, projectId: string | null = active): number | null { if (!id.startsWith("pipe-")) return null; const hit = projectId ? bookOf(projectId).ids.find((entry) => entry.id === id) : undefined; if (hit) return hit.at; const legacy = Number(id.slice("pipe-".length)); return Number.isFinite(legacy) ? legacy : null; } /** 관을 옮김 — 옛 자리 id 가 새 자리로(새 자리에 남은 옛 기록은 뺌) · 옮긴 관의 id. */ export function movePipeId(fromM: number, toM: number, projectId: string | null = active): string { if (!projectId) return `pipe-${toM.toFixed(2)}`; const book = bookOf(projectId); const moving = nearestWithin(book.ids, fromM, (entry) => entry.at); if (!moving) return pipeIdAt(toM, projectId); const stale = nearestWithin( book.ids.filter((entry) => entry !== moving), toM, (entry) => entry.at, ); const ids = book.ids .filter((entry) => entry !== stale) .map((entry) => (entry === moving ? { ...entry, at: toM } : entry)); writeState("pipe-ids", { next: book.next, ids }, projectId); return moving.id; } /** 관 목록에 세션 id 를 얹은 사본 — 측점 계획의 덮는 구조물 id(종단 측점 id · 알약 짝)가 세션 id. */ export function withPipeIds( pipes: ReadonlyArray, projectId: string | null = active, ): Array { return pipes.map((pipe) => ({ ...pipe, id: pipeIdAt(pipe.chainage_m, projectId) })); } /** 관을 옮김 — 관 세션 id 가 새 자리로(323-5 · id 그대로) · 그 관을 골라 둔 고름이면 자리만 새로(부재키 * 그대로 · 옛 자리 이름 id 면 세션 id 로). 다른 것을 골랐으면 그대로. */ export function followMovedPipe(projectId: string | null, fromM: number, toM: number): void { const id = movePipeId(fromM, toM, projectId); const picked = readSelection(projectId); if (!picked || picked.chainageM === null || !isNear(picked.chainageM, fromM)) return; const sid = picked.structureId; if (sid !== null && !sid.startsWith("pipe-")) return; selectDesign( projectId, { chainageM: toM, structureId: sid === null ? null : id }, { partKey: picked.partKey }, ); }