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={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 { 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(); 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]{ 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';