Files
Aislo/A00_Common/design/logic/design_calc_node.ts
T
eomsangdonandClaude Opus 5.5 4313eda5ac feat(472-3 등록부): B05 · B06 계산 등록부 · 서버 Node 입구 하나 · 경계 시험
- design_calc_registry.ts — 순수 자료 · 1.1 표 # 마다 id 하나(43) · 입력 · 출력 · 실행 자리 · 단계 · 실행기 · 짝 · 거울
- design_calc_node.ts — id → 함수 표(구조물 면적 · 관 길이 · 누가토량 · 유토 배분 · 터파기 · 3D) · build:design-calc
- 나머지 id 실행기 = 지금 파이썬 이름 · 472-4 동안 표시
- test_472_3_calc_registry — 실행기 있음 · STAGE_DEPS 같음 · 서버 쪽 코드 브라우저 밖 · Node 입구 도달 경계(알려진 끌림 목록) · server-calc · corridor 묶음과 같은 값
- 기존 묶음 · 파이썬 · design_pipeline.ts 그대로

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

157 lines
6.9 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 }] } } }
* 출력 { results: { [id]: 값 }, available: [id…] }
* — 차례는 요청 차례가 아니라 기댐 차례(누가토량 앞에 구조물 면적을 상세에 얹음 ·
* server-calc 묶음과 같은 차례).
* 끝 코드: 0 성공 / 2 인자 오류 / 4 모르는 id
* ========================================================================== */
import { readFileSync, writeFileSync } from "node:fs";
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";
type HaulOptions = NonNullable<Parameters<typeof computeHaulPlan>[2]>;
interface CalcInputs {
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;
}>;
};
}
/** 한 번 돌림 — 같은 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> = {
// 구조물 측점 면적 · 관 길이를 상세에 얹음(자리에서) — 뒤 누가토량이 고친 면적 위에서 쌓임.
"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,
),
})),
}),
// 편집 전 정본 — 계획선 샘플은 저장분 그대로(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 }));