Files
Aislo/A00_Common/design/logic/design_calc_node.ts
T
eomsangdonandClaude Opus 5.5 4b6800e387 feat(472-6 6-B 노선 바뀐 [저장] 한 본문): 노선 저장 · 관 · 구조물 다시 얹기 · 횡단 고침 넘김을 서버 한 창구로 — 전부 아니면 없음
- POST /design/save 에 route(노선 초안) · carry(옛 노선 세션 칸) 칸 · 서버 = 노선 저장 → Node facility.rebase(rebasePipeDraft · rebaseStructureDraft) → 6-A 차례(새 노선) → Node stations.carry(routeCarryPatches) → 횡단 저장
- 되돌림 범위 = 노선 행 · 자식 표 · 단계 · 배수 폴더 · 노선 파일 · 지표면 확정(설계 지형 바꿈)
- 손 상단측은 안 넘김(요구 274) · 노선 바뀐 [확정] = [저장](새 노선 본 뒤 다시)
- 브라우저 = 노선 초안 + 사용자 칸만 한 본문 · 옛 차례 길(saveRouteDraft 뒤 flush · carryAfterRouteSave · keepDraftsOnNewRoute) · 부름 0 된 flush 창구 여섯 · saveCachedCrossPatches 걷음
- 등록부 R9 stations.carry · F10 facility.rebase · 옛 차례 단언 시험 한 본문 꼴로 · test_472_6_route_save(옛 길 = 새 길 · 실패 되돌림)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 12:07:12 +09:00

247 lines
11 KiB
TypeScript

/* =============================================================================
* A00_Common/design/logic/design_calc_node.ts
* 계산 등록부(`design_calc_registry.ts`)의 서버 Node 입구 하나(PLAN 472-3) — id → 함수 표.
* 등록부에서 실행기가 `node:<id>` 인 줄만 여기 있음(나머지는 472-4 동안 파이썬).
* 브라우저가 쓰는 같은 TS 를 그대로 부름 — 계산을 두 벌로 짜지 않음.
*
* 실행: node <묶음> <입력.json> <출력.json> (package.json `build:design-calc`)
* 입력 { run: [id…], inputs: { detail?: 종횡단 상세(API 꼴) · route_points?: 노선 폴리라인 ·
* context?: { earthwork_conversion · natural_spoil_min_ground_slope ·
* haul_equipment_limits · 채집석 · 구조물 잔토(B08) } ·
* excavation?: { pipes: [{ diameter_mm }] · walls: [{ height_m · average_thickness_m ·
* has_foundation }] } ·
* cross_design?: { standard: 표준 횡단(config 위에 저장분 덮은 한 벌) ·
* stations: [{ samples · design_elevation_m · options: 파이썬 `compute_cross_design` 키 }] } ·
* berm?: { structures: 구조물 정본 목록 · chainages: [측점 m] } ·
* rebase? · carry?(노선 바뀐 [저장] · `design_calc_node_carry.ts`) } }
* 출력 { results: { [id]: 값 }, available: [id…] }
* — 차례는 요청 차례가 아니라 기댐 차례(누가토량 앞에 구조물 면적을 상세에 얹음 ·
* server-calc 묶음과 같은 차례).
* 끝 코드: 0 성공 / 2 인자 오류 / 4 모르는 id
* ========================================================================== */
import { readFileSync, writeFileSync } from "node:fs";
import { bermSpansFromStructures, bermSpecAt } from "@util/common_util_berm_spans";
import type { BermSpec } from "@util/common_util_cross_berm";
import { computeCrossDesign } from "@util/common_util_cross_design";
import type { CrossDesignOptions, CrossGroundSample } from "@util/common_util_cross_design";
import { pipeTrenchWidthM, wallTrenchAreaM2, wallTrenchWidthM } from "@util/common_util_excavation";
import { computeMassHaul, massHaulPayload } from "@util/common_util_mass_haul";
import type { MassHaulResult } from "@util/common_util_mass_haul";
import { computeHaulPlan, haulPlanPayload } from "@util/common_util_mass_haul_balance";
import type { RoutePoint } from "../../../B05_Profile/B05_Profile_Api_Fetch";
import { buildCorridor } from "../../../B05_Profile/B05_Profile_UI_Corridor_Build";
import { corridorHash, serialize } from "../../../B05_Profile/B05_Profile_UI_Corridor_Envelope";
import type { SectionDetailResponse } from "../../../B06_Section/B06_Section_Api_Fetch";
import {
applyStructureAreaRows,
extraWallRows,
structureAreaRows,
} from "../../../B06_Section/B06_Section_Structure_Layouts";
import { carryRunners, type CarryInputs } from "./design_calc_node_carry";
import { followRunners, type FollowInputs } from "./design_calc_node_follow";
type HaulOptions = NonNullable<Parameters<typeof computeHaulPlan>[2]>;
interface CalcInputs extends FollowInputs, CarryInputs {
detail?: SectionDetailResponse;
route_points?: RoutePoint[];
context?: Partial<
Pick<
HaulOptions,
| "collected_stone_deduction_m3"
| "collected_stone_by_ground_m3"
| "collected_stone_ground_unknown_m3"
| "structure_spoil_m3"
| "structure_spoil_points"
>
> & {
earthwork_conversion?: Parameters<typeof computeMassHaul>[1];
natural_spoil_min_ground_slope?: number | null;
haul_equipment_limits?: Parameters<typeof computeHaulPlan>[1];
};
excavation?: {
pipes?: Array<{ diameter_mm?: number | null }>;
walls?: Array<{
height_m?: number | null;
average_thickness_m?: number | null;
has_foundation?: boolean;
}>;
};
berm?: {
structures: Parameters<typeof bermSpansFromStructures>[0];
chainages: number[];
};
cross_design?: {
standard: CrossDesignOptions["standard"];
stations?: Array<{
samples: CrossGroundSample[];
design_elevation_m: number | null;
options: Record<string, unknown>;
}>;
};
}
/** 파이썬 `compute_cross_design` 키 → TS 짝 키(거울 시험 test_b06_cross_design_mirror 표와 같음). */
const CROSS_OPTION_KEYS: Record<string, keyof CrossDesignOptions> = {
ground_type: "groundType",
section_mode: "sectionMode",
ditch_side: "ditchSide",
ditch_type: "ditchType",
paved: "paved",
rock_boundary_offset_m: "rockBoundaryOffsetM",
two_stage_slope: "twoStageSlope",
cut_slope_ratio: "cutSlopeRatio",
ditch_enabled: "ditchEnabled",
ditch_choice: "ditchChoice",
surface_drop_m: "surfaceDropM",
plan_radius_m: "planRadiusM",
curve_outer_side: "curveOuterSide",
curve_widening_m: "curveWideningM",
shoulder_extend_left_m: "shoulderExtendLeftM",
shoulder_extend_right_m: "shoulderExtendRightM",
tilted: "tilted",
tilt_pct: "tiltPct",
};
/** 파이썬 BermSpec(NamedTuple → dict) → TS BermSpec. */
function bermOf(value: unknown): BermSpec | null {
if (!value || typeof value !== "object") return null;
const berm = value as Record<string, number | null>;
return {
widthM: berm.width_m as number,
intervalM: berm.interval_m as number,
slopeDeg: berm.slope_deg as number,
depthM: berm.depth_m as number,
depthLeftM: berm.depth_left_m ?? null,
depthRightM: berm.depth_right_m ?? null,
};
}
function crossOptions(
raw: Record<string, unknown>,
standard: CrossDesignOptions["standard"],
): CrossDesignOptions {
const options: Record<string, unknown> = { standard };
for (const [key, value] of Object.entries(raw)) {
if (key === "berm") options.berm = bermOf(value);
else if (key in CROSS_OPTION_KEYS) options[CROSS_OPTION_KEYS[key]] = value;
else throw new Error(`모르는 횡단 설계 키: ${key}`);
}
return options as unknown as CrossDesignOptions;
}
/** 한 번 돌림 — 같은 id 는 한 번만 셈(memo) · 앞 id 는 `need` 로. */
function runDesignCalc(run: readonly string[], inputs: CalcInputs) {
const memo = new Map<string, unknown>();
const sections = inputs.detail?.cross_sections ?? [];
const context = inputs.context ?? {};
let massHaul: MassHaulResult | null = null;
const RUNNERS: Record<string, () => unknown> = {
...followRunners(inputs), // 측점 열쇠 · 따라감 · 꼭짓점 세움(472-4 ①)
...carryRunners(inputs), // 노선 바뀐 [저장] 다시 얹기 — 관 · 구조물 초안 · 횡단 고침(472-6 6-B)
// 구조물 측점 면적 · 관 길이를 상세에 얹음(자리에서) — 뒤 누가토량이 고친 면적 위에서 쌓임.
"structure.layouts": () => {
const areas = structureAreaRows(sections);
applyStructureAreaRows(sections, areas);
return { areas, extra_walls: extraWallRows(sections) };
},
"masshaul.cumulative": () => {
need("structure.layouts");
const conversion = context.earthwork_conversion;
massHaul = conversion
? computeMassHaul(sections, conversion, context.natural_spoil_min_ground_slope ?? undefined)
: null;
return massHaul ? massHaulPayload(massHaul) : null;
},
// 두 벌 — haul_plan = 거르지 않은 수량 정본(B08) · haul_plan_drawing = 잔진동 거른 그림.
"haul.plan": () => {
need("masshaul.cumulative");
const result = massHaul;
if (!result) return null;
const planFor = (drawing: boolean) =>
computeHaulPlan(result, context.haul_equipment_limits, {
collected_stone_deduction_m3: context.collected_stone_deduction_m3 ?? null,
collected_stone_by_ground_m3: context.collected_stone_by_ground_m3 ?? null,
collected_stone_ground_unknown_m3: context.collected_stone_ground_unknown_m3 ?? null,
structure_spoil_m3: context.structure_spoil_m3 ?? null,
structure_spoil_points: context.structure_spoil_points ?? null,
conversion: context.earthwork_conversion ?? null,
drawing,
});
const plan = planFor(false);
const drawing = planFor(true);
return {
...(plan ? { haul_plan: haulPlanPayload(plan) } : {}),
...(drawing ? { haul_plan_drawing: haulPlanPayload(drawing) } : {}),
};
},
"excavation.trench": () => ({
pipes: (inputs.excavation?.pipes ?? []).map((pipe) => ({
width_m: pipeTrenchWidthM(pipe.diameter_mm),
})),
walls: (inputs.excavation?.walls ?? []).map((wall) => ({
width_m: wallTrenchWidthM(wall.average_thickness_m),
area_m2: wallTrenchAreaM2(
wall.height_m,
wall.average_thickness_m,
wall.has_foundation !== false,
),
})),
}),
// 소단 구간(C4) — 구조물 정본 → 구간 · 측점마다 제원(겹치면 먼저 놓은 것).
"cross.berm_spans": () => {
const spans = bermSpansFromStructures(inputs.berm?.structures ?? []);
return { spans, at: (inputs.berm?.chainages ?? []).map((c) => bermSpecAt(spans, c)) };
},
// 측점마다 TS 짝(`computeCrossDesign`) — 못 세우면 그 자리 { error }(파이썬은 ValueError 로 되던짐).
"cross.design": () => {
const batch = inputs.cross_design;
return (batch?.stations ?? []).map((station) => {
try {
const options = crossOptions(station.options, batch!.standard);
return computeCrossDesign(station.samples, station.design_elevation_m, options);
} catch (error) {
return { error: error instanceof Error ? error.message : String(error) };
}
});
},
// 편집 전 정본 — 계획선 샘플은 저장분 그대로(corridor 묶음과 같은 값) · 측점 둘 미만이면 null.
"corridor.mesh": () => {
const detail = inputs.detail;
if (!detail) return null;
const routePoints = inputs.route_points ?? [];
const samples = detail.longitudinal?.design_profiles?.[0]?.samples;
const build = buildCorridor(detail.cross_sections, routePoints, samples);
return build ? serialize(build, corridorHash(detail, routePoints)) : null;
},
};
function need(id: string): unknown {
if (!memo.has(id)) memo.set(id, RUNNERS[id]());
return memo.get(id);
}
const unknown = run.filter((id) => !(id in RUNNERS));
const results: Record<string, unknown> = {};
if (!unknown.length) for (const id of run) results[id] = need(id);
return { results, unknown, available: Object.keys(RUNNERS) };
}
const [inputPath, outputPath] = process.argv.slice(2);
if (!inputPath || !outputPath) {
console.error("사용법: node <묶음> <입력.json> <출력.json>");
process.exit(2);
}
const request = JSON.parse(readFileSync(inputPath, "utf8")) as {
run?: string[];
inputs?: CalcInputs;
};
const { results, unknown, available } = runDesignCalc(request.run ?? [], request.inputs ?? {});
if (unknown.length) {
console.error(`모르는 id: ${unknown.join(", ")}`);
process.exit(4);
}
writeFileSync(outputPath, JSON.stringify({ results, available }));