import { describe, expect, it } from 'vitest'; import { ABUT_SHAPE_DEFAULT, footingEnds, normalizeShape, pileRowsMm, resolveShape, shapeIssues, wallThicknessMm, type AbutShape } from '../src/core/abut/shape'; describe('교대 형상 입력 (시안 ② 벽체·흉벽 · 확대기초·말뚝)', () => { it('the defaults equal the spec values (wall +1.6 … −0.8, footing +2.8 … −1.8, piles 650 / 2000)', () => { const r = resolveShape(); expect(r.wallFrontM).toBeCloseTo(1.6, 9); expect(r.wallBackM).toBeCloseTo(-0.8, 9); expect(r.parapetBackM).toBeCloseTo(-0.5, 9); expect(r.footingFrontM).toBeCloseTo(2.8, 9); expect(r.footingBackM).toBeCloseTo(-1.8, 9); expect(r.pileEdgeM).toBeCloseTo(0.65, 9); expect(shapeIssues({})).toEqual([]); expect(ABUT_SHAPE_DEFAULT.bracketWidthMm).toBe(300); expect(wallThicknessMm(ABUT_SHAPE_DEFAULT)).toBe(2400); // 300 + 500 + 1600 (자동) }); it('a changed 브라켓 폭 moves the back face; an old saved 벽체 두께 still opens as the same wall', () => { expect(resolveShape({ bracketWidthMm: 0 }).wallBackM).toBeCloseTo(-0.5, 9); expect(normalizeShape({ wallThicknessMm: 2000 } as Partial)).toEqual({ bracketWidthMm: -100 }); expect(resolveShape({ wallThicknessMm: 2000 } as Partial).wallBackM).toBeCloseTo(-0.4, 9); }); it('refuses a footing that does not cover the wall, and non-positive sizes', () => { expect(shapeIssues({ footingWidthMm: 3300 }).join(' ')).toContain('뒷굽'); // 3300 − 1200 − 2400 < 0 expect(shapeIssues({ footingToeMm: 0 }).join(' ')).toContain('앞굽'); expect(shapeIssues({ pileRowGap2Mm: 2000 }).join(' ')).toContain('열 위치'); // 650 + 2000 + 2000 ≥ 4600 − 0 expect(shapeIssues({ pileCountRow1: 1.5 }).join(' ')).toContain('열별 말뚝 본수'); expect(shapeIssues({ bracketWidthMm: -1 }).join(' ')).toContain('브라켓 폭'); expect(shapeIssues({ seatStepIgnoreMm: -1 }).join(' ')).toContain('인접 무시 기준'); expect(shapeIssues({ pileSpacingMm: 0 }).join(' ')).toContain('말뚝'); }); }); import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import * as XLSX from 'xlsx'; import { applyShapeEdit, SHAPE_EDIT_FIELDS } from '../src/core/abut/edit'; import { buildAbut } from '../src/core/abut/geometry'; import { buildMembers } from '../src/core/abut/members'; import { parseAbutments } from '../src/core/abut/parse'; const RAG = fileURLToPath(new URL('../../../RAG/운곡교P1입력.xlsx', import.meta.url)); const rec = parseAbutments(XLSX.read(readFileSync(RAG), { type: 'buffer' })).abutments[0]; describe('형상 입력이 계산에 반영됨 (시안 ② 카드)', () => { it('the card has one field per shape value, in the mockup order', () => { expect(SHAPE_EDIT_FIELDS.map((f) => f.key)[0]).toBe('wallFrontMm'); expect(SHAPE_EDIT_FIELDS).toHaveLength(17); expect(SHAPE_EDIT_FIELDS.filter((f) => f.group === 'footing').map((f) => f.key)).toEqual([ 'footingWidthMm', 'footingThicknessMm', 'footingToeMm', 'blindingMarginMm', 'blindingThicknessMm', 'pileDiameterMm', 'pileEdgeMm', 'pileSpacingMm', ]); }); it('the new drawing values do not change the geometry (the model ignores them)', () => { expect(resolveShape({ shrinkJointCount: 3, parapetLevelLeftMm: 800 })).toEqual(resolveShape()); }); it('the shrink-joint count must be a whole number, not negative', () => { expect(shapeIssues({ shrinkJointCount: 1.5 }).join(' ')).toContain('수축줄눈'); expect(shapeIssues({ shrinkJointCount: -1 }).join(' ')).toContain('수축줄눈'); expect(shapeIssues({ shrinkJointCount: 0 })).toEqual([]); }); it('a footing that does not cover the wall is refused by the edit rule', () => { expect(() => applyShapeEdit(rec, 'footingWidthMm', 3300)).toThrow('뒷굽'); expect(rec.shape).toBeUndefined(); }); it('a changed footing thickness moves the footing top used by the walls', () => { const edited = applyShapeEdit(rec, 'footingThicknessMm', 1300); const model = buildAbut({ name: 'A1', type: 'A1', seatEl: rec.seatEl, ip1: rec.ip1, ip2: rec.ip2, ip3: rec.ip3, bearings: rec.bearings.map((b) => ({ no: b.no, pos: b.pos, z: b.z, seatEl: b.seatEl })) }); const base = buildMembers(model, { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm }); const more = buildMembers(model, { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm }, edited.shape); expect(more.footingTopEl - base.footingTopEl).toBeCloseTo(0.2, 6); }); it('a changed pile spacing changes the number of piles per row', () => { const model = buildAbut({ name: 'A1', type: 'A1', seatEl: rec.seatEl, ip1: rec.ip1, ip2: rec.ip2, ip3: rec.ip3, bearings: rec.bearings.map((b) => ({ no: b.no, pos: b.pos, z: b.z, seatEl: b.seatEl })) }); const wider = buildMembers(model, {}, { pileSpacingMm: 3000 }); expect(wider.piles[0].s.length).toBeLessThan(6); }); it('인접 무시 기준: steps smaller than the value join one segment (A1 steps are 71 mm)', () => { const model = buildAbut({ name: 'A1', type: 'A1', seatEl: rec.seatEl, ip1: rec.ip1, ip2: rec.ip2, ip3: rec.ip3, bearings: rec.bearings.map((b) => ({ no: b.no, pos: b.pos, z: b.z, seatEl: b.seatEl })) }); const foundation = { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm }; expect(buildMembers(model, foundation).wallSegments).toHaveLength(5); expect(buildMembers(model, foundation, { seatStepIgnoreMm: 100 }).wallSegments).toHaveLength(1); }); it('저판: 폭 4600 · 앞굽 1200 → 뒷굽 1000 (자동), the ends +2.8 / −1.8; old 앞굽 끝 · 뒷굽 끝 files open the same', () => { expect(footingEnds(ABUT_SHAPE_DEFAULT)).toEqual({ frontMm: 2800, backMm: -1800, heelMm: 1000 }); expect(normalizeShape({ footingFrontMm: 3000, footingBackMm: -2000 } as Partial)).toEqual({ footingToeMm: 1400, footingWidthMm: 5000 }); }); it('열 위치 650 · 2000 · 1300 · (650) gives the rows −1.15 / +0.85 / +2.15; 열별 본수 overrides the rule', () => { expect(pileRowsMm(ABUT_SHAPE_DEFAULT)).toEqual({ back: -1150, middle: 850, front: 2150, frontEdgeMm: 650 }); const model = buildAbut({ name: 'A1', type: 'A1', seatEl: rec.seatEl, ip1: rec.ip1, ip2: rec.ip2, ip3: rec.ip3, bearings: rec.bearings.map((b) => ({ no: b.no, pos: b.pos, z: b.z, seatEl: b.seatEl })) }); const rule = buildMembers(model, {}); expect(rule.piles.map((r) => r.s.length)).toEqual([6, 6, 6]); const more = buildMembers(model, {}, { pileCountRow1: 7 }); expect(more.piles.map((r) => r.s.length)).toEqual([6, 6, 7]); // 3열 배면, 2열, 1열 전면 expect(more.piles[2].t).toBeCloseTo(2.15, 9); }); });