import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import * as XLSX from 'xlsx'; import * as THREE from 'three'; 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 { abutFootingGeom, abutFootingRebar } from '../src/core/abut/rebar/footing'; import { skewAngle } from '../src/core/abut/rebar/skew-layout'; import { abutSkewK } from '../src/view/abut-scene'; import { buildAbutFootingRebar3D } from '../src/view/abut-rebar3d'; // 예제 A1 (교대확대기초철근.md §13). const RAG = fileURLToPath(new URL('../../../RAG/운곡교P1입력.xlsx', import.meta.url)); const rec = parseAbutments(XLSX.read(readFileSync(RAG), { type: 'buffer' })).abutments[0]; const model = buildAbut(toAbutInput(rec)); const members = buildMembers(model, { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm }); const geom = abutFootingGeom(model.length, members.shape, skewAngle(rec.skewDeg)); const k = abutSkewK(model); const back = members.shape.footingBackM; const bottom = members.footingBottomEl!; const build = (dir: 'skew' | 'perp') => { const r = abutFootingRebar(geom, { dir }); return { r, scene: buildAbutFootingRebar3D(geom, r, k, back, bottom) }; }; // Segments per bar: ⊓ / ⊔ have 3, the others 1. const PIECES: Record = { B1: 3, B2: 3, B5: 3, B6: 3 }; describe('교대 저판 3D 철근 (③, 2026-11-04)', () => { for (const dir of ['skew', 'perp'] as const) { it(`draws every 철근표 bar with its count (${dir})`, () => { const { r, scene } = build(dir); for (const b of r.bars) expect(scene.byName.get(b.mark)?.length, b.mark).toBe(b.count * (PIECES[b.mark] ?? 1)); }); it(`keeps every bar inside the 저판 (${dir})`, () => { const { scene } = build(dir); const top = bottom + geom.H / 1000; const front = back + geom.W / 1000; const box = new THREE.Box3(); for (const mesh of scene.group.children as THREE.Mesh[]) { mesh.geometry.computeBoundingBox(); box.copy(mesh.geometry.boundingBox!); const tMid = (box.min.z + box.max.z) / 2; const s0 = k * tMid; expect(box.min.y).toBeGreaterThan(bottom); expect(box.max.y).toBeLessThan(top); expect(box.min.z).toBeGreaterThan(back); expect(box.max.z).toBeLessThan(front); // Along the axis, within the skew ends at that t (a bar diameter of slack for the cylinder's sides). expect(box.min.x).toBeGreaterThan(s0 + Math.min(0, k * (box.min.z - tMid), k * (box.max.z - tMid)) - 0.03); expect(box.max.x).toBeLessThan(s0 + geom.L / 1000 + Math.max(0, k * (box.min.z - tMid), k * (box.max.z - tMid)) + 0.03); } }); } it('dims or hides the bars not selected (선택만 보기)', () => { const { scene } = build('skew'); scene.show(new Set(['B9']), true); expect(scene.byName.get('B9')!.every((m) => m.visible)).toBe(true); expect(scene.byName.get('B1')!.some((m) => m.visible)).toBe(false); scene.show(null, false); expect(scene.byName.get('B1')!.every((m) => m.visible)).toBe(true); }); });