Files
Pier_v1/tests/solplate-scene.test.ts
ypshimandClaude Opus 5.5 3bd2340609 feat(bearing): 솔플레이트 표에 '방위각 검사 (″)' 열 (Q-SH4)
교각·교대 솔플레이트 상세 TABLE에 받침별로 엑셀 방위각과 짝 받침 방향의 차이(초)를 보인다.
10″ 이하 "≤ 10″", 넘으면 WS01, 짝이 없으면 "— (짝 없음)".
- BearingSole.azimuthCheckSec (짝의 from/to 쪽에 맞는 값), 표 행 azimuthSec
- 테스트: P1 전열 ≤ 0.5″ · 후열 없음, A1 ≤ 0.5″, P1 ③ 방위각 75°59′ 오타 → 약 87″ · WS01 그 받침만
- 브라우저 확인(운곡교 완도·광주 데이터): P1-광주 전열 짝 A1-광주 0.0–0.3″, 후열 짝 P2-광주 0.0–0.1″

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 15:39:42 +09:00

95 lines
5.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest';
import * as THREE from 'three';
import { buildPier } from '../src/core/geometry';
import { solplateGeometry } from '../src/core/solplate';
import { buildPierScene } from '../src/view/scene';
import { loadInputWorkbook, loadP1, p1Pairs } from './helpers';
import { parseAbutments } from '../src/core/abut/parse';
import { girderPairs } from '../src/core/bearing-pair';
// Bridge context (A1 ↔ P1 girder pairs) as in the app, so the solplate slope is computed (2026-11-06).
const P1CTX = { pairs: p1Pairs() };
const FRONT = { 1: 3, 2: 3, 3: 3, 4: 3, 5: 3 };
const a1 = parseAbutments(loadInputWorkbook()).abutments.find((a) => a.name === 'A1')!;
const A1S = { name: 'A1', bearings: a1.bearings.map((b) => ({ no: b.no, x: b.pos.e, y: b.pos.n, z: b.z, azimuthDeg: b.azimuthDeg! })) };
describe('솔플레이트 모델과 3D (결정 2026-11-06, Q-SH1–9)', () => {
it('every bearing: h3 181, L 800 × W 740 (A 700 × B 640 + 100), T 30, T1 38 / T2 22', () => {
const m = buildPier(loadP1(), P1CTX);
expect(m.blocks.every((b) => b.plan.h3 === 181 && b.plan.L === 800 && b.plan.W === 740 && b.plan.T === 30 && b.plan.T1 === 38 && b.plan.T2 === 22)).toBe(true);
});
it('S 3 % typed on the front row → T 34, T1 46 / T2 22, and the front blocks drop 4 mm (girder underside kept)', () => {
const base = buildPier(loadP1(), P1CTX);
const m = buildPier({ ...loadP1(), sole: { slopePct: FRONT } }, P1CTX);
const b1 = m.blocks.find((b) => b.no === 1)!, b0 = base.blocks.find((b) => b.no === 1)!;
expect(b1.plan).toMatchObject({ T: 34, T1: 46, T2: 22 });
expect(b1.top).toBeCloseTo(b0.top - 0.004, 9);
expect(b1.padTop + (b1.plan.h3 + b1.plan.T) / 1000).toBeCloseTo(b0.padTop + (b0.plan.h3 + b0.plan.T) / 1000, 9);
expect(m.soles.find((s) => s.no === 1)!.slopeSource).toBe('수정됨');
// The back row keeps its own T (30): one T per row (Q-SH9).
expect(m.blocks.find((b) => b.no === 6)!.plan.T).toBe(30);
});
it('방위각 검사: P1 ③ azimuth typed as 75°59′ (CAD) shows ≈ 87″ and WS01 on that bearing only', () => {
const p1 = loadP1();
const typo = { ...p1, bearings: p1.bearings.map((b) => (b.no === 3 ? { ...b, azimuthDeg: 90 - (75 + 59 / 60) } : b)) };
const pairs = girderPairs([A1S, { name: 'P1', bearings: typo.bearings.map((b) => ({ no: b.no, x: b.x, y: b.y, z: b.z, azimuthDeg: b.azimuthDeg })) }]);
const m = buildPier(typo, { pairs });
const s3 = m.soles.find((s) => s.no === 3)!;
expect(s3.azimuthCheckSec!).toBeGreaterThan(80);
expect(s3.warnings).toContain('WS01');
expect(m.soles.filter((s) => s.no !== 3 && s.row === '전열').every((s) => s.azimuthCheckSec! <= 0.5 && !s.warnings.includes('WS01'))).toBe(true);
});
it('WS02: T typed 30 at S 3 % leaves T2 18 < 22', () => {
const m = buildPier({ ...loadP1(), sole: { slopePct: FRONT, tByRow: { 전열: 30 } } }, P1CTX);
const s = m.soles.find((x) => x.no === 1)!;
expect(s).toMatchObject({ T: 30, tTyped: true, T2: 18 });
expect(s.warnings).toContain('WS02');
});
it('without a pair or 종단경사 the slope is 0 (WS03): T 22, the block rises 8 mm', () => {
const alone = buildPier(loadP1());
expect(alone.soles.every((s) => s.slopePct === 0 && s.warnings.includes('WS03') && s.T === 22)).toBe(true);
const withSlope = buildPier({ ...loadP1(), longitudinalSlope: -0.0191 });
expect(withSlope.soles.every((s) => s.slopeSource === '입력' && Math.abs(s.slopePct + 1.91) < 1e-9 && s.T === 30)).toBe(true);
});
it('T1 sits on the 오르막 쪽: the thick end of P1 ① points towards A1 (the higher girder end)', () => {
const m = buildPier(loadP1(), P1CTX);
const b = m.blocks.find((x) => x.no === 1)!;
const g = solplateGeometry(b, b.plan);
// Top corners: index (ai·2 + ci)·2 + 1; the thick end has the larger top.
const tops = [0, 1, 2, 3].map((q) => g.corners[q * 2 + 1]);
const thick = tops.filter((p) => p[1] - g.bottom > (g.T1 + g.T2) / 2), thin = tops.filter((p) => p[1] - g.bottom < (g.T1 + g.T2) / 2);
const mid = (ps: number[][]) => [0, 2].map((i) => ps.reduce((s, p) => s + p[i], 0) / ps.length);
const [ds, dn] = [mid(thick)[0] - mid(thin)[0], mid(thick)[1] - mid(thin)[1]];
const toThick = { e: ds * m.u.e + dn * m.nrm.e, n: ds * m.u.n + dn * m.nrm.n };
const p1 = loadP1().bearings[0];
const pair = P1CTX.pairs.find((p) => p.to.no === 1)!;
// A1 lies against the azimuth direction from P1 (pairs look ahead from A1 to P1).
const az = (p1.azimuthDeg * Math.PI) / 180;
expect(pair.uphill).toBe('A1');
expect(toThick.e * -Math.sin(az) + toThick.n * -Math.cos(az)).toBeGreaterThan(0);
});
it('the 3D scene has one solplate wedge per bearing, and its highest point is the thick end of the tallest one', () => {
const m = buildPier(loadP1(), P1CTX);
const scene = buildPierScene(m);
const wedges = scene.parts.solplates.children;
expect(wedges.length).toBe(10);
const box = new THREE.Box3().setFromObject(scene.parts.solplates);
const top = Math.max(...m.blocks.map((b) => solplateGeometry(b, b.plan).bottom + solplateGeometry(b, b.plan).T1));
expect(box.max.y).toBeCloseTo(top, 3);
});
it('the 교량받침 본체 is one box per bearing, height h3 (181 mm)', () => {
const scene = buildPierScene(buildPier(loadP1(), P1CTX));
const bodies = scene.parts.bearings.children;
expect(bodies.length).toBe(10);
const one = new THREE.Box3().setFromObject(bodies[0]);
expect(one.max.y - one.min.y).toBeCloseTo(0.181, 3);
});
});