Files
Aislo/A00_Common/design/logic/design_calc_node.ts
T
eomsangdonandClaude Opus 5.5 f15ca04bb0 merge(511-B · 511-C 합치기): origin/sub_laptop_4(511-C 유토 배분 E2 · 판 haul 줄) 받음 — 공유 파일 충돌 풂
- Node 입구 design_calc_node.ts 등록부 밖 id = 시험 길 · 단계 · 화면 길(PAGE_IDS) · haul.view
- calc_runner = PAGE_IDS(cross.refresh) · VIEW_IDS(haul.view) 둘 다 받음
- calc_sites.py 머리 글 = B-4 cross · 511-C haul(기본 server) 함께 · 묶음 13 줄
- 시험 309_2 · 309_2_browser · 511_b1 = 열린 줄에 haul 더함 · 화면 시험 「지금 서버」 = haul 기본 server 로 1 줄(바꾼 뒤 2 줄)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 15:55:17 +09:00

294 lines
14 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`)
* node <묶음> --serve 늘 떠 있음 — 표준 입력 한 줄 = 요청 JSON · 표준 출력 한 줄 = 출력 JSON(511-A)
* 입력 { 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` 키 ·
* chainage_m? }] · facilities?: { pipes · structures }(시설 구간 · 511-B B-4) } ·
* berm?: { structures: 구조물 정본 목록 · chainages: [측점 m] } ·
* rebase? · carry?(노선 바뀐 [저장] · `design_calc_node_carry.ts`) ·
* profile? · carry_profile?(종단 · `design_calc_node_profile.ts` · 511-B B-3) ·
* definition? · attach? · cross_enforce?(형상 얹기 · [저장] 횡단 · `design_calc_node_cross.ts` · 511-B B-4) ·
* stages?(묶음 단계 · `design_calc_node_stages.ts` · 511-B) ·
* stations?(측점 · `design_calc_node_sections.ts` · 511-B B-2) ·
* route_polyline?(노선 · `design_calc_node_route.ts` · 511-B B-5) } }
* 출력 { results: { [id]: 값 }, available: [id…] }
* — 차례는 요청 차례가 아니라 기댐 차례(누가토량 앞에 구조물 면적을 상세에 얹음 ·
* server-calc 묶음과 같은 차례).
* — 시험 길 id(`design_calc_node_trial.ts`) · 단계 id(`stage.*`)도 돌지만 `available` 에는 없음.
* 끝 코드: 0 성공 / 1 셈 실패 / 2 인자 오류 / 4 모르는 id(--serve 는 그 줄 { error })
* ========================================================================== */
import { readFileSync, writeFileSync } from "node:fs";
import { bermSpansFromStructures, bermSpecAt } from "@util/common_util_berm_spans";
import { computeCrossDesign } from "@util/common_util_cross_design";
import type { CrossDesignOptions, CrossGroundSample } from "@util/common_util_cross_design";
import type { CulvertPipePoint } from "@util/common_util_culvert_sets";
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 {
facilityAt,
facilityRanges,
type FacilityStructure,
} from "../../../B06_Section/logic/B06_Section_Cross_Enforce";
import { crossOptions } from "../../../B06_Section/logic/B06_Section_Cross_Save";
import {
applyStructureAreaRows,
extraWallRows,
structureAreaRows,
} from "../../../B06_Section/B06_Section_Structure_Layouts";
import { carryRunners, type CarryInputs } from "./design_calc_node_carry";
import { crossRunners, PAGE_IDS, useDefinition, type CrossInputs } from "./design_calc_node_cross";
import { followRunners, type FollowInputs } from "./design_calc_node_follow";
import { profileRunners, type ProfileInputs } from "./design_calc_node_profile";
import { routeRunners, type RouteInputs } from "./design_calc_node_route";
import { sectionsRunners, type SectionsInputs } from "./design_calc_node_sections";
import { STAGE_IDS, stageRunners, type StagesInputs } from "./design_calc_node_stages";
import { plain, TRIAL_IDS, trialRunners, type TrialInputs } from "./design_calc_node_trial";
type HaulOptions = NonNullable<Parameters<typeof computeHaulPlan>[2]>;
interface CalcInputs
extends
FollowInputs,
ProfileInputs,
CarryInputs,
CrossInputs,
TrialInputs,
StagesInputs,
SectionsInputs,
RouteInputs {
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>;
/** 시설 구간을 얹을 누가거리(`facilities` 와 함께 · 511-B B-4) */
chainage_m?: number;
}>;
/** 관 지점 · 구조물 정본 → 측점마다 포장 · 기울임 · 세월교 하강(`facilityRanges` · C3) */
facilities?: { pipes: CulvertPipePoint[]; structures: FacilityStructure[] | null };
};
}
/** 한 번 돌림 — 같은 id 는 한 번만 셈(memo) · 앞 id 는 `need` 로 · 단계 id 는 약속(기다림). */
async function runDesignCalc(run: readonly string[], inputs: CalcInputs) {
const memo = new Map<string, unknown>();
useDefinition(inputs.definition); // 구조물 표 = 서버 정의(M02 반영 · 511-B B-4)
const sections = inputs.detail?.cross_sections ?? [];
const context = inputs.context ?? {};
let massHaul: MassHaulResult | null = null;
const RUNNERS: Record<string, () => unknown> = {
...followRunners(inputs), // 측점 열쇠 · 따라감 · 꼭짓점 세움(472-4 ①)
...profileRunners(inputs), // 첫 계획선 · 계획선 짓기 · 계획고 옮김(511-B B-3)
...carryRunners(inputs), // 노선 바뀐 [저장] 다시 얹기 — 관 · 구조물 초안 · 횡단 고침(472-6 6-B)
...trialRunners(inputs), // 계산 자리 「서버」 시험 길(511-A)
...stageRunners(inputs), // 묶음 단계 노선 ~ 횡단(511-B B-1)
...crossRunners(inputs), // 형상 얹기 · [저장] 횡단 한 바퀴(511-B B-4)
...sectionsRunners(inputs), // 측점 목록 · 서버 측점 생성(511-B B-2)
...routeRunners(inputs), // 노선 폴리라인 · 곡선표(511-B B-5)
// 구조물 측점 면적 · 관 길이를 상세에 얹음(자리에서) — 뒤 누가토량이 고친 면적 위에서 쌓임.
"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 창구 · 511-C) — 인자 그대로(args) · 등록부 밖. 시험 길 표에 두면
// 브라우저가 끄는 파일(`plain`)이라 배분 모듈이 첫 묶음에 실림 — 그래서 이 입구에만.
"haul.view": () =>
plain(
computeHaulPlan(
...((inputs.args?.["haul.view"] ?? []) as Parameters<typeof computeHaulPlan>),
),
),
// 두 벌 — 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;
const ranges = batch?.facilities
? facilityRanges(batch.facilities.pipes, batch.facilities.structures ?? [])
: null;
return (batch?.stations ?? []).map((station) => {
try {
const options = {
...crossOptions(station.options, batch!.standard),
...(ranges && station.chainage_m != null ? facilityAt(ranges, station.chainage_m) : {}),
};
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] = await need(id);
const outside = [...TRIAL_IDS, ...STAGE_IDS, ...PAGE_IDS, "haul.view"];
const available = Object.keys(RUNNERS).filter((id) => !outside.includes(id));
return { results, unknown, available };
}
type CalcRequest = { run?: string[]; inputs?: CalcInputs };
/** 한 줄 요청 → 한 줄 답 · 실패는 그 줄 { error }(프로세스는 삶). */
async function answerLine(line: string): Promise<unknown> {
try {
const request = JSON.parse(line) as CalcRequest;
const t0 = performance.now();
const { results, unknown } = await runDesignCalc(request.run ?? [], request.inputs ?? {});
const ms = Math.round((performance.now() - t0) * 100) / 100;
return unknown.length ? { error: `모르는 id: ${unknown.join(", ")}` } : { results, ms };
} catch (error) {
return { error: error instanceof Error ? error.message : String(error) };
}
}
/** 늘 떠 있음(511-A) — 한 줄 받아 한 줄 냄 · 받은 차례대로(단계 id 는 기다림). */
function serve(): void {
let buffer = "";
let queue: Promise<void> = Promise.resolve();
process.stdin.setEncoding("utf8");
process.stdin.on("data", (chunk: string) => {
buffer += chunk;
for (let at = buffer.indexOf("\n"); at >= 0; at = buffer.indexOf("\n")) {
const line = buffer.slice(0, at);
buffer = buffer.slice(at + 1);
queue = queue.then(async () => {
process.stdout.write(`${JSON.stringify(await answerLine(line))}\n`);
});
}
});
process.stdin.on("end", () => void queue.then(() => process.exit(0)));
}
const [inputPath, outputPath] = process.argv.slice(2);
if (inputPath === "--serve") serve();
else if (!inputPath || !outputPath) {
console.error("사용법: node <묶음> <입력.json> <출력.json> | node <묶음> --serve");
process.exit(2);
} else {
const request = JSON.parse(readFileSync(inputPath, "utf8")) as CalcRequest;
void runDesignCalc(request.run ?? [], request.inputs ?? {}).then(
({ results, unknown, available }) => {
if (unknown.length) {
console.error(`모르는 id: ${unknown.join(", ")}`);
process.exit(4);
}
writeFileSync(outputPath, JSON.stringify({ results, available }));
process.exit(0); // 묶음 타이머를 기다리지 않음
},
(error: unknown) => {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
},
);
}