import { Vector3 } from 'three'; import type { PierInput } from '../src/core/types'; import { entityBounds } from './projection'; import type { PierModel } from '../src/core/geometry'; import { modelPrimitives, projectPoint, projectModel } from './projection'; import { modelBreaks, shortenedY } from './breaks'; import type { Entity, Point, Source } from './types'; import { visibleOrder } from './iso-visibility'; function clipFace(points:Vector3[],y:number,above:boolean):Vector3[]{ const out:Vector3[]=[]; for(let i=0;i=0)out.push(a);if((da>=0)!==(db>=0))out.push(a.clone().lerp(b,(y-a.y)/(b.y-a.y)));} return out; } /** Vector axonometry from the very same concrete/bearing solids and cylinder dimensions as the live model. */ export function isometricEntities(model:PierModel,shorten:boolean,direction:number):Entity[]{ const primitives=modelPrimitives(model),faces:{points:Vector3[];source:Source;layer:Entity['layer']}[]=[]; for(const solid of primitives.solids)for(const points of solid.faces)faces.push({points,source:solid.source,layer:solid.layer}); for(const c of primitives.cylinders){ const ring=(z:number)=>Array.from({length:24},(_,i)=>new Vector3(c.center.x+c.radius*Math.cos(i*Math.PI/12),z,c.center.z+c.radius*Math.sin(i*Math.PI/12))); const low=ring(c.bottom),high=ring(c.top);faces.push({points:[...low].reverse(),source:c.source,layer:'STRUCTURE'},{points:high,source:c.source,layer:'STRUCTURE'}); low.forEach((a,i)=>faces.push({points:[a,low[(i+1)%24],high[(i+1)%24],high[i]],source:c.source,layer:'STRUCTURE'})); } const cuts=shorten?modelBreaks(model,100):[],theta=direction*Math.PI/2,datum=Math.min(...model.columns.map(c=>c.bottom),...model.piles.map(c=>c.bottom),...model.footings.map(c=>c.bottom))*1000; const project=(p:Vector3):{p:Point;depth:number}=>{ const [s,n]=projectPoint(model,p,'top'),a=s*Math.cos(theta)-n*Math.sin(theta),b=s*Math.sin(theta)+n*Math.cos(theta),z=shortenedY(p.y*1000,cuts)-shortenedY(datum,cuts); return{p:[(a-b)*Math.sqrt(3)/2,z+(a+b)*.5],depth:a+b-2*z}; }; const projected=faces.flatMap(face=>{ let parts=[face.points];for(const c of cuts)parts=parts.flatMap(p=>[clipFace(p,c.from/1000,false),clipFace(p,c.to/1000,true)]).filter(p=>p.length>2); return parts.map(points=>{const projected=points.map(project),v=points[1].clone().sub(points[0]),w=points[2].clone().sub(points[0]),normal=v.cross(w).normalize(); const gray=Math.round(185+40*Math.abs(normal.y)+12*Math.abs(normal.x));return{vertices:projected.map(p=>new Vector3(p.p[0],p.p[1],p.depth)),edges:points.map(()=>true),data:{color:`#${gray.toString(16).repeat(3)}`,source:face.source,layer:face.layer}};}); }); return visibleOrder(projected).flatMap(({vertices,edges,data:face}):Entity[]=>{ const points:Point[]=vertices.map(p=>[p.x,p.y]); return [{kind:'hatch',points,pattern:'SOLID',spacing:100,angle:45,color:face.color,source:face.source,layer:'HATCH'}, ...points.flatMap((a,i):Entity[]=>edges[i]?[{kind:'line',a,b:points[(i+1)%points.length],source:face.source,layer:face.layer}]:[])]; }); } const keyOutlineCache=new WeakMap(); /** E/N key plan: the connecting polyline joins actual pier centres, without inventing a road alignment. */ export function keyplanEntities(models:readonly PierModel[],selected:PierModel,scale:number,startLabel:string,endLabel:string):Entity[]{ const available=models.length?models:[selected],origin=available[0].pivot,entities:Entity[]=[],centres:Point[]=[]; const lastModel=available[available.length-1],theta=available.length>1?Math.atan2(lastModel.pivot.n-origin.n,lastModel.pivot.e-origin.e):0; const rotate=(p:Point):Point=>[p[0]*Math.cos(theta)+p[1]*Math.sin(theta),-p[0]*Math.sin(theta)+p[1]*Math.cos(theta)]; const source={member:'keyplan',pier:selected.name,index:0}; for(const model of available){ const centre=rotate([(model.pivot.e-origin.e)*1000,(model.pivot.n-origin.n)*1000]);centres.push(centre); let outline=keyOutlineCache.get(model);if(!outline){outline=projectModel(model,'top').filter(e=>e.layer==='STRUCTURE'&&e.source.member==='coping');keyOutlineCache.set(model,outline);} const transform=(p:Point):Point=>rotate([(model.bp1.e-origin.e)*1000+p[0]*model.u.e+p[1]*model.nrm.e,(model.bp1.n-origin.n)*1000+p[0]*model.u.n+p[1]*model.nrm.n]); for(const e of outline)if(e.kind==='line')entities.push({...e,a:transform(e.a),b:transform(e.b),layer:model.name===selected.name?'BEARING':'STRUCTURE',source:{...e.source,member:'keyplan-pier'}}); entities.push({kind:'circle',center:centre,radius:1.2*scale,layer:model.name===selected.name?'BEARING':'CENTER',source:{member:'keyplan-centre',pier:model.name,index:0}}, {kind:'text',point:[centre[0]+1.8*scale,centre[1]+2*scale],text:model.name,height:2.2*scale,layer:'TEXT',source:{member:'keyplan-label',pier:model.name,index:0}}); } if(centres.length>1)entities.unshift({kind:'polyline',points:centres,closed:false,layer:'CENTER',source}); const first=centres[0],last=centres[centres.length-1]; entities.push({kind:'text',point:[first[0],first[1]-5*scale],text:startLabel,height:2*scale,layer:'TEXT',source}, {kind:'text',point:[last[0],last[1]+6*scale],text:endLabel,height:2*scale,layer:'TEXT',source}); const x=Math.max(...centres.map(p=>p[0]))+10*scale,y=0,north:Point=[Math.sin(theta),Math.cos(theta)],tip:Point=[x+north[0]*7*scale,y+north[1]*7*scale]; entities.push({kind:'line',a:[x,y],b:tip,layer:'TEXT',source},{kind:'text',point:[tip[0]+scale,tip[1]+scale],text:'N',height:2*scale,layer:'TEXT',source}, {kind:'polyline',points:[[tip[0]-north[0]*2*scale+north[1]*scale,tip[1]-north[1]*2*scale-north[0]*scale],tip,[tip[0]-north[0]*2*scale-north[1]*scale,tip[1]-north[1]*2*scale+north[0]*scale]],closed:false,layer:'TEXT',source}); return entities; } /** Actual projected model points anchor all isometric dimensions and EL leaders. */ export function isometricInfo(model:PierModel,input:PierInput,geometry:Entity[],scale:number,direction=0,dimensions=true):Entity[]{ const bounds=entityBounds(geometry),out:Entity[]=[],primitives=modelPrimitives(model); const datum=Math.min(...model.columns.map(c=>c.bottom),...model.piles.map(c=>c.bottom),...model.footings.map(c=>c.bottom))*1000,theta=direction*Math.PI/2; const project=(p:Vector3):Point=>{const [s,n]=projectPoint(model,p,'top'),a=s*Math.cos(theta)-n*Math.sin(theta),b=s*Math.sin(theta)+n*Math.cos(theta);return [(a-b)*Math.sqrt(3)/2,p.y*1000-datum+(a+b)*.5];}; const source={pier:model.name,member:'iso-info',index:0}; const text=(p:Point,value:string)=>out.push({kind:'text',point:p,text:value,height:2.5*scale,layer:'ELEVATION',source,align:'left'}); const line=(a:Point,b:Point)=>out.push({kind:'line',a,b,layer:'ELEVATION',source}); const rows:{point:Point;name:string;el:number}[]=[]; const column=primitives.cylinders.find(c=>c.source.member==='column'); if(column){ const bottom=project(column.center.clone().setY(column.bottom)),top=project(column.center.clone().setY(column.top)); if(dimensions)out.push({kind:'dimension',a:bottom,b:top,position:bounds.minX-4*scale,axis:'y',height:2.5*scale,value:(column.top-column.bottom)*1000,label:`H=${((column.top-column.bottom)*1000).toFixed(0)}`,source,layer:'DIMENSION'}); if(Number.isFinite(input.groundEl))rows.push({point:project(column.center.clone().setY(input.groundEl)),name:'지반고',el:input.groundEl}); } const pile=primitives.cylinders.find(c=>c.source.member==='pile'); if(pile)rows.push({point:project(pile.center.clone().setY(pile.bottom)),name:'말뚝 하단',el:pile.bottom}); const footing=primitives.solids.find(s=>s.source.member==='footing'); if(footing){const points=footing.faces.flat(),el=Math.min(...points.map(p=>p.y)),p=points.find(p=>Math.abs(p.y-el)<1e-7)!;rows.push({point:project(p),name:'기초 하단',el});} for(const [member,name,field] of [['block','교좌면','top'],['pad','몰탈 상면','padTop']] as const){ const solid=primitives.solids.find(s=>s.source.member===member);if(!solid)continue; const block=model.blocks[solid.source.index];if(!block)continue;const el=block[field],points=solid.faces.flat(),p=points.reduce((a,b)=>Math.abs(a.y-el)a.point[1]-b.point[1]);let last=-Infinity; for(const row of rows){ const y=Math.max(row.point[1],last+8*scale);last=y;const x=bounds.maxX+3*scale; line(row.point,[x-scale,y]);line([x-scale,y],[x+13*scale,y]); out.push({kind:'polyline',points:[[row.point[0]+scale,row.point[1]+.6*scale],row.point,[row.point[0]+scale,row.point[1]-.6*scale]],closed:false,layer:'ELEVATION',source}); text([x,y+3.3*scale],row.name);text([x,y],`EL. ${row.el.toFixed(3)}`); } return out; }