merge(511-B B-5 follow 합치기): origin/sub_laptop_3(follow 줄 엶 · /calc follow 서버 맥락) 받음 — 공유 파일 충돌 풂
- /calc 창구 차례 = 격자 경로 막기 → 단계 · 화면 길 서버 맥락(B-4) → follow 저장 정본 맥락(B-5 · stages 다시 짬) - 까닭 글 = cross 그대로 · pair_B5 걷음(follow 엶) - 시험 511_b1 · 309_2 · 309_2_browser = 잠긴 단계 줄 없음 · 잠금 예시 = form_default · 화면 열린 줄에 P5 P6 F1 F2 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
/* =============================================================================
|
||||
* A00_Common/design/design_follow_server.ts
|
||||
* 계산 자리 follow 「서버」(PLAN 511-B B-5) — 끌기 도구 꼴: 조작 중 = 화면 셈(미리보기) · 잠잠 뒤 서버 Node
|
||||
* route · follow 두 단계(`stage.follow` · until follow · 저장 정본 맥락은 창구가 프로젝트 id 로 붙임)를 묻고
|
||||
* 답이 오면 묶음을 노선부터 다시 돌림 → 같은 노선 초안이면 서버 값(곁 자료 다시 붙임)으로 바꿈.
|
||||
* 판이 both 면 null(화면 셈 그대로). 열쇠 = 노선 초안 + 저장 노선 계획고 편집(follow 는 노선에만 기댐).
|
||||
* ========================================================================== */
|
||||
|
||||
import { readState } from "../b_page_state";
|
||||
import {
|
||||
withFollowSides,
|
||||
type FollowSides,
|
||||
} from "../../B05_Profile/logic/B05_Profile_Pipeline_Grade";
|
||||
import { calcOnServerNow } from "./design_calc_sites";
|
||||
import { afterQuiet, serverCalc } from "./design_calc_server";
|
||||
import type { FollowPatch, PipelineInputs } from "./logic/design_pipeline_types";
|
||||
|
||||
const ID = "stage.follow";
|
||||
let held: { key: string; patch: FollowPatch } | null = null;
|
||||
let asked: string | null = null;
|
||||
|
||||
/** 서버 follow 값 — 같은 열쇠 답이 있으면 그 값 · 없으면 null(화면 셈) + 잠잠 뒤 묻고 `redraw`. */
|
||||
export function followFromServer(
|
||||
projectId: string,
|
||||
inputs: Readonly<PipelineInputs>,
|
||||
before: unknown,
|
||||
savedEdits: unknown,
|
||||
redraw: () => void,
|
||||
): FollowPatch | null {
|
||||
if (!calcOnServerNow("follow") || !inputs.route) return null;
|
||||
const key = JSON.stringify([inputs.route, savedEdits]);
|
||||
if (held?.key === key) return held.patch;
|
||||
if (asked === key) return null;
|
||||
asked = key;
|
||||
const { route, alignment, pipes, structures, plusStations } = inputs;
|
||||
const stages = {
|
||||
until: "follow",
|
||||
input: { route, alignment, pipes, structures, plusStations },
|
||||
before_draft: before,
|
||||
pipe_ids: readState("pipe-ids", projectId),
|
||||
saved_edits: savedEdits,
|
||||
};
|
||||
const body = JSON.stringify({ run: [ID], inputs: { stages } });
|
||||
void afterQuiet(ID, (signal) => serverCalc("follow", [ID], body, signal)).then((results) => {
|
||||
if (asked === key) asked = null;
|
||||
const got = results?.[ID] as ({ patch: FollowPatch } & FollowSides) | undefined;
|
||||
if (!got) return;
|
||||
held = { key, patch: withFollowSides(got.patch, got) };
|
||||
redraw();
|
||||
});
|
||||
return null;
|
||||
}
|
||||
@@ -29,6 +29,7 @@ export { pipelineWork } from "./logic/design_pipeline";
|
||||
import { createStationsStage, snapOptions, stationDiff } from "./logic/design_pipeline_stations";
|
||||
import { stationsOnServer } from "./design_stations_server";
|
||||
import { routeOnServer } from "./design_route_server";
|
||||
import { followFromServer } from "./design_follow_server";
|
||||
import type {
|
||||
ChangeSource,
|
||||
FollowPatch,
|
||||
@@ -303,8 +304,15 @@ function createPagePipeline(
|
||||
const { pipes, structures, plusStations } = inputsNow();
|
||||
return { alignment: kept, pipes, structures, plusStations };
|
||||
}
|
||||
// 계산 자리 follow 「서버」(511-B B-5) — 끌기 중 화면 셈 · 잠잠 뒤 서버 값으로 바꿈
|
||||
const before = beforeDraft ?? facilitiesNow();
|
||||
const remote = followFromServer(projectId, ctx.inputs, before, savedEdits(), () =>
|
||||
pipe.refresh("route"),
|
||||
);
|
||||
const saved = { pipes: savedPipes, structures: savedStructures };
|
||||
return followStage({ ...ctx, inputs: placedFollowInputs(ctx, savedLine(), saved) });
|
||||
return (
|
||||
remote ?? followStage({ ...ctx, inputs: placedFollowInputs(ctx, savedLine(), saved) })
|
||||
);
|
||||
},
|
||||
stations: createStationsStage({
|
||||
intervalM: options.stationIntervalM,
|
||||
|
||||
@@ -20,12 +20,14 @@
|
||||
import { readFileSync, statSync } from "node:fs";
|
||||
import { bermSpansFromStructures, bermSpecAt } from "@util/common_util_berm_spans";
|
||||
import type { FollowingStructure } from "@util/common_util_route_follow";
|
||||
import { useCatalogDefinition, type CatalogDefinition } from "@util/common_util_structure_catalog";
|
||||
import type { PlanPipe, PlanStructure, PlanType } from "@util/common_util_structure_station";
|
||||
import { parseDtmGrid, type DtmGrid } from "@util/common_util_surface_sampler";
|
||||
import type { RoutePlanResponse } from "../../../B05_Profile/B05_Profile_Api_Replan";
|
||||
import {
|
||||
createFollowStage,
|
||||
createPlanlineStage,
|
||||
followSides,
|
||||
profileStage,
|
||||
} from "../../../B05_Profile/logic/B05_Profile_Pipeline_Grade";
|
||||
import { shapeStatus } from "../../../B05_Profile/logic/B05_Profile_Pipeline_Detail";
|
||||
@@ -40,14 +42,23 @@ import {
|
||||
type AlignmentEdits,
|
||||
} from "../../../B05_Profile/logic/B05_Profile_UI_Profile_Alignment";
|
||||
import { readAlignment } from "../../../B05_Profile/logic/B05_Profile_UI_Profile_Data";
|
||||
import type { SectionDetailResponse } from "../../../B06_Section/B06_Section_Api_Fetch";
|
||||
import type {
|
||||
LongitudinalSection,
|
||||
SectionDetailResponse,
|
||||
} from "../../../B06_Section/B06_Section_Api_Fetch";
|
||||
import { createCrossStage } from "../../../B06_Section/logic/B06_Section_Cross_Stage";
|
||||
import { pipeBook } from "../../../B06_Section/B06_Section_Route_Carry_Patch";
|
||||
import { plantedBase } from "./design_planted_base";
|
||||
import { createDesignPipeline } from "./design_pipeline";
|
||||
import { AFTER_STAGES, BROWSER_STAGES, createDesignPipeline } from "./design_pipeline";
|
||||
import { contextCross, placedFollowInputs } from "./design_pipeline_stage_inputs";
|
||||
import { createStationsStage, snapOptions } from "./design_pipeline_stations";
|
||||
import type { PipelineInputs, StageName, Vertex } from "./design_pipeline_types";
|
||||
import type {
|
||||
PipelineInputs,
|
||||
PipelineStages,
|
||||
StageContext,
|
||||
StageName,
|
||||
Vertex,
|
||||
} from "./design_pipeline_types";
|
||||
import { plain } from "./design_calc_node_trial";
|
||||
|
||||
export interface StagesInputs {
|
||||
@@ -67,6 +78,13 @@ export interface StagesInputs {
|
||||
interval_m?: number;
|
||||
grid?: { path: string; design: "las" | "sheet" } | null;
|
||||
input: Partial<PipelineInputs>;
|
||||
/** 구조물 정의(서버 창구가 붙임 · 화면 `useCatalogDefinition` 꼴) — 측점 · 카드 이름 표. */
|
||||
definition?: CatalogDefinition | null;
|
||||
/** 저장 종단(계획고 편집 = `readAlignment(·).edits` · 세션 초안 `saved_edits` 없을 때 · 창구가 붙임). */
|
||||
longitudinal?: LongitudinalSection | null;
|
||||
/** "follow" = route · follow 두 단계만(격자 · 상세 안 씀 · 계산 자리 follow 「서버」 · 511-B B-5) —
|
||||
* stage.follow = { patch · plus · followed }(곁 자료 = 페이지가 다시 붙임). */
|
||||
until?: "follow";
|
||||
};
|
||||
}
|
||||
|
||||
@@ -107,6 +125,12 @@ async function runStages(
|
||||
plusStations: stages.input.plusStations ?? null,
|
||||
};
|
||||
const stored = detail && readAlignment(detail.longitudinal);
|
||||
if (stages.definition) useCatalogDefinition(stages.definition);
|
||||
// 페이지 `savedEdits` 짝 — 세션 초안 없으면 저장 종단 편집 · 없으면 빈 편집(창구가 longitudinal 을 붙인 때)
|
||||
const storedEdits = () =>
|
||||
stages.longitudinal === undefined
|
||||
? null
|
||||
: ((stages.longitudinal && readAlignment(stages.longitudinal)?.edits) ?? emptyEdits());
|
||||
const book = pipeBook(stages.pipe_ids ?? null);
|
||||
// 따라감 기준(페이지 `beforeDraft ?? 가게`) — 초안 전 관 · 구조물(null = 저장분)
|
||||
const before = stages.before_draft ?? stages.input;
|
||||
@@ -116,7 +140,7 @@ async function runStages(
|
||||
saved: () => ({
|
||||
pipes: before.pipes ?? savedPipes,
|
||||
structures: before.structures ?? savedStructures,
|
||||
alignment: stages.saved_edits ?? inputs.alignment ?? emptyEdits(),
|
||||
alignment: stages.saved_edits ?? storedEdits() ?? inputs.alignment ?? emptyEdits(),
|
||||
}),
|
||||
stationsAt: (route, patch, at) =>
|
||||
buildStations(
|
||||
@@ -128,50 +152,50 @@ async function runStages(
|
||||
).map((station) => station.chainageM),
|
||||
});
|
||||
const crossStage = createCrossStage({ grid: () => grid });
|
||||
const pipe = createDesignPipeline({
|
||||
stages: {
|
||||
route: createRouteStage({
|
||||
minRadiusM: () => plan.min_radius_m ?? 0,
|
||||
savedLine: () => ({ line: savedLine(), curves: savedCurves() }),
|
||||
}),
|
||||
follow: (ctx) =>
|
||||
followStage({
|
||||
...ctx,
|
||||
inputs: placedFollowInputs(ctx, savedLine(), {
|
||||
pipes: savedPipes,
|
||||
structures: savedStructures,
|
||||
}),
|
||||
const table: PipelineStages = {
|
||||
route: createRouteStage({
|
||||
minRadiusM: () => plan.min_radius_m ?? 0,
|
||||
savedLine: () => ({ line: savedLine(), curves: savedCurves() }),
|
||||
}),
|
||||
follow: (ctx) =>
|
||||
followStage({
|
||||
...ctx,
|
||||
inputs: placedFollowInputs(ctx, savedLine(), {
|
||||
pipes: savedPipes,
|
||||
structures: savedStructures,
|
||||
}),
|
||||
stations: createStationsStage({
|
||||
intervalM: stages.interval_m,
|
||||
types: () => enabled,
|
||||
saved: () => plan,
|
||||
}),
|
||||
ground: createGroundStage({
|
||||
grid: () => grid,
|
||||
remote: () => Promise.reject(new Error("DTM 격자 없음 — 서버 표본 길은 파이썬")),
|
||||
}),
|
||||
planline: createPlanlineStage({
|
||||
savedBase: (at) =>
|
||||
plantedBase({ base: null, detail }, at, types, (pipes) =>
|
||||
pipes.map((pipe) => ({ ...pipe, id: book.at(pipe.chainage_m) })),
|
||||
),
|
||||
savedLine,
|
||||
storedPvi: () => (stored ? toAlignmentBase(stored).basePvi : null),
|
||||
}),
|
||||
profile: profileStage,
|
||||
cross: (ctx) => {
|
||||
const status = shapeStatus(
|
||||
ctx.inputs.pipes ?? savedPipes,
|
||||
ctx.inputs.structures ?? savedStructures,
|
||||
types,
|
||||
);
|
||||
const list = Array.isArray(ctx.inputs.structures) ? ctx.inputs.structures : [];
|
||||
const spans = bermSpansFromStructures(list);
|
||||
return crossStage.stage(contextCross(ctx, status, (at) => bermSpecAt(spans, at)));
|
||||
},
|
||||
stations: createStationsStage({
|
||||
intervalM: stages.interval_m,
|
||||
types: () => enabled,
|
||||
saved: () => plan,
|
||||
}),
|
||||
ground: createGroundStage({
|
||||
grid: () => grid,
|
||||
remote: () => Promise.reject(new Error("DTM 격자 없음 — 서버 표본 길은 파이썬")),
|
||||
}),
|
||||
planline: createPlanlineStage({
|
||||
savedBase: (at) =>
|
||||
plantedBase({ base: null, detail }, at, types, (pipes) =>
|
||||
pipes.map((pipe) => ({ ...pipe, id: book.at(pipe.chainage_m) })),
|
||||
),
|
||||
savedLine,
|
||||
storedPvi: () => (stored ? toAlignmentBase(stored).basePvi : null),
|
||||
}),
|
||||
profile: profileStage,
|
||||
cross: (ctx) => {
|
||||
const status = shapeStatus(
|
||||
ctx.inputs.pipes ?? savedPipes,
|
||||
ctx.inputs.structures ?? savedStructures,
|
||||
types,
|
||||
);
|
||||
const list = Array.isArray(ctx.inputs.structures) ? ctx.inputs.structures : [];
|
||||
const spans = bermSpansFromStructures(list);
|
||||
return crossStage.stage(contextCross(ctx, status, (at) => bermSpecAt(spans, at)));
|
||||
},
|
||||
});
|
||||
};
|
||||
if (stages.until === "follow") return followOnly(table, inputs);
|
||||
const pipe = createDesignPipeline({ stages: table });
|
||||
try {
|
||||
const record = await pipe.start(inputs);
|
||||
await pipe.settled();
|
||||
@@ -184,6 +208,29 @@ async function runStages(
|
||||
}
|
||||
}
|
||||
|
||||
/** route · follow 두 단계만 — 묶음 첫 바퀴와 같은 맥락(앞 출력 없음 = 저장분에서 · 페이지 「다시 열기」 꼴). */
|
||||
async function followOnly(
|
||||
table: PipelineStages,
|
||||
inputs: PipelineInputs,
|
||||
): Promise<Record<StageName, unknown>> {
|
||||
const none = Object.fromEntries(
|
||||
[...BROWSER_STAGES, ...AFTER_STAGES].map((name) => [name, null]),
|
||||
) as StageContext<"route">["out"];
|
||||
const at = <K extends StageName>(out: StageContext<K>["out"]): StageContext<K> => ({
|
||||
generation: 1,
|
||||
range: null,
|
||||
signal: new AbortController().signal,
|
||||
inputs,
|
||||
out,
|
||||
before: none,
|
||||
prev: null,
|
||||
});
|
||||
const route = await table.route!(at<"route">(none));
|
||||
const follow = await table.follow!(at<"follow">({ ...none, route }));
|
||||
const out = { route: plain(route), follow: { patch: plain(follow), ...followSides(follow) } };
|
||||
return { stations: null, profile: null, cross: null, ...out } as Record<StageName, unknown>;
|
||||
}
|
||||
|
||||
/** id → 셈(입구 `RUNNERS` 에 얹음 · `available` 에는 안 넣음) — 바퀴 하나를 id 들이 나눠 씀. */
|
||||
export function stageRunners(inputs: StagesInputs): Record<string, () => Promise<unknown>> {
|
||||
let run: Promise<Record<StageName, unknown>> | null = null;
|
||||
|
||||
@@ -83,6 +83,8 @@ export interface DesignPipeline {
|
||||
value: unknown,
|
||||
part?: RangePart | null,
|
||||
): Promise<PipelineRunRecord> | null;
|
||||
/** 입력 그대로 그 출처부터 다시 셈 — 늦게 온 서버 값(계산 자리 follow 「서버」 · 511-B B-5). */
|
||||
refresh(source: ChangeSource): Promise<PipelineRunRecord> | null;
|
||||
/** `name` = 기록 `work` 칸 이름(알림 받는 쪽 몫 ms · 349-0s). */
|
||||
subscribe(fn: (event: PipelineEvent) => void, name?: string): () => void;
|
||||
output<K extends StageName>(name: K): PipelineOutputs[K] | null;
|
||||
@@ -353,6 +355,11 @@ export function createDesignPipeline(options: { stages: PipelineStages }): Desig
|
||||
: { sources: [source], fromM: 0, toM: Infinity, keys: [] };
|
||||
return begin(range);
|
||||
},
|
||||
refresh(source) {
|
||||
if (busy) return null;
|
||||
inputVer[SOURCE_INPUT[source]] += 1;
|
||||
return begin({ sources: [source], fromM: 0, toM: Infinity, keys: [] });
|
||||
},
|
||||
subscribe(fn, name = "알림") {
|
||||
listeners.set(fn, name);
|
||||
return () => listeners.delete(fn);
|
||||
|
||||
Reference in New Issue
Block a user