/* 79 흙막이 · 세월교 · 박스 형상 헬퍼 — 화면 TS 를 그 자리에서 트랜스파일해 Node 로 돌림. * 입력 = 73-4 픽스처(검증_sub7 상세 일부) · 결과 = 표준출력 JSON 한 벌. 짝 = `test_79_guard_shape.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, structureAreaRows } = loadTs(B06("B06_Section_Structure_Layouts.ts")); const { bodyGuardBase } = loadTs(B06("B06_Section_UI_Cross_Body_Guard.ts")); const { computeBoxLayout } = loadTs(B06("B06_Section_UI_Cross_Box_Geom.ts")); const { computeCulvertLayout } = loadTs(B06("B06_Section_UI_Cross_Culvert_Geom.ts")); const { computeRevetmentLayout } = loadTs(B06("B06_Section_UI_Cross_Revetment.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 = {}; /** 벽 윗면 · 바닥 · 전면(관 끝 자리) ↔ 본체 끝 윗면 · 바닥 · 자리. */ function guardFit(section, list = sections) { const layouts = computeStoredLayouts(section, list); const base = bodyGuardBase(section, section.samples); return (layouts.culvert?.walls ?? []).map((w) => { const end = base.ends[w.role]; const top = Math.max(...w.points.map((p) => p.elevation)); const bottom = Math.min(...w.points.map((p) => p.elevation)); return { role: w.role, outward: w.outward, topGap: r3(top - end.top), bottomGap: r3(bottom - end.bottom), pureHeight: r3(w.height + 0.5), endHeight: r3(end.top - end.bottom), autoHeight: r3(end.height), }; }); } // ① 높이 빈 칸 = 본체 끝 높이 · 윗면 = 본체 윗면(세월교 · 박스) const blank = (s) => { const c = clone(s); for (const role of ["inlet", "outlet"]) c.culvert[role].revet_height_m = null; return c; }; const boxBlank = blank(at(352.14)); const fordBlank = blank(at(173.087)); out.boxAuto = guardFit( boxBlank, sections.map((s) => (s === at(352.14) ? boxBlank : s)), ); out.fordAuto = guardFit( fordBlank, sections.map((s) => (s === at(173.087) ? fordBlank : s)), ); // 높이를 적으면 윗면은 그대로 · 바닥이 따라 움직임 out.boxSet = guardFit(at(352.14)); // ② 세월교 흙막이 쪽 ㄴ형 측벽 · 선 걷음 const fordLayouts = computeStoredLayouts(at(173.087), sections); out.fordParts = fordLayouts.ford.sides.map((s) => ({ role: s.role, parts: s.parts.length })); const fordOneGuard = clone(at(173.087)); fordOneGuard.culvert.outlet = { role: "outlet", structure: "" }; const oneList = sections.map((s) => (s === at(173.087) ? fordOneGuard : s)); out.fordOneGuardParts = computeStoredLayouts(fordOneGuard, oneList).ford.sides.map((s) => ({ role: s.role, parts: s.parts.length, })); // ③ 박스 본체 수평 — 좌 · 우 표고 조정이 달라도 const box = at(352.14); const tilted = computeBoxLayout(box, box.samples, { left: { lengthM: 0, riseM: 0.6 }, right: { lengthM: 0, riseM: 0 }, }); out.boxLevel = { left: r3(tilted.sides[0].topElevation), right: r3(tilted.sides[1].topElevation), slope: tilted.slopeRatio, }; // ④ 박스 날개벽 쪽 끝 절 · 성토선(흙막이 없는 박스) const boxWing = clone(box); delete boxWing.culvert; const wingLayout = computeBoxLayout(boxWing, boxWing.samples); out.boxWing = wingLayout.sides.map((s) => ({ role: s.role, cut: s.cutLine ? 1 : 0, fill: s.fillSegments.length, })); // ⑤ 흙막이 끝 절토 = 수평공간 뒤 절토 경사 — 수평공간 칸 값을 따름 function cutTails(section, list) { const culvert = computeStoredLayouts(section, list).culvert; return [...culvert.outletFill.segments, ...culvert.basinFill.segments] .filter((g) => g.kind === "cut") .map((g) => ({ points: g.points.length, berm: r3(Math.abs(g.points[1].offset - g.points[0].offset)), flat: r3(g.points[1].elevation - g.points[0].elevation), })); } out.cutDefault = [cutTails(at(352.14), sections), cutTails(at(173.087), sections)]; const berm2 = clone(at(352.14)); berm2.culvert.inlet.revet_berm_m = 2; berm2.culvert.outlet.revet_berm_m = 2; out.cutBerm2 = cutTails( berm2, sections.map((s) => (s === at(352.14) ? berm2 : s)), ); // ⑥ 관 보호공 기본 자리 = 제일 위(성토선 0 지점 · d 0) · 관 길이 m 단위 const pipe = clone(at(180)); pipe.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: "돌쌓기(찰)" }, }; pipe.design.inlet_structure = "revet"; const pipeLayout = computeCulvertLayout(pipe, pipe.samples, undefined, "revet"); out.pipe = pipeLayout ? { d: { inlet: pipeLayout.revetShift.inlet.d, outlet: pipeLayout.revetShift.outlet.d }, walls: pipeLayout.walls.map((w) => ({ role: w.role, topBelowEdge: r3( (w.outward > 0 ? pipe.design.road_edges.left : pipe.design.road_edges.right).elevation_m - w.topJoint.elevation, ), })), lengthM: pipeLayout.pipe.lengthM, } : null; // ⑥-2 손으로 x 를 넣은 관 보호공 = 종전 자리(d 기본 = 경사길이 4m 지점 · x 원점 그대로 · 79b) const pipeX = computeCulvertLayout( pipe, pipe.samples, { outlet: { x: 1, d: null, h: null, m: null } }, "revet", ); out.pipeStoredX = pipeX ? { x: pipeX.revetShift.outlet.x, d: r3(pipeX.revetShift.outlet.d) } : null; // ⑦ 독립 기슭막이 · 옹벽 기본 자리 = 제일 위(d 0) const revet = clone(at(180)); revet.revetment = { structure_id: "t", type_id: "masonry_wet", name: "t", start_m: 170, end_m: 190, anchor_m: 180, form: "돌쌓기(찰)", height_m: 2, side: "우", }; const own = computeRevetmentLayout(revet, null); out.revet = own ? { d: own.appliedAdjust.d, topBelowEdge: r3(revet.design.road_edges.right.elevation_m - own.top.elevation), linked: own.linked, } : null; // 저장 x 가 있는 벽 = 종전 자리 그대로(x 원점 = 경사길이 4m 지점 · d 빈 칸 = 종전 기본 · 79b) const autoX = computeRevetmentLayout(revet, null).walls[0].bottomBack.offset; const storedX = computeRevetmentLayout(revet, { x: 1, d: null }); const storedXD = computeRevetmentLayout(revet, { x: 1, d: 1 }); out.revetStoredX = { x: storedX.appliedAdjust.x, d: storedX.appliedAdjust.d, // 노견 → 4m 지점(3.07) + x 1 = 제일 위 자리에서 바깥 4.07 m shift: r3(Math.abs(storedX.walls[0].bottomBack.offset - autoX)), shiftXD: r3(Math.abs(storedXD.walls[0].bottomBack.offset - autoX)), }; const neighbour = clone(revet); neighbour.chainage_m = 170; out.revetLinked = computeRevetmentLayout(neighbour, null)?.linked ?? null; out.areas = structureAreaRows(sections).filter((r) => [173.087, 352.14].some((c) => Math.abs(r.chainage_m - c) < 0.6), ); process.stdout.write(JSON.stringify(out, null, 1));