import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import * as XLSX from 'xlsx'; import { describe, expect, it } from 'vitest'; import { buildAbut } from '../src/core/abut/geometry'; import { parseAbutments, toAbutInput } from '../src/core/abut/parse'; import { buildMembers } from '../src/core/abut/members'; import { buildWings } from '../src/core/abut/wings'; import { abutPlanOutline } from '../src/view/abut-scene'; import { abutWingCornerSvg, abutWingPlanSvg, abutWingSideSvg } from '../src/view/abut-wing-svg'; const RAG = fileURLToPath(new URL('../../../RAG/운곡교P1입력.xlsx', import.meta.url)); const rec = parseAbutments(XLSX.read(readFileSync(RAG), { type: 'buffer' })).abutments[0]; const build = (r = rec) => { const model = buildAbut(toAbutInput(r)); const members = buildMembers(model, { bottomEl: r.foundationBottomEl, pileLengthMm: r.pileLengthMm }, r.shape); return { model, members, wings: buildWings(model, members, r.wings, r.wingDims) }; }; describe('날개벽 치수 입력 (측별) 과 연동', () => { it('defaults: 6500 · 450 · 1500 · haunch 300 on both sides; 단축형 runs to the 뒷굽 끝 (1.807 m)', () => { const { wings } = build(); for (const w of wings) expect([w.lengthM, w.thicknessM, w.endHeightM, w.haunchM]).toEqual([6.5, 0.45, 1.5, 0.3]); const short = build({ ...rec, wings: { left: 'short', right: 'short' } }).wings[0]; expect(short.lengthM).toBeCloseTo(short.heelDistanceM, 3); expect(short.heelDistanceM).toBeCloseTo(1.807, 3); }); it('a thicker wing narrows the 접속슬래브 (slab width = L − Σ(t/|sin Δ| + 20))', () => { const base = build().model.wing!.slabWidthMm; const thick = build({ ...rec, wingDims: { left: { thicknessMm: 550 }, right: { thicknessMm: 550 } } }); expect(base - thick.model.wing!.slabWidthMm).toBeGreaterThan(195); // 2 × 100 / |sin Δ| ≈ 200.8 expect(thick.wings[0].thicknessM).toBeCloseTo(0.55, 9); }); it('haunch 0 drops the 연직 헌치 and the 저판 상면 헌치 of that wing', () => { const w = build({ ...rec, wingDims: { left: { haunchMm: 0 }, right: {} } }).wings.find((x) => x.side === 'left')!; expect(w.haunches).toHaveLength(0); expect(w.footingHaunch).toBeUndefined(); }); }); describe('날개벽 삽도 (시안 ② 날개벽 카드)', () => { it('평면: both wings, the slab between them and its width', () => { const { model, members, wings } = build(); const svg = abutWingPlanSvg({ lengthM: model.length, outline: abutPlanOutline(model, members, wings), leftLabel: '좌 · 일반형', rightLabel: '우 · 일반형', leftSEnd: wings.find((w) => w.side === 'left')!.sEnd, slabWidthM: model.wing!.slabWidthMm / 1000, }); for (const s of ['좌 · 일반형', '우 · 일반형', `접속슬래브 폭 ${(model.wing!.slabWidthMm / 1000).toFixed(3)} m`]) expect(svg).toContain(s); expect(svg.match(/ { const { members, wings } = build(); const w = wings[0]; const svg = abutWingSideSvg({ name: '좌 날개벽', form: w.form, lengthM: w.lengthM, endHeightM: w.endHeightM, heelDistanceM: w.heelDistanceM, topLine: w.topLine, bottomLine: w.bottomLine, bands: w.bands, seatEl: rec.seatEl, footingTopEl: members.footingTopEl, parapetTopEl: w.topStartEl, bracketHeightM: members.shape.bracketHeightM, wallBackM: members.shape.wallBackM, parapetBackM: members.shape.parapetBackM, wallFrontM: members.shape.wallFrontM, footingFrontM: members.shape.footingFrontM, footingBackM: members.shape.footingBackM, footingThicknessM: members.shape.footingThicknessM, }); for (const s of ['6500 (교좌면 끝에서, 날개벽 축 방향)', '1500', '등단면', '변화단면1', '변화단면2', '뒷굽 끝 1.807 m', '교좌면 EL.44.367', 'EL.41.995', '기초 하단 EL.40.895']) expect(svg).toContain(s); expect(svg).toMatch(/흉벽 상단 EL\.4[67]\.\d{3}/); expect(svg).toMatch(/브라켓 상면 EL\.4[56]\.\d{3}/); expect(svg.match(/ { const svg = abutWingCornerSvg({ parapetMm: 500, bracketMm: 300, wingThicknessMm: 450, haunchMm: 300, slabGapWingMm: 20, slabGapParapetMm: 5, chamferMm: 300 }); for (const s of ['헌치 300 × 300', '브라켓 300 (슬래브 아래)', '슬래브 모따기 300 × 300', '날개벽과 20 · 흉벽과 5', '흉벽 500', '날개벽 450']) expect(svg).toContain(s); }); });