Files
Pier_v1/tests/solplate-scene.test.ts
T
ypshimandClaude Opus 5.5 a0b0929e13 feat(bearing): 교각 솔플레이트를 2026-11-06 결정으로 (짝 받침 S · 열 T · 거더 하면 고정)
받침방위각_솔플레이트검토.md §3과 시안(Version 110–113)을 따른다 (사용자 결정 2026-11-06: 시안 적용).
11-03 C안(받침높이 H 고정, h3 = H − T, S 입력)을 대체한다.
- S: 교량 전체(A1, P1 … Pn, A2) 거더 짝 받침의 z 차 / 수평거리. 없으면 교각 종단경사 입력 →
  다른 열 같은 순번 값(가정, WS03) → 0 %(WS03). 받침별 직접 수정(수정됨)
- 평면 L = A + 100, W = B + 100 (받침별 A × B, 기본 700 × 640 가정), h3 181
- T = 열마다 max(22, ⌈22 + L·|S|/2⌉), 열 T 직접 수정 가능(T2 < T_min → WS02).
  T1은 오르막 쪽(3D 쐐기의 두꺼운 끝도 그쪽), T2 반대쪽
- 거더 하면 고정: 블럭 상면 = z − (T − T_ref), T_ref 30
- 짝 받침은 불러오기·프로젝트 열기 때 교량 전체로 계산해 교각 생성에 넘김
- ② 카드: h3·T_ref·T_min·여유·열 T·종단경사 입력, 표에 짝 받침·S 근거·받침별 S/A/B 수정·검산 W08·검사
- P1 검증(A1과 함께): S −1.908 … −1.902 %, L 800 · W 740 · T 30 · T1 38 · T2 22, 블럭 높이 변화 없음
- 블럭·솔플레이트 테스트는 A1–P1 짝 문맥으로 실행 (짝이 없으면 S 0 · T 22 · 블럭 +8 mm)
교대(QA-15 C안)와 Pier 시트 종단경사 열 읽기는 아직 남음.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 14:48:10 +09:00

80 lines
4.4 KiB
TypeScript
Raw 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 { loadP1, p1Pairs } from './helpers';
// 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 };
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('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);
});
});