Files
Pier_v1/verify/dump/multicol-review.dump.ts
ypshimandClaude Haiku 5.5 2e0bfb6fc8 feat(rebar): 현장타설 말뚝 · 다주형 코핑 형상과 철근 계산
- 현장타설 기둥·말뚝 형상(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>
2026-11-06 13:15:01 +09:00

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);