/* ============================================================================= * A00_Common/design/design_facility_lane.ts * B05 · B06 한 페이지 **시설 줄 한 벌**(요구 271-5 · 462) — 구조물 + 관 → 알약 · 비정규 측점 · 흙덮기 관 · * 관 id 장부 맞춤 · 평면 마커 원천. * * 페이지가 세움 — 보기가 숨어도 · 아직 안 섰어도 늘 셈(순수 계산 · 서버 GET 없음). 종단도에 알약 · 측점 · * 흙덮기 관을 쓰는 자리는 여기 하나 · 보기는 읽기만(`marks` · `stations` · `pipes`) · 바뀜은 `onChange`. * 입력 = 관 알림(배수유역도 · B06 관 목록) · 구조물(보기 고침 · 저장분) · 종류 표 · 측점 간격 · 추가 측점(가게) * 관 id 장부 = 관 고정 id 가 쥔 세션 id 를 그 자리로(411-B) · 헛 id 정리(411-E) — 어느 보기에서 고쳐도 * 계획고 편집 옮김(사라진 측점 → 새 측점 · 213-2) = 사용자 고침만 · 노선 따라감(`follow`)은 안 옮김(계산 묶음 몫 · 349-0d) * 소단 사본(세션 `berm`) = 구조물 소단 구간 — 쓰는 자리 여기 하나(B05 만 열어도 · 같으면 안 씀 · 쓰면 cross 한 번 · 467) * ========================================================================== */ import { useDesignStore } from "./design_store"; import { pageRouteId } from "./design_page_route"; import { readState } from "../b_page_state"; import { bermSpansFromStructures, writeBermSpans, type BermSpan, } from "../../B06_Section/B06_Section_UI_Page_Persist"; import { pipeIdAt, placePipeIds, prunePipeIds, withPipeIds } from "./design_pipe_ids"; import { calcOnServerNow } from "./design_calc_sites"; import { bermRequest, calcChecked, calcOnServerOnly } from "./design_calc_server"; import { plannedIrregular } from "./design_structure_status"; import { planStations } from "../../common_util/common_util_structure_station"; import { followPipeReports } from "../../B05_Profile/logic/B05_Profile_Pipeline_Follow"; import { followedStation } from "../../B05_Profile/logic/B05_Profile_UI_Profile_Alignment"; import { followSnapped } from "../../B05_Profile/B05_Profile_UI_Structures_Snap"; import { pipesToStructureMarks, type StructureInstance, type StructureType, } from "../../B05_Profile/B05_Profile_Api_Structures"; import type { IrregularStation } from "../../B05_Profile/B05_Profile_UI_IrregularStations"; import type { MinCoverPipe } from "../../B05_Profile/B05_Profile_UI_Profile_MinCover"; /** 관 한 건 — 배수유역도 알림 · B06 관 목록 어느 쪽이든(`id` = 관 고정 id · 새 관은 없음). */ export interface LanePipe { chainage_m: number; id?: string | null; facility: string; options?: Record | null; start_m?: number | null; end_m?: number | null; } /** 시설 줄이 쓰는 종단도 몫. */ export interface LaneProfile { setStructureTypes: (types: StructureType[]) => void; setStructures: (next: StructureInstance[]) => void; setIrregularStations: (stations: IrregularStation[]) => void; setMinCoverPipes: (pipes: ReadonlyArray) => void; resetStationEdit: (chainageM: number, moveTo?: number) => void; } export interface FacilityLane { /** 종류 표 · 저장분 구조물(보기가 받은 것 — 같은 페이지 자료 줄). */ setTypes: (types: StructureType[]) => void; setStored: (structures: StructureInstance[]) => void; /** 구조물 · 관 목록이 바뀜 — `carryEdits` = 사용자 고침(계획고 편집 옮김) · 불러옴은 false. */ set: ( next: { structures?: StructureInstance[]; pipes?: ReadonlyArray }, carryEdits?: boolean, ) => void; /** 노선을 놓아 계산 묶음이 가게 관 · 구조물을 다시 매김(되돌림 포함) — 편집 안 옮김. */ follow: () => void; setIntervalM: (intervalM: number) => void; /** [초기선 복원] — 관 투영 · 측점만 비움(관 정본은 배수유역 몫). */ clearProjected: () => void; /** 다시 셈(편집 안 옮김) · 종단도에 다시 씀 — 종단도가 막 섰을 때 · 다시 그렸을 때(추가 측점이 늦게 옴). */ paint: () => void; pipes: () => LanePipe[]; structures: () => StructureInstance[]; types: () => StructureType[]; /** 알약 = 구조물 + 관(가상 구조물 · 관 세션 id) — 평면 마커도 이것. */ marks: () => StructureInstance[]; stations: () => IrregularStation[]; /** 종류 표 · 구조물 · 관 알림을 다 받음 = 측점 계획이 섬(그 전 측점은 「모름」 · 473). */ ready: () => boolean; intervalM: () => number; onChange: (fn: () => void) => () => void; dispose: () => void; } const run = (fn: () => void): void => { try { fn(); } catch (error) { console.error("[facility_lane]", error); } }; export function createFacilityLane( projectId: string, profile: () => LaneProfile | null, ): FacilityLane { const store = useDesignStore(projectId, null); let types: StructureType[] = []; let stored: StructureInstance[] | null = null; let structures: StructureInstance[] = []; let structuresKnown = false; // 구조물을 받기 전 = 소단 사본 안 씀(빈 목록으로 저장분 소단을 지우지 않게) /** 마지막 관 알림 — 노선 따라감 · 관 초안이 바뀌면 이것을 초안 측점으로 다시 맞춤. */ let reported: LanePipe[] | null = null; let pipes: LanePipe[] = []; let sessionIds = new Map(); // 관 고정 id → 세션 id(알약 id) let stations: IrregularStation[] = []; let intervalM = 20; let painted: { to: LaneProfile; stations: string } | null = null; const listeners = new Set<() => void>(); /** 화면 관 목록 = 알림을 가게 관 초안 측점으로(노선 따라 다시 매김 · 349-0d) · 관 세션 id 도 그 자리로 — * 짝 = 관 고정 id(411-B · 앞 관을 지나 옮겨도 · 되돌려도 그 관) · id 없는 관(막 놓은 관)은 자리로. */ const shownPipes = (list: ReadonlyArray): LanePipe[] => { const shown = followPipeReports( list.map((pipe) => ({ ...pipe, id: pipe.id ?? undefined })), store.get("pipes"), ).sort((x, y) => x.chainage_m - y.chainage_m); placePipeIds( shown.flatMap((pipe) => { const sid = pipe.id && sessionIds.get(pipe.id); return sid ? [[sid, pipe.chainage_m] as const] : []; }), projectId, ); sessionIds = new Map( shown.flatMap((pipe) => pipe.id ? [[pipe.id, pipeIdAt(pipe.chainage_m, projectId)] as const] : [], ), ); prunePipeIds( shown.map((pipe) => pipe.chainage_m), projectId, ); // 헛 id 정리(411-E) return shown; }; /** 옛 측점에 남은 계획고 편집(유령 변화점)을 옮기거나 지움 — 옮김 = 0.006m 안 새 측점 · 같은 id · * 사라진 것 하나와 새것 하나(옮긴 관 · 213-2) · 없으면 맞춘 자리의 새 측점(234-1 · 272) · 나머지는 지움. */ const carry = (next: IrregularStation[]): void => { const to = profile(); if (!to) return; const nextKeys = new Set(next.map((station) => station.chainage_m.toFixed(3))); const oldKeys = new Set(stations.map((station) => station.chainage_m.toFixed(3))); const gone = stations.filter((station) => !nextKeys.has(station.chainage_m.toFixed(3))); const born = next.filter((station) => !oldKeys.has(station.chainage_m.toFixed(3))); gone.forEach((station) => { const same = followedStation(station, gone, born) ?? followSnapped(station, born); to.resetStationEdit(station.chainage_m, same?.chainage_m); }); }; const marks = (): StructureInstance[] => [...structures, ...pipesToStructureMarks(pipes)]; /** 소단 사본 = 지금 구조물의 소단 구간(467 · 사본 없는 동안 다시 셈 = 저장분 `design.berm`) · 같으면 안 씀 · * 쓰면 저장 노선 cross 한 번 더(같은 값 측점은 다시 셈 기억이 건너뜀 · 묶음은 늦게 불러옴 · 순환 없음). */ function syncBerm(): void { const routeId = pageRouteId(projectId); if (!structuresKnown || routeId === null) return; // 계산 자리 quantity = server 면 잠잠 뒤 서버 값만(511-A) · 아니면 브라우저 셈 if (calcOnServerNow("quantity")) { void calcOnServerOnly("quantity", "C4.copy", bermRequest(structures)).then( (next) => next && putBerm(routeId, next), ); return; } putBerm( routeId, calcChecked("quantity", "cross.berm_spans", bermSpansFromStructures, [structures]), ); } function putBerm(routeId: number, next: BermSpan[]): void { const copy = readState("berm", projectId, routeId); if (Array.isArray(copy) && JSON.stringify(copy) === JSON.stringify(next)) return; writeBermSpans(projectId, routeId, next); void Promise.all([import("./design_pipeline_cross"), import("./design_pipeline_page")]) .then(([cross, page]) => { cross.forceCross(); page.pipelineCross(projectId); }) .catch(() => undefined); } /** 측점 계획 입력이 다 옴 — 모름(관 알림 전 · 구조물 받기 전)을 빈 목록으로 셈하면 종단도가 저장 시설 측점 * (관 등)을 뺀 선으로 횡단을 다시 셈 · 유토 배지가 잠깐 틀림(473 · 요구 272). */ const known = (): boolean => types.length > 0 && reported !== null && structuresKnown; function paint(force = false): void { const to = profile(); if (!to) return; to.setStructureTypes([...types]); to.setStructures(marks()); const signature = JSON.stringify(stations); // 입력이 다 오기 전 = 측점 계획 없음(빈 목록을 먼저 쓰면 종단도가 측점을 지웠다 다시 셈 · 467 · 473) · // 같은 측점이면 안 씀 — 측점을 쓰면 종단도가 계산 묶음을 다시 돌림 if (known() && (force || painted?.to !== to || painted.stations !== signature)) { to.setIrregularStations(stations); painted = { to, stations: signature }; } to.setMinCoverPipes(pipes as MinCoverPipe[]); } /** 다시 셈 — 종류 표를 받기 전이면 측점은 미룸(받으면 다시 맞춤). */ function refresh(carryEdits: boolean, force = false): void { pipes = reported ? shownPipes(reported) : []; if (known()) { const next = plannedIrregular( planStations(withPipeIds(pipes, projectId), structures, types), intervalM, ); if (carryEdits) carry(next); stations = next; } paint(force); listeners.forEach(run); } // 추가 측점(368-2) — 초안 · 저장분이 바뀌면 측점 계획을 다시 맞춤 const stopPlus = store.subscribe("plusStations", () => refresh(true)); return { setTypes(next) { types = next; refresh(false); }, setStored(next) { stored = next; }, set(next, carryEdits = true) { if (next.structures) { structures = next.structures; structuresKnown = true; } if (next.pipes) reported = [...next.pipes]; refresh(carryEdits); if (next.structures) run(syncBerm); }, follow() { const drafted = store.get("structures"); structures = drafted ?? stored ?? []; structuresKnown ||= drafted !== null || stored !== null; refresh(false); run(syncBerm); }, setIntervalM(next) { if (next > 0 && next !== intervalM) { intervalM = next; refresh(false); } }, clearProjected() { reported = []; pipes = []; stations = []; paint(true); listeners.forEach(run); }, paint: () => refresh(false, true), pipes: () => pipes, structures: () => structures, types: () => types, marks, stations: () => stations, ready: known, intervalM: () => intervalM, onChange(fn) { listeners.add(fn); return () => void listeners.delete(fn); }, dispose() { stopPlus(); listeners.clear(); }, }; }