feat(abut): 교대 3D에 받침 본체 · 솔플레이트 (교각과 같은 개념)

교대 3D는 받침블럭 콘크리트와 몰탈패드만 그렸다. ② 받침 카드와 같은 솔플레이트(abutSoles)로 교각처럼 그린다.
- 받침 본체: 솔플레이트 평면에서 사방 100 mm 뺀 상자, 높이 h3, 패드 상면에서
- 솔플레이트: L은 거더 방향(블럭 회전 = 방위각, 적용한 계산 방위각 포함), W는 그 직각, T1은 오르막 쪽
- 거더 하면 고정: T ≠ T_ref이면 블럭 상면 · 패드가 (T − T_ref)만큼 내려감 (받침 ELEVATION TABLE과 일치)
- 부재 표시: '받침·받침블럭'에 교대 bearings, '솔플레이트'에 교대 solplates 연결
- 테스트: A1(짝 P1) 본체 높이 245 · 솔플 상면 z + 245 + 38, 두꺼운 끝이 오르막(A1) 쪽, S −3 % → 패드 −4 mm ·
  상면 유지, soles 없이 부를 때 기존 형상. 브라우저: 교대만 · 교각+교대 가져오기 예외 0, 받침 쌓임 확인

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Opus 5.5 committed 2026-11-06 16:03:57 +09:00
1 parent c7acfccd51
commit 1fda705b75
3 files changed
+113 -6

No files matched your search

+68
View File
@@ -0,0 +1,68 @@
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();
});
});