/* 91 원지반 · 암 제약 삭제 · 유출측 복합경사 헬퍼 — 화면 TS 를 그 자리에서 트랜스파일해 Node 로 돌림. * 입력 = 73-4 픽스처(세월교 173.09 · 박스 352.14 · 관 180) · 결과 = 표준출력 JSON 한 벌. 짝 = `test_91_ground_free.py`. */ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); const ROOT = path.join(__dirname, "..", ".."); const cache = new Map(); // 화면 전용 모듈(토스트 · 글) — 기하 계산엔 안 쓰임 const STUBS = { "@ui/ui_template_toast": { showToast: () => undefined }, }; function loadTs(file) { const full = path.resolve(file); if (cache.has(full)) return cache.get(full); if (full.endsWith(".json")) { const data = JSON.parse(fs.readFileSync(full, "utf8")); cache.set(full, data); return data; } const js = ts.transpileModule(fs.readFileSync(full, "utf8"), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020, esModuleInterop: true, }, }).outputText; const box = { exports: {} }; cache.set(full, box.exports); const localRequire = (request) => { if (STUBS[request]) return STUBS[request]; const alias = { "@util/": "common_util", "@ui/": "ui_template", "@config/": "config" }; for (const [prefix, dir] of Object.entries(alias)) if (request.startsWith(prefix)) return loadTs(resolve(path.join(ROOT, dir, request.slice(prefix.length)))); return loadTs(resolve(path.join(path.dirname(full), request))); }; new Function("exports", "module", "require", js)(box.exports, box, localRequire); cache.set(full, box.exports); return box.exports; } const resolve = (base) => [base, `${base}.ts`, path.join(base, "index.ts")].find( (p) => fs.existsSync(p) && fs.statSync(p).isFile(), ) ?? `${base}.ts`; const B06 = (f) => path.join(ROOT, "B06_Section", f); const { computeStoredLayouts } = loadTs(B06("B06_Section_Structure_Layouts.ts")); const fixture = JSON.parse( fs.readFileSync(path.join(__dirname, "fixtures", "b06_73_4_body_guard_sections.json"), "utf8"), ); const sections = fixture.cross_sections; const at = (c) => sections.find((s) => Math.abs(s.chainage_m - c) < 0.6); const clone = (v) => JSON.parse(JSON.stringify(v)); const r3 = (v) => Math.round(v * 1000) / 1000; const out = {}; /** 원지반 사본 — [from, to] 안 샘플을 dz 만큼 옮김. */ const shiftGround = (s, dz, from = -Infinity, to = Infinity) => { for (const p of s.samples) if (p.offset_m >= from && p.offset_m <= to && p.elevation_m != null) p.elevation_m += dz; }; const layoutsOf = (s, base) => computeStoredLayouts( s, sections.map((x) => (x === base ? s : x)), ); // ① 91-2 · 107-2 세월교 바닥 — 기본 · 손 값(측벽 높이)은 원지반을 바꿔도 그대로 · 흙막이 기본 높이도 const FORD = at(173.087); function fordFloor(edit) { const s = clone(FORD); for (const role of ["inlet", "outlet"]) s.culvert[role].revet_height_m = null; edit(s); const { ford, culvert } = layoutsOf(s, FORD); return ford.sides.map((side) => { const wall = culvert.walls.find((w) => w.role === side.role); return { role: side.role, slabTop: r3(side.slabTopElevation), wallBottom: wall ? r3(Math.min(...wall.points.map((p) => p.elevation))) : null, }; }); } const hand = (s) => { s.design.ford_adjust = { inlet: { heightM: 2.0, lateralM: 0, slopeM: 0 }, outlet: { heightM: 2.4, lateralM: 0, slopeM: 0 }, }; }; out.fordAuto = fordFloor(() => undefined); out.fordAutoBedDown = fordFloor((s) => shiftGround(s, -2, -2, 2)); out.fordHand = fordFloor(hand); out.fordHandBedDown = fordFloor((s) => { hand(s); shiftGround(s, -2, -2, 2); }); // ② 91-3 다단 자동 자리 — 관 180 유출 흙막이 + 2단 · 원지반을 옮겨도 단 자리 같음 · 사면 1.0 m const { computeCulvertLayout } = loadTs(B06("B06_Section_UI_Cross_Culvert_Geom.ts")); function tiers(dz) { const s = clone(at(180)); shiftGround(s, dz); s.culvert = { type: "pipe", chainage_m: 180, pipe_kind: "흄관", diameter_m: 1.0, min_cover_m: 0.5, inlet: { role: "inlet", structure: "기슭막이", revet_form: "돌쌓기(찰)" }, outlet: { role: "outlet", structure: "기슭막이", revet_form: "돌쌓기(찰)" }, }; s.design.inlet_structure = "revet"; const tier = { x: null, d: null, h: 2, m: null }; const layout = computeCulvertLayout(s, s.samples, { extras: [tier, tier] }, "revet"); return { walls: layout.extraWalls.map((w) => [r3(w.bottomBack.offset), r3(w.bottomBack.elevation)]), slopes: layout.outletFill.segments.slice(0, 2).map((g) => r3(g.lengthM)), }; } out.tiers = [tiers(0), tiers(0.5), tiers(-0.5)]; // ⑤ 91-3 전면 기울기 — 단면유형(원지반 자동판정)과 무관 · 성토/절토 칸만 const { leanOf } = loadTs(B06("logic/B06_Section_UI_Cross_Lean.ts")); out.lean = ["left_cut", "right_cut", "both_fill", "both_cut"].map((mode) => [ leanOf({ form: "돌쌓기(찰)", height_m: 1.5, section_mode: mode, side: "좌" }), leanOf({ form: "돌쌓기(찰)", height_m: 1.5, section_mode: mode, side: "좌", face_role: "절토" }), ]); // ③ 91-4 유출측 절토 복합경사 — 꼭짓점 사이 경사(수평/수직) · 사면 길이 · 끝 = 원지반 교점 const { groundInterpolator } = loadTs(B06("logic/B06_Section_UI_Cross_Culvert_Solve.ts")); function profile(points, samples) { const groundAt = groundInterpolator(samples); const legs = points.slice(1).map((p, i) => { const dx = Math.abs(p.offset - points[i].offset); const dz = p.elevation - points[i].elevation; return { ratio: dz > 1e-9 ? r3(dx / dz) : null, slant: r3(Math.hypot(dx, dz)), run: r3(dx) }; }); const end = points[points.length - 1]; return { legs, endGap: r3(Math.abs(groundAt(end.offset) - end.elevation)) }; } const withBerm = (s) => { s.design.berm = { width_m: 0.5, interval_m: 3, slope_deg: 0 }; }; // 흙막이 수평공간 뒤(박스 352.14 · 세월교 173.09) function guardTails(base, edit = () => undefined) { const s = clone(base); edit(s); const { culvert } = layoutsOf(s, base); const tagged = (segments, role) => segments.map((g) => ({ ...g, role })); return [ ...tagged(culvert.outletFill.segments, "outlet"), ...tagged(culvert.basinFill.segments, "inlet"), ] .filter((g) => g.kind === "cut") .map((g) => ({ role: g.role, ...profile(g.points, s.samples) })); } out.guardTails = [...guardTails(at(352.14)), ...guardTails(FORD)]; // 박스 끝 · 세월교 측벽 끝(흙막이 없음) const { computeBoxLayout } = loadTs(B06("B06_Section_UI_Cross_Box_Geom.ts")); const { computeFordLayout } = loadTs(B06("B06_Section_UI_Cross_Ford_Geom.ts")); function endCuts(base, compute) { const s = clone(base); delete s.culvert; s.uphill_side = "left"; // 절토 쪽(우측)을 유출로 return compute(s, s.samples) .sides.filter((side) => side.cutLine) .map((side) => profile(side.cutLine, s.samples)); } out.boxEnd = endCuts(at(352.14), computeBoxLayout); out.fordEnd = endCuts(FORD, computeFordLayout); // 절토 쪽 배관 유출 벽 뒤(관 340 · 유출 = 우측 절토) · 소단 있음/없음 function cutOutlet(edit = () => undefined) { const s = clone(at(340)); s.uphill_side = "left"; s.culvert = { type: "pipe", chainage_m: 340, pipe_kind: "흄관", diameter_m: 1.0, min_cover_m: 0.5, inlet: { role: "inlet", structure: "기슭막이", revet_form: "돌쌓기(찰)" }, outlet: { role: "outlet", structure: "기슭막이", revet_form: "돌쌓기(찰)" }, }; s.design.inlet_structure = "revet"; edit(s); const layout = computeCulvertLayout(s, s.samples, undefined, "revet"); // 벽 뒤 절토 = 유출 끝 연결선(139-4) — 벽 모서리 절토는 또 잇지 않음(186-1) const start = layout.outletFill.segments.at(-1).points[0]; const pts = layout.designTrim.minSlope.points; const from = pts.findIndex((p) => Math.abs(p.offset - start.offset) < 1e-9); return profile(pts.slice(from), s.samples); } out.cutOutlet = cutOutlet(); out.cutOutletBerm = cutOutlet(withBerm); process.stdout.write(JSON.stringify(out));