- 현장타설 기둥·말뚝 형상(cast-in-place-shape)과 주철근·횡철근·커플러 계산(rebar/cast-in-place, cast-layout) - 다주형 코핑 철근(multicol-coping), 기둥 높이 · 양단부 연장 - 철근표 요약 · 재료 수량 · 3D 뷰 연결과 삽도(cast-rebar-svg) - 검사 기준 갱신과 회귀 테스트, 검증 스크립트 보관 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
76 lines
5.3 KiB
TypeScript
76 lines
5.3 KiB
TypeScript
/** Read-only diagnostic; a passing dump means the evidence was written, not design compliance. */
|
|
import { it, expect } from 'vitest';
|
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
import { resolve } from 'node:path';
|
|
import { loadP1 } from '../../tests/helpers';
|
|
import { buildPier } from '../../src/core/geometry';
|
|
import { buildRebar3D, type BarRecord } from '../../src/view/rebar3d';
|
|
import { castRebarLayout } from '../../src/core/rebar/cast-layout';
|
|
import { columnInputFor, columnRebar } from '../../src/core/rebar/column';
|
|
import { rebarSummary } from '../../src/core/rebar/summary';
|
|
import { buildQuantity } from '../../src/core/rebar/quantity';
|
|
import { DEFAULT_MATERIAL } from '../../src/core/rebar/common';
|
|
|
|
it('records multi-column placement, concrete-envelope and quantity evidence', () => {
|
|
const out=resolve('.verify/multicol-fixed-20261106'); mkdirSync(out,{recursive:true});
|
|
const cases: unknown[]=[];
|
|
for(const [type,columnCount] of [['2주형',2],['3주형',3]] as const) {
|
|
for(const mode of ['footing','cast-default','cast-zones'] as const) {
|
|
const input={...loadP1(),type,columnCount,foundationType:mode==='footing'?'말뚝기초' as const:'현장타설말뚝' as const,
|
|
...(mode!=='footing'?{groundEl:30,pileLengthMm:24000}:{}),
|
|
...(mode==='cast-zones'?{castInPlaceRebar:{column:{endZoneLengthMm:3000}},castInPlaceShape:{clayBottomEl:16}}:{})};
|
|
const model=buildPier(input), rb=buildRebar3D(model), summary=rebarSummary(model);
|
|
const polygon=model.copingParts[0].profile;
|
|
const underside=(x:number)=>{
|
|
const heights:number[]=[];
|
|
polygon.forEach((p,i)=>{
|
|
const q=polygon[(i+1)%polygon.length];
|
|
if(Math.abs(q[0]-p[0])<1e-9)return;
|
|
if(x>=Math.min(p[0],q[0])-1e-9 && x<=Math.max(p[0],q[0])+1e-9)heights.push(p[1]+(q[1]-p[1])*(x-p[0])/(q[0]-p[0]));
|
|
});
|
|
return Math.min(...heights);
|
|
};
|
|
const envelope=(filter:(r:BarRecord)=>boolean)=>{
|
|
let minimum=Infinity,worst:unknown,below=0,samples=0;
|
|
for(const record of rb.records.filter(filter)) {
|
|
for(let i=1;i<record.pts.length;i++) {
|
|
const a=record.pts[i-1],b=record.pts[i];
|
|
const fractions=[...Array.from({length:21},(_,j)=>j/20),...polygon.map(p=>(p[0]-a[0])/(b[0]-a[0])).filter(t=>Number.isFinite(t)&&t>=0&&t<=1)];
|
|
for(const fraction of fractions) {
|
|
const x=a[0]+(b[0]-a[0])*fraction,y=a[1]+(b[1]-a[1])*fraction;
|
|
const lower=underside(x); if(!Number.isFinite(lower))continue;
|
|
const gap=(y-lower)*1000; samples++;if(gap<0)below++;
|
|
if(gap<minimum){minimum=gap;worst={name:record.name,xMm:x*1000,barEl:y,concreteUndersideEl:lower};}
|
|
}
|
|
}
|
|
}
|
|
return{minimumCentreAboveUndersideMm:minimum,belowConcreteSamples:below,samples,worst};
|
|
};
|
|
const between=model.columns.slice(1).map((c,i)=>{const x=(model.columns[i].s+c.s)/2;return{xMm:x*1000,concreteUndersideEl:underside(x)};});
|
|
const details=mode==='footing'?columnRebar(columnInputFor(model)):undefined;
|
|
const cast=castRebarLayout(model);
|
|
const perColumn=model.columns.map((c,i)=>{
|
|
const bars=cast.bars.filter(b=>b.columnIndex===i);
|
|
const counts:Record<string,number>={};bars.forEach(b=>counts[b.name]=(counts[b.name]??0)+1);
|
|
const seg=model.segments.find(s=>c.s>=s.s0&&c.s<=s.s1)!;
|
|
return{index:i+1,centreMm:c.s*1000,bottomEl:c.bottom,levelEl:c.top,localCopingTopEl:seg.top,
|
|
intendedTieTopEl:(c.top+seg.top)/2,legacyTieTopEl:details?c.bottom+columnRebar(columnInputFor(model,undefined,i)).zTopT/1000:null,
|
|
castBars:bars.length,castCounts:counts,castMainLengthM:bars.filter(b=>b.kind==='main').reduce((s,b)=>s+b.lengthMm/1000,0)};
|
|
});
|
|
const q=buildQuantity(summary.rows,DEFAULT_MATERIAL,summary.incomplete);
|
|
const mainRows=summary.rows.filter(r=>r.name==='P1'||r.name==='P2').map(r=>({name:r.name,rowCount:r.count,pieceCount:r.pieces?.reduce((s,p)=>s+p.count,0)}));
|
|
cases.push({type,mode,inputBasis:'public/samples/운곡교P1입력.xlsx, geometry type substituted; cast ground30m and length24000mm',
|
|
copingLengthMm:model.copingL*1000,diameterMm:model.columnDiameterMm,levelEl:model.levelBottom,between,perColumn,
|
|
copingProfile:polygon,c7Paths:rb.records.filter(r=>r.name==='C7').slice(0,2).map(r=>r.pts),
|
|
C7C8:envelope(r=>r.name==='C7'||r.name==='C8'),shear:envelope(r=>/^C(9|10|11)$/.test(r.name)||r.name==='C9·C11 사이'),
|
|
legacyLowerDenseLengthMm:details?.lE,workbookOnePointFiveDMm:1.5*model.columnDiameterMm,
|
|
legacyTopDenseHoops:details?.p5.filter(z=>z>=columnInputFor(model).Hc-1.5*model.columnDiameterMm).length,
|
|
mainRows,quantityKg:q.kg,incomplete:summary.incomplete});
|
|
rb.group.traverse(o=>{const mesh=o as import('three').Mesh;mesh.geometry?.dispose();const m=mesh.material;if(Array.isArray(m))m.forEach(x=>x.dispose());else m?.dispose();});
|
|
}
|
|
}
|
|
writeFileSync(resolve(out,'geometry-diagnostic.json'),JSON.stringify(cases,null,2));
|
|
console.log(JSON.stringify(cases.map((c:any)=>({type:c.type,mode:c.mode,C7C8min:c.C7C8.minimumCentreAboveUndersideMm,shearMin:c.shear.minimumCentreAboveUndersideMm,denseMm:c.legacyLowerDenseLengthMm,perColumn:c.perColumn.map((p:any)=>p.castBars),mainRows:c.mainRows}))));
|
|
expect(cases).toHaveLength(6);
|
|
},30000);
|