import { describe, expect, it } from 'vitest'; import { abutBlockRebar, abutBarDepths } from '../src/core/abut/rebar/block'; // 교대 받침블럭 (교대 기획서 §10 · 받침블럭철근.md §6.6): 블럭 5개, 1200 × 1200, 높이 125 (패드 50 포함, 콘크리트 75). const blocks = [1, 2, 3, 4, 5].map((no, i) => ({ no, s: 1 + i * 2, t: 0.2, rotDeg: 0, seatEl: 44.367, blockHeightMm: 125 })); const bottom = 40.895; const A1 = abutBlockRebar(blocks, bottom); const bar = (r: typeof A1, m: 'S1' | 'S2') => r.bars.find((b) => b.mark === m)!; describe('교대 받침블럭 배근 A1 (AD6, 교대 기획서 §10)', () => { it('블럭 콘크리트 75 (블럭 125 − 패드 50), 깊이: S2 50 · S1 62.7 (H13 두 개)', () => { expect(A1.concreteH).toBe(75); expect(abutBarDepths(A1.settings)).toEqual({ S2: 50, S1: 50 + 6.35 + 6.35 }); }); it('본수: 블럭마다 8본 (7@125, 끝 162.5), 5개 → S1 40 · S2 40', () => { expect(bar(A1, 'S1').count).toBe(40); expect(bar(A1, 'S2').count).toBe(40); expect(A1.paths.filter((p) => p.mark === 'S1' && p.block === 1).length).toBe(8); // 블럭 1 (s = 1000 mm): S2 본은 블럭 모서리에서 162.5, 125 간격 → 첫 본 s = 1000 − 600 + 162.5 = 562.5. const s2 = A1.paths.filter((p) => p.mark === 'S2' && p.block === 1).map((p) => p.pts[0][0]).sort((a, b) => a - b); expect(s2[0]).toBeCloseTo(562.5, 6); expect(s2[1] - s2[0]).toBeCloseTo(125, 6); expect(s2.length).toBe(8); }); it('길이 2500 (1100 직선 + 다리 700 × 2), 묻힘: S2 675 · S1 688', () => { expect([bar(A1, 'S1').length, bar(A1, 'S2').length]).toEqual([2500, 2500]); expect(Math.round(A1.embedment.S2)).toBe(675); expect(Math.round(A1.embedment.S1)).toBe(688); }); it('간섭 (Q-S4, 경고만): S1 다리가 W2 층을 지남 → RB-AK01 (W), 다른 오류 없음', () => { const codes = A1.issues.map((i) => `${i.code}:${i.level}`); expect(codes).toContain('RB-AK01:W'); expect(A1.issues.filter((i) => i.level === 'E')).toEqual([]); }); it('간격 수정 → RB-AK03 "수정됨"', () => { const r = abutBlockRebar(blocks, bottom, { spacingS1: 150 }); expect(r.issues.map((i) => i.code)).toContain('RB-AK03'); expect(bar(r, 'S1').modified).toBe(true); expect(bar(r, 'S2').modified).toBe(false); }); it('회전 (방위각): 블럭 축이 돌면 경로 점도 같이 돈다 (교대축 방향 길이 보존)', () => { const r = abutBlockRebar([{ ...blocks[0], rotDeg: 30 }], bottom); const p = r.paths.find((q) => q.mark === 'S1')!; const len = Math.hypot(p.pts[2][0] - p.pts[1][0], p.pts[2][1] - p.pts[1][1]); expect(len).toBeCloseTo(1100, 6); }); }); import { applyBlockRebar } from '../src/core/abut/edit'; import { checkStoredAbutments } from '../src/core/project'; import { parseAbutments } from '../src/core/abut/parse'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import * as XLSX from 'xlsx'; describe('③ 받침블럭 배근 입력 저장', () => { const rec = parseAbutments(XLSX.read(readFileSync(fileURLToPath(new URL('../../../RAG/운곡교P1입력.xlsx', import.meta.url))), { type: 'buffer' })).abutments[0]; it('stores only the changed values; undefined links back; refuses bad values', () => { const a = applyBlockRebar(rec, { spacingS1: 150, legLength: 800 }); expect(a.rebar?.block).toEqual({ spacingS1: 150, legLength: 800 }); expect(applyBlockRebar(a, { spacingS1: undefined }).rebar?.block).toEqual({ legLength: 800 }); expect(() => applyBlockRebar(rec, { legLength: 0 })).toThrow(); }); it('the project file keeps the 받침블럭 input and refuses a non-number', () => { expect(() => checkStoredAbutments([applyBlockRebar(rec, { coverTop: 60 })])).not.toThrow(); expect(() => checkStoredAbutments([{ ...rec, rebar: { block: { coverTop: 'x' } } as never }])).toThrow('받침블럭 배근'); }); }); import { abutBlockPlanSvg, abutBlockSectionSvg } from '../src/view/abut-block-rebar-svg'; describe('③ 받침블럭 삽도', () => { it('평면: S1 · S2 번호 풍선, 단면: 묻힘 치수 문자열 · 몰탈패드 · 교좌면; 선택하면 나머지 흐리게 / 숨김', () => { const p = abutBlockPlanSvg(A1); expect(p).toContain('data-pick="S1"'); expect(p).toContain('data-pick="S2"'); const s = abutBlockSectionSvg(A1); expect(s).toContain('블럭 콘크리트 75 · 묻힘 S2 675 · S1 688'); expect(s).toContain('몰탈패드 50'); expect(abutBlockSectionSvg(A1, { marks: new Set(['S2']), only: true })).toContain('opacity="0"'); }); });