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;itext((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)=>{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(' / ')}`}; }