feat(365-2): 횡단 고침 → 묶음 cross 입력 · 입력 지문 고리(최대 3바퀴)
- 횡단 입력 = 부를 때마다 저장 상세 + 세션 고침(지반 유형 · 측구 · 2단 경사 · 암 경계 · 암 경사 · 측점 열쇠) + 표준 — 상세 객체마다 기억 걷음 - B06 고침(DesignSync 다시 셈 끝 · 암 경계) 뒤 pipelineCross — 노선 초안 동안 B06 고침이 카드(새 노선 상세)에 보임 - 고리 — 열 때 · 페이지 들어올 때 입력(세션 · 가게)을 묶음 입력(넘긴 값 · follow 뒤 값)과 견줌 · 다르면 한 바퀴 · 끝난 뒤 다시 · 3바퀴 넘으면 「계산 불안정」 한 줄 - 시험 test_365_page_pipeline(초안 동안 고침 카드 · 같은 입력 0바퀴 · 세션만 바뀜 1바퀴 · 불안정 3바퀴 알림 1) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TteK8mfYZetk5P3bEZkyVZ
This commit is contained in:
@@ -32,6 +32,7 @@ import { saveSharedDesign, type DesignPageShared } from "./design_shared";
|
||||
import { guardDesignLeave } from "./design_leave";
|
||||
import { usePipeIds } from "./design_pipe_ids";
|
||||
import { setRouteShiftOpener } from "../../B06_Section/B06_Section_UI_Cross_Center";
|
||||
import { reconcilePagePipeline } from "./design_pipeline_page";
|
||||
import "./design_page.css";
|
||||
|
||||
export type { DesignPageShared } from "./design_shared";
|
||||
@@ -244,6 +245,7 @@ export function renderDesignPage(root: HTMLElement): MountedPage {
|
||||
if (slot) {
|
||||
slot.show();
|
||||
applyView(view);
|
||||
void reconcilePagePipeline(projectId); // 들어올 때 입력(세션 · 가게) 대 묶음 맞춤(365-2)
|
||||
} else mountView(view, true);
|
||||
// 검증용 — 보기 전환에 든 시간(ms)과 횟수를 화면 밖에서 수치로 본다.
|
||||
const probe = window as unknown as { __designPage?: { view: string; switchMs: number[] } };
|
||||
|
||||
@@ -8,9 +8,11 @@
|
||||
* 넘기는 칸 = 서버 노선 저장(`CARRY_INPUT_KEYS`)과 같은 일곱(지반 유형 · 측구 종류 · 측구 선택 · 2단 경사 ·
|
||||
* 암 경계 · 암 경사 · 소단) + 얹힌 시설 맥락(포장 · 물넘이 기울임 · 세월교 노면 하강). 단면 유형 · 측구
|
||||
* 방향은 새 지형이 정함(`logic/B05_Profile_Pipeline_Detail.ts` `crossWithUphill`) · 노견 확장 · 소단
|
||||
* 깊이는 안 넘김. 값 = 상세에 얹힌 설계(세션 고침은 종단도 · 횡단 보기가 다시 셈하며 이미 얹음).
|
||||
* 깊이는 안 넘김. 값 = 상세에 얹힌 설계 위에 세션 고침(측점 열쇠 칸 · 지반 유형 · 측구 · 2단 경사 · 암 경계 ·
|
||||
* 암 경사) — 노선 초안 동안 B06 고침은 저장 상세에 안 얹혀도 입력에 들어감(365-2). 부를 때마다 모음.
|
||||
* ========================================================================== */
|
||||
|
||||
import { readState } from "../b_page_state";
|
||||
import type { CrossSection, SectionDetailResponse } from "../../B06_Section/B06_Section_Api_Fetch";
|
||||
import {
|
||||
effectiveStandardCross,
|
||||
@@ -71,11 +73,45 @@ function carried(section: CrossSection): Record<string, unknown> {
|
||||
};
|
||||
}
|
||||
|
||||
/** 저장 상세 + 저장 노선 측점(열쇠) → cross 입력 · 시설 제원 — 표준단면 · 상세가 없으면 null. */
|
||||
/** 세션 칸 하나를 측점 열쇠마다 — 옛 열쇠(누가거리 · `at:`)는 읽는 쪽이 옮긴 뒤에 들어옴(빼고 봄). */
|
||||
function sessionCells(
|
||||
name: "crossdesign" | "rockb" | "cutslope",
|
||||
projectId: string,
|
||||
routeId: number,
|
||||
): Array<[string, unknown]> {
|
||||
const raw = readState<Record<string, unknown>>(name, projectId, routeId);
|
||||
if (!raw || typeof raw !== "object") return [];
|
||||
return Object.entries(raw).filter(([key]) => key.includes(":") && !key.startsWith("at:"));
|
||||
}
|
||||
|
||||
/** 세션 횡단 고침 → 측점 열쇠마다 넘기는 입력 조각(`carried` 와 같은 칸 이름). */
|
||||
function sessionCarried(projectId: string, routeId: number): CrossInput["byKey"] {
|
||||
const out: CrossInput["byKey"] = {};
|
||||
for (const [key, value] of sessionCells("crossdesign", projectId, routeId)) {
|
||||
const choice = (value ?? {}) as Record<string, unknown>;
|
||||
const entry = (out[key] ??= {});
|
||||
if (typeof choice.ground_type === "string") entry.groundType = choice.ground_type;
|
||||
if (typeof choice.ditch_type === "string") entry.ditchType = choice.ditch_type;
|
||||
if ("ditch_choice" in choice)
|
||||
entry.ditchChoice = typeof choice.ditch_choice === "boolean" ? choice.ditch_choice : null;
|
||||
if ("two_stage_slope" in choice) entry.twoStageSlope = Boolean(choice.two_stage_slope);
|
||||
}
|
||||
for (const [key, value] of sessionCells("rockb", projectId, routeId))
|
||||
if (finite(value) !== null) (out[key] ??= {}).rockBoundaryOffsetM = value;
|
||||
for (const [key, value] of sessionCells("cutslope", projectId, routeId)) {
|
||||
const ratio = finite(value);
|
||||
if (ratio !== null) (out[key] ??= {}).cutSlopeRatio = ratio > 0 ? ratio : null;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 저장 상세 + 저장 노선 측점(열쇠) + 세션 고침(`routeId` 칸) → cross 입력 · 시설 제원 — 표준단면 ·
|
||||
* 상세가 없으면 null. */
|
||||
export function pipelineCrossSource(
|
||||
projectId: string,
|
||||
detail: SectionDetailResponse | null,
|
||||
savedStations: ReadonlyArray<PipelineStation>,
|
||||
routeId: number | null = null,
|
||||
): PipelineCrossSource | null {
|
||||
const standard = detail && effectiveStandardCross(projectId);
|
||||
if (!detail || !standard) return null;
|
||||
@@ -94,6 +130,9 @@ export function pipelineCrossSource(
|
||||
for (const name of SPEC_KEYS) if (section[name] !== undefined) own[name] = section[name];
|
||||
specs.set(best.key, own as SectionSpecs);
|
||||
}
|
||||
if (routeId !== null)
|
||||
for (const [key, entry] of Object.entries(sessionCarried(projectId, routeId)))
|
||||
byKey[key] = { ...byKey[key], ...entry };
|
||||
return {
|
||||
// 암 경계 기본값 = 서버 config(화면이 문맥에서 기억) — 입력 없는 측점(초안 새 측점)도 서버처럼(349-0p)
|
||||
input: { standard, byKey, rockBoundaryDefaultM: readRockBoundaryDefault(projectId) },
|
||||
|
||||
@@ -12,7 +12,10 @@
|
||||
*
|
||||
* 입력 — 노선 = 편집기 다리(`RouteDraftBridge` · 캐시 칸 route-draft) · 계획고 = 종단도 편집
|
||||
* (`pipelineGrade` · 노선 초안 동안은 alignment-draft 노선 `DRAFT_ROUTE_ID`) · 횡단 = 저장 상세의 측점
|
||||
* 입력(`design_pipeline_cross.ts`) · 관 = 가게 초안 → 저장 관 목록(좌표 · id) · 구조물 = 가게 초안 → 저장분.
|
||||
* 입력 + 세션 고침(`design_pipeline_cross.ts` · 부를 때마다 모음 · 횡단 고침 뒤 `pipelineCross` · 365-2) ·
|
||||
* 관 = 가게 초안 → 저장 관 목록(좌표 · id) · 구조물 = 가게 초안 → 저장분.
|
||||
* 고리(365-2) — 열 때 · 페이지 들어올 때(`reconcilePagePipeline`) 입력(세션 · 가게)을 묶음이 쥔 입력과 견줌 →
|
||||
* 다르면 한 바퀴 → 끝난 뒤 다시 견줌 · 최대 `RECONCILE_ROUNDS` · 넘으면 `onUnstable`(「계산 불안정」 한 줄).
|
||||
* 출력 — 가게 stations · profile · footprint · generation · 노선 초안이 있으면 종단도가 새 첫 계획선 ·
|
||||
* 옮긴 계획고 · 새 노선 상세(`pipelineDetail` — 지반 · 측점 · 횡단 면적 → 유토곡선)를 그림.
|
||||
* 첫 계산 = 열 때 한 번(저장 노선 · 발자국 · 저장 안 한 노선 초안이 있으면 그 노선).
|
||||
@@ -22,15 +25,18 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { createDesignPipeline, publishToStore, type DesignPipeline } from "./logic/design_pipeline";
|
||||
import { SOURCE_INPUT } from "./logic/design_pipeline_types";
|
||||
export { pipelineWork } from "./logic/design_pipeline";
|
||||
import { pipelineCrossSource, type PipelineCrossSource } from "./design_pipeline_cross";
|
||||
import { createStationsStage } from "./logic/design_pipeline_stations";
|
||||
import type {
|
||||
ChangeSource,
|
||||
FollowPatch,
|
||||
PipelineInputs,
|
||||
PipelineOutputs,
|
||||
PipelineRunRecord,
|
||||
PipelineStation,
|
||||
RangePart,
|
||||
RouteDraft,
|
||||
StageFn,
|
||||
Vertex,
|
||||
@@ -102,6 +108,8 @@ import type {
|
||||
|
||||
/** 노선 초안 동안의 계획고 초안 칸 — alignment-draft 의 노선 0(노선 번호는 [저장] 때 남). */
|
||||
export const DRAFT_ROUTE_ID = 0;
|
||||
/** 입력 ↔ 묶음 입력 맞춤 상한(바퀴) — 넘으면 「계산 불안정」(365-2). */
|
||||
export const RECONCILE_ROUNDS = 3;
|
||||
|
||||
/** 종단도(`design_profile`)가 묶음에 내주는 것. */
|
||||
export interface ProfileLink {
|
||||
@@ -137,6 +145,8 @@ export interface PagePipelineOptions {
|
||||
routeId: number,
|
||||
saved: SectionDetailResponse | null,
|
||||
) => (stations: PipelineStation[]) => Promise<string>;
|
||||
/** 고리가 `RECONCILE_ROUNDS` 바퀴 안에 안 맞음 — 「계산 불안정」 한 줄(365-2). */
|
||||
onUnstable?: () => void;
|
||||
}
|
||||
|
||||
interface PagePipeline {
|
||||
@@ -145,6 +155,8 @@ interface PagePipeline {
|
||||
grade(edits: AlignmentEdits): void;
|
||||
restart(): void;
|
||||
earthwork(context: MassHaulStageContext | null): void;
|
||||
cross(): void;
|
||||
reconcile(): Promise<number>;
|
||||
detail(): SectionDetailResponse | null;
|
||||
dispose(): void;
|
||||
}
|
||||
@@ -193,6 +205,13 @@ export const restartPagePipeline = (projectId: string): void => pages.get(projec
|
||||
export const pipelineEarthwork = (projectId: string, context: MassHaulStageContext | null): void =>
|
||||
pages.get(projectId)?.earthwork(context);
|
||||
|
||||
/** 횡단 고침(지반 유형 · 측구 · 암 · 소단 · 표준 — B06 · 365-2) — 세션 횡단 입력을 다시 모아 cross 부터. */
|
||||
export const pipelineCross = (projectId: string): void => pages.get(projectId)?.cross();
|
||||
|
||||
/** 입력(세션 · 가게) 대 묶음 입력을 견줘 맞춤(365-2) — 돈 바퀴 수(0 = 같음 · -1 = 넘음 · 묶음 없음 = 0). */
|
||||
export const reconcilePagePipeline = (projectId: string): Promise<number> =>
|
||||
pages.get(projectId)?.reconcile() ?? Promise.resolve(0);
|
||||
|
||||
/** 노선 초안 동안 종단도가 그릴 새 노선 상세 — 초안이 없으면 null(저장 상세). */
|
||||
export const pipelineDetail = (projectId: string): SectionDetailResponse | null =>
|
||||
pages.get(projectId)?.detail() ?? null;
|
||||
@@ -219,10 +238,9 @@ function createPagePipeline(
|
||||
let savedStructures: PlanStructure[] = [];
|
||||
let savedPipes: PlanPipe[] = [];
|
||||
let earthwork: MassHaulStageContext | null = null;
|
||||
/** 저장 노선 측점(열쇠) · 그것으로 모은 횡단 입력 — 저장 상세가 바뀌면 다시. */
|
||||
/** 저장 노선 측점(열쇠) · 마지막으로 모은 횡단 입력(시설 제원 — 새 노선 상세가 씀). */
|
||||
let savedStations: PipelineStation[] = [];
|
||||
let crossSource: { detail: SectionDetailResponse; source: PipelineCrossSource | null } | null =
|
||||
null;
|
||||
let crossSource: PipelineCrossSource | null = null;
|
||||
/** 저장 정본(서버가 준 상세 · 노선 번호) — 측점 갈아끼움(swap)이 만든 새 상세는 지반 표본 배열을
|
||||
* 그대로 둠 → 정본 아님(묶음 출력 → 상세 → 다시 묶음 고리를 끊음). */
|
||||
let canon: { routeId: unknown; samples: unknown; detail: SectionDetailResponse | null } | null =
|
||||
@@ -286,12 +304,17 @@ function createPagePipeline(
|
||||
emptyEdits()
|
||||
);
|
||||
};
|
||||
/** 횡단 입력 — 부를 때마다 저장 상세 + 세션 고침 · 표준을 모음(상세 객체마다 기억하면 고침이 안 감 · 365-2). */
|
||||
const crossNow = (): PipelineCrossSource | null => {
|
||||
const detail = canon?.detail ?? null;
|
||||
if (!detail) return null;
|
||||
if (crossSource?.detail !== detail || !crossSource.source)
|
||||
crossSource = { detail, source: pipelineCrossSource(projectId, detail, savedStations) };
|
||||
return crossSource.source;
|
||||
const routeId = canon?.routeId;
|
||||
const source = pipelineCrossSource(
|
||||
projectId,
|
||||
canon?.detail ?? null,
|
||||
savedStations,
|
||||
typeof routeId === "number" ? routeId : null,
|
||||
);
|
||||
if (source) crossSource = source;
|
||||
return source;
|
||||
};
|
||||
const groundDeps = groundStageDeps(projectId);
|
||||
const crossStage = createCrossStage({ grid: groundDeps.grid }); // 미교차 넓힘 = 같은 지형 격자(349-0j)
|
||||
@@ -460,7 +483,7 @@ function createPagePipeline(
|
||||
ground,
|
||||
profile,
|
||||
pipe.output("cross"),
|
||||
crossSource?.source?.specs ?? new Map(),
|
||||
crossSource?.specs ?? new Map(),
|
||||
options.stationIntervalM ?? profile.policy.station_interval_m,
|
||||
)
|
||||
: null;
|
||||
@@ -517,11 +540,48 @@ function createPagePipeline(
|
||||
// 초안 둔 채 열기(새로고침 · B06 바로 열기 · 365-1) — 초안 전 = 지금 저장 상세(세션 그대로) · 측점은 가게에 냄
|
||||
held = inputs.route ? (held ?? holdNow()) : null;
|
||||
dropping = null;
|
||||
void pipe.start(inputs);
|
||||
Object.assign(given, inputs);
|
||||
const run = (lastRun = pipe.start(inputs));
|
||||
openGeneration = pipe.generation;
|
||||
quietGeneration = inputs.route ? -1 : pipe.generation;
|
||||
void run.then(() => reconcile()); // 열 때 셈하는 사이 바뀐 입력(세션 · 가게)도 맞춤
|
||||
};
|
||||
|
||||
/** 묶음에 넘긴 입력(follow 가 고치기 전) · 마지막으로 띄운 차례 — 고리가 견줌 · 기다림. */
|
||||
const given: Partial<PipelineInputs> = {};
|
||||
let lastRun: Promise<unknown> = Promise.resolve();
|
||||
const feed = (source: ChangeSource, value: unknown, part: RangePart | null = null) => {
|
||||
given[SOURCE_INPUT[source]] = value as never;
|
||||
const run = pipe.update(source, value, part);
|
||||
if (run) lastRun = run;
|
||||
return run;
|
||||
};
|
||||
let reconciling: Promise<number> | null = null;
|
||||
/** 입력 지문 대 묶음 입력 — 넘긴 값 · follow 뒤 값 둘 다와 다르면 한 바퀴(노선 = 다리 길) · 끝난 뒤 다시 견줌. */
|
||||
function reconcile(): Promise<number> {
|
||||
if (!started || disposed) return Promise.resolve(0);
|
||||
return (reconciling ??= (async () => {
|
||||
for (let round = 0; ; round += 1) {
|
||||
let seen: Promise<unknown> | null = null;
|
||||
while (seen !== lastRun) await (seen = lastRun); // 기다리는 사이 뜬 차례까지
|
||||
await pipe.settled();
|
||||
if (disposed || canon?.routeId !== savedSource().routeId) return round; // 정본 바뀜 = restart 몫
|
||||
const now = inputsNow();
|
||||
const stale = (Object.entries(SOURCE_INPUT) as Array<[ChangeSource, keyof PipelineInputs]>)
|
||||
.filter(([, key]) => !same(now[key], pipe.input(key)) && !same(now[key], given[key]))
|
||||
.map(([source, key]): [ChangeSource, unknown] => [source, now[key]]);
|
||||
if (!stale.length) return round;
|
||||
if (round >= RECONCILE_ROUNDS) {
|
||||
options.onUnstable?.();
|
||||
return -1;
|
||||
}
|
||||
for (const [source, value] of stale)
|
||||
if (source === "route") bridge.edited(value as RouteDraft | null);
|
||||
else feed(source, value);
|
||||
}
|
||||
})().finally(() => (reconciling = null)));
|
||||
}
|
||||
|
||||
/** 저장 노선(그린 선 · 곡선표 · 기본 반경) · 그 측점 열쇠 — [저장] 뒤 다시 받음(`restart`). */
|
||||
const loadPlan = (): Promise<void> =>
|
||||
fetchRoutePlan(projectId)
|
||||
@@ -555,13 +615,10 @@ function createPagePipeline(
|
||||
.catch(() => undefined);
|
||||
|
||||
const unsubscribe = [
|
||||
store.subscribe(
|
||||
"pipes",
|
||||
(pipes) => started && !echo && void pipe.update("pipe", pipes ?? savedPipes),
|
||||
),
|
||||
store.subscribe("pipes", (pipes) => started && !echo && void feed("pipe", pipes ?? savedPipes)),
|
||||
store.subscribe(
|
||||
"structures",
|
||||
(list) => started && !echo && void pipe.update("structure", list ?? savedStructures),
|
||||
(list) => started && !echo && void feed("structure", list ?? savedStructures),
|
||||
),
|
||||
];
|
||||
|
||||
@@ -582,7 +639,7 @@ function createPagePipeline(
|
||||
if (!started) return start();
|
||||
const before = pipe.output("route")?.line ?? null;
|
||||
const after = draft ? draftLine(draft, plan?.min_radius_m ?? 0).line : savedLine();
|
||||
const run = pipe.update("route", draft, routeRange(before, after));
|
||||
const run = feed("route", draft, routeRange(before, after));
|
||||
if (run && !draft && wasDraft) {
|
||||
quietGeneration = pipe.generation;
|
||||
dropping = run.then(finishDrop);
|
||||
@@ -598,7 +655,7 @@ function createPagePipeline(
|
||||
if (same(edits, pipe.input("alignment"))) return;
|
||||
const nodes = pipe.output("planline")?.basePvi.map((node) => node.chainage_m) ?? [];
|
||||
const before = pipe.input("alignment") as AlignmentEdits | null;
|
||||
void pipe.update("grade", edits, gradeRange(before, edits, nodes));
|
||||
void feed("grade", edits, gradeRange(before, edits, nodes));
|
||||
},
|
||||
restart() {
|
||||
if (!started) return start();
|
||||
@@ -607,8 +664,13 @@ function createPagePipeline(
|
||||
earthwork(context) {
|
||||
earthwork = context;
|
||||
const cross = crossNow()?.input ?? null;
|
||||
if (started && cross) void pipe.update("cross", cross);
|
||||
if (started && cross) void feed("cross", cross);
|
||||
},
|
||||
cross() {
|
||||
const cross = started ? (crossNow()?.input ?? null) : null;
|
||||
if (cross) void feed("cross", cross);
|
||||
},
|
||||
reconcile,
|
||||
detail: () => shown,
|
||||
dispose() {
|
||||
disposed = true;
|
||||
|
||||
@@ -209,6 +209,7 @@ function startPagePipeline(shared: DesignPageShared, profile: DesignProfile): vo
|
||||
stationIntervalM: SECTION_STATION_INTERVAL_M,
|
||||
onCorridorStale: () => each((hooks) => hooks.onCorridorStale?.()),
|
||||
onFacilitiesFollowed: (far) => each((hooks) => hooks.onFacilitiesFollowed?.(far)),
|
||||
onUnstable: () => showToast(L("Design_Pipeline_Unstable"), "warning"),
|
||||
structureAreas: ({ cross_sections: rows }) =>
|
||||
applyStructureAreaRows(rows, structureAreaRows(rows)),
|
||||
// 초안 시작 — 초안 전 상태를 떠 둠 · 버리면 그대로(측점이 달라도 · 349-0o) · 상세가 없으면 묶음 측점으로 갈아끼움
|
||||
|
||||
Reference in New Issue
Block a user