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 { guardDesignLeave } from "./design_leave";
|
||||||
import { usePipeIds } from "./design_pipe_ids";
|
import { usePipeIds } from "./design_pipe_ids";
|
||||||
import { setRouteShiftOpener } from "../../B06_Section/B06_Section_UI_Cross_Center";
|
import { setRouteShiftOpener } from "../../B06_Section/B06_Section_UI_Cross_Center";
|
||||||
|
import { reconcilePagePipeline } from "./design_pipeline_page";
|
||||||
import "./design_page.css";
|
import "./design_page.css";
|
||||||
|
|
||||||
export type { DesignPageShared } from "./design_shared";
|
export type { DesignPageShared } from "./design_shared";
|
||||||
@@ -244,6 +245,7 @@ export function renderDesignPage(root: HTMLElement): MountedPage {
|
|||||||
if (slot) {
|
if (slot) {
|
||||||
slot.show();
|
slot.show();
|
||||||
applyView(view);
|
applyView(view);
|
||||||
|
void reconcilePagePipeline(projectId); // 들어올 때 입력(세션 · 가게) 대 묶음 맞춤(365-2)
|
||||||
} else mountView(view, true);
|
} else mountView(view, true);
|
||||||
// 검증용 — 보기 전환에 든 시간(ms)과 횟수를 화면 밖에서 수치로 본다.
|
// 검증용 — 보기 전환에 든 시간(ms)과 횟수를 화면 밖에서 수치로 본다.
|
||||||
const probe = window as unknown as { __designPage?: { view: string; switchMs: number[] } };
|
const probe = window as unknown as { __designPage?: { view: string; switchMs: number[] } };
|
||||||
|
|||||||
@@ -8,9 +8,11 @@
|
|||||||
* 넘기는 칸 = 서버 노선 저장(`CARRY_INPUT_KEYS`)과 같은 일곱(지반 유형 · 측구 종류 · 측구 선택 · 2단 경사 ·
|
* 넘기는 칸 = 서버 노선 저장(`CARRY_INPUT_KEYS`)과 같은 일곱(지반 유형 · 측구 종류 · 측구 선택 · 2단 경사 ·
|
||||||
* 암 경계 · 암 경사 · 소단) + 얹힌 시설 맥락(포장 · 물넘이 기울임 · 세월교 노면 하강). 단면 유형 · 측구
|
* 암 경계 · 암 경사 · 소단) + 얹힌 시설 맥락(포장 · 물넘이 기울임 · 세월교 노면 하강). 단면 유형 · 측구
|
||||||
* 방향은 새 지형이 정함(`logic/B05_Profile_Pipeline_Detail.ts` `crossWithUphill`) · 노견 확장 · 소단
|
* 방향은 새 지형이 정함(`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 type { CrossSection, SectionDetailResponse } from "../../B06_Section/B06_Section_Api_Fetch";
|
||||||
import {
|
import {
|
||||||
effectiveStandardCross,
|
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(
|
export function pipelineCrossSource(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
detail: SectionDetailResponse | null,
|
detail: SectionDetailResponse | null,
|
||||||
savedStations: ReadonlyArray<PipelineStation>,
|
savedStations: ReadonlyArray<PipelineStation>,
|
||||||
|
routeId: number | null = null,
|
||||||
): PipelineCrossSource | null {
|
): PipelineCrossSource | null {
|
||||||
const standard = detail && effectiveStandardCross(projectId);
|
const standard = detail && effectiveStandardCross(projectId);
|
||||||
if (!detail || !standard) return null;
|
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];
|
for (const name of SPEC_KEYS) if (section[name] !== undefined) own[name] = section[name];
|
||||||
specs.set(best.key, own as SectionSpecs);
|
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 {
|
return {
|
||||||
// 암 경계 기본값 = 서버 config(화면이 문맥에서 기억) — 입력 없는 측점(초안 새 측점)도 서버처럼(349-0p)
|
// 암 경계 기본값 = 서버 config(화면이 문맥에서 기억) — 입력 없는 측점(초안 새 측점)도 서버처럼(349-0p)
|
||||||
input: { standard, byKey, rockBoundaryDefaultM: readRockBoundaryDefault(projectId) },
|
input: { standard, byKey, rockBoundaryDefaultM: readRockBoundaryDefault(projectId) },
|
||||||
|
|||||||
@@ -12,7 +12,10 @@
|
|||||||
*
|
*
|
||||||
* 입력 — 노선 = 편집기 다리(`RouteDraftBridge` · 캐시 칸 route-draft) · 계획고 = 종단도 편집
|
* 입력 — 노선 = 편집기 다리(`RouteDraftBridge` · 캐시 칸 route-draft) · 계획고 = 종단도 편집
|
||||||
* (`pipelineGrade` · 노선 초안 동안은 alignment-draft 노선 `DRAFT_ROUTE_ID`) · 횡단 = 저장 상세의 측점
|
* (`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 · 노선 초안이 있으면 종단도가 새 첫 계획선 ·
|
* 출력 — 가게 stations · profile · footprint · generation · 노선 초안이 있으면 종단도가 새 첫 계획선 ·
|
||||||
* 옮긴 계획고 · 새 노선 상세(`pipelineDetail` — 지반 · 측점 · 횡단 면적 → 유토곡선)를 그림.
|
* 옮긴 계획고 · 새 노선 상세(`pipelineDetail` — 지반 · 측점 · 횡단 면적 → 유토곡선)를 그림.
|
||||||
* 첫 계산 = 열 때 한 번(저장 노선 · 발자국 · 저장 안 한 노선 초안이 있으면 그 노선).
|
* 첫 계산 = 열 때 한 번(저장 노선 · 발자국 · 저장 안 한 노선 초안이 있으면 그 노선).
|
||||||
@@ -22,15 +25,18 @@
|
|||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
|
|
||||||
import { createDesignPipeline, publishToStore, type DesignPipeline } from "./logic/design_pipeline";
|
import { createDesignPipeline, publishToStore, type DesignPipeline } from "./logic/design_pipeline";
|
||||||
|
import { SOURCE_INPUT } from "./logic/design_pipeline_types";
|
||||||
export { pipelineWork } from "./logic/design_pipeline";
|
export { pipelineWork } from "./logic/design_pipeline";
|
||||||
import { pipelineCrossSource, type PipelineCrossSource } from "./design_pipeline_cross";
|
import { pipelineCrossSource, type PipelineCrossSource } from "./design_pipeline_cross";
|
||||||
import { createStationsStage } from "./logic/design_pipeline_stations";
|
import { createStationsStage } from "./logic/design_pipeline_stations";
|
||||||
import type {
|
import type {
|
||||||
|
ChangeSource,
|
||||||
FollowPatch,
|
FollowPatch,
|
||||||
PipelineInputs,
|
PipelineInputs,
|
||||||
PipelineOutputs,
|
PipelineOutputs,
|
||||||
PipelineRunRecord,
|
PipelineRunRecord,
|
||||||
PipelineStation,
|
PipelineStation,
|
||||||
|
RangePart,
|
||||||
RouteDraft,
|
RouteDraft,
|
||||||
StageFn,
|
StageFn,
|
||||||
Vertex,
|
Vertex,
|
||||||
@@ -102,6 +108,8 @@ import type {
|
|||||||
|
|
||||||
/** 노선 초안 동안의 계획고 초안 칸 — alignment-draft 의 노선 0(노선 번호는 [저장] 때 남). */
|
/** 노선 초안 동안의 계획고 초안 칸 — alignment-draft 의 노선 0(노선 번호는 [저장] 때 남). */
|
||||||
export const DRAFT_ROUTE_ID = 0;
|
export const DRAFT_ROUTE_ID = 0;
|
||||||
|
/** 입력 ↔ 묶음 입력 맞춤 상한(바퀴) — 넘으면 「계산 불안정」(365-2). */
|
||||||
|
export const RECONCILE_ROUNDS = 3;
|
||||||
|
|
||||||
/** 종단도(`design_profile`)가 묶음에 내주는 것. */
|
/** 종단도(`design_profile`)가 묶음에 내주는 것. */
|
||||||
export interface ProfileLink {
|
export interface ProfileLink {
|
||||||
@@ -137,6 +145,8 @@ export interface PagePipelineOptions {
|
|||||||
routeId: number,
|
routeId: number,
|
||||||
saved: SectionDetailResponse | null,
|
saved: SectionDetailResponse | null,
|
||||||
) => (stations: PipelineStation[]) => Promise<string>;
|
) => (stations: PipelineStation[]) => Promise<string>;
|
||||||
|
/** 고리가 `RECONCILE_ROUNDS` 바퀴 안에 안 맞음 — 「계산 불안정」 한 줄(365-2). */
|
||||||
|
onUnstable?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PagePipeline {
|
interface PagePipeline {
|
||||||
@@ -145,6 +155,8 @@ interface PagePipeline {
|
|||||||
grade(edits: AlignmentEdits): void;
|
grade(edits: AlignmentEdits): void;
|
||||||
restart(): void;
|
restart(): void;
|
||||||
earthwork(context: MassHaulStageContext | null): void;
|
earthwork(context: MassHaulStageContext | null): void;
|
||||||
|
cross(): void;
|
||||||
|
reconcile(): Promise<number>;
|
||||||
detail(): SectionDetailResponse | null;
|
detail(): SectionDetailResponse | null;
|
||||||
dispose(): void;
|
dispose(): void;
|
||||||
}
|
}
|
||||||
@@ -193,6 +205,13 @@ export const restartPagePipeline = (projectId: string): void => pages.get(projec
|
|||||||
export const pipelineEarthwork = (projectId: string, context: MassHaulStageContext | null): void =>
|
export const pipelineEarthwork = (projectId: string, context: MassHaulStageContext | null): void =>
|
||||||
pages.get(projectId)?.earthwork(context);
|
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(저장 상세). */
|
/** 노선 초안 동안 종단도가 그릴 새 노선 상세 — 초안이 없으면 null(저장 상세). */
|
||||||
export const pipelineDetail = (projectId: string): SectionDetailResponse | null =>
|
export const pipelineDetail = (projectId: string): SectionDetailResponse | null =>
|
||||||
pages.get(projectId)?.detail() ?? null;
|
pages.get(projectId)?.detail() ?? null;
|
||||||
@@ -219,10 +238,9 @@ function createPagePipeline(
|
|||||||
let savedStructures: PlanStructure[] = [];
|
let savedStructures: PlanStructure[] = [];
|
||||||
let savedPipes: PlanPipe[] = [];
|
let savedPipes: PlanPipe[] = [];
|
||||||
let earthwork: MassHaulStageContext | null = null;
|
let earthwork: MassHaulStageContext | null = null;
|
||||||
/** 저장 노선 측점(열쇠) · 그것으로 모은 횡단 입력 — 저장 상세가 바뀌면 다시. */
|
/** 저장 노선 측점(열쇠) · 마지막으로 모은 횡단 입력(시설 제원 — 새 노선 상세가 씀). */
|
||||||
let savedStations: PipelineStation[] = [];
|
let savedStations: PipelineStation[] = [];
|
||||||
let crossSource: { detail: SectionDetailResponse; source: PipelineCrossSource | null } | null =
|
let crossSource: PipelineCrossSource | null = null;
|
||||||
null;
|
|
||||||
/** 저장 정본(서버가 준 상세 · 노선 번호) — 측점 갈아끼움(swap)이 만든 새 상세는 지반 표본 배열을
|
/** 저장 정본(서버가 준 상세 · 노선 번호) — 측점 갈아끼움(swap)이 만든 새 상세는 지반 표본 배열을
|
||||||
* 그대로 둠 → 정본 아님(묶음 출력 → 상세 → 다시 묶음 고리를 끊음). */
|
* 그대로 둠 → 정본 아님(묶음 출력 → 상세 → 다시 묶음 고리를 끊음). */
|
||||||
let canon: { routeId: unknown; samples: unknown; detail: SectionDetailResponse | null } | null =
|
let canon: { routeId: unknown; samples: unknown; detail: SectionDetailResponse | null } | null =
|
||||||
@@ -286,12 +304,17 @@ function createPagePipeline(
|
|||||||
emptyEdits()
|
emptyEdits()
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
/** 횡단 입력 — 부를 때마다 저장 상세 + 세션 고침 · 표준을 모음(상세 객체마다 기억하면 고침이 안 감 · 365-2). */
|
||||||
const crossNow = (): PipelineCrossSource | null => {
|
const crossNow = (): PipelineCrossSource | null => {
|
||||||
const detail = canon?.detail ?? null;
|
const routeId = canon?.routeId;
|
||||||
if (!detail) return null;
|
const source = pipelineCrossSource(
|
||||||
if (crossSource?.detail !== detail || !crossSource.source)
|
projectId,
|
||||||
crossSource = { detail, source: pipelineCrossSource(projectId, detail, savedStations) };
|
canon?.detail ?? null,
|
||||||
return crossSource.source;
|
savedStations,
|
||||||
|
typeof routeId === "number" ? routeId : null,
|
||||||
|
);
|
||||||
|
if (source) crossSource = source;
|
||||||
|
return source;
|
||||||
};
|
};
|
||||||
const groundDeps = groundStageDeps(projectId);
|
const groundDeps = groundStageDeps(projectId);
|
||||||
const crossStage = createCrossStage({ grid: groundDeps.grid }); // 미교차 넓힘 = 같은 지형 격자(349-0j)
|
const crossStage = createCrossStage({ grid: groundDeps.grid }); // 미교차 넓힘 = 같은 지형 격자(349-0j)
|
||||||
@@ -460,7 +483,7 @@ function createPagePipeline(
|
|||||||
ground,
|
ground,
|
||||||
profile,
|
profile,
|
||||||
pipe.output("cross"),
|
pipe.output("cross"),
|
||||||
crossSource?.source?.specs ?? new Map(),
|
crossSource?.specs ?? new Map(),
|
||||||
options.stationIntervalM ?? profile.policy.station_interval_m,
|
options.stationIntervalM ?? profile.policy.station_interval_m,
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
@@ -517,11 +540,48 @@ function createPagePipeline(
|
|||||||
// 초안 둔 채 열기(새로고침 · B06 바로 열기 · 365-1) — 초안 전 = 지금 저장 상세(세션 그대로) · 측점은 가게에 냄
|
// 초안 둔 채 열기(새로고침 · B06 바로 열기 · 365-1) — 초안 전 = 지금 저장 상세(세션 그대로) · 측점은 가게에 냄
|
||||||
held = inputs.route ? (held ?? holdNow()) : null;
|
held = inputs.route ? (held ?? holdNow()) : null;
|
||||||
dropping = null;
|
dropping = null;
|
||||||
void pipe.start(inputs);
|
Object.assign(given, inputs);
|
||||||
|
const run = (lastRun = pipe.start(inputs));
|
||||||
openGeneration = pipe.generation;
|
openGeneration = pipe.generation;
|
||||||
quietGeneration = inputs.route ? -1 : 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`). */
|
/** 저장 노선(그린 선 · 곡선표 · 기본 반경) · 그 측점 열쇠 — [저장] 뒤 다시 받음(`restart`). */
|
||||||
const loadPlan = (): Promise<void> =>
|
const loadPlan = (): Promise<void> =>
|
||||||
fetchRoutePlan(projectId)
|
fetchRoutePlan(projectId)
|
||||||
@@ -555,13 +615,10 @@ function createPagePipeline(
|
|||||||
.catch(() => undefined);
|
.catch(() => undefined);
|
||||||
|
|
||||||
const unsubscribe = [
|
const unsubscribe = [
|
||||||
store.subscribe(
|
store.subscribe("pipes", (pipes) => started && !echo && void feed("pipe", pipes ?? savedPipes)),
|
||||||
"pipes",
|
|
||||||
(pipes) => started && !echo && void pipe.update("pipe", pipes ?? savedPipes),
|
|
||||||
),
|
|
||||||
store.subscribe(
|
store.subscribe(
|
||||||
"structures",
|
"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();
|
if (!started) return start();
|
||||||
const before = pipe.output("route")?.line ?? null;
|
const before = pipe.output("route")?.line ?? null;
|
||||||
const after = draft ? draftLine(draft, plan?.min_radius_m ?? 0).line : savedLine();
|
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) {
|
if (run && !draft && wasDraft) {
|
||||||
quietGeneration = pipe.generation;
|
quietGeneration = pipe.generation;
|
||||||
dropping = run.then(finishDrop);
|
dropping = run.then(finishDrop);
|
||||||
@@ -598,7 +655,7 @@ function createPagePipeline(
|
|||||||
if (same(edits, pipe.input("alignment"))) return;
|
if (same(edits, pipe.input("alignment"))) return;
|
||||||
const nodes = pipe.output("planline")?.basePvi.map((node) => node.chainage_m) ?? [];
|
const nodes = pipe.output("planline")?.basePvi.map((node) => node.chainage_m) ?? [];
|
||||||
const before = pipe.input("alignment") as AlignmentEdits | null;
|
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() {
|
restart() {
|
||||||
if (!started) return start();
|
if (!started) return start();
|
||||||
@@ -607,8 +664,13 @@ function createPagePipeline(
|
|||||||
earthwork(context) {
|
earthwork(context) {
|
||||||
earthwork = context;
|
earthwork = context;
|
||||||
const cross = crossNow()?.input ?? null;
|
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,
|
detail: () => shown,
|
||||||
dispose() {
|
dispose() {
|
||||||
disposed = true;
|
disposed = true;
|
||||||
|
|||||||
@@ -209,6 +209,7 @@ function startPagePipeline(shared: DesignPageShared, profile: DesignProfile): vo
|
|||||||
stationIntervalM: SECTION_STATION_INTERVAL_M,
|
stationIntervalM: SECTION_STATION_INTERVAL_M,
|
||||||
onCorridorStale: () => each((hooks) => hooks.onCorridorStale?.()),
|
onCorridorStale: () => each((hooks) => hooks.onCorridorStale?.()),
|
||||||
onFacilitiesFollowed: (far) => each((hooks) => hooks.onFacilitiesFollowed?.(far)),
|
onFacilitiesFollowed: (far) => each((hooks) => hooks.onFacilitiesFollowed?.(far)),
|
||||||
|
onUnstable: () => showToast(L("Design_Pipeline_Unstable"), "warning"),
|
||||||
structureAreas: ({ cross_sections: rows }) =>
|
structureAreas: ({ cross_sections: rows }) =>
|
||||||
applyStructureAreaRows(rows, structureAreaRows(rows)),
|
applyStructureAreaRows(rows, structureAreaRows(rows)),
|
||||||
// 초안 시작 — 초안 전 상태를 떠 둠 · 버리면 그대로(측점이 달라도 · 349-0o) · 상세가 없으면 묶음 측점으로 갈아끼움
|
// 초안 시작 — 초안 전 상태를 떠 둠 · 버리면 그대로(측점이 달라도 · 349-0o) · 상세가 없으면 묶음 측점으로 갈아끼움
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
* - `reconcileStaleDesigns`: 낡은 design 일괄 재계산(B05 와 같은 창구)
|
* - `reconcileStaleDesigns`: 낡은 design 일괄 재계산(B05 와 같은 창구)
|
||||||
* - `syncBermSpans` : 구조물 목록의 소단을 세션 사본으로 펴기
|
* - `syncBermSpans` : 구조물 목록의 소단을 세션 사본으로 펴기
|
||||||
* - `applyPanelToAll` : 표준 횡단면 설정 [전체 반영]
|
* - `applyPanelToAll` : 표준 횡단면 설정 [전체 반영]
|
||||||
|
* 고친 뒤 계산 묶음 cross 입력도 다시(`pipelineCross` · 노선 초안 동안 카드 = 묶음 새 노선 상세 · 365-2).
|
||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -28,6 +29,7 @@ import type { CrossDesignChange, SectionViewController } from "./B06_Section_UI_
|
|||||||
import type { StructureInstance } from "../B05_Profile/B05_Profile_Api_Structures";
|
import type { StructureInstance } from "../B05_Profile/B05_Profile_Api_Structures";
|
||||||
import { setCrossDetailHost } from "./B06_Section_UI_Cross_Detail";
|
import { setCrossDetailHost } from "./B06_Section_UI_Cross_Detail";
|
||||||
import { useStructureIds } from "./B06_Section_Station_Keys";
|
import { useStructureIds } from "./B06_Section_Station_Keys";
|
||||||
|
import { pipelineCross } from "../A00_Common/design/design_pipeline_page";
|
||||||
|
|
||||||
export interface DesignSyncContext {
|
export interface DesignSyncContext {
|
||||||
projectId: string | null;
|
projectId: string | null;
|
||||||
@@ -125,6 +127,7 @@ export function createDesignSync(ctx: DesignSyncContext): DesignSyncController {
|
|||||||
.detail()
|
.detail()
|
||||||
?.cross_sections.find((section) => Math.abs(section.chainage_m - chainageM) < 0.01)?.design;
|
?.cross_sections.find((section) => Math.abs(section.chainage_m - chainageM) < 0.01)?.design;
|
||||||
if (design && design.ground_type !== "soil") void reconcileStaleDesigns({ force: true });
|
if (design && design.ground_type !== "soil") void reconcileStaleDesigns({ force: true });
|
||||||
|
else if (projectId) pipelineCross(projectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -184,6 +187,7 @@ export function createDesignSync(ctx: DesignSyncContext): DesignSyncController {
|
|||||||
const detail = error instanceof Error ? ` ${error.message}` : "";
|
const detail = error instanceof Error ? ` ${error.message}` : "";
|
||||||
showToast(`${L("B06_Design_Failed")}${detail}`, "error");
|
showToast(`${L("B06_Design_Failed")}${detail}`, "error");
|
||||||
}
|
}
|
||||||
|
pipelineCross(projectId); // 세션 고침 · 표준 → 묶음(같은 입력이면 안 셈)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -199,6 +199,7 @@ async function run() {
|
|||||||
const savedBase = await savedBaseOf();
|
const savedBase = await savedBaseOf();
|
||||||
const shop = store.useDesignStore(P, null);
|
const shop = store.useDesignStore(P, null);
|
||||||
const holds = [];
|
const holds = [];
|
||||||
|
let unstable = 0;
|
||||||
const link = () => ({
|
const link = () => ({
|
||||||
pipelineSource: () => ({
|
pipelineSource: () => ({
|
||||||
base: savedBase,
|
base: savedBase,
|
||||||
@@ -215,6 +216,7 @@ async function run() {
|
|||||||
holds.push(saved === job.detail ? "saved" : saved ? "other" : "null");
|
holds.push(saved === job.detail ? "saved" : saved ? "other" : "null");
|
||||||
return async () => "saved";
|
return async () => "saved";
|
||||||
},
|
},
|
||||||
|
onUnstable: () => (unstable += 1),
|
||||||
};
|
};
|
||||||
const open = (label) => {
|
const open = (label) => {
|
||||||
const bridge = pageMod.routeDraftBridge(P, link(), options);
|
const bridge = pageMod.routeDraftBridge(P, link(), options);
|
||||||
@@ -249,6 +251,56 @@ async function run() {
|
|||||||
stages: Object.keys(reopenRun.stages),
|
stages: Object.keys(reopenRun.stages),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ⑤ 초안 동안 B06 고침 — 세션 칸(측점 열쇠) → pipelineCross → 카드(묶음 새 노선 상세)
|
||||||
|
const pipe = pageMod.pagePipeline(P);
|
||||||
|
const target = pipe.output("ground").stations.find((s) => s.kind === "grid" && s.chainageM > 30);
|
||||||
|
const cell = (ground) => ({ [target.key]: { ground_type: ground, section_mode: "left_cut" } });
|
||||||
|
const groundAt = () =>
|
||||||
|
pageMod
|
||||||
|
.pipelineDetail(P)
|
||||||
|
.cross_sections.find((s) => Math.abs(s.chainage_m - target.chainageM) < 1e-3)?.design
|
||||||
|
?.ground_type ?? null;
|
||||||
|
const before = groundAt();
|
||||||
|
page.writeState("crossdesign", cell("blasting_rock"), P, 7);
|
||||||
|
let mark = runs().length;
|
||||||
|
pageMod.pipelineCross(P);
|
||||||
|
await settle(mark + 1);
|
||||||
|
await tick(20);
|
||||||
|
const edit = {
|
||||||
|
key: target.key,
|
||||||
|
before,
|
||||||
|
after: groundAt(),
|
||||||
|
input: pipe.input("cross").byKey[target.key]?.groundType ?? null,
|
||||||
|
runs: runs().length - mark,
|
||||||
|
sources: runs()[runs().length - 1].sources,
|
||||||
|
};
|
||||||
|
// ⑥ 고리 — 같은 입력 = 0 바퀴 · 세션만 바뀜(알림 없음) = 1 바퀴 · 안 맞음 = 3 바퀴 뒤 알림
|
||||||
|
mark = runs().length;
|
||||||
|
const same = await pageMod.reconcilePagePipeline(P);
|
||||||
|
const sameRuns = runs().length - mark;
|
||||||
|
page.writeState("crossdesign", cell("ripping_rock"), P, 7);
|
||||||
|
mark = runs().length;
|
||||||
|
const once = await pageMod.reconcilePagePipeline(P);
|
||||||
|
const loop = {
|
||||||
|
same,
|
||||||
|
sameRuns,
|
||||||
|
once,
|
||||||
|
onceRuns: runs().length - mark,
|
||||||
|
onceGround: groundAt(),
|
||||||
|
};
|
||||||
|
let n = 0;
|
||||||
|
const realStandard = STUBS["B06_Section_UI_Standard_Panel"].effectiveStandardCross;
|
||||||
|
STUBS["B06_Section_UI_Standard_Panel"].effectiveStandardCross = () => ({
|
||||||
|
...job.standard,
|
||||||
|
n: (n += 1),
|
||||||
|
});
|
||||||
|
mark = runs().length;
|
||||||
|
loop.unstable = await pageMod.reconcilePagePipeline(P);
|
||||||
|
loop.unstableRuns = runs().length - mark;
|
||||||
|
loop.alerts = unstable;
|
||||||
|
STUBS["B06_Section_UI_Standard_Panel"].effectiveStandardCross = realStandard;
|
||||||
|
await pageMod.reconcilePagePipeline(P);
|
||||||
|
|
||||||
// ④ 초안 버림 — 열 때 맡긴 초안 전 상세로 되돌림(저장 정본)
|
// ④ 초안 버림 — 열 때 맡긴 초안 전 상세로 되돌림(저장 정본)
|
||||||
again.bridge.edited(null);
|
again.bridge.edited(null);
|
||||||
const dropRun = await settle(runs().length + 1);
|
const dropRun = await settle(runs().length + 1);
|
||||||
@@ -260,7 +312,7 @@ async function run() {
|
|||||||
};
|
};
|
||||||
pageMod.disposePagePipeline(P);
|
pageMod.disposePagePipeline(P);
|
||||||
page.clearDrafts(P);
|
page.clearDrafts(P);
|
||||||
return { kept, b05, b06, dropped };
|
return { kept, b05, b06, dropped, edit, loop };
|
||||||
}
|
}
|
||||||
|
|
||||||
run()
|
run()
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
- 노선 초안 둔 채 다시 열기(새로고침 · B06 바로 열기 · 편집기 없음) = B05 길과 같은 초안 상세
|
- 노선 초안 둔 채 다시 열기(새로고침 · B06 바로 열기 · 편집기 없음) = B05 길과 같은 초안 상세
|
||||||
(측점 · 계획고 · 면적) · 측점을 가게에 다시 냄 · 초안 전 = 저장 상세(열 때 맡김) → 버리면 saved.
|
(측점 · 계획고 · 면적) · 측점을 가게에 다시 냄 · 초안 전 = 저장 상세(열 때 맡김) → 버리면 saved.
|
||||||
- 초안 동안 측점 갈아끼움은 메모리만 — 세션 상세(저장 노선)를 새 측점 + 옛 설계로 덮지 않음.
|
- 초안 동안 측점 갈아끼움은 메모리만 — 세션 상세(저장 노선)를 새 측점 + 옛 설계로 덮지 않음.
|
||||||
|
못박는 것(365-2)
|
||||||
|
- 초안 동안 B06 고침(세션 칸 · 측점 열쇠) → `pipelineCross` → 묶음 cross 입력 · 카드에 보임.
|
||||||
|
- 고리 — 같은 입력 = 0 바퀴 · 세션만 바뀜 = 1 바퀴 · 안 맞으면 3 바퀴 뒤 「계산 불안정」 한 번.
|
||||||
|
|
||||||
짝: `helper_365_page_pipeline.cjs`(진짜 TS · 서버 창구만 가짜 · 틀 = 346-2 시험) ·
|
짝: `helper_365_page_pipeline.cjs`(진짜 TS · 서버 창구만 가짜 · 틀 = 346-2 시험) ·
|
||||||
`helper_365_swap_entry.ts` + `helper_365_swap.mjs`(진짜 상세 가게 · vite ssr).
|
`helper_365_swap_entry.ts` + `helper_365_swap.mjs`(진짜 상세 가게 · vite ssr).
|
||||||
@@ -99,6 +102,20 @@ def test_365_1_초안_버림_저장_정본(r):
|
|||||||
assert d["restoreWhy"] == "saved"
|
assert d["restoreWhy"] == "saved"
|
||||||
|
|
||||||
|
|
||||||
|
def test_365_2_초안_동안_횡단_고침_카드에(r):
|
||||||
|
e = r["edit"]
|
||||||
|
assert e["before"] != "blasting_rock"
|
||||||
|
assert e["input"] == "blasting_rock" and e["after"] == "blasting_rock"
|
||||||
|
assert e["runs"] == 1 and e["sources"] == ["cross"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_365_2_고리_바퀴_수(r):
|
||||||
|
loop = r["loop"]
|
||||||
|
assert loop["same"] == 0 and loop["sameRuns"] == 0 # 같은 입력 = 안 돎
|
||||||
|
assert loop["once"] == 1 and loop["onceRuns"] == 1 and loop["onceGround"] == "ripping_rock"
|
||||||
|
assert loop["unstable"] == -1 and loop["unstableRuns"] == 3 and loop["alerts"] == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def swap(tmp_path_factory) -> dict:
|
def swap(tmp_path_factory) -> dict:
|
||||||
work = tmp_path_factory.mktemp("b365")
|
work = tmp_path_factory.mktemp("b365")
|
||||||
|
|||||||
@@ -1047,4 +1047,8 @@ export const ui_locales_b2 = {
|
|||||||
],
|
],
|
||||||
B06_Std_Note_Rock: ["암", "Rock"],
|
B06_Std_Note_Rock: ["암", "Rock"],
|
||||||
B06_Std_Note_Paved: ["포장", "Paved"],
|
B06_Std_Note_Paved: ["포장", "Paved"],
|
||||||
|
Design_Pipeline_Unstable: [
|
||||||
|
"계산 불안정 — 입력과 결과가 세 바퀴 안에 안 맞음",
|
||||||
|
"Calculation unstable — inputs and results did not settle in three rounds",
|
||||||
|
],
|
||||||
} as const satisfies Record<string, LocaleEntry>;
|
} as const satisfies Record<string, LocaleEntry>;
|
||||||
|
|||||||
Reference in New Issue
Block a user