spec §3·§4.4(2026-11-06 개정)와 3주형형상배근.md §2.2(2주형 DWG 실측)를 따른다. - 기둥 사이 하면: Level 끝에서 헌치 수평 1500 동안 500 올라가 평탄, 다음 기둥 앞에서 내려옴 (이전 Level + 300, 헌치 300 × 300 대체) - 다주형 Level 폭 기본값 = D + 200 (입력값이 있으면 입력 우선). T형은 2100 유지 - 오목 평탄부가 없으면 RB-M01 오류 - 3주형 테스트는 오목형이 성립하는 20 m 코핑으로 바꿈 (P1 길이 12 m는 RB-M01) - 2주형 DWG 치수(L 24200, D 3000) 하면 꺾임점 테스트 추가 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
135 lines
9.5 KiB
TypeScript
135 lines
9.5 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { buildPier } from '../src/core/geometry';
|
|
import { castRebarLayout } from '../src/core/rebar/cast-layout';
|
|
import { rebarSummary } from '../src/core/rebar/summary';
|
|
import { buildQuantity, unitWeight } from '../src/core/rebar/quantity';
|
|
import { DEFAULT_MATERIAL } from '../src/core/rebar/common';
|
|
import { castRebarSvg } from '../src/view/cast-rebar-svg';
|
|
import { parseProject, serializeProject, SCHEMA_VERSION, type ProjectFile } from '../src/core/project';
|
|
import { loadP1 } from './helpers';
|
|
|
|
const base = () => ({ ...loadP1(), foundationType: '현장타설말뚝' as const, groundEl: 30, pileLengthMm: 24000 });
|
|
const layout = (changes: Partial<ReturnType<typeof base>> = {}) => castRebarLayout(buildPier({ ...base(), ...changes }));
|
|
|
|
describe('dedicated cast-in-place physical reinforcement', () => {
|
|
it('generates both main layers on both sides of ground and one interface coupler per line', () => {
|
|
const result = layout();
|
|
for (const part of ['기둥', '말뚝']) for (const mark of ['P1', 'P2', 'P3']) expect(result.bars.some(b=>b.name===`${part}-${mark}`)).toBe(true);
|
|
const joints = result.bars.filter(b=>b.name.startsWith('접합-'));
|
|
expect(joints).toHaveLength(92);
|
|
expect(joints.filter(b=>b.name==='접합-P1 커플러').every(b=>Math.abs((b.pts[0][1]+b.pts[1][1])/2-30)<1e-8)).toBe(true);
|
|
expect(joints.filter(b=>b.name==='접합-P2 커플러').every(b=>Math.abs((b.pts[0][1]+b.pts[1][1])/2-30.15)<1e-8)).toBe(true);
|
|
const bottoms = result.bars.filter(b=>b.name==='말뚝-P1').flatMap(b=>b.pts.map(p=>p[1]));
|
|
expect(Math.min(...bottoms)).toBeCloseTo(6 + (250 + 31.8/2)/1000);
|
|
expect(result.bars.some(b=>b.name.includes('P4')||b.name==='말뚝-P6')).toBe(false);
|
|
});
|
|
it('gives the same centre-line endpoints to both main pieces at every joint', () => {
|
|
const result=layout();
|
|
for(const joint of result.bars.filter(b=>b.kind==='coupler' && b.name.startsWith('접합-'))) {
|
|
const centre=joint.pts[0].map((v,i)=>(v+joint.pts[1][i])/2);
|
|
const endpoints=result.bars.filter(b=>b.kind==='main').flatMap(b=>[b.pts[0],b.pts.at(-1)!]);
|
|
expect(endpoints.filter(p=>p.every((v,i)=>Math.abs(v-centre[i])<1e-8))).toHaveLength(2);
|
|
}
|
|
});
|
|
it('keeps every generated ordinary hoop surface inside the section and above the tip', () => {
|
|
const centre = buildPier(base()).columns[0].s;
|
|
for (const bar of layout().bars.filter(b=>b.kind!=='coupler')) for(const point of bar.pts) {
|
|
if(bar.kind==='main' || bar.kind==='tie') expect(Math.hypot(point[0]-centre,point[2])+bar.diameterMm/2000).toBeLessThanOrEqual(1+1e-7);
|
|
expect(point[1]-bar.diameterMm/2000).toBeGreaterThanOrEqual(6.25-1e-7);
|
|
}
|
|
});
|
|
it('uses each section diameter, grade and count without silently equating mismatched joints', () => {
|
|
const result=layout({castInPlaceRebar:{column:{mainCount:44,mainDiameterMm:35.8,mainGradeMpa:400},pile:{mainCount:48,mainGradeMpa:500}}});
|
|
expect(result.bars.some(b=>b.name.startsWith('접합-'))).toBe(false);
|
|
expect(result.missing.some(m=>m.includes('불일치'))).toBe(true);
|
|
expect(result.bars.filter(b=>b.name==='기둥-P1').every(b=>b.diameterMm===35.8 && b.gradeMpa===400)).toBe(true);
|
|
});
|
|
it('converts clear cover to the main centre correctly and preserves two different cover conventions', () => {
|
|
const centre=layout({castInPlaceRebar:{column:{coverMm:125,coverReference:'main-center'}}});
|
|
const clear=layout({castInPlaceRebar:{column:{coverMm:125,coverReference:'clear'}}});
|
|
const x=buildPier(base()).columns[0].s;
|
|
expect(centre.bars.find(b=>b.name==='기둥-P1')!.pts[0][0]-x).toBeCloseTo(.875);
|
|
expect(clear.bars.find(b=>b.name==='기둥-P1')!.pts[0][0]-x).toBeCloseTo((1000-125-19.1-15.9)/1000);
|
|
});
|
|
it('places dense hoops in the manual upper column and beta/clay lower pile zones', () => {
|
|
const result=layout({castInPlaceShape:{clayBottomEl:16},castInPlaceRebar:{column:{endZoneLengthMm:2000}}});
|
|
for (const part of ['기둥','말뚝']) expect(result.bars.some(b=>b.name===`${part}-P5`)).toBe(true);
|
|
for(const b of result.bars.filter(b=>b.name==='말뚝-P5')) expect(b.pts[0][1]).toBeGreaterThan(15);
|
|
for(const b of result.bars.filter(b=>b.name==='말뚝-P5')) expect(b.pts[0][1]).toBeLessThan(18);
|
|
expect(result.bars.filter(b=>b.name==='말뚝-P5 띠철근 커플러')).toHaveLength(result.bars.filter(b=>b.name==='말뚝-P5').length);
|
|
});
|
|
it('reports missing/reversed/out-of-range end zones while keeping ordinary rebar visible', () => {
|
|
for(const settings of [{},{castInPlaceShape:{clayBottomEl:4}},{castInPlaceRebar:{column:{endZoneLengthMm:99999}}}]) {
|
|
const result=layout(settings);
|
|
expect(result.bars.some(b=>b.name==='기둥-P1')).toBe(true);
|
|
expect(result.missing.some(m=>m.includes('경계')||m.includes('단부구간'))).toBe(true);
|
|
}
|
|
});
|
|
it('uses a real helix with rise and analytical length when spiral is selected', () => {
|
|
const result=layout({castInPlaceRebar:{pile:{tieKind:'spiral',tieSpacingMm:250}}});
|
|
const helix=result.bars.find(b=>b.name==='말뚝-P3 나선')!;
|
|
expect(helix.pts.at(-1)![1]).toBeGreaterThan(helix.pts[0][1]);
|
|
expect(helix.lengthMm).toBeGreaterThan((helix.pts.at(-1)![1]-helix.pts[0][1])*1000);
|
|
expect(result.missing.some(m=>m.includes('나선철근'))).toBe(true);
|
|
});
|
|
it('merges continuous mains into one physical bar across the joint', () => {
|
|
const result=layout({castInPlaceRebar:{connection:{mode:'continuous'}}});
|
|
expect(result.bars.filter(b=>b.name==='기둥·말뚝-P1')).toHaveLength(46);
|
|
expect(result.bars.filter(b=>b.name==='기둥·말뚝-P2')).toHaveLength(46);
|
|
expect(result.bars.some(b=>b.kind==='coupler')).toBe(false);
|
|
expect(result.bars.find(b=>b.name==='기둥·말뚝-P1')!.pts[0][1]).toBeLessThan(30);
|
|
});
|
|
it('does not claim a missing lap or mismatched joint is connected', () => {
|
|
const result=layout({castInPlaceRebar:{connection:{mode:'lap'}}});
|
|
expect(result.bars.some(b=>b.name.startsWith('접합-'))).toBe(false);
|
|
expect(result.missing.some(m=>m.includes('겹침'))).toBe(true);
|
|
});
|
|
it('excludes an invalid pile cage rather than silently clipping it', () => {
|
|
const result=layout({castInPlaceRebar:{pile:{cageLengthMm:24000}}});
|
|
expect(result.bars.some(b=>b.member==='pile')).toBe(false);
|
|
expect(result.missing.some(m=>m.includes('선단 피복'))).toBe(true);
|
|
});
|
|
it('rejects coupler lengths that exceed the joint space without crashing the drawing', () => {
|
|
const result=layout({castInPlaceRebar:{connection:{couplerLengthMm:5000}}});
|
|
expect(result.bars.some(b=>b.name.startsWith('접합-'))).toBe(false);
|
|
expect(result.missing.some(m=>m.includes('커플러 길이'))).toBe(true);
|
|
});
|
|
it('does not draw a multi-turn hoop when the lap exceeds the circumference', () => {
|
|
const result=layout({castInPlaceRebar:{pile:{concreteStrengthMpa:0.001}}});
|
|
expect(result.missing.some(m=>m.includes('겹침길이가 원주 이상'))).toBe(true);
|
|
expect(result.bars.some(b=>b.name==='말뚝-P1')).toBe(true);
|
|
});
|
|
it('repeats physical pieces per actual column without multiplying piece counts again', () => {
|
|
const one=layout(), three=layout({type:'3주형',columnCount:3,copingLengthMm:20000});
|
|
expect(three.bars.filter(b=>b.kind==='main')).toHaveLength(one.bars.filter(b=>b.kind==='main').length*3);
|
|
expect(new Set(three.bars.map(b=>b.columnIndex)).size).toBe(3);
|
|
});
|
|
it('uses the same physical pieces, diameters and grades for table length and mass', () => {
|
|
const input={...base(),castInPlaceRebar:{column:{mainGradeMpa:400,mainDiameterMm:35.8}}};
|
|
const model=buildPier(input), result=castRebarLayout(model), summary=rebarSummary(model);
|
|
const q=buildQuantity(summary.rows.filter(r=>r.diameterMm!==undefined),DEFAULT_MATERIAL,summary.incomplete);
|
|
const steel=result.bars.filter(b=>b.kind!=='coupler');
|
|
expect(q.groups.flatMap(g=>g.diameters.flatMap(d=>d.lines)).reduce((s,l)=>s+l.pieceCount,0)).toBe(steel.length);
|
|
const mass=steel.reduce((s,b)=>s+b.lengthMm/1000*unitWeight(b.diameterMm),0);
|
|
expect(q.kg).toBeCloseTo(mass,8);
|
|
expect(q.groups.find(g=>g.grade===400)!.diameters.some(d=>d.dMm===35.8)).toBe(true);
|
|
expect(q.couplers.reduce((s,c)=>s+c.count,0)).toBe(result.bars.filter(b=>b.kind==='coupler').length);
|
|
});
|
|
it('groups identical hoops as one length and merges nominal/equivalent diameter subtotals', () => {
|
|
const summary=rebarSummary(buildPier(base()));
|
|
const hoop=summary.rows.find(r=>r.name==='말뚝-P3')!;
|
|
expect(hoop.pieces).toHaveLength(1);
|
|
expect(hoop.pieces![0].count).toBe(hoop.count);
|
|
expect(hoop.length).toBeGreaterThan(0);
|
|
const q=buildQuantity(summary.rows,DEFAULT_MATERIAL,summary.incomplete);
|
|
expect(q.groups.find(g=>g.grade===400)!.diameters.filter(d=>Math.abs(d.dMm-19.1)<1e-8)).toHaveLength(1);
|
|
});
|
|
it('restores generated bar paths and quantities from the real project schema', () => {
|
|
const input={...base(),castInPlaceRebar:{column:{mainCount:48},pile:{mainCount:48},connection:{mode:'coupler' as const,couplerLengthMm:180}}};
|
|
const file:ProjectFile={schemaVersion:SCHEMA_VERSION,project:{name:'검증',bridge:'',client:'',author:'',crs:'',memo:''},source:{fileName:'',sheets:[]},history:[],piers:[{id:'P1',input,defaults:input,draft:input}]};
|
|
const restored=parseProject(serializeProject(file)).piers[0].input;
|
|
expect(castRebarLayout(buildPier(restored))).toEqual(castRebarLayout(buildPier(input)));
|
|
expect(castRebarSvg(buildPier(restored),castRebarLayout(buildPier(restored)))).toContain('<polyline');
|
|
});
|
|
});
|