Files
Pier_v1/도면작성/bearing-detail.ts
T
ypshim f51865c57b Merge origin/master with current Pierzainer development
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.
2026-11-06 17:10:33 +09:00

150 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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(' / ')}`};
}