import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import * as XLSX from 'xlsx'; import { describe, expect, it } from 'vitest'; import { buildAbut, type AbutInput } from '../src/core/abut/geometry'; import { parseAbutments, toAbutInput } from '../src/core/abut/parse'; import { buildMembers } from '../src/core/abut/members'; import { buildWings, WING_LENGTH_M } from '../src/core/abut/wings'; const RAG = fileURLToPath(new URL('../../../RAG/운곡교P1입력.xlsx', import.meta.url)); const a1Input = toAbutInput(parseAbutments(XLSX.read(readFileSync(RAG), { type: 'buffer' })).abutments[0]); const SKEW = 95 + 2 / 60 + 52 / 3600; const GIRDER = 75 + 56 / 60 + 55.9 / 3600; // A2 합성 예제 (spec §10.2): ① 쪽이 IP1 끝. const a2Input: 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, foundationBottomEl: 40.895, pileLengthMm: 21600, }; describe('교대 A2 합성 · 날개벽 (AA3b, spec_abut §10.2)', () => { const model = buildAbut(a2Input); const members = buildMembers(model, { bottomEl: 40.895, pileLengthMm: 21600 }); const [left, right] = buildWings(model, members); it('좌 날개벽 (① 쪽, IP1 끝): top 46.838 at the start, 46.714 at the end, bottom end 45.214', () => { expect(left.side).toBe('left'); expect(left.sEnd).toBeCloseTo(0, 9); expect(left.topStartEl).toBeCloseTo(46.838, 2); expect(left.topEndEl).toBeCloseTo(46.714, 2); expect(left.bottomEndEl).toBeCloseTo(45.214, 2); }); it('우 날개벽 (⑤ 쪽, IP3 끝): top 47.183 → 47.059, bottom end 45.559', () => { expect(right.side).toBe('right'); expect(right.sEnd).toBeCloseTo(model.length, 9); expect(right.topStartEl).toBeCloseTo(47.183, 2); expect(right.topEndEl).toBeCloseTo(47.059, 2); expect(right.bottomEndEl).toBeCloseTo(45.559, 2); }); it('the wing is 6.5 m long along the wing line and 0.45 m thick inward', () => { const [p0, p1] = left.corners; expect(Math.hypot(p1.e - p0.e, p1.n - p0.n)).toBeCloseTo(WING_LENGTH_M, 6); const [, , q2, q3] = left.corners; expect(Math.hypot(q2.e - p1.e, q2.n - p1.n)).toBeCloseTo(0.45, 6); expect(Math.hypot(q3.e - p0.e, q3.n - p0.n)).toBeCloseTo(0.45, 6); }); it('the wing starts at the axis end with the wing direction on the 배면 side (AA2 line 75.32°)', () => { const [p0, p1] = left.corners; const deg = (Math.atan2(p1.n - p0.n, p1.e - p0.e) * 180) / Math.PI; const cad = ((deg % 360) + 360) % 360; expect(cad).toBeCloseTo(75.32, 1); }); it('a 단축형 wing bottom sits at the footing top (41.995)', () => { const short = buildWings(model, members, { left: 'short', right: 'none' }); expect(short).toHaveLength(1); expect(short[0].bottomEndEl).toBeCloseTo(41.995, 3); }); it('no wing when the side is 없음', () => { expect(buildWings(model, members, { left: 'none', right: 'none' })).toEqual([]); }); }); describe('교대 A1 날개벽 (운곡교P1입력.xlsx)', () => { const model = buildAbut(a1Input); const members = buildMembers(model, { bottomEl: a1Input.foundationBottomEl, pileLengthMm: a1Input.pileLengthMm }); const wings = buildWings(model, members); it('A1 has two general wings; ① 쪽 wing starts at the IP3 end (the bearing ① is near IP3)', () => { expect(wings.map((w) => w.side)).toEqual(['left', 'right']); const left = wings[0]; expect(left.sEnd).toBeCloseTo(model.length, 9); expect(left.topStartEl).toBeCloseTo(46.835, 2); // A1 날개벽 경사는 +1.91 % (벽체에서 멀어질수록 높아짐): 끝이 0.124 m 높다. expect(left.topEndEl - left.topStartEl).toBeCloseTo(0.124, 2); }); }); describe('교대 날개벽 하단 선 · 헌치 (AA3c, spec_abut §4.7)', () => { const a1 = buildAbut(a1Input); const a1m = buildMembers(a1, { bottomEl: a1Input.foundationBottomEl, pileLengthMm: a1Input.pileLengthMm }); const [a1Left] = buildWings(a1, a1m); const a2 = buildAbut(a2Input); const a2m = buildMembers(a2, { bottomEl: 40.895, pileLengthMm: 21600 }); const [a2Left] = buildWings(a2, a2m); it('뒷굽 끝까지의 날개벽 거리 1.807 m (단축형 표와 같다)', () => { expect(a1Left.heelDistanceM).toBeCloseTo(1.807, 2); expect(a2Left.heelDistanceM).toBeCloseTo(1.807, 2); }); it('하단선: 뒷굽 끝까지 저판 상면 41.995, 그 밖은 단부 하단까지 직선 (A2 좌: 45.214)', () => { const line = a2Left.bottomLine; expect(line[0].el).toBeCloseTo(41.995, 3); expect(line[1].dM).toBeCloseTo(1.807, 2); expect(line[1].el).toBeCloseTo(41.995, 3); expect(line[2].el).toBeCloseTo(45.214, 2); }); it('two vertical 300 × 300 haunches at the 내측면 corners: upper at the 흉벽 배면, lower at the 벽체 배면', () => { expect(a2Left.haunches.map((h) => h.kind)).toEqual(['upper', 'lower']); for (const h of a2Left.haunches) { const [c0, c1, c2] = h.corners; expect(Math.hypot(c1.e - c0.e, c1.n - c0.n)).toBeCloseTo(0.3, 6); expect(Math.hypot(c2.e - c0.e, c2.n - c0.n)).toBeCloseTo(0.3, 6); } }); it('the 저판 상면 haunch runs from the 벽체 배면 to the 뒷굽 on the 저판 상면; the 뒷굽 haunch is gone', () => { const f = a2Left.footingHaunch!; expect(f.el).toBeCloseTo(41.995, 3); expect(f.dMTo).toBeGreaterThan(f.dMFrom); expect((a2Left as { heelHaunch?: unknown }).heelHaunch).toBeUndefined(); }); it('upper haunch runs from the bracket top (흉벽 상단 − 0.85) to the wing top', () => { const [up, low] = a2Left.haunches; // The upper haunch rests on the highest point of the 브라켓 top over its footprint (the top falls along s), never below it. expect(up.zBottomEl).toBeGreaterThanOrEqual(46.838 - 0.85 - 1e-6); expect(up.zBottomEl).toBeLessThan(46.838 - 0.85 + 0.05); expect(up.zTopEl).toBeGreaterThan(up.zBottomEl); expect(low.zBottomEl).toBeCloseTo(41.995, 3); expect(low.zTopEl).toBeCloseTo(46.838 - 0.85, 2); }); }); describe('날개벽 수평 단 (QA-12 초안 적용, spec_abut §4.7)', () => { const a2 = buildAbut(a2Input); const a2m = buildMembers(a2, { bottomEl: 40.895, pileLengthMm: 21600 }); const [a2Left] = buildWings(a2, a2m); it('등단면 down to 1.5 m below the wing end, then 변화단면1 to the seat EL and 변화단면2 to the footing top', () => { const [equal, v1, v2] = a2Left.bands; expect(equal.name).toBe('등단면'); expect(equal.zBottomEl).toBeCloseTo(45.214, 2); expect(v1.name).toBe('변화단면1'); expect(v1.zBottomEl).toBeCloseTo(44.367, 3); expect(v2.name).toBe('변화단면2'); expect(v2.zBottomEl).toBeCloseTo(41.995, 3); }); it('bands are contiguous from the wing top to the footing top', () => { const b = buildWings(a2, a2m)[0].bands; for (let i = 1; i < b.length; i++) expect(b[i].zTopEl).toBeCloseTo(b[i - 1].zBottomEl, 6); expect(b[b.length - 1].zBottomEl).toBeCloseTo(41.995, 3); }); });