feat(472-4 ② Node 입구): cross.design 실행기 — TS computeCrossDesign 을 측점 묶음으로(파이썬 키 받음 · 못 세운 자리 error)

- 아직 부르는 곳 없음 — 다음 커밋에서 [저장] · [확정] 재계산이 씀
- 고정 자료 견줌: [저장] 59 회 · 노선 체인 179 회 — 면적 · 계획고 · 노면 끝 0 차 · 설계선 점 13 · 34 곳 1e-4 m(반올림 동점)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 09:08:37 +09:00
co-authored by Claude Opus 5.5
parent 0be32b8c50
commit 4752cd223f
+75 -1
View File
@@ -9,7 +9,9 @@
* context?: { earthwork_conversion · natural_spoil_min_ground_slope ·
* haul_equipment_limits · 채집석 · 구조물 잔토(B08) } ·
* excavation?: { pipes: [{ diameter_mm }] · walls: [{ height_m · average_thickness_m ·
* has_foundation }] } } }
* has_foundation }] } ·
* cross_design?: { standard: 표준 횡단(config 위에 저장분 덮은 한 벌) ·
* stations: [{ samples · design_elevation_m · options: 파이썬 `compute_cross_design` 키 }] } } }
* 출력 { results: { [id]: 값 }, available: [id…] }
* — 차례는 요청 차례가 아니라 기댐 차례(누가토량 앞에 구조물 면적을 상세에 얹음 ·
* server-calc 묶음과 같은 차례).
@@ -17,6 +19,9 @@
* ========================================================================== */
import { readFileSync, writeFileSync } from "node:fs";
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";
@@ -58,6 +63,63 @@ interface CalcInputs {
has_foundation?: boolean;
}>;
};
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` 로. */
@@ -117,6 +179,18 @@ function runDesignCalc(run: readonly string[], inputs: CalcInputs) {
),
})),
}),
// 측점마다 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;