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 { abutSoles, resolveBearingRule } from '../src/core/abut/bearing-table'; import { buildAbutGroup } from '../src/view/abut-scene'; import { loadInputWorkbook, p1Pairs } from './helpers'; // 교대 받침 본체 · 솔플레이트 3D (2026-11-06, 교각과 같은 개념). A1 with its P1 pair: S −1.908 %, T 30, h3 245. // Vertex positions are 32-bit floats: at EL 44 m they carry ~1.5 µm, so heights are compared to 5 decimals. const rec = parseAbutments(loadInputWorkbook()).abutments.find((a) => a.name === 'A1')!; const model = buildAbut(toAbutInput(rec)); const ctx = { pairs: p1Pairs(), slopeI: rec.slopeI }; const soles = abutSoles(model, resolveBearingRule(), rec.bearingHeightMm, ctx); const group = buildAbutGroup(model, undefined, undefined, undefined, undefined, undefined, undefined, soles); const meshes = (name: string) => (group.getObjectByName(name) as THREE.Group).children as THREE.Mesh[]; const box = (m: THREE.Object3D) => new THREE.Box3().setFromObject(m); describe('교대 받침 본체 · 솔플레이트 3D', () => { it('one bearing body per bearing, height h3 (245), on the pad top (z)', () => { expect(meshes('bearings')).toHaveLength(5); model.bearings.forEach((b, i) => { const bb = box(meshes('bearings')[i]); expect(bb.max.y - bb.min.y).toBeCloseTo(0.245, 5); expect(bb.min.y).toBeCloseTo(b.z, 5); }); }); it('one solplate wedge per bearing: from z + h3 up to T1 (38 mm) at its thick end', () => { expect(meshes('solplates')).toHaveLength(5); model.bearings.forEach((b, i) => { const bb = box(meshes('solplates')[i]); expect(bb.min.y).toBeCloseTo(b.z + 0.245, 5); expect(bb.max.y).toBeCloseTo(b.z + 0.245 + 0.038, 5); }); }); it('T1 sits on the 오르막 쪽: A1 is higher than P1, so the thick end points away from P1 (against the azimuth)', () => { model.bearings.forEach((b, i) => { const pos = meshes('solplates')[i].geometry.getAttribute('position') as THREE.BufferAttribute; const pts = Array.from({ length: pos.count }, (_, k) => new THREE.Vector3(pos.getX(k), pos.getY(k), pos.getZ(k))); const topY = Math.max(...pts.map((p) => p.y)); const thick = pts.filter((p) => topY - p.y < 1e-6), thin = pts.filter((p) => p.y > b.z + 0.245 + 1e-6 && topY - p.y > 1e-3); const mid = (ps: THREE.Vector3[]) => ps.reduce((a, p) => a.add(p), new THREE.Vector3()).multiplyScalar(1 / ps.length); const d = mid(thick).sub(mid(thin)); // Group without a pivot: local x = s along u, local z = t along the front (nLeft · frontSide). const front = { e: model.nLeft.e * model.frontSide, n: model.nLeft.n * model.frontSide }; const dir = { e: d.x * model.u.e + d.z * front.e, n: d.x * model.u.n + d.z * front.n }; const az = (b.azimuthDeg! * Math.PI) / 180; expect(dir.e * Math.sin(az) + dir.n * Math.cos(az)).toBeLessThan(0); }); }); it('거더 하면 고정: S −3 % typed → T 34, the pad and block top drop 4 mm and the solplate top stays', () => { const steep = abutSoles(model, resolveBearingRule({ slopePct: -3 }), rec.bearingHeightMm, ctx); const g = buildAbutGroup(model, undefined, undefined, undefined, undefined, undefined, undefined, steep); const body = box((g.getObjectByName('bearings') as THREE.Group).children[0]); expect(body.min.y).toBeCloseTo(model.bearings[0].z - 0.004, 5); // Centre thickness T: the top at the middle of the wedge is pad top + h3 + T, the same as with T 30. expect(body.min.y + 0.245 + 0.034).toBeCloseTo(model.bearings[0].z + 0.245 + 0.03, 5); }); it('without soles the group keeps only the block and pad (old callers)', () => { const plain = buildAbutGroup(model); expect(plain.getObjectByName('bearings')).toBeUndefined(); expect(plain.getObjectByName('solplates')).toBeUndefined(); }); });