Files
Pier_v1/tests/abut-geometry.test.ts
T
ypshimandClaude Haiku 5.5 1d31d2f3a9 feat(abut): 교대 형상·입력 패널과 교각 받침·솔플레이트 개편
- 교대 형상(src/core/abut, 3D 장면, 목록·카메라·맞춤)과 교대 입력 패널 추가
- 교각 받침 기준 카드 제거, 솔플레이트 규칙을 교각별 입력으로 변경
- 받침 ELEVATION·솔플레이트 TABLE을 모델값으로 연결
- 교대 입력값 카드: 형식 행에 시점·종점 표시, 제목을 "A1 입력값 [입력]"으로 변경
- 테스트 614건 통과, 타입 검사 통과

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
2026-11-03 14:51:34 +09:00

140 lines
5.7 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 { buildAbut, type AbutInput } from '../src/core/abut/geometry';
// Abut 시트의 A1 (운곡교, 2026-10-30 6차). 시트 x = 북, y = 동 → 내부 (E, N) = (y, x).
const A1: AbutInput = {
name: 'A1',
type: 'A1',
seatEl: 44.367,
ip1: { e: 179131.917023, n: 259815.91973 },
ip2: { e: 179131.907126, n: 259815.91805 },
ip3: { e: 179119.891533, n: 259817.959709 },
bearings: [
{ no: 1, pos: { e: 179121.1404, n: 259818.5035 }, z: 44.492 },
{ no: 2, pos: { e: 179123.6148, n: 259818.0835 }, z: 44.563 },
{ no: 3, pos: { e: 179126.0891, n: 259817.6635 }, z: 44.634 },
{ no: 4, pos: { e: 179128.5634, n: 259817.2435 }, z: 44.705 },
{ no: 5, pos: { e: 179131.0377, n: 259816.8236 }, z: 44.776 },
],
};
describe('교대 A1 축과 받침 (AA1, spec_abut §10.1)', () => {
const m = buildAbut(A1);
it('axis IP1 → IP3 is 12.197 m', () => {
expect(m.length).toBeCloseTo(12.197, 3);
});
it('bearing s from IP1: 11.057 / 8.547 / 6.037 / 3.528 / 1.018 m (①–⑤)', () => {
const expected = [11.057, 8.547, 6.037, 3.528, 1.018];
m.bearings.forEach((b, i) => expect(b.s).toBeCloseTo(expected[i], 2));
});
it('seat EL ①–⑤: 44.367 / 44.438 / 44.509 / 44.580 / 44.651 (block 125 mm, pad included)', () => {
const expected = [44.367, 44.438, 44.509, 44.58, 44.651];
m.bearings.forEach((b, i) => {
expect(b.seatEl).toBeCloseTo(expected[i], 3);
expect(b.blockHeightMm).toBe(125);
});
});
it('step boundaries from IP1: 2.273 / 4.783 / 7.292 / 9.802 m', () => {
const expected = [2.273, 4.783, 7.292, 9.802];
expect(m.stepBounds).toHaveLength(4);
m.stepBounds.forEach((s, i) => expect(s).toBeCloseTo(expected[i], 2));
});
it('the bearings are on the left of 1 → n for A1 (front), so the layout matches the type (AW03 clear)', () => {
expect(m.frontLeftOfBearingDir).toBe(true);
expect(m.layoutMatchesType).toBe(true);
});
it('the bearing 1 lies 0.745 m from the axis (spec §10.1 전면 판정)', () => {
expect(Math.abs(m.bearings[0].lateral)).toBeCloseTo(0.745, 2);
});
it('an A2 label on the same layout fails the type check (AW03)', () => {
expect(buildAbut({ ...A1, type: 'A2' }).layoutMatchesType).toBe(false);
});
it('a bearing with its own 교좌면 EL uses it and its block height is z − seat', () => {
const own = buildAbut({ ...A1, bearings: A1.bearings.map((b, i) => (i === 2 ? { ...b, seatEl: 44.5 } : b)) });
expect(own.bearings[2].seatEl).toBeCloseTo(44.5, 6);
expect(own.bearings[2].blockHeightMm).toBe(134);
expect(own.bearings[0].blockHeightMm).toBe(125);
});
it('refuses an abutment with fewer than two bearings or coincident IP1 and IP3', () => {
expect(() => buildAbut({ ...A1, bearings: A1.bearings.slice(0, 1) })).toThrow('받침이 2개 이상');
expect(() => buildAbut({ ...A1, ip3: A1.ip1 })).toThrow('같은 점');
});
});
// AA2 (spec_abut §4.7, §10.1, §10.2): 날개벽 선, 날개벽 경사, AW01, 접속슬래브 폭.
const SKEW = 95 + 2 / 60 + 52 / 3600; // 95°02′52″
const GIRDER = 75 + 56 / 60 + 55.9 / 3600; // 거더 방향 CAD 75°56′55.9″
const A1_WING: AbutInput = { ...A1, skewDeg: SKEW, slopeI: -0.0191, girderAzimuthDeg: GIRDER };
// A2 합성 예제 (spec §10.2): A1을 중심 C 기준으로 180° 돌리고, 받침 번호를 거꾸로 매겨 1 → n 방향이 A1과 같게 한다.
const A2_WING: AbutInput = {
name: 'A2',
type: 'A2',
seatEl: 44.367,
ip1: { e: 179161.0318, n: 259932.3259 },
ip2: { e: 179161.0417, n: 259932.3276 },
ip3: { e: 179173.0573, n: 259930.2859 },
bearings: [
{ no: 1, pos: { e: 179161.9111, n: 259931.422 }, z: 44.492 },
{ no: 2, pos: { e: 179164.3854, n: 259931.0021 }, z: 44.563 },
{ no: 3, pos: { e: 179166.8597, n: 259930.5821 }, z: 44.634 },
{ no: 4, pos: { e: 179169.334, n: 259930.1621 }, z: 44.705 },
{ no: 5, pos: { e: 179171.8084, n: 259929.7421 }, z: 44.776 },
],
skewDeg: SKEW,
slopeI: -0.0191,
girderAzimuthDeg: GIRDER,
};
describe('교대 날개벽 · 접속슬래브 폭 (AA2, spec_abut §4.7–4.8, §10)', () => {
const w = buildAbut(A1_WING).wing!;
it('A1 wing line is 255.32° CAD, on the 배면 side (not the bridge side)', () => {
expect(w.lineDeg).toBeCloseTo(255.32, 1);
});
it('AW01: the wing line is within 1° of the girder direction (0.63°)', () => {
expect(w.girderDiffDeg).toBeCloseTo(0.63, 1);
expect(w.aw01Ok).toBe(true);
});
it('A1 wing slope is +1.91 % (A1 = −i), wing rising away from the wall', () => {
expect(w.slopePct).toBeCloseTo(1.91, 2);
});
it('slab width 11.253 m with both general wings; 11.725 with one; 12.197 with none', () => {
expect(w.slabWidthMm).toBeCloseTo(11253, -1);
expect(buildAbut({ ...A1_WING, wings: { left: 'general', right: 'none' } }).wing!.slabWidthMm).toBeCloseTo(11725, -1);
expect(buildAbut({ ...A1_WING, wings: { left: 'none', right: 'none' } }).wing!.slabWidthMm).toBeCloseTo(12197, -1);
});
it('A2 synthetic: the bearings are on the right of 1 → n, so the layout matches A2 (AW03 clear)', () => {
const m2 = buildAbut(A2_WING);
expect(m2.frontLeftOfBearingDir).toBe(false);
expect(m2.layoutMatchesType).toBe(true);
expect(m2.bearings[0].s).toBeCloseTo(1.018, 2);
expect(m2.bearings[4].s).toBeCloseTo(11.057, 2);
});
it('A2 synthetic: the wing line is 75.32° (배면 쪽, 교량 반대쪽), not 255.32° (the wrong 1→n rotation)', () => {
const w2 = buildAbut(A2_WING).wing!;
expect(w2.lineDeg).toBeCloseTo(75.32, 1);
expect(w2.aw01Ok).toBe(true);
expect(w2.slopePct).toBeCloseTo(-1.91, 2);
expect(w2.slabWidthMm).toBeCloseTo(11253, -1);
});
it('without skew there is no wing block', () => {
expect(buildAbut(A1).wing).toBeUndefined();
});
});