feat(계산묶음): 346-1 · 346-2 남은 단계 붙임 — cross · masshaul · haul · drainage · corridor · 노선 초안 동안 유토곡선 = 새 노선 횡단

- design_pipeline_page: 단계 다섯 한 줄씩(부르기만) · 단면 유형 기본값 = 새 지형 상단측(서버 노선 저장 규칙) · haul · corridor 는 노선 초안 동안만 · 열 때 한 번 셈(발자국 바로 보임)
- 노선 초안 동안 종단도 = 새 노선 상세(pipelineDetail — 지반 · 측점 · 횡단 면적 → 유토곡선) · 저장 노선 횡단 다시 셈 안 함
- 횡단 입력 = 저장 상세의 측점 입력을 측점 열쇠로(design_pipeline_cross · 넘기는 칸 = 서버 CARRY_INPUT_KEYS 와 같음)
- 저장 관 목록(좌표 · id) = 관 입력 바탕 · 첫 계산도 저장 노선 기준으로 좌표 따라감(createFollowStage)
- 노선 초안 동안 계획고 = alignment-draft 노선 0 칸(DRAFT_ROUTE_ID) · 초안 버리면 비움 · [저장] 이음 = sub5
- 상단측 짝(logic/B05_Profile_Pipeline_Detail uphillSide ↔ Engine_Sections_Core uphill_side) · 측점 열쇠 = common_util_station_key 한 벌
- 시험 test_346_2_pipeline_cross_page 14 · 옛 시험 둘 새 꼴에 맞춤(317 · 230_2) · 346-1 도움 파일 창구 셋 가짜

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 12:32:11 +09:00
co-authored by Claude Opus 5.5
parent 5795f359cc
commit a35d49b4e2
13 changed files with 1098 additions and 86 deletions
@@ -0,0 +1,95 @@
/* =============================================================================
* A00_Common/design/design_pipeline_cross.ts
* 계산 묶음 cross 입력 모음(PLAN 346-2 붙임) — 저장 노선 상세의 측점마다 횡단 입력 · 얹힌 시설 제원을
* **측점 열쇠**로 모음. 노선이 바뀌어도 같은 열쇠 측점(격자 번호 · 관 id · 구조물 id)이 그 입력을 받음.
* 열쇠 = 저장 노선을 stations 단계와 같은 함수로 짚은 측점(`buildStations`)과 누가거리로 짝지음 —
* 묶음 측점과 같은 열쇠 규칙(관 = 저장 관 id).
*
* 넘기는 칸 = 서버 노선 저장(`CARRY_INPUT_KEYS`)과 같은 일곱(지반 유형 · 측구 종류 · 측구 선택 · 2단 경사 ·
* 암 경계 · 암 경사 · 소단) + 얹힌 시설 맥락(포장 · 물넘이 기울임 · 세월교 노면 하강). 단면 유형 · 측구
* 방향은 새 지형이 정함(`logic/B05_Profile_Pipeline_Detail.ts` `crossWithUphill`) · 노견 확장 · 소단
* 깊이는 안 넘김. 값 = 상세에 얹힌 설계(세션 고침은 종단도 · 횡단 보기가 다시 셈하며 이미 얹음).
* ========================================================================== */
import type { CrossSection, SectionDetailResponse } from "../../B06_Section/B06_Section_Api_Fetch";
import { effectiveStandardCross } from "../../B06_Section/B06_Section_UI_Standard_Panel";
import type { CrossInput, SectionSpecs } from "../../B05_Profile/logic/B05_Profile_Pipeline_Detail";
import type { PipelineStation } from "./design_pipeline_types";
export interface PipelineCrossSource {
input: CrossInput;
/** 측점 열쇠 → 얹힌 시설 제원. */
specs: Map<string, SectionSpecs>;
}
const SPEC_KEYS = [
"structure",
"culvert",
"ford",
"box",
"ford_pavement",
"revetment",
"pavement",
"underdrain",
] as const;
/** 같은 측점으로 보는 누가거리 차(m) — 측점 열쇠 글 자릿수(0.01 m). */
const SAME_M = 0.01;
const finite = (value: unknown): number | null =>
typeof value === "number" && Number.isFinite(value) ? value : null;
/** 측점 하나의 넘기는 입력(cross 단계 `CrossStationInput` 꼴). */
function carried(section: CrossSection): Record<string, unknown> {
const design = (section.design ?? {}) as unknown as Record<string, unknown>;
const berm = design.berm as
{ width_m: number; interval_m: number; slope_deg: number; depth_m?: number } | undefined;
const cutSlope = finite(design.cut_slope_ratio_user);
const drop = Number(section.ford?.overflow_depth_m);
return {
...(typeof design.ground_type === "string" ? { groundType: design.ground_type } : {}),
...(typeof design.ditch_type === "string" ? { ditchType: design.ditch_type } : {}),
ditchChoice: typeof design.ditch_choice === "boolean" ? design.ditch_choice : null,
twoStageSlope: Boolean(design.two_stage_slope),
rockBoundaryOffsetM: finite(design.rock_boundary_offset_m),
cutSlopeRatio: cutSlope !== null && cutSlope > 0 ? cutSlope : null,
berm: berm
? {
widthM: berm.width_m,
intervalM: berm.interval_m,
slopeDeg: berm.slope_deg,
depthM: berm.depth_m,
}
: null,
paved: Boolean(section.pavement?.length) || Boolean(section.ford_pavement),
tilted: Boolean(section.ford_pavement || section.underdrain),
tiltPct: section.ford_pavement?.slope_pct ?? section.underdrain?.slope_pct ?? null,
surfaceDropM: drop > 0 ? drop : 0,
};
}
/** 저장 상세 + 저장 노선 측점(열쇠) → cross 입력 · 시설 제원 — 표준단면 · 상세가 없으면 null. */
export function pipelineCrossSource(
projectId: string,
detail: SectionDetailResponse | null,
savedStations: ReadonlyArray<PipelineStation>,
): PipelineCrossSource | null {
const standard = detail && effectiveStandardCross(projectId);
if (!detail || !standard) return null;
const byKey: CrossInput["byKey"] = {};
const specs = new Map<string, SectionSpecs>();
for (const section of detail.cross_sections) {
let best: PipelineStation | null = null;
for (const station of savedStations) {
const gap = Math.abs(station.chainageM - section.chainage_m);
if (gap < SAME_M && (!best || gap < Math.abs(best.chainageM - section.chainage_m)))
best = station;
}
if (!best || byKey[best.key]) continue;
byKey[best.key] = carried(section);
const own: Record<string, unknown> = {};
for (const name of SPEC_KEYS) if (section[name] !== undefined) own[name] = section[name];
specs.set(best.key, own as SectionSpecs);
}
return { input: { standard, byKey }, specs };
}
+176 -47
View File
@@ -1,39 +1,58 @@
/* =============================================================================
* A00_Common/design/design_pipeline_page.ts
* B05 · B06 한 페이지의 계산 묶음 하나(PLAN 346-1) — 프로젝트마다 하나 · 페이지를 떠나면 버림
* (종단도 `dispose`). 단계 붙임 · 입력 모음 · 가게 잇기만 — 셈은 단계 파일 몫.
* B05 · B06 한 페이지의 계산 묶음 하나(PLAN 346-1 · 346-2 붙임) — 프로젝트마다 하나 · 페이지를 떠나면
* 버림(종단도 `dispose`). 단계 붙임 · 입력 모음 · 가게 잇기만 — 셈은 단계 파일 몫.
*
* route · ground `B05_Profile/logic/B05_Profile_Pipeline_RouteGround.ts`(sub8)
* follow · planline · profile `B05_Profile/logic/B05_Profile_Pipeline_Grade.ts`
* stations `design_pipeline_stations.ts` · footprint `B05_Profile_Footprint_Stage.ts`(sub6)
* cross · masshaul · haul(sub2) · drainage(sub3) · corridor(sub5) — 붙으면 `stages` 한 줄씩
* (없으면 건너뜀 · 계약 규칙).
* cross · masshaul · haul `B06_Section_Cross_Stage` · `logic/B05_Profile_MassHaul_Stage` ·
* `B05_Profile_Api_HaulPlan`(sub2) · drainage 배수유역 판 `pipelineStage`(sub3) · corridor 3D 「낡음」.
* haul · corridor 는 노선 초안 동안만(저장 노선은 유토곡선 판 · 횡단 다시 셈이 맡음).
*
* 입력 — 노선 = 편집기 다리(`RouteDraftBridge` · 캐시 칸 route-draft) · 계획고 = 종단도 편집
* (`pipelineGrade`) · 관 · 구조물 = 가게 초안(없으면 구조물은 저장분). 출력 = 가게 stations ·
* profile · footprint · generation + 노선 초안이 있으면 종단도가 새 첫 계획선 · 옮긴 계획고를 그림.
* 첫 계산은 처음 고칠 때(또는 저장 안 한 노선 초안이 있을 때) — 열기만 하면 안 셈.
* [노선 반영] · [저장]이 서버 정본 — 여기 값은 미리보기(346-3 전까지).
* (`pipelineGrade` · 노선 초안 동안은 alignment-draft 노선 `DRAFT_ROUTE_ID`) · 횡단 = 저장 상세의 측점
* 입력(`design_pipeline_cross.ts`) · 관 = 가게 초안 → 저장 관 목록(좌표 · id) · 구조물 = 가게 초안 → 저장분.
* 출력 — 가게 stations · profile · footprint · generation · 노선 초안이 있으면 종단도가 새 첫 계획선 ·
* 옮긴 계획고 · 새 노선 상세(`pipelineDetail` — 지반 · 측점 · 횡단 면적 → 유토곡선)를 그림.
* 첫 계산 = 열 때 한 번(저장 노선 · 발자국 · 저장 안 한 노선 초안이 있으면 그 노선).
* [저장](sub5)이 서버 정본 — 노선을 먼저 보내고 `pendingAlignment(DRAFT_ROUTE_ID)` 를 새 노선에 보냄.
* ========================================================================== */
import { createDesignPipeline, publishToStore, type DesignPipeline } from "./design_pipeline";
import { pipelineCrossSource, type PipelineCrossSource } from "./design_pipeline_cross";
import { createStationsStage } from "./design_pipeline_stations";
import type { PipelineInputs, RouteDraft, Vertex } from "./design_pipeline_types";
import type {
PipelineInputs,
PipelineStation,
RouteDraft,
StageFn,
Vertex,
} from "./design_pipeline_types";
import { useDesignStore } from "./design_store";
import { fetchDetailPipePoints } from "../../B04_PreProcess/B04_PreProcess_Api_Fetch";
import type { SectionDetailResponse } from "../../B06_Section/B06_Section_Api_Fetch";
import { createCrossStage } from "../../B06_Section/B06_Section_Cross_Stage";
import {
emptyEdits,
type AlignmentBase,
type AlignmentEdits,
type ProfileAlignment,
} from "../../B05_Profile/B05_Profile_UI_Profile_Alignment";
import {
markAlignmentSaved,
readAlignmentDraft,
} from "../../B05_Profile/B05_Profile_UI_Profile_Edit";
import {
analysisTerrain,
fetchRoutePlan,
type RoutePlanResponse,
} from "../../B05_Profile/B05_Profile_Api_Replan";
import { createHaulStage } from "../../B05_Profile/B05_Profile_Api_HaulPlan";
import { fetchStructures, fetchStructureTypes } from "../../B05_Profile/B05_Profile_Api_Structures";
import {
designTerrainGridNow,
groundStageDeps,
loadDesignTerrainGrid,
} from "../../B05_Profile/B05_Profile_Api_TerrainGrid";
import {
createGroundStage,
@@ -41,25 +60,57 @@ import {
draftLine,
} from "../../B05_Profile/logic/B05_Profile_Pipeline_RouteGround";
import {
createFollowStage,
createPlanlineStage,
followStage,
gradeRange,
profileStage,
routeRange,
} from "../../B05_Profile/logic/B05_Profile_Pipeline_Grade";
import {
crossWithUphill,
draftSectionDetail,
type CrossInput,
} from "../../B05_Profile/logic/B05_Profile_Pipeline_Detail";
import { buildStations } from "../../B05_Profile/logic/B05_Profile_Sections_Stations";
import { footprintStage } from "../../B05_Profile/logic/B05_Profile_Footprint_Stage";
import type { PlanType } from "../../common_util/common_util_structure_station";
import {
createMassHaulStage,
type MassHaulStageContext,
} from "../../B05_Profile/logic/B05_Profile_MassHaul_Stage";
import type {
PlanPipe,
PlanStructure,
PlanType,
} from "../../common_util/common_util_structure_station";
import type {
RouteDraftBridge,
RouteDraftEdit,
} from "../../B05_Profile/B05_Profile_UI_RouteEdit_Draft";
/** 노선 초안 동안의 계획고 초안 칸 — alignment-draft 의 노선 0(노선 번호는 [저장] 때 남). */
export const DRAFT_ROUTE_ID = 0;
/** 종단도(`design_profile`)가 묶음에 내주는 것. */
export interface ProfileLink {
/** 저장 노선의 종단 기준(측점 한 벌) · 지금 계획고 편집. */
pipelineSource(): { base: AlignmentBase | null; edits: AlignmentEdits };
/** 저장 노선의 종단 기준(측점 한 벌) · 지금 계획고 편집 · 저장 상세 · 노선 번호. */
pipelineSource(): {
base: AlignmentBase | null;
edits: AlignmentEdits;
detail?: SectionDetailResponse | null;
routeId?: number | null;
};
/** 노선 초안으로 셈한 첫 계획선 · 옮긴 계획고를 그림. */
adoptPipeline(base: AlignmentBase, edits: AlignmentEdits, draft: boolean): void;
/** 새 노선 횡단이 왔음 — 다시 그림(유토곡선). */
redraw?(): void;
/** 배수유역 판 — drainage 단계. */
side?: { pipelineStage: StageFn<"drainage"> } | null;
}
export interface PagePipelineOptions {
stationIntervalM?: number;
/** 노선 초안 동안 종단 · 횡단이 바뀜 — 3D 「낡음」. */
onCorridorStale?: () => void;
}
interface PagePipeline {
@@ -67,6 +118,8 @@ interface PagePipeline {
bridge: RouteDraftBridge;
grade(edits: AlignmentEdits): void;
restart(): void;
earthwork(context: MassHaulStageContext | null): void;
detail(): SectionDetailResponse | null;
dispose(): void;
}
@@ -77,7 +130,7 @@ const same = (a: unknown, b: unknown): boolean => JSON.stringify(a) === JSON.str
export function routeDraftBridge(
projectId: string,
link: ProfileLink,
options: { stationIntervalM?: number } = {},
options: PagePipelineOptions = {},
): RouteDraftBridge {
let page = pages.get(projectId);
if (!page) pages.set(projectId, (page = createPagePipeline(projectId, link, options)));
@@ -88,9 +141,17 @@ export function routeDraftBridge(
export const pipelineGrade = (projectId: string, edits: AlignmentEdits): void =>
pages.get(projectId)?.grade(edits);
/** 저장 종단 기준이 바뀜(새 상세 · 구조물 측점) — 셈 중이면 처음부터 다시. */
/** 저장 종단 기준이 바뀜(새 상세 · 구조물 측점) — 처음부터 다시. */
export const restartPagePipeline = (projectId: string): void => pages.get(projectId)?.restart();
/** 유토곡선 설정(토량환산계수 · 자연방토 경사) — masshaul 단계 재료 · 표준단면이 막 왔으면 횡단 입력도. */
export const pipelineEarthwork = (projectId: string, context: MassHaulStageContext | null): void =>
pages.get(projectId)?.earthwork(context);
/** 노선 초안 동안 종단도가 그릴 새 노선 상세 — 초안이 없으면 null(저장 상세). */
export const pipelineDetail = (projectId: string): SectionDetailResponse | null =>
pages.get(projectId)?.detail() ?? null;
/** 지금 묶음(개발 · 시험) — 없으면 null. */
export const pagePipeline = (projectId: string): DesignPipeline | null =>
pages.get(projectId)?.pipe ?? null;
@@ -103,12 +164,21 @@ export function disposePagePipeline(projectId: string): void {
function createPagePipeline(
projectId: string,
link: ProfileLink,
options: { stationIntervalM?: number },
options: PagePipelineOptions,
): PagePipeline {
const store = useDesignStore(projectId, null);
/** 노선 0 칸 — 종단도 편집 가게(`createProfileEditStore`)와 같은 자리(지금 프로젝트). */
const draftAlignment = useDesignStore(null, DRAFT_ROUTE_ID);
let plan: RoutePlanResponse | null = null;
let types: PlanType[] = [];
let savedStructures: unknown[] = [];
let savedStructures: PlanStructure[] = [];
let savedPipes: PlanPipe[] = [];
let earthwork: MassHaulStageContext | null = null;
/** 저장 노선 측점(열쇠) · 그것으로 모은 횡단 입력 — 저장 상세가 바뀌면 다시. */
let savedStations: PipelineStation[] = [];
let crossSource: { detail: SectionDetailResponse; source: PipelineCrossSource | null } | null =
null;
let shown: SectionDetailResponse | null = null;
let ready = false;
let started = false;
/** 앞 종단이 노선 초안으로 셈한 것인가 — 초안을 버리면 저장 기준으로 다시 그림. */
@@ -117,75 +187,127 @@ function createPagePipeline(
const savedLine = (): Vertex[] | null =>
plan ? plan.planned.map(([x, y]): Vertex => [x, y]) : null;
const savedCurves = () =>
(plan?.curves ?? []).map(({ start, apex, end, radius_m }) => ({ start, apex, end, radius_m }));
const crossNow = (): PipelineCrossSource | null => {
const detail = link.pipelineSource().detail ?? null;
if (!detail) return null;
if (crossSource?.detail !== detail || !crossSource.source)
crossSource = { detail, source: pipelineCrossSource(projectId, detail, savedStations) };
return crossSource.source;
};
const crossStage = createCrossStage();
const haulStage = createHaulStage(projectId, () => link.pipelineSource().routeId ?? undefined);
const pipe = createDesignPipeline({
stages: {
route: createRouteStage({
minRadiusM: () => plan?.min_radius_m ?? 0,
savedLine: () =>
plan && {
line: savedLine()!,
curves: plan.curves.map(({ start, apex, end, radius_m }) => ({
start,
apex,
end,
radius_m,
})),
},
savedLine: () => plan && { line: savedLine()!, curves: savedCurves() },
}),
follow: followStage,
follow: createFollowStage({ savedLine }),
stations: createStationsStage({ intervalM: options.stationIntervalM, types: () => types }),
ground: createGroundStage(groundStageDeps(projectId)),
planline: createPlanlineStage({ savedBase: () => link.pipelineSource().base, savedLine }),
profile: profileStage,
// 단면 유형 기본값 = 새 지형 상단측(서버 노선 저장 규칙) — 단계 본문은 그대로 부름
cross: (ctx) =>
crossStage.stage({
...ctx,
inputs: {
...ctx.inputs,
cross: crossWithUphill(
ctx.inputs.cross as CrossInput | null,
ctx.out.ground?.stations ?? [],
),
},
}),
footprint: footprintStage,
masshaul: createMassHaulStage(() => earthwork),
haul: (ctx) => (ctx.inputs.route ? haulStage(ctx) : (ctx.prev ?? null)),
drainage: link.side?.pipelineStage,
corridor: (ctx) => {
if (ctx.inputs.route) options.onCorridorStale?.();
return null;
},
},
});
pipe.subscribe(publishToStore(store));
pipe.subscribe((event) => {
if (event.kind !== "stage" || event.stage !== "profile") return;
if (event.kind !== "stage" || (event.stage !== "profile" && event.stage !== "cross")) return;
const draft = pipe.input("route") !== null;
const saved = link.pipelineSource().detail ?? null;
const ground = pipe.output("ground");
const profile = pipe.output("profile") as ProfileAlignment | null;
shown =
draft && saved && ground && profile
? draftSectionDetail(
saved,
ground,
profile,
pipe.output("cross"),
crossSource?.source?.specs ?? new Map(),
options.stationIntervalM ?? profile.policy.station_interval_m,
)
: null;
if (event.stage === "cross") return void (draft && link.redraw?.());
const base = pipe.output("planline");
const edits = (pipe.input("alignment") as AlignmentEdits | null) ?? emptyEdits();
if (draft) draftAlignment.set("alignmentDraft", edits);
else if (drawnDraft) markAlignmentSaved(DRAFT_ROUTE_ID); // 초안을 버림 — 노선 0 칸도 비움
if ((draft || drawnDraft) && base) link.adoptPipeline(base, edits, draft);
drawnDraft = draft;
});
const inputsNow = (): PipelineInputs => ({
route: store.get("routeDraft"),
alignment: link.pipelineSource().edits,
cross: null,
// ponytail: 저장 관 목록(좌표)은 아직 안 받음 — 관 초안만. 346-3 에서 관 정본을 가게로.
pipes: store.get("pipes") ?? [],
structures: store.get("structures") ?? savedStructures,
});
const inputsNow = (): PipelineInputs => {
const route = store.get("routeDraft");
return {
route,
alignment: (route && readAlignmentDraft(DRAFT_ROUTE_ID)) || link.pipelineSource().edits,
cross: crossNow()?.input ?? null,
pipes: store.get("pipes") ?? savedPipes,
structures: store.get("structures") ?? savedStructures,
};
};
const start = (): void => {
if (disposed || !ready || !link.pipelineSource().base) return;
started = true;
void pipe.start(inputsNow());
};
/** 저장 노선(그린 선 · 곡선표 · 기본 반경) — [노선 반영] 뒤 다시 받음(`restart`). */
/** 저장 노선(그린 선 · 곡선표 · 기본 반경) · 그 측점 열쇠 — [저장] 뒤 다시 받음(`restart`). */
const loadPlan = (): Promise<void> =>
fetchRoutePlan(projectId)
.then((routePlan) => void (plan = routePlan))
.then((routePlan) => {
plan = routePlan;
savedStations = buildStations(savedLine()!, savedCurves(), savedPipes, savedStructures, {
intervalM: options.stationIntervalM,
types,
stationSnap: routePlan.station_snap ?? false,
});
crossSource = null;
})
.catch(() => undefined);
designTerrainGridNow(projectId); // 지형 격자 미리 받기(받는 동안은 서버 길)
void Promise.all([
loadPlan(),
fetchStructureTypes().catch(() => []),
fetchStructures(projectId).catch(() => null),
fetchDetailPipePoints(projectId).catch(() => null),
loadDesignTerrainGrid(projectId).catch(() => null), // 지형 격자(없으면 서버 길)
])
.then(([, structureTypes, list]) => {
.then(([structureTypes, list, pipes]) => {
types = structureTypes as PlanType[];
savedStructures = list?.structures ?? [];
savedStructures = (list?.structures ?? []) as PlanStructure[];
savedPipes = (pipes?.pipe_points ?? []).map(({ lonlat: _lonlat, ...pipe }) => pipe);
return loadPlan();
})
.then(() => {
ready = plan !== null;
if (store.get("routeDraft")) start(); // 저장 안 한 노선 초안 — 열 때부터 그 노선으로
start(); // 열 때 한 번 — 발자국 · 저장 안 한 노선 초안이 있으면 그 노선으로
})
.catch(() => undefined);
const unsubscribe = [
store.subscribe("pipes", (pipes) => started && void pipe.update("pipe", pipes ?? [])),
store.subscribe("pipes", (pipes) => started && void pipe.update("pipe", pipes ?? savedPipes)),
store.subscribe(
"structures",
(list) => started && void pipe.update("structure", list ?? savedStructures),
@@ -220,8 +342,15 @@ function createPagePipeline(
void pipe.update("grade", edits, gradeRange(before, edits, nodes));
},
restart() {
if (started) void loadPlan().then(start);
if (!started) return start();
void loadPlan().then(start);
},
earthwork(context) {
earthwork = context;
const cross = crossNow()?.input ?? null;
if (started && cross) void pipe.update("cross", cross);
},
detail: () => shown,
dispose() {
disposed = true;
unsubscribe.forEach((stop) => stop());
+19 -14
View File
@@ -71,7 +71,7 @@ import {
} from "../../B05_Profile/B05_Profile_UI_Profile_Drag";
import { planCurveWarning, sampleAt } from "../../B05_Profile/B05_Profile_UI_Profile_Guard";
import { listedBase } from "./design_profile_base";
import { disposePagePipeline, pipelineGrade, restartPagePipeline } from "./design_pipeline_page";
import * as pagePipeline from "./design_pipeline_page";
import type { RouteProfilePanelCallbacks } from "../../B05_Profile/B05_Profile_UI_Profile_Panel_Types";
import type { ElevationWindowResult } from "@util/common_util_chart_ywindow";
import { frameOf } from "@util/common_util_view_window";
@@ -220,7 +220,7 @@ export function createDesignProfile(ctx: DesignProfileContext) {
let storedBase: AlignmentBase | null = null;
let alignment: ProfileAlignment | null = null;
let store = createProfileEditStore(null, emptyEdits(), () => rebuild());
/** 계산 묶음이 노선 초안으로 셈한 종단을 그리는 동안의 편집 한 벌 — 메모리만(346-1). */
/** 계산 묶음이 노선 초안으로 셈한 종단을 그리는 동안의 편집 한 벌 — 노선 0 칸(alignment-draft · 346-1). */
let preview: typeof store | null = null;
/** 서버 저장분 — 되돌리기 복원이 초안을 다시 읽을 때 기준으로 쓴다. */
let savedEdits: AlignmentEdits = emptyEdits();
@@ -350,7 +350,7 @@ export function createDesignProfile(ctx: DesignProfileContext) {
*/
function rebuild(): void {
if (base) alignment = buildAlignment(base, store.edits());
pipelineGrade(projectId, store.edits());
pagePipeline.pipelineGrade(projectId, store.edits());
// 계획고가 바뀌면 측점별 횡단 단면적도 함께 바뀐다 — 전 측점을 다시 계산해 공유
// 캐시에 얹는다. 그래야 **횡단 기준** 유토곡선과 횡단 카드가 따라 움직인다
// (2026-08-03 사용자 보고: B05에서 계획선을 끌어도 횡단 곡선이 그대로였음).
@@ -369,14 +369,15 @@ export function createDesignProfile(ctx: DesignProfileContext) {
detail: () => detail,
routeId: () => routeId,
edits: () => store.edits(),
alignment: () => alignment,
alignment: () => (store === preview ? null : alignment),
debounceMs: () => (root.dataset.view === "cross" ? 120 : CROSS_PREVIEW_DEBOUNCE_MS),
onApplied: (updated) => {
draw();
if (updated.length) callbacks?.onCrossDesignsUpdated?.(updated);
},
});
const scheduleCrossPreview = (): void => crossPreview.schedule();
// 노선 초안 동안은 저장 노선 횡단을 안 고침 — 새 노선 횡단은 계산 묶음 cross(346-2)
const scheduleCrossPreview = (): void => void (store !== preview && crossPreview.schedule());
/**
* 높이 캐스케이드(2026-08-05 사용자 확정): 자리가 모자라면 ① 종단이 먼저 최소까지
@@ -404,13 +405,14 @@ export function createDesignProfile(ctx: DesignProfileContext) {
function draw(): void {
renderProfile({
onStaleDesigns: crossPreview.scheduleIfStale, // 그린 뒤 생긴 낡음도 다시 셈(317-1)
// 그린 뒤 생긴 낡음도 다시 셈(317-1) · 노선 초안 동안은 묶음 cross 를 기다림
onStaleDesigns: (key) => (store === preview ? key !== "" : crossPreview.scheduleIfStale(key)),
body,
callbacks,
massHaul,
tableOverlay,
store,
detail: () => detail,
detail: () => pagePipeline.pipelineDetail(projectId) ?? detail,
alignment: () => alignment,
base: () => base,
stationInterval: () => stationInterval,
@@ -502,7 +504,7 @@ export function createDesignProfile(ctx: DesignProfileContext) {
// 이월분은 base 설정 후 미저장 초안으로 커밋한다(확정 시 전송·재탐색 후 새로고침에도 유지).
if (carried) store.replace(carried);
alignment = base ? buildAlignment(base, store.edits()) : null;
restartPagePipeline(projectId);
pagePipeline.restartPagePipeline(projectId);
// 유토곡선 balloon 위치 캐시 — 두 보기가 **같은 scope**를 써서 같은 자리를 공유한다.
// 영구저장소 값(detail.balloon_offsets)이 있으면 그것이 이긴다.
configureBalloonOffsets(
@@ -540,6 +542,7 @@ export function createDesignProfile(ctx: DesignProfileContext) {
*/
setEarthworkContext(next: RouteMassHaulContext | null) {
massHaul.setContext(next);
pagePipeline.pipelineEarthwork(projectId, next);
draw();
},
setSelectedStation(stationId: string | null) {
@@ -579,7 +582,7 @@ export function createDesignProfile(ctx: DesignProfileContext) {
irregularStations = stations;
base = listed();
if (base) alignment = buildAlignment(base, store.edits());
restartPagePipeline(projectId);
pagePipeline.restartPagePipeline(projectId);
draw();
},
/** 구조물 타입 레지스트리를 받아 마크 색·약호에 쓴다(최초 1회 · 우클릭 목록은 사이드 패널 몫). */
@@ -625,14 +628,16 @@ export function createDesignProfile(ctx: DesignProfileContext) {
},
isDirty: () => store.dirty(),
/** 계산 묶음(346-1) — 저장 노선의 종단 기준 · 지금 계획고 편집. */
pipelineSource: () => ({ base: listed(), edits: store.edits() }),
/** 노선 초안으로 셈한 첫 계획선 · 옮긴 계획고를 그림 — 초안 동안 편집은 메모리만(저장 노선에
* 안 씀 · [노선 반영] · [저장]이 서버 정본) · 초안을 버리면 저장 기준 · 세션 편집으로 돌아옴. */
pipelineSource: () => ({ base: listed(), edits: store.edits(), detail, routeId }),
/** 노선 초안으로 셈한 첫 계획선 · 옮긴 계획고를 그림 — 초안 동안 편집은 노선 0 칸(저장 노선에
* 안 씀 · [저장]이 노선 뒤 새 노선에 보냄) · 초안을 버리면 저장 기준 · 세션 편집으로 돌아옴. */
adoptPipeline(nextBase: AlignmentBase, edits: AlignmentEdits, draft: boolean) {
const sameEdits = JSON.stringify(edits) === JSON.stringify(store.edits());
if (draft && store === preview && base === nextBase && sameEdits) return; // 이미 그림
if (draft && (store !== preview || !sameEdits))
store = preview = createProfileEditStore(null, edits, () => rebuild());
store = preview = createProfileEditStore(pagePipeline.DRAFT_ROUTE_ID, edits, () =>
rebuild(),
);
if (!draft && store === preview)
store = createProfileEditStore(routeId, savedEdits, () => rebuild());
if (!draft) preview = null;
@@ -682,7 +687,7 @@ export function createDesignProfile(ctx: DesignProfileContext) {
drainagePanel?.load(projectId);
},
dispose() {
disposePagePipeline(projectId);
pagePipeline.disposePagePipeline(projectId);
setListening(false);
drag.dispose();
crossPreview.dispose();
+1
View File
@@ -488,6 +488,7 @@ export async function mountProfileView(
// 계산 묶음(346-1) — 노선을 고칠 때마다 종단이 따라옴(미리보기 · [노선 반영]이 정본).
draft: routeDraftBridge(activeProjectId, profilePanel, {
stationIntervalM: panel.values().stationInterval ?? undefined,
onCorridorStale: () => sections.setCorridorPending(true),
}),
});