feat(설계묶음): 344-4 계산 묶음 계약 — 단계 입력 · 출력 · 측점 · 노선 초안 모양 · route-draft 캐시 칸 · 결과 칸 셋

- design_pipeline_types.ts 새 — 단계 차례 · 주인 · deps · 키별 모양(측점 · 지반 · 노선 초안 · 발자국)
- b_page_state 등록표 — route-draft(draft · project) · design-stations · design-profile · design-footprint(result)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TteK8mfYZetk5P3bEZkyVZ
This commit is contained in:
2026-10-16 10:59:09 +09:00
co-authored by Claude Opus 5.5
parent 2879f9bea9
commit 5e8df73763
2 changed files with 258 additions and 0 deletions
+7
View File
@@ -208,6 +208,9 @@ export const STATE_REGISTRY = {
scope: "route",
legacy: (_p, r) => `b05-profile-alignment-draft:${r}`,
},
/** 노선 초안(344-4) — 노드 · 곡선 · 반경 · 스냅 · 지형 · rev(`RouteDraft` · `design_pipeline_types`).
* 노선 번호는 [저장]마다 새로 나므로 project 범위 · 가게 키 `routeDraft`. */
"route-draft": { bucket: "draft", scope: "project" },
/* ── ④ 계산 결과 ─────────────────────────────────────────────────────── */
/** 노선·종단 최신 응답. 페이지를 오갈 때 이 값으로 먼저 그린다. */
@@ -235,6 +238,10 @@ export const STATE_REGISTRY = {
/** B05 계획노선 편집 창 — 마지막에 적용한 초기노선 갈래(로직 · 비중 · 노드 · 비교값).
* 노선을 다시 계산해 결과를 버릴 때도 이 값은 새 노선의 출발점이라 되살린다(`_Apply`). */
"routeedit-initial": { bucket: "result", scope: "project" },
/** 계산 묶음 결과(344-4) — 측점 · 종단 · 평면 발자국 · 가게가 메모리 우선 · 멈춘 뒤 한 번 씀. */
"design-stations": { bucket: "result", scope: "project" },
"design-profile": { bucket: "result", scope: "project" },
"design-footprint": { bucket: "result", scope: "project" },
} as const satisfies Record<string, StateEntry>;
export type StateName = keyof typeof STATE_REGISTRY;
+251
View File
@@ -0,0 +1,251 @@
/* =============================================================================
* A00_Common/design/design_pipeline_types.ts
* 실시간 계산 묶음 **계약**(PLAN 344-4) — 키별 모양 · 단계 입력 · 출력. 다른 창(sub1 · sub2 ·
* sub3 · sub6 · sub8)은 이 모양대로 단계 함수를 만들어 `createDesignPipeline({ stages })` 에 넣음.
* 계산 묶음 본체 = `design_pipeline.ts`.
*
* 흐름 — 한 방향:
* 입력 route(노선 초안) · alignment(계획고) · cross(횡단 입력) · pipes(관) · structures(구조물)
* ↓ update(출처, 값, 범위) — 조작마다 차례 번호(generation) +1 · 옛 계산 · 옛 요청 버림
* 브라우저 즉시(차례대로 · 단계 사이마다 번호 확인 · 단계마다 결과 바로 내보냄)
* route → follow → stations → ground → planline → profile → cross → footprint → masshaul
* 뒤(기다리지 않음 · 새 조작이 오면 AbortSignal 로 끊김 · 옛 번호 결과 버림)
* haul(흙 배분 · 서버 Node) · drainage(배수유역 「낡음」 표시) · corridor(3D 「낡음」 표시)
*
* 규칙:
* · 단계는 자기 출력만 돌려줌 — 입력 · 다른 단계 출력을 고치지 않음(ctx 의 값은 읽기만).
* · 예외 하나 = follow — 노선이 바뀐 차례에만 · pipes · structures · alignment 를 한 번 고쳐
* 돌려줌(관 · 구조물 좌표 따라감 · 계획고 옮김). 묶음이 입력에 넣고 다음 단계로 감.
* · 단계 함수가 없으면(아직 안 붙음) 건너뜀 — 출력은 앞 값 그대로(처음엔 null).
* · 기대는 값(deps)의 판이 그대로면 다시 안 셈(같은 입력 다시 안 셈).
* · range = 마지막으로 이 단계가 끝난 뒤 쌓인 바뀐 범위 전부 — 범위만 셈 · 범위 밖은 ctx.prev 재사용.
*
* 단계 · 주인 · 입력(deps) → 출력:
* | 단계 | 주인 | deps | 출력 |
* | route | sub8 | input.route | RouteLine |
* | follow | sub1 | route | FollowPatch(입력 고침) |
* | stations | sub1·sub2| route · input.pipes · input.structures | PipelineStation[](지반 null) |
* | ground | sub8 | stations | GroundOutput |
* | planline | sub1 | ground | AlignmentBase(첫 계획선) |
* | profile | sub1 | planline · input.alignment | ProfileAlignment |
* | cross | sub2 | profile · ground · input.cross · pipes · structures | CrossOutput |
* | footprint | sub6 | cross · ground | PlanFootprint |
* | masshaul | sub2 | cross | unknown(sub2 모양) |
* | haul | sub2 | masshaul | unknown(서버 응답) |
* | drainage | sub3 | route · input.pipes | 「낡음」 표시(출력 안 씀) |
* | corridor | sub5 | profile · cross | 「낡음」 표시(출력 안 씀) |
*
* 가게 키(`design_store.ts`) — routeDraft(입력 · 세션 즉시) · stations(ground 단계 출력) ·
* profile · footprint(메모리 우선 · 멈춘 뒤 한 번 세션) · generation(메모리만).
* ========================================================================== */
import type {
AlignmentBase,
AlignmentEdits,
ProfileAlignment,
} from "../../B05_Profile/B05_Profile_UI_Profile_Alignment";
/** 평면 좌표(m) — 도면 좌표계 x · y. */
export type Vertex = [number, number];
/* ── 입력 ───────────────────────────────────────────────────────────────── */
/** 노선 초안 — 캐시 칸 `route-draft`(draft · project 범위) · [저장] 때 서버로 · 노선 편집기가 씀. */
export interface RouteDraft {
/** 놓을 때마다 +1 — 같은 초안인지 보는 판 번호. */
rev: number;
/** 노드(시점 · 교각점 · 종점) — 사용자가 끄는 꺾임점. 원호는 route 단계가 만듦. */
nodes: Vertex[];
/** 꺾임점마다 곡선 둠 · 못박은 반경(m · null = 기본) · 붙든 값 · 붙든 곡선 길이(m). 길이 = nodes. */
curveOn: boolean[];
curveRadius: Array<number | null>;
curveLock: Array<"radius" | "arc" | null>;
curveArc: Array<number | null>;
/** [측점 스냅](요구 224-2) — 켜면 호 안 측점 하나를 곡선 중점으로. */
stationSnap: boolean;
/** 분석 지형(`las` · `sheet` · null = 확정 지표면) — ground 단계가 고를 격자. */
terrain: string | null;
}
/** 묶음이 받는 입력 다섯. 없으면 null(= 저장분 그대로). cross · pipes · structures 모양은 주인 몫. */
export interface PipelineInputs {
route: RouteDraft | null;
alignment: AlignmentEdits | null;
/** 횡단 입력(crossdesign · cutslope · berm · std-cross 등 초안 묶음) — 모양 = sub2. */
cross: unknown;
/** 관 목록(`DetailPipeInput[]`) — 좌표가 정본 · 누가거리는 stations 가 다시 매김. */
pipes: unknown;
/** 구조물 목록(`StructureInstance[]`) — 위와 같음. */
structures: unknown;
}
export type InputKey = keyof PipelineInputs;
/** 조작 출처 — 노선 · 계획고 · 횡단 · 관 · 구조물. */
export type ChangeSource = "route" | "grade" | "cross" | "pipe" | "structure";
/** 출처 → 입력 칸. */
export const SOURCE_INPUT: Record<ChangeSource, InputKey> = {
route: "route",
grade: "alignment",
cross: "cross",
pipe: "pipes",
structure: "structures",
};
/**
* 바뀐 범위(누가거리 m) — 부르는 쪽이 정함:
* 노선 = 처음 바뀐 접선점부터 끝(toM = Infinity) · 계획고 = 이웃 꼭짓점 사이 · 횡단 = 그 측점(fromM = toM).
* 여러 조작이 쌓이면 합침(가장 앞 ~ 가장 뒤 · keys 는 모음). null = 전 구간.
*/
export interface ChangeRange {
sources: ChangeSource[];
fromM: number;
toM: number;
/** 측점 키로 정한 범위(횡단 조작) — 비면 거리로만. */
keys: string[];
}
/** update 가 받는 범위 한 조각 — sources 는 출처로 채움. */
export interface RangePart {
fromM: number;
toM: number;
keys?: string[];
}
/* ── 출력 ───────────────────────────────────────────────────────────────── */
/** route 단계 — 원호가 섞인 그린 노선(`_RouteEdit_Curve` 짝) · 누가거리. */
export interface RouteLine {
line: Vertex[];
/** line 정점마다 누가거리(m). */
chainage: number[];
lengthM: number;
}
/** follow 단계 — 노선이 바뀐 차례에 한 번만 입력을 고침. 안 고치는 칸은 비움. */
export interface FollowPatch {
pipes?: unknown;
structures?: unknown;
alignment?: AlignmentEdits;
}
/** 측점 종류 — 격자(간격) · 구조물 · 관 · 끝(시점 · 종점). */
export type StationKind = "grid" | "structure" | "pipe" | "end";
/** 횡단 지반 표본 한 점 — 중심에서 거리(m · 좌 +) · 높이(null = 빈칸). */
export interface GroundSample {
offsetM: number;
z: number | null;
}
export interface StationGround {
centerZ: number | null;
/** 중심 → 바깥 차례 · offsetM 은 둘 다 양수. */
left: GroundSample[];
right: GroundSample[];
}
/** 측점 한 자리 — stations 가 만들고 ground 가 지반을 채움. 가게 키 `stations`. */
export interface PipelineStation {
/** 측점 키 — 규칙 = 344-3(sub2 · 0.01 m · 격자 = 격자 번호 · 구조물 · 관 = id). */
key: string;
chainageM: number;
x: number;
y: number;
/** 진행 방향(도 · 북 0 시계) · 좌측 단위 벡터(`_RouteEdit_Frame` 짝). */
azimuthDeg: number;
left: Vertex;
kind: StationKind;
/** 구조물 · 관 id — 격자 · 끝은 null. */
id: string | null;
/** ground 단계 전엔 null. */
ground: StationGround | null;
/** 평면 곡선반경(m · 직선 null) · 확폭이 붙는 바깥쪽 · 확폭량(m). */
planRadiusM: number | null;
curveOuterSide: "left" | "right" | null;
curveWideningM: number | null;
}
/** ground 단계 — 지반 채운 측점 · 종단 지반선(1 m). */
export interface GroundOutput {
stations: PipelineStation[];
longitudinal: { chainageM: number[]; z: Array<number | null> };
}
/** cross 단계 — 측점 키 → 그 측점 결과(면적은 유토곡선 재료 · detail 모양 = sub2). */
export interface CrossStationResult {
key: string;
chainageM: number;
cutAreaM2: number;
fillAreaM2: number;
detail: unknown;
}
export interface CrossOutput {
byKey: Record<string, CrossStationResult>;
}
/** footprint 단계 — 평면 발자국(도로 끝 · 비탈 끝) · 시점 → 종점 차례 · 가게 키 `footprint`. */
export interface PlanFootprint {
roadLeft: Vertex[];
roadRight: Vertex[];
slopeLeft: Vertex[];
slopeRight: Vertex[];
}
/** 단계 이름 → 출력. drainage · corridor 는 표시만 하므로 출력이 없음(null). */
export interface PipelineOutputs {
route: RouteLine;
follow: FollowPatch;
stations: PipelineStation[];
ground: GroundOutput;
planline: AlignmentBase;
profile: ProfileAlignment;
cross: CrossOutput;
footprint: PlanFootprint;
masshaul: unknown;
haul: unknown;
drainage: null;
corridor: null;
}
export type StageName = keyof PipelineOutputs;
/* ── 단계 함수 ──────────────────────────────────────────────────────────── */
export interface StageContext<K extends StageName> {
/** 이 계산의 차례 번호 — 다르면 이미 버려진 계산. */
generation: number;
/** 마지막으로 이 단계가 끝난 뒤 쌓인 바뀐 범위(null = 전 구간 · 처음). */
range: ChangeRange | null;
/** 새 조작이 오면 끊김 — fetch 에 그대로 넘김. */
signal: AbortSignal;
/** 입력(읽기만). */
inputs: Readonly<PipelineInputs>;
/** 앞 단계 출력(이번 계산 것 · 없으면 null · 읽기만). */
out: Readonly<{ [S in StageName]: PipelineOutputs[S] | null }>;
/** 이 단계 앞 출력 — 범위 밖을 재사용. */
prev: PipelineOutputs[K] | null;
}
export type StageFn<K extends StageName> = (
ctx: StageContext<K>,
) => PipelineOutputs[K] | Promise<PipelineOutputs[K]>;
export type PipelineStages = { [K in StageName]?: StageFn<K> };
/* ── 시간 기록(window.__aisloPipeline · 개발용) ─────────────────────────── */
export interface PipelineRunRecord {
generation: number;
sources: ChangeSource[];
/** 단계 → ms(뒤 단계는 끝난 때 채움 · 건너뜀은 안 적음). */
stages: Partial<Record<StageName, number>>;
/** 건너뛴 단계(같은 입력 · 함수 없음). */
skipped: StageName[];
/** 브라우저 단계 합 ms. */
totalMs: number;
/** done = 브라우저 단계 끝 · dropped = 새 조작에 밀려 버림 · error = 단계가 던짐. */
outcome: "running" | "done" | "dropped" | "error";
}