import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import * as THREE from 'three'; import * as XLSX from 'xlsx'; import { describe, expect, it } from 'vitest'; import { buildAbut } from '../src/core/abut/geometry'; import { parseAbutments, toAbutInput } from '../src/core/abut/parse'; import { buildMembers } from '../src/core/abut/members'; import { buildWings } from '../src/core/abut/wings'; import { resolveSlab, slabGradePct, slabSection } from '../src/core/abut/slab'; import { abutSlabGeom, abutSlabRebar } from '../src/core/abut/rebar/slab'; import { abutFootingGeom, abutFootingRebar } from '../src/core/abut/rebar/footing'; import { skewAngle } from '../src/core/abut/rebar/skew-layout'; import { abutWallGeom, abutWallRebar } from '../src/core/abut/rebar/wall'; import { abutParapetGeom, abutParapetRebar, slabSpanAt } from '../src/core/abut/rebar/parapet'; import { abutWingGeom, abutWingRebar } from '../src/core/abut/rebar/wing'; import { abutPlanOutline, abutSkewK, buildAbutGroup } from '../src/view/abut-scene'; import { buildAbutRebar3D } from '../src/view/abut-rebar3d'; // 교대 A1: 모든 3D 철근이 콘크리트(저판 · 벽체 · 흉벽 · 브라켓 · 날개벽 · 헌치) 안에 있는지 (2026-11-04, 화면에서 날개벽 WR12 갈고리 · // WR7-3 · WR10 다리가 밖으로 나간 것을 고친 뒤). 철근 축을 ~100 mm 간격으로 찍고 메시 안팎은 광선 교차 수 홀짝으로 본다. // P10 다웰은 접속슬래브로 300 올라가므로 위 절반만 밖에 있어도 된다. const res: Record = {}; { const xlsx = fileURLToPath(new URL('../../../RAG/운곡교P1입력.xlsx', import.meta.url)); const rec = parseAbutments(XLSX.read(readFileSync(xlsx), { type: 'buffer' })).abutments[0]; const model = buildAbut(toAbutInput(rec)); const members = buildMembers(model, { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm }); const wings = buildWings(model, members); const angle = skewAngle(rec.skewDeg); const fg = abutFootingGeom(model.length, members.shape, angle); const footing = abutFootingRebar(fg); const wg = abutWallGeom(model.length, members, angle); const wall = abutWallRebar(wg, footing); const slab = resolveSlab(); const k = abutSkewK(model); const tM = (wg.xb / 1000 + members.shape.parapetBackM) / 2; const span = slabSpanAt(abutPlanOutline(model, members, wings, slab).slab, tM, k)!; const pg = abutParapetGeom(wg, members.shape, model.bearings[0].s, { B: model.wing!.slabWidthMm, a: span.a, b: span.b, nearT: members.shape.parapetBackM * 1000 - slab.parapetGapMm, chamferA: 300, chamferB: 300 }); const para = abutParapetRebar(pg, wall); const seats = members.wallSegments.map((sg) => ({ from: sg.sFrom * 1000, to: sg.sTo * 1000, z: (sg.seatEl - members.footingBottomEl!) * 1000 })); const wingR = wings.map((w) => abutWingRebar(abutWingGeom(model, members, w, footing.z.B68, seats, k))); const grade = slabGradePct(slab, model.wing?.slopePct ?? 0); const slabG = abutSlabGeom(slab.form, abutPlanOutline(model, members, wings, slab).slab, slabSection(slab, grade, members.shape), { startEl: members.parapetTopStartEl, endEl: members.parapetTopEndEl, L: model.length, bottomEl: members.footingBottomEl! }, [300, 300], slab.lengthMm, grade); const rebar = buildAbutRebar3D(fg, footing, k, members.shape.footingBackM, members.footingBottomEl!, wall, para, wingR, abutSlabRebar(slabG)); const group = buildAbutGroup(model, members, wings); group.updateMatrixWorld(true); const solids: THREE.Mesh[] = []; group.traverse((o) => { if (o instanceof THREE.Mesh && !['ground', 'blinding', 'piles'].some((n) => o.parent?.name === n || o.name === n)) solids.push(o); }); const ray = new THREE.Raycaster(); const inside = (p: THREE.Vector3) => solids.some((m) => { ray.set(p, new THREE.Vector3(0.577, 0.577, 0.577)); return ray.intersectObject(m, false).length % 2 === 1; }); for (const [name, meshes] of rebar.byName) { for (const mesh of meshes) { const geo = mesh.geometry as THREE.CylinderGeometry; geo.computeBoundingBox(); // Cylinder axis: the two cap centres = bbox centre ± half length along its direction; sample the bbox diagonal's centre line instead. const pos = geo.getAttribute('position'); const a = new THREE.Vector3(pos.getX(0), pos.getY(0), pos.getZ(0)); const b = new THREE.Vector3(pos.getX(pos.count - 1), pos.getY(pos.count - 1), pos.getZ(pos.count - 1)); const n = Math.max(2, Math.ceil(a.distanceTo(b) / 0.1)); const r = (res[name] ??= { out: 0, total: 0 }); for (let i = 0; i <= n; i++) { const p = a.clone().lerp(b, i / n); r.total++; if (!inside(p)) { r.out++; } } } } } describe('교대 A1 3D 철근이 콘크리트 안에 있음 (접속슬래브 포함)', () => { it('저판 · 벽체 · 흉벽 · 날개벽 · 접속슬래브 철근과 P10 다웰 모두 밖으로 나간 점이 없음', () => { const outside = Object.entries(res).filter(([, v]) => v.out > 0).map(([name, v]) => `${name} ${v.out}/${v.total}`); expect(outside).toEqual([]); expect(Object.keys(res).length).toBeGreaterThan(36); for (const m of ['R1', 'R2', 'R3', 'R4', 'R7', 'R8', 'P10']) expect(res[m]?.total, m).toBeGreaterThan(0); }); });