/* ============================================================================= * E03_StabSection_Walls_Node.ts * E 2D 해석 모형의 벽(옹벽 · 석축 · 돌쌓기 · 관 기슭막이) 단면 꼭짓점을 **서버가** 내는 입구(532-4). * * 도형은 B06 화면이 그리는 TS 그대로(`computeStoredLayouts` — 정본만 읽는 한 벌 · CLAUDE.md 5장 ②) · * 지표 = B06 면적과 같은 「실제로 그려지는 설계선」(`drawnProfile` · 벽 상단에서 끊김). 여기에 계산 없음. * 파이썬 껍데기 = `E03_StabSection_Walls.py` · 번들 = `npm run build:e03-walls`. * * 실행: node <번들> <입력.json> <출력.json> * 입력 { definition?: 구조물 정의 · sections: 형상 얹은 B06 횡단(chainage_m · samples · design · revetment …) } * 출력 { stations: [{ chainage_m, walls: [{ kind, name, structure_id, form, polygon: [[offset, z], …] }], * design_line: [[offset, z], …] | null(트림 없음 = 설계선 그대로) }] } — 입력 차례 그대로 * 끝 코드: 0 성공 / 2 인자 오류 * ========================================================================== */ import { readFileSync, writeFileSync } from "node:fs"; import { drawnProfile } from "@util/common_util_cross_structure_areas"; import { useCatalogDefinition, type CatalogDefinition } from "@util/common_util_structure_catalog"; import type { CrossSection } from "../B06_Section/B06_Section_Api_Fetch"; import type { WallLayout } from "../B06_Section/logic/B06_Section_Cross_Culvert_Types"; import { computeStoredLayouts, trimOfLayouts, } from "../B06_Section/logic/B06_Section_Structure_Layouts"; /** 트림 경계 바로 바깥 점 — 벽 쪽 연직 단차를 살림(`computeStructureAreas` 와 같은 값). */ const BOUNDARY_EPS_M = 1e-6; type Pair = [number, number]; function stationWalls(section: CrossSection, sections: readonly CrossSection[]) { const layouts = computeStoredLayouts(section, sections); const walls: Array<{ kind: string; name: string | null; structure_id: string | null; form: string | null; polygon: Pair[]; }> = []; const push = (kind: string, list: readonly WallLayout[] | undefined, name: string | null) => { for (const wall of list ?? []) { if (wall.points.length >= 3) walls.push({ kind, name, structure_id: kind === "own" ? (layouts?.own?.structureId ?? null) : null, form: wall.form, polygon: wall.points.map((p) => [p.offset, p.elevation]), }); } }; push("own", layouts?.own?.walls, layouts?.own?.name ?? null); push("culvert", layouts?.culvert?.walls, "기슭막이"); push("culvert", layouts?.culvert?.basinExtras, "기슭막이"); const trim = layouts ? trimOfLayouts(layouts) : null; const line = layouts?.design.design_line; let design_line: Pair[] | null = null; if (trim && Array.isArray(line) && line.length >= 2) { const ground = section.samples .filter((s) => s.valid !== false && typeof s.elevation_m === "number") .map((s) => ({ offset: s.offset_m ?? 0, elevation: s.elevation_m as number })) .sort((a, b) => a.offset - b.offset); const profile = drawnProfile({ designLine: line as Array<{ offset_m: number; elevation_m: number }>, ground, trim, }); if (profile && ground.length >= 2) { const lo = ground[0].offset; const hi = ground[ground.length - 1].offset; const grid = new Set(); const add = (o: number) => { if (o >= lo && o <= hi) grid.add(Math.round(o * 1e6) / 1e6); }; ground.forEach((g) => add(g.offset)); line.forEach((p) => add(p.offset_m)); for (const b of [trim.minOffset, trim.maxOffset]) [b, b - BOUNDARY_EPS_M, b + BOUNDARY_EPS_M].forEach(add); trim.minSlope?.points.forEach((p) => add(p.offset)); trim.maxSlope?.points.forEach((p) => add(p.offset)); design_line = [...grid].sort((a, b) => a - b).map((o) => [o, profile.drawnZ(o)]); } } return { chainage_m: section.chainage_m, walls, design_line }; } const [inputPath, outputPath] = process.argv.slice(2); if (!inputPath || !outputPath) { console.error("사용법: node <번들> <입력.json> <출력.json>"); process.exit(2); } const input = JSON.parse(readFileSync(inputPath, "utf8")) as { definition?: CatalogDefinition | null; sections: CrossSection[]; }; if (input.definition) useCatalogDefinition(input.definition); const sections = input.sections ?? []; writeFileSync( outputPath, JSON.stringify({ stations: sections.map((s) => stationWalls(s, sections)) }), "utf8", );