/* ============================================================================= * A00_Common/design/design_pipeline_page_publish.ts * 페이지 계산 묶음 → 가게(stations · profile · footprint · generation) 잇기(`design_pipeline_page` 700줄 쪼갬 · 466). * * 조용한 차례(열 때 · 노선 초안을 버린 차례)는 측점 · 종단을 바로 안 냄(상세 = 저장 정본 · 초안 전 상세 그대로) · * 그 목록을 「보인 측점」으로 적어 뒤 같은 목록 차례도 안 냄(관 · 구조물 메아리가 상세를 갈아끼우지 않게 · 349-0q). * 조용한 차례 끝(열 때 · 버린 차례의 되돌림 뒤) = 가게 측점 · 종단 = 지금 상세가 쥔 측점 · 그 차례 저장 기준 종단 * (466 · 요구 272) — 상세에서 지은 목록이라 갈아끼움은 「같은 목록」(349-0o 그대로) · 가게에 초안 측점이 안 남음. * 화면 선만 바뀜(정본 그대로) = 상세가 그 선 대비 낡았으면 cross 한 번 더(`recrossIfStale` · 466). * 상세 가게는 늦게 불러옴(가게 ↔ 묶음 순환 없음 · Section_Store `recrossSaved` 와 같은 꼴). * ========================================================================== */ import { publishToStore, type DesignPipeline } from "./logic/design_pipeline"; import type { PipelineOutputs, PipelineStation, StationKind } from "./logic/design_pipeline_types"; import type { DesignStore } from "./design_store"; import type { CrossSection } from "../../B06_Section/B06_Section_Api_Fetch"; export interface PagePublisher { /** 조용한 차례 끝(열 때 · 노선 초안을 버린 되돌림 뒤) — 가게 측점 = 지금 상세 측점 · 종단 = 그 차례 종단. * 상세 없음(떠남 · 아직 안 받음) · 뒤 차례가 떴으면 안 냄(그 차례 몫). */ shown(projectId: string, routeId: number): Promise; /** 열고 맞춤까지 끝 — 그 뒤로 낸 측점이 없으면(조용히 엶) `shown`(지난 페이지 초안 측점이 남지 않게). */ opened(projectId: string, since: number, routeId: unknown): Promise; /** 화면 선만 바뀜 — 상세가 그 선 대비 낡았으면 다 돈 뒤 cross 한 번 더. */ recheck(projectId: string, routeId: number): void; } /** 열쇠 머리 → 측점 종류(`grid:` · `pipe:` · `structure:` · `end:` · `curve:`). */ const kindOfKey = (key: string): StationKind => { const head = key.slice(0, key.indexOf(":")); return (["grid", "pipe", "structure", "end", "curve"] as const).find((k) => k === head) ?? "grid"; }; /** 상세 횡단 → 가게 측점 꼴(같은 자리 · 열쇠 묶음 측점이 없을 때 — 자리 · 축은 상세 것 · 지반은 안 실음). */ function fromSection(section: CrossSection, key: string): PipelineStation { const kind = kindOfKey(key); const raw = section as unknown as { center_x?: number; center_y?: number; azimuth_deg?: number; frame?: { left_xy?: [number, number] }; }; return { key, chainageM: section.chainage_m, x: raw.center_x ?? 0, y: raw.center_y ?? 0, azimuthDeg: raw.azimuth_deg ?? 0, left: raw.frame?.left_xy ?? [0, 0], kind, id: kind === "pipe" || kind === "structure" ? key.slice(key.indexOf(":") + 1) : null, ground: null, planRadiusM: null, curveOuterSide: null, curveWideningM: null, }; } /** 묶음 출력을 가게에 냄 — `quietGeneration` = 측점 · 종단을 안 내는 차례 번호(부를 때마다 읽음). */ export function publishPagePipeline( pipe: DesignPipeline, store: DesignStore, quietGeneration: () => number, ): PagePublisher { const publish = publishToStore(store); /** 상세가 맞춘 측점 목록(열쇠 · 자리) — 열 때 · 되돌린 차례 측점 · 마지막으로 낸 측점. 같은 목록이면 가게에 * 다시 안 냄(되돌린 상세 측점이 묶음 측점과 달라도 · 관 · 구조물 메아리가 상세를 갈아끼우지 않게 · 349-0q). */ let stationsShown: string | null = null; /** 마지막으로 가게에 측점을 낸 차례 번호. */ let published = -1; pipe.subscribe((event) => { const saved = event.kind === "stage" && (event.stage === "ground" || event.stage === "profile"); const quiet = saved && event.generation === quietGeneration(); if (event.kind === "stage" && event.stage === "ground") { const list = JSON.stringify( (event.value as PipelineOutputs["ground"] | null)?.stations.map((station) => [ station.key, station.chainageM, station.x, station.y, ]) ?? null, ); if (!quiet && list === stationsShown) return; stationsShown = list; } if (!quiet) publish(event); if (!quiet && event.kind === "stage" && event.stage === "ground") published = event.generation; }, "가게"); const publisher: PagePublisher = { async opened(projectId, since, routeId) { if (typeof routeId !== "number" || published >= since || pipe.input("route")) return; await publisher.shown(projectId, routeId).catch(() => undefined); }, async shown(projectId, routeId) { const generation = pipe.generation; const [{ peekSectionDetail }, { registeredKeys, stationIntervalOf }] = await Promise.all([ import("../../B06_Section/B06_Section_Section_Store"), import("../../B06_Section/B06_Section_Station_Keys"), ]); if (pipe.generation !== generation || pipe.input("route")) return; const detail = peekSectionDetail(projectId, routeId); if (!detail) return; const sections = detail.cross_sections; const keys = registeredKeys(projectId, routeId, sections, stationIntervalOf(detail)); const ground = pipe.output("ground")?.stations ?? []; const stations = sections.map((section, i): PipelineStation => { const hit = ground.find( (station) => station.key === keys[i] && Math.abs(station.chainageM - section.chainage_m) < 1e-3, ); return hit ? { ...hit, chainageM: section.chainage_m } : fromSection(section, keys[i]); }); store.set("stations", stations); store.set("profile", pipe.output("profile")); }, recheck(projectId, routeId) { void import("../../B06_Section/B06_Section_Section_Store") .then((section) => section.recrossIfStale(projectId, routeId)) .catch(() => undefined); }, }; return publisher; }