/** 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;ij/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{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={};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);