Files
Pier_v1/도면작성/drawing.ts
T

191 lines
16 KiB
TypeScript

import { foundationNotes } from './foundation-notes';
import { sectionCut } from './reference-symbols';
import { Vector3 } from 'three';
import type { PierInput } from '../src/core/types';
import type { PierModel } from '../src/core/geometry';
import { entityBounds, localPoint, modelPrimitives, projectModel, projectPoint } from './projection';
import { modelBreaks, shortenedBounds } from './breaks';
import { bearingDetail, scheduleEntities } from './bearing-detail';
import { templateSlots, defaultSheet, wrappedLines } from './template';
import { isometricEntities, isometricInfo, keyplanEntities } from './project-views';
import { hatchGeometry } from './hatches';
import { componentDimensions } from './component-dimensions';
import { optimiseLayout } from './quality-layout';
import { formatMm } from './dimension-graphics';
import type { Bounds, DrawingDocument, DrawingSettings, DrawingView, Entity, Point, Rect, Source, ViewId } from './types';
export const DEFAULT_SETTINGS: DrawingSettings = { paper:'A3',scale:'auto',dimensions:true,centers:true,elevations:true,shorten:true,hatches:true,isoDirection:0 };
const SCALES=[10,20,25,30,40,50,75,100,150,200,250,300,400,500,750,1000,1500,2000];
export const VIEW_TITLES: Record<ViewId,string>={front:'횡단면도',side:'단면도 · 측면',top:'두부보 평면도',footing:'단면도 C-C · 기초 평면',section:'A-A 기둥 단면',detail:'상세 A · 받침 및 교좌면',schedule:'받침 및 교좌면 상세 변화 치수표',heights:'받침 높이·표고표',iso:'ISOMETRIC VIEW',keyplan:'KEY PLAN'};
function slots(width: number,height: number): Record<ViewId,Rect> {
return templateSlots(width,height);
}
function recommend(bounds: Bounds,slot: Rect,id: ViewId): number {
const side=id==='front'||id==='side';
const need=Math.max((bounds.maxX-bounds.minX)/Math.max(10,slot.width-(side?36:24)),(bounds.maxY-bounds.minY)/Math.max(10,slot.height-(id==='top'?47:31)));
return SCALES.find(s=>s>=need)??Math.ceil(need/500)*500;
}
function annotate(model: PierModel,id: ViewId,b: Bounds,scale: number,settings: DrawingSettings,input:PierInput,geometry?:Entity[]): Entity[] {
const out: Entity[]=[],source: Source={member:'annotation',index:0,pier:model.name},h=2.3*scale;
const line=(a:Point,c:Point,layer:Entity['layer']='CENTER')=>out.push({kind:'line',a,b:c,layer,source});
const text=(p:Point,value:string,layer:Entity['layer']='TEXT',align:'left'|'center'='left')=>out.push({kind:'text',point:p,text:value,height:h,layer,source,align});
const dim=(a:Point,c:Point,position:number,axis:'x'|'y')=>out.push({kind:'dimension',a,b:c,position,axis,height:h,value:Math.abs(axis==='x'?c[0]-a[0]:c[1]-a[1]),source,layer:'DIMENSION'});
if(settings.dimensions) {
dim([b.minX,b.minY],[b.maxX,b.minY],b.minY-(id==='top'?22:7)*scale,'x');
dim([b.minX,b.minY],[b.minX,b.maxY],b.minX-5*scale,'y');
}
if(id==='section') {
if(settings.centers) { line([b.minX-3*scale,0],[b.maxX+3*scale,0]);line([0,b.minY-3*scale],[0,b.maxY+3*scale]); }
text([0,b.maxY+4*scale],`Ø${formatMm(model.columns[0].diameter*1000)}`,'SECTION','center');
return settings.hatches===false?out.filter(e=>e.kind!=='hatch'):out;
}
if(settings.centers) {
const seen=new Set<string>();
model.columns.forEach((c,i)=>{
const p=projectPoint(model,localPoint(model,c.k,c.s,0,0),id),r=c.diameter*500;
const identity=p[0].toFixed(3);if((id==='front'||id==='side')&&seen.has(identity))return;seen.add(identity);
if(id==='front'||id==='side')line([p[0],c.bottom*1000-3*scale],[p[0],c.top*1000+3*scale]);
else {line([p[0]-r-3*scale,p[1]],[p[0]+r+3*scale,p[1]]);line([p[0],p[1]-r-3*scale],[p[0],p[1]+r+3*scale]);}
});
if(id==='top') {
const world=[model.bp1,...(model.kink?[model.kink]:[]),model.bp2];
world.slice(1).forEach((p,i)=>line(projectPoint(model,new Vector3(world[i].e-model.pivot.e,0,-(world[i].n-model.pivot.n)),id),projectPoint(model,new Vector3(p.e-model.pivot.e,0,-(p.n-model.pivot.n)),id)));
}
}
if(id==='front'||id==='side') {
if(settings.dimensions) {
const c=model.columns[0],p=projectPoint(model,localPoint(model,c.k,c.s,0,0),id);
dim([p[0],c.bottom*1000],[p[0],c.top*1000],b.minX-9*scale,'y');
const f=model.footings[0];
if(f)dim([b.minX,f.bottom*1000],[b.minX,f.top*1000],b.minX-9*scale,'y');
const elevations=model.copingParts.flatMap(part=>part.profile.map(p=>p[1]*1000));
dim([b.minX,Math.min(...elevations)],[b.minX,Math.max(...elevations)],b.minX-13*scale,'y');
}
if(settings.elevations) {
const els=[model.copingRefTop,model.levelBottom,...model.columns.map(c=>c.bottom),...model.footings.map(f=>f.bottom),...model.piles.map(p=>p.bottom)];
const unique=[...new Set(els.map(v=>Math.round(v*1e6)/1e6))].sort((a,c)=>a-c);
let previous=-Infinity;
for(const el of unique) {
const y=el*1000,labelY=Math.max(y,previous+3.4*scale);previous=labelY;
line([b.maxX+scale,y],[b.maxX+3*scale,labelY],'ELEVATION');
out.push({kind:'polyline',points:[[b.maxX,y],[b.maxX+1.3*scale,y+scale],[b.maxX+1.3*scale,y-scale]],closed:true,layer:'ELEVATION',source});
text([b.maxX+3.5*scale,labelY+.3*scale],`EL. ${el.toFixed(3)}`,'ELEVATION');
}
}
if(id==='front'&&model.columns.length>1&&settings.dimensions) {
const xs=model.columns.map(c=>projectPoint(model,localPoint(model,c.k,c.s,c.bottom,0),id)[0]).sort((a,c)=>a-c);
xs.slice(1).forEach((x,i)=>dim([xs[i],b.minY],[x,b.minY],b.minY-16*scale,'x'));
}
if(id==='front') {
const c=model.columns[0],p=projectPoint(model,localPoint(model,c.k,c.s,(c.top+c.bottom)/2,0),id),radius=c.diameter*500;
const cut=settings.shorten===false?undefined:modelBreaks(model,scale).find(b=>b.member==='column');
if(cut&&p[1]>cut.from&&p[1]<cut.to)p[1]=cut.to+.3*c.diameter*1000;
line([p[0]-radius-3*scale,p[1]],[p[0]+radius+3*scale,p[1]],'SECTION');
out.push(...sectionCut([p[0]-radius-3*scale,p[1]],scale,'A',false,source),...sectionCut([p[0]+radius+3*scale,p[1]],scale,'A',true,source));
}
}
if(id==='top') {
model.bearings.forEach((bearing,i)=>{
const p=projectPoint(model,localPoint(model,bearing.k,bearing.s,bearing.z,bearing.n),id);
const s={member:'bearing-location',index:i,pier:model.name};
const rowMean=model.bearings.filter(x=>x.row===bearing.row).reduce((sum,x)=>sum+projectPoint(model,localPoint(model,x.k,x.s,x.z,x.n),id)[1],0)/model.bearings.filter(x=>x.row===bearing.row).length;
const upper=rowMean>(b.minY+b.maxY)/2,labelY=upper?b.maxY+5*scale:b.minY-5*scale;
out.push({kind:'line',a:p,b:[p[0],labelY+(upper?-1.7:1.7)*scale],layer:'TEXT',source:s});
out.push({kind:'circle',center:[p[0],labelY],radius:1.7*scale,layer:'TEXT',source:s});
out.push({kind:'text',point:[p[0],labelY-.8*scale],text:String(bearing.no),height:1.8*scale,align:'center',layer:'TEXT',source:s});
});
if(settings.dimensions) for(const row of ['전열','후열']) {
const points=model.bearings.filter(p=>p.row===row).map(p=>projectPoint(model,localPoint(model,p.k,p.s,p.z,p.n),id)).sort((a,c)=>a[0]-c[0]);
const mean=points.reduce((sum,p)=>sum+p[1],0)/Math.max(1,points.length),above=mean>(b.minY+b.maxY)/2;
points.slice(1).forEach((p,i)=>dim(points[i],p,(above?b.maxY+12*scale:b.minY-12*scale),'x'));
}
}
if(id==='footing'&&model.piles.length){
if(settings.centers)for(const pile of model.piles){
const p=projectPoint(model,localPoint(model,pile.k,pile.s,0,pile.n),id),r=pile.diameter*500+1.5*scale;
line([p[0]-r,p[1]],[p[0]+r,p[1]]);line([p[0],p[1]-r],[p[0],p[1]+r]);
}
const pile=model.piles[0],p=projectPoint(model,localPoint(model,pile.k,pile.s,0,pile.n),id),elbow:Point=[b.minX,b.minY-6*scale],end:Point=[elbow[0]+3*scale,elbow[1]];
line(p,elbow,'SECTION');line(elbow,end,'SECTION');
out.push({kind:'hatch',points:Array.from({length:16},(_,i)=>[p[0]+.5*scale*Math.cos(i*Math.PI/8),p[1]+.5*scale*Math.sin(i*Math.PI/8)] as Point),pattern:'SOLID',spacing:scale,angle:0,color:'#ff0000',layer:'SECTION',source});
text([end[0]+scale,end[1]+scale],`말뚝 Ø${formatMm(pile.diameter*1000)} · ${model.piles.length}본`);
}
out.push(...foundationNotes(model,input,id,b,scale));
if(id==='front'&&model.footings[0]){const y=model.footings[0].top*1000;line([b.minX-3*scale,y],[b.maxX+3*scale,y],'SECTION');out.push(...sectionCut([b.minX-3*scale,y],scale,'C',false,source).filter(e=>e.kind!=='text'),...sectionCut([b.maxX+3*scale,y],scale,'C',true,source).filter(e=>e.kind!=='text'));out.push({kind:'text',point:[b.minX-5*scale,y+2*scale],text:'C',height:2.5*scale,align:'right',layer:'SECTION',source});}
if(settings.dimensions)out.push(...componentDimensions(model,id,geometry??projectModel(model,id),scale));
return settings.hatches===false?out.filter(e=>e.kind!=='hatch'):out;
}
export function generateDrawing(model: PierModel,input: PierInput,project: {name:string;bridge:string},settings: DrawingSettings,context?:{models:readonly PierModel[]}): DrawingDocument {
const size={A3:[420,297],A2:[594,420],A1:[841,594]}[settings.paper];
const paper={name:settings.paper,width:size[0],height:size[1]},positions=slots(...size as [number,number]);
const primitives=modelPrimitives(model),warnings: string[]=[];
if(!model.columns.length)throw new Error('모델에 기둥 형상이 없습니다.');
const missing=model.blocks.filter(b=>!b.plan).length;
if(missing)warnings.push(`받침 ${missing}/${model.blocks.length}개의 본체·솔플레이트 제원이 없습니다. 해당 치수표는 미확인으로, 형상은 모델의 블럭·몰탈·중심으로 표시합니다.`);
if(model.kink)warnings.push('꺾임 교각의 전체폭·간격 치수는 정투영 거리입니다. 축선 실장은 별도 확인하세요.');
const views: DrawingView[]=(['front','side','top','footing','section'] as ViewId[]).map(id=>{
const source={member:'column',index:0,pier:model.name};
const geometry: Entity[]=id==='section'?[{kind:'circle',center:[0,0],radius:model.columns[0].diameter*500,layer:'STRUCTURE',source}]:projectModel(model,id,primitives);
const geometryBounds=entityBounds(geometry),slot=positions[id],wide=id==='front'||id==='side';
let scale=settings.scale==='auto'?recommend(geometryBounds,slot,id):settings.scale;
if(settings.scale==='auto'&&wide&&settings.shorten!==false)for(let i=0;i<3;i++)scale=recommend(shortenedBounds(geometryBounds,modelBreaks(model,scale)),slot,id);
const breaks=wide&&settings.shorten!==false?modelBreaks(model,scale):undefined;
const entities=[...geometry,...annotate(model,id,geometryBounds,scale,settings,input)];
const bounds={minX:geometryBounds.minX-(wide?17:12)*scale,maxX:geometryBounds.maxX+(wide&&settings.elevations?18:10)*scale,
minY:geometryBounds.minY-(id==='top'?27:20)*scale,maxY:geometryBounds.maxY+(id==='top'?19:10)*scale};
return{id,title:VIEW_TITLES[id],entities,geometryBounds,bounds,scale,slot,breaks};
});
if(settings.scale==='auto'){
const common=Math.max(views[0].scale,views[1].scale);
for(const v of views.slice(0,2)){
const b=v.geometryBounds;v.entities=[...v.entities.filter(e=>e.source.member!=='annotation'),...annotate(model,v.id,b,common,settings,input)];
v.scale=common;v.breaks=settings.shorten===false?undefined:modelBreaks(model,common);
v.bounds={minX:b.minX-17*common,maxX:b.maxX+(settings.elevations?18:10)*common,minY:b.minY-20*common,maxY:b.maxY+10*common};
}
}
let detailScale=50,detail=bearingDetail(model,detailScale,settings.detailBearing,settings.dimensions,settings.centers);
for(let i=0;i<3;i++){
const b=entityBounds(detail.entities),s=positions.detail,need=Math.max((b.maxX-b.minX)/(s.width-6),(b.maxY-b.minY)/(s.height-12));
detailScale=SCALES.find(x=>x>=need)??200;detail=bearingDetail(model,detailScale,settings.detailBearing,settings.dimensions,settings.centers);
}
const db=entityBounds(detail.entities);
views.push({id:'detail',title:detail.title,entities:detail.entities,geometryBounds:db,bounds:{minX:db.minX-50,maxX:db.maxX+50,minY:db.minY-100,maxY:db.maxY+100},scale:detailScale,slot:positions.detail});
const table=scheduleEntities(model,positions.schedule.width,'variation'),tb=entityBounds(table);
// Table text is paper-sized; increase its allotted height or sheet size rather than silently omit rows.
views.push({id:'schedule',title:VIEW_TITLES.schedule,entities:table,geometryBounds:tb,bounds:tb,scale:100,slot:positions.schedule});
const heights=scheduleEntities(model,positions.heights.width,'heights'),hb=entityBounds(heights);
views.push({id:'heights',title:VIEW_TITLES.heights,entities:heights,geometryBounds:hb,bounds:hb,scale:100,slot:positions.heights});
const iso=isometricEntities(model,false,settings.isoDirection??0),ib=entityBounds(iso),islot=positions.iso;
const iscale=SCALES.find(s=>s>=Math.max((ib.maxX-ib.minX)/(islot.width-4),(ib.maxY-ib.minY)/(islot.height-8)))??2000;
views.push({id:'iso',title:'ISOMETRIC VIEW',entities:settings.hatches===false?iso.filter(e=>e.kind!=='hatch'):iso,geometryBounds:ib,bounds:ib,scale:iscale,slot:islot});
const cfg=settings.sheet??defaultSheet(),models=context?.models??[model];let keyscale=1000,key=keyplanEntities(models,model,keyscale,cfg.startLabel,cfg.endLabel);
for(let i=0;i<3;i++){const b=entityBounds(key),s=positions.keyplan;keyscale=Math.max(100,Math.ceil(Math.max((b.maxX-b.minX)/(s.width-8),(b.maxY-b.minY)/(s.height-8))/100)*100);key=keyplanEntities(models,model,keyscale,cfg.startLabel,cfg.endLabel);}
const kb=entityBounds(key);views.push({id:'keyplan',title:'KEY PLAN',entities:key,geometryBounds:kb,bounds:kb,scale:keyscale,slot:positions.keyplan});
if(settings.hatches!==false)for(const v of views.filter(v=>['front','side','top','footing','section','detail'].includes(v.id)))v.entities=[...hatchGeometry(v.entities,v.scale),...v.entities];
if(wrappedLines(cfg.notes,133,cfg.noteHeight).length*cfg.noteHeight*1.65>155)warnings.push('NOTE 원문이 지정 영역을 넘습니다. 노트 편집에서 문자 높이/내용을 조정하세요.');
const doc:DrawingDocument={project:project.name,bridge:project.bridge,pier:model.name,units:'mm',views,paper,warnings,settings:{...settings},notes:[
`${input.type} · 기둥 ${model.columns.length}개 · ${input.foundationType}`,
`받침 전열 ${model.bearings.filter(b=>b.row==='전열').length} / 후열 ${model.bearings.filter(b=>b.row==='후열').length}`,
`기준 E ${model.pivot.e.toFixed(3)}`,`기준 N ${model.pivot.n.toFixed(3)}`,
'치수 mm / 표고 EL m','정면 기준: 첫 교각 로컬 축','A-A: 첫 기둥의 동일 원형 단면',
settings.shorten===false?'기둥·말뚝 전체 길이 표시':'기둥·말뚝 생략 표현 / 치수·EL은 실값',
'모델에 없는 변단면·모따기 생략','숨은선은 개별 부재의 형상선',
]};
const originals=new Map(views.map(v=>[v.id,v])),geometry=new Map(views.slice(0,5).map(v=>[v.id,v.entities.filter(e=>e.kind!=='hatch'&&e.source.member!=='annotation'&&e.source.member!=='bearing-location')]));
optimiseLayout(doc,(id,scale)=>{
const original=originals.get(id)!;let entities=original.entities;
if(geometry.has(id))entities=[...geometry.get(id)!,...annotate(model,id,original.geometryBounds,scale,settings,input,geometry.get(id))];
else if(id==='detail')entities=bearingDetail(model,scale,settings.detailBearing,settings.dimensions,settings.centers).entities;
else if(id==='iso')entities=[...original.entities,...(settings.elevations?isometricInfo(model,input,original.entities,scale,settings.isoDirection??0,settings.dimensions):[])];
else if(id==='keyplan')entities=keyplanEntities(models,model,scale,cfg.startLabel,cfg.endLabel);
const breaks=['front','side'].includes(id)&&settings.shorten!==false?modelBreaks(model,scale):undefined;
if(settings.hatches!==false&&id!=='iso')entities=[...hatchGeometry(entities,scale),...entities];
return{...original,entities:entities.map(e=>({...e})),scale,breaks,paperOrigin:undefined};
});
return doc;
}
export { dimensionGraphics, formatMm } from './dimension-graphics';