Preserve local abutment, cast-in-place rebar and compact header features alongside remote drawing, ground-link and quantity workspaces. Adapt drawing and takeoff consumers to generated solplate plans, retain current P1 regression coverage, and resolve bearing-detail annotation collisions. Validation: 122 test files / 975 tests passed; production build passed; 10 browser integration checks passed.
150 lines
11 KiB
TypeScript
150 lines
11 KiB
TypeScript
import { DIM, type PierModel } from '../src/core/geometry';
|
||
import { pairEccentricities } from '../src/core/bearing-layout';
|
||
import { INTEGRATED_BLOCK } from '../src/core/integrated-block';
|
||
import { solplateGeometry } from '../src/core/solplate';
|
||
import { stretchMatrix } from '../src/view/scene';
|
||
import { localPoint, modelPrimitives, silhouette } from './projection';
|
||
import type { Entity, Point, Source } from './types';
|
||
|
||
export type BearingScheduleRow={index:number;no:number;row:'전열'|'후열';A:number|null;B:number|null;C:number|null;D:number|null;S:number|null;L:number|null;W:number|null;T:number|null;T1:number|null;T2:number|null;elx:number;h1:number;h2:number;h3:number|null;E:number|null};
|
||
const value=(n:number|null,digits=1)=>n===null?'미확인':n.toLocaleString('en-US',{maximumFractionDigits:digits});
|
||
|
||
/** All cells refer to generated model solids. No reference drawing's sample values are copied. */
|
||
export function bearingSchedule(model:PierModel):BearingScheduleRow[] {
|
||
const pairs=pairEccentricities(model.blocks);
|
||
return model.blocks.map((b,index)=>{
|
||
const p=b.plan,ib=model.integratedBlocks.find(x=>(b.row==='전열'?x.frontNo:x.backNo)===b.no);
|
||
const pair=pairs.find(x=>b.row==='전열'&&x.frontNo===b.no);
|
||
let C:number|null=null;
|
||
if(pair){
|
||
const back=model.blocks.find(x=>x.row==='후열'&&x.no===pair.backNo)!;
|
||
const backPoint=localPoint(model,back.k,back.s,0,back.n).applyMatrix4(stretchMatrix(model,b.k).invert());
|
||
const angle=b.rowSkewDeg*Math.PI/180;
|
||
C=Math.abs(-(backPoint.x-b.s)*Math.sin(angle)+(backPoint.z-b.n)*Math.cos(angle))*1000;
|
||
}
|
||
const plate=p?solplateGeometry(b,p):undefined;
|
||
const sole=model.soles.find(s=>s.no===b.no);
|
||
const h1=(b.top-b.bottom)*1000,h2=(b.padTop-b.top)*1000;
|
||
return {index,no:b.no,row:b.row,A:sole?.aMm??null,B:sole?.bMm??null,C,D:b.row==='전열'&&ib?INTEGRATED_BLOCK.length:null,
|
||
S:p?.slopePct??null,L:plate?plate.L*1000:null,W:plate?plate.W*1000:null,T:p?.T??null,T1:plate?plate.T1*1000:null,T2:plate?plate.T2*1000:null,
|
||
elx:b.padTop,h1,h2,h3:p?.h3??null,E:p?h1+h2+p.h3+p.T:null};
|
||
}).sort((a,b)=>(a.row===b.row?a.no-b.no:a.row==='전열'?-1:1));
|
||
}
|
||
|
||
export function scheduleEntities(model:PierModel,width:number,mode:'all'|'variation'|'heights'='all'):Entity[] {
|
||
const entities:Entity[]=[],factor=100;
|
||
const source:Source={pier:model.name,member:'bearing-table',index:0};
|
||
const line=(a:Point,b:Point)=>entities.push({kind:'line',a:[a[0]*factor,-a[1]*factor],b:[b[0]*factor,-b[1]*factor],layer:'TEXT',source});
|
||
const text=(x:number,y:number,t:string,height=1.7,s=source,align:'left'|'center'='center')=>entities.push({kind:'text',point:[x*factor,-y*factor],text:t,height:height*factor,layer:'TEXT',source:s,align});
|
||
const rows=bearingSchedule(model),rowH=rows.length>12?3.5:4.2,baseline=rowH===3.5?2.7:3.2,headH=8;
|
||
const widths=[12,10,10,11,10,12,13,13,8,9,9],total=widths.reduce((a,b)=>a+b,0),xs=[0];
|
||
widths.forEach(w=>xs.push(xs[xs.length-1]+w/total*width));
|
||
// Original template: A/B/C/D/S + grouped solplate size and thickness columns.
|
||
text(0,-3.2,'받침 및 교좌면 상세 변화 치수표',2.5,source,'left');
|
||
const bottom=headH+rows.length*rowH;
|
||
line([0,0],[width,0]);line([0,headH],[width,headH]);line([0,bottom],[width,bottom]);
|
||
for(let i=0;i<xs.length;i++)line([xs[i],i===7||i===9||i===10?4:0],[xs[i],bottom]);
|
||
line([xs[6],4],[width,4]);
|
||
['NO./열','A','B','C','D','S(%)'].forEach((h,i)=>text((xs[i]+xs[i+1])/2,4.9,h));
|
||
text((xs[6]+xs[8])/2,2.9,'솔플레이트 크기');text((xs[8]+xs[11])/2,2.9,'솔플레이트 두께');
|
||
['교축 L','교직 W','T','T1','T2'].forEach((h,i)=>text((xs[i+6]+xs[i+7])/2,6.9,h,1.55));
|
||
rows.forEach((r,i)=>{
|
||
const y=headH+i*rowH,s={...source,index:r.index};
|
||
if(i)line([0,y],[width,y]);
|
||
const cells=[`${r.row==='전열'?'전':'후'} ${r.no}`,value(r.A),value(r.B),r.C===null?'—':value(r.C),r.D===null?'—':value(r.D),r.S===null?'미확인':r.S.toFixed(3),value(r.L),value(r.W),value(r.T),value(r.T1),value(r.T2)];
|
||
cells.forEach((c,j)=>text((xs[j]+xs[j+1])/2,y+baseline,c,2.5,s));
|
||
});
|
||
if(mode==='variation'){
|
||
['단위 mm · 미확인: 제원 없음 · —: 비해당 / C: 열간격 · D: 일체 길이','L=A+여유, W=B+여유, T1·T2=round(T±L×|S|/200)'].forEach((t,i)=>text(0,bottom+4+i*4,t,2.5,source,'left'));
|
||
return entities;
|
||
}
|
||
if(mode==='heights')entities.length=0;
|
||
let y=mode==='heights'?0:bottom+4.5;
|
||
text(0,y,'상세 A 높이·표고 (EL.X: 몰탈 상면)',2.1,source,'left');y+=2;
|
||
const secondWidths=[.13,.19,.14,.12,.14,.12,.16],sx=[0];secondWidths.forEach(w=>sx.push(sx[sx.length-1]+w*width));
|
||
const start=y,h=5,end=y+h+rows.length*rowH;
|
||
for(const x of sx)line([x,start],[x,end]);line([0,start],[width,start]);line([0,start+h],[width,start+h]);
|
||
['NO./열','EL.X(m)','h1','h2','h3','T','E'].forEach((v,j)=>text((sx[j]+sx[j+1])/2,start+3.4,v));
|
||
rows.forEach((r,i)=>{
|
||
const yy=start+h+i*rowH,s={...source,index:r.index};
|
||
const cells=[`${r.row==='전열'?'전':'후'} ${r.no}`,r.elx.toFixed(3),value(r.h1),value(r.h2),value(r.h3),value(r.T),value(r.E)];
|
||
cells.forEach((v,j)=>text((sx[j]+sx[j+1])/2,yy+baseline,v,2.5,s));line([0,yy+rowH],[width,yy+rowH]);
|
||
});
|
||
const notes=['단위 mm (EL만 m) · 미확인: 모델 제원 없음 · —: 비해당',
|
||
'C: 전·후열 중심 간격(전열 교좌면 축 투영), 전열에 표기',
|
||
'D: 일체 교좌면의 모델 길이 · 분리/미대응 받침은 —',
|
||
'L=A+여유, W=B+여유, T1·T2=round(T±L×|S|/200)',
|
||
'h1 블럭 / h2 몰탈 / h3 받침 / E=h1+h2+h3+T'];
|
||
notes.forEach((n,i)=>text(0,end+4+i*4,n,2.5,source,'left'));
|
||
return entities;
|
||
}
|
||
|
||
/** Detail A: actual selected bearing pair in its stretch frame, with a plan below. */
|
||
export function bearingDetail(model:PierModel,scale:number,key?:string,dimensions=true,centers=true):{entities:Entity[];title:string} {
|
||
const selected=model.blocks.find(b=>`${b.row}:${b.no}`===key)??model.blocks[0];
|
||
if(!selected)return{entities:[],title:'상세 A · 받침 없음'};
|
||
const pair=pairEccentricities(model.blocks).find(p=>selected.row==='전열'?p.frontNo===selected.no:p.backNo===selected.no);
|
||
const blocks=pair?model.blocks.filter(b=>b.row==='전열'?b.no===pair.frontNo:b.no===pair.backNo):[selected];
|
||
const indices=blocks.map(b=>model.blocks.indexOf(b));
|
||
const integratedIndices=model.integratedBlocks.map((b,i)=>blocks.some(x=>x.row==='전열'&&x.no===b.frontNo)?i:-1).filter(i=>i>=0);
|
||
const solids=modelPrimitives(model).solids.filter(s=>s.source.member==='integrated-block'?integratedIndices.includes(s.source.index):['block','pad','bearing','solplate'].includes(s.source.member)&&indices.includes(s.source.index));
|
||
const anchor=blocks.find(b=>b.row==='전열')??selected,inverse=stretchMatrix(model,anchor.k).invert(),angle=anchor.rowSkewDeg*Math.PI/180;
|
||
const frame=(p:ReturnType<typeof localPoint>)=>{const q=p.clone().applyMatrix4(inverse),s=q.x,n=q.z;return q.set(s*Math.cos(angle)+n*Math.sin(angle),q.y,-s*Math.sin(angle)+n*Math.cos(angle));};
|
||
const local=blocks.map(b=>frame(localPoint(model,b.k,b.s,0,b.n)));
|
||
const cx=local.reduce((sum,p)=>sum+p.z,0)/blocks.length,cy=local.reduce((sum,p)=>sum+p.x,0)/blocks.length,datum=Math.min(...blocks.map(b=>b.bottom));
|
||
const planY=-1900,entities:Entity[]=[],source:Source={member:'bearing-detail',pier:model.name,index:model.blocks.indexOf(selected)};
|
||
for(const solid of solids)for(const view of ['section','plan']) {
|
||
const polygons=solid.faces.map(face=>face.map(p=>{const q=frame(p);return view==='section'?[(q.z-cx)*1000,(q.y-datum)*1000] as Point:[(q.z-cx)*1000,(q.x-cy)*1000+planY] as Point;}));
|
||
for(const [a,b] of silhouette(polygons))entities.push({kind:'line',a,b,layer:solid.layer,source:solid.source});
|
||
}
|
||
const line=(a:Point,b:Point,layer:Entity['layer']='CENTER')=>entities.push({kind:'line',a,b,layer,source});
|
||
const text=(point:Point,t:string,height=1.8*scale,align:'left'|'center'|'right'='left')=>entities.push({kind:'text',point,text:t,height,align,layer:'TEXT',source});
|
||
const dim=(a:Point,b:Point,position:number,axis:'x'|'y',label?:string)=>entities.push({kind:'dimension',a,b,position,axis,value:Math.abs(axis==='x'?a[0]-b[0]:a[1]-b[1]),height:1.7*scale,label,layer:'DIMENSION',source});
|
||
blocks.forEach((block,i)=>{
|
||
const x=(local[i].z-cx)*1000,z=(block.padTop-datum)*1000,idx=model.blocks.indexOf(block),s={member:'bearing-number',index:idx,pier:model.name};
|
||
entities.push({kind:'circle',center:[x,z+6*scale],radius:1.7*scale,layer:'TEXT',source:s});
|
||
entities.push({kind:'text',point:[x,z+5.2*scale],text:String(block.no),height:1.8*scale,align:'center',layer:'TEXT',source:s});
|
||
line([x,z],[x,z+4.3*scale],'TEXT');
|
||
text([x,z+9*scale],`${block.row} · EL.X ${block.padTop.toFixed(3)}`,1.5*scale,'center');
|
||
if(centers)line([x,-2*scale],[x,z+4*scale]);
|
||
text([x,planY+2*scale],`${block.row} ${block.no}`,1.65*scale,'center');
|
||
if(dimensions){
|
||
const p=block.plan,sign=x<0?-1:1,near=x+sign*(700+4*scale);
|
||
const zs=[block.bottom,block.top,block.padTop,...(p?[block.padTop+p.h3/1000,block.padTop+(p.h3+p.T)/1000]:[])].map(v=>(v-datum)*1000);
|
||
// Short height labels are led out with spacing; actual dimension values remain in the table.
|
||
['h1','h2','h3','T'].slice(0,zs.length-1).forEach((name,j)=>{
|
||
const yy=(zs[j]+zs[j+1])/2,target=(j*3-2)*scale;
|
||
line([x+sign*DIM.padSize*500,yy],[near,target],'TEXT');
|
||
text([near+sign*scale,target],name,1.55*scale,sign<0?'right':'left');
|
||
});
|
||
}
|
||
});
|
||
if(pair&&dimensions){const a=(local[0].z-cx)*1000,b=(local[1].z-cx)*1000;dim([a,planY],[b,planY],planY-1000,'x','C');}
|
||
const ib=model.integratedBlocks.find(b=>blocks.some(x=>x.row==='전열'&&x.no===b.frontNo));
|
||
if(ib&&dimensions)dim([-INTEGRATED_BLOCK.length/2,planY-600],[INTEGRATED_BLOCK.length/2,planY-600],planY-1000-5*scale,'x','D');
|
||
const rows=bearingSchedule(model);
|
||
if(dimensions)blocks.forEach((block,i)=>{
|
||
const row=rows.find(r=>r.index===model.blocks.indexOf(block))!,p=block.plan;
|
||
const x=(local[i].z-cx)*1000,y=(local[i].x-cy)*1000+planY;
|
||
if(!p)return;
|
||
// Symbolic dimensions retain measured numeric values for CAD and the schedule.
|
||
if(row.A!==null)dim([x-row.A/2,y],[x+row.A/2,y],planY+700+3*scale,'x','A');
|
||
const side=x<0?-1:1;
|
||
if(row.A!==null&&row.B!==null)dim([x,y-row.B/2],[x,y+row.B/2],x+side*(row.A/2+4*scale),'y','B');
|
||
const z=(block.padTop+p.h3/1000-datum)*1000;
|
||
const half=p.L/2;
|
||
if(row.E!==null)dim([x,(block.bottom-datum)*1000],[x,(block.bottom-datum)*1000+row.E],x+side*(700+15*scale),'y','E');
|
||
for(const [symbol,edge,thickness] of [['T1',-1,row.T1],['T2',1,row.T2]] as const){
|
||
if(thickness!==null)dim([x+edge*half,z],[x+edge*half,z+thickness],x+side*(half+(edge===-1?2:6)*scale),'y',symbol);
|
||
}
|
||
const slopeY=Math.max(z+(row.T??0)+4*scale,(block.padTop-datum)*1000+13*scale);
|
||
line([x-half,slopeY],[x+half,slopeY],'DIMENSION');
|
||
line([x+half,slopeY],[x+half-1.5*scale,slopeY+scale],'DIMENSION');
|
||
text([x,slopeY+scale],'S(%)',1.7*scale,'center');
|
||
});
|
||
text([0,-650],blocks.some(b=>!b.plan)?'받침 제원 미확인 · 치수표 참조':'문자 치수: 치수표 참조',1.4*scale,'center');
|
||
text([0,planY-1200-5*scale],'교좌면 평면',1.65*scale,'center');
|
||
text([0,planY-1200-9*scale],'가로 교축(n) / 세로 교직(s)',1.65*scale,'center');
|
||
return{entities,title:`상세 A · ${blocks.map(b=>`${b.no}(${b.row==='전열'?'전':'후'})`).join(' / ')}`};
|
||
}
|