Files
Aislo/A00_Common/design/design_pipeline_page_publish.ts
T
eomsangdonandClaude Opus 5.5 6971c42c1e fix(466 상세 가게 · 묶음 출력): 페이지를 떠나면 상세 메모리 비움 · 조용한 차례도 가게 측점 = 상세 측점
- 상세 가게 forgetSectionMemory — 페이지 dispose 에서 메모리 · 받는 중 약속만 비움(세션 정본 그대로) · 다시 열기 = 새로고침 길
- 묶음 열 때 관 · 구조물 · 추가 측점 초안이 있으면 측점을 냄 — 새로고침 · 다시 열기 = 초안 측점(옛: 새로고침 뒤 관 초안 측점 빠짐)
- 버린 노선 초안 차례 끝 · 조용히 연 차례 끝 = 가게 측점 · 종단 = 지금 상세 측점 · 저장 기준(갈아끼움 없음)
- 화면 선만 바뀜(비정규 측점 도착) = 상세가 그 선 대비 낡았으면 cross 한 번 더(recrossIfStale)
- 시험 test_272_detail_memory · test_272_drop_store_stations

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 07:27:40 +09:00

125 lines
6.3 KiB
TypeScript

/* =============================================================================
* 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<void>;
/** 열고 맞춤까지 끝 — 그 뒤로 낸 측점이 없으면(조용히 엶) `shown`(지난 페이지 초안 측점이 남지 않게). */
opened(projectId: string, since: number, routeId: unknown): Promise<void>;
/** 화면 선만 바뀜 — 상세가 그 선 대비 낡았으면 다 돈 뒤 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;
}