- 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>
294 lines
14 KiB
TypeScript
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);
|
|
},
|
|
);
|
|
}
|