import { Vector3, type Plane } from 'three'; import { DIM, blockFramePoint, type PierModel } from '../src/core/geometry'; import { INTEGRATED_BLOCK } from '../src/core/integrated-block'; import { solplateGeometry } from '../src/core/solplate'; import { stretchMatrix, mitrePlane } from '../src/view/scene'; import type { Bounds, Entity, Layer, Point, Source, ViewId } from './types'; type Face = Vector3[]; type Solid = { faces: Face[]; source: Source; layer: Layer }; export type Cylinder = { center: Vector3; bottom: number; top: number; radius: number; source: Source }; const EPS = .001; // mm, drawing topology tolerance; source dimensions are never rounded here. const cross = (a: Point, b: Point) => a[0]*b[1]-a[1]*b[0]; const sub = (a: Point, b: Point): Point => [a[0]-b[0],a[1]-b[1]]; const key = (p: Point) => `${Math.round(p[0]/EPS)},${Math.round(p[1]/EPS)}`; const edgeKey = (a: Point,b: Point) => [key(a),key(b)].sort().join('|'); function clip(face: Face, plane?: Plane): Face { if (!plane) return face; const out: Face=[]; for (let i=0;i=-1e-9) out.push(a); if ((da>=0)!==(db>=0)) out.push(a.clone().lerp(b,da/(da-db))); } return out; } export function localPoint(model: PierModel,k: number,s: number,z: number,n: number): Vector3 { return new Vector3(s,z,n).applyMatrix4(stretchMatrix(model,k)); } export function projectPoint(model: PierModel,point: Vector3,view: ViewId): Point { const east=point.x+model.pivot.e-model.bp1.e,north=-point.z+model.pivot.n-model.bp1.n; const along=(east*model.u.e+north*model.u.n)*1000; const across=(east*model.nrm.e+north*model.nrm.n)*1000; return view==='front' ? [along,point.y*1000] : view==='side' ? [across,point.y*1000] : [along,across]; } function area(p: Point[]): number { return p.reduce((sum,a,i)=>sum+cross(a,p[(i+1)%p.length]),0)/2; } function inside(p: Point,poly: Point[]): boolean { let hit=false; for(let i=0,j=poly.length-1;ip[1])!==(b[1]>p[1]) && p[0]<(b[0]-a[0])*(p[1]-a[1])/(b[1]-a[1])+a[0]) hit=!hit; } return hit; } /** Boundary of the union of orthographically projected solid faces (no screenshot or mesh raster). */ export function silhouette(polygons: Point[][]): [Point,Point][] { const valid=polygons.filter(p=>p.length>=3&&Math.abs(area(p))>EPS*EPS); const edges=valid.flatMap(p=>p.map((a,i)=>[a,p[(i+1)%p.length]] as [Point,Point])); const result=new Map(); for(const [a,b] of edges) { const d=sub(b,a),length=Math.hypot(...d); if(length1e-8) { const t=cross(q,r)/det,u=cross(q,d)/det; if(t>0&&t<1&&u>=-1e-9&&u<=1+1e-9)ts.push(t); } else if(Math.abs(cross(q,d))0&&t<1)ts.push(t); } } } const sorted=[...new Set(ts.map(t=>Math.round(t*1e10)/1e10))].sort((x,y)=>x-y); const at=(t: number): Point=>[a[0]+d[0]*t,a[1]+d[1]*t]; for(let i=1;iinside(left,p))!==valid.some(p=>inside(right,p)))result.set(edgeKey(start,end),[start,end]); } } return [...result.values()]; } function prism(model: PierModel,k: number,profile: Point[],n0: number,n1: number,source: Source,layer: Layer,cut=true): Solid { const front=profile.map(([s,z])=>localPoint(model,k,s,z,n0)); const back=profile.map(([s,z])=>localPoint(model,k,s,z,n1)); const faces=[front,[...back].reverse(),...front.map((a,i)=>[a,front[(i+1)%front.length],back[(i+1)%back.length],back[i]])]; return { faces:faces.map(f=>clip(f,cut?mitrePlane(model,k):undefined)).filter(f=>f.length>=3),source,layer }; } function box(model: PierModel,k: number,s: number,n: number,length: number,width: number,bottom: number,top: number,source: Source,layer: Layer,skew=0,cut=false): Solid { const corners=[[-length/2,-width/2],[length/2,-width/2],[length/2,width/2],[-length/2,width/2]].map(([a,c])=>blockFramePoint({s,n,rowSkewDeg:skew},a,c)); const low=corners.map(([a,c])=>localPoint(model,k,a,bottom,c)); const high=corners.map(([a,c])=>localPoint(model,k,a,top,c)); return {source,layer,faces:[[...low].reverse(),high,...low.map((a,i)=>[a,low[(i+1)%4],high[(i+1)%4],high[i]])].map(f=>clip(f,cut?mitrePlane(model,k):undefined)).filter(f=>f.length>=3)}; } /** Derived, disposable drawing primitives; the supplied PierModel remains the only geometry source. */ export function modelPrimitives(model: PierModel): { solids: Solid[]; cylinders: Cylinder[] } { const solids: Solid[]=[],cylinders: Cylinder[]=[]; const source=(member: string,index: number): Source=>({member,index,pier:model.name}); model.copingParts.forEach((p,i)=>solids.push(prism(model,p.k,p.profile,p.nFrom,p.nTo,source('coping',i),'STRUCTURE'))); model.footings.forEach((f,i)=>solids.push(box(model,f.k,f.s,0,f.length,f.width,f.bottom,f.top,source('footing',i),'STRUCTURE',0,true))); model.blindings.forEach((b,i)=>solids.push(box(model,b.k,b.s,0,b.length,b.width,b.top-b.thickness,b.top,source('blinding',i),'STRUCTURE',0,true))); model.columns.forEach((c,i)=>cylinders.push({center:localPoint(model,c.k,c.s,0,0),bottom:c.bottom,top:c.top,radius:c.diameter/2,source:source('column',i)})); model.piles.forEach((p,i)=>cylinders.push({center:localPoint(model,p.k,p.s,0,p.n),bottom:p.bottom,top:p.top,radius:p.diameter/2,source:source('pile',i)})); model.blocks.forEach((b,i)=>{ if(!model.integratedBlocks.length)solids.push(box(model,b.k,b.s,b.n,DIM.blockSize,DIM.blockSize,b.bottom,b.top,source('block',i),'BEARING',b.rowSkewDeg)); solids.push(box(model,b.k,b.s,b.n,DIM.padSize,DIM.padSize,b.padTop-DIM.padThickness,b.padTop,source('pad',i),'BEARING',b.rowSkewDeg)); if(b.plan) { solids.push(box(model,b.k,b.s,b.n,b.plan.W/1000-.2,b.plan.L/1000-.2,b.padTop,b.padTop+b.plan.h3/1000,source('bearing',i),'BEARING',b.rowSkewDeg)); const corners=solplateGeometry(b,b.plan).corners.map(([s,z,n])=>localPoint(model,b.k,s,z,n)); const faces=[[0,4,6,2],[1,3,7,5],[0,1,5,4],[2,6,7,3],[0,2,3,1],[4,5,7,6]].map(indices=>indices.map(index=>corners[index])); solids.push({source:source('solplate',i),layer:'BEARING',faces}); } }); model.integratedBlocks.forEach((b,i)=>{ const k=model.blocks.find(p=>p.no===b.frontNo&&p.row==='전열')!.k; for(const [top,n] of [[b.topFront,b.n-INTEGRATED_BLOCK.length/4000],[b.topBack,b.n+INTEGRATED_BLOCK.length/4000]]) solids.push(box(model,k,b.s,n,INTEGRATED_BLOCK.depth/1000,INTEGRATED_BLOCK.length/2000,b.bottom,top,source('integrated-block',i),'BEARING',b.rowSkewDeg)); }); return {solids,cylinders}; } export function projectModel(model: PierModel,view: ViewId,primitives=modelPrimitives(model)): Entity[] { const entities: Entity[]=[]; const footingOnly=view==='footing'; for(const solid of primitives.solids) { if(footingOnly&&!['footing','blinding'].includes(solid.source.member))continue; if(view==='top'&&['footing','blinding'].includes(solid.source.member))continue; const polygons=solid.faces.map(face=>face.map(p=>projectPoint(model,p,view))); const edges=silhouette(polygons),outer=new Set(edges.map(([a,b])=>edgeKey(a,b))); for(const [a,b] of edges)entities.push({kind:'line',a,b,layer:solid.layer,source:solid.source}); const hidden=new Set(); for(const poly of polygons)for(let i=0;iEPS) { hidden.add(id); entities.push({kind:'line',a,b,layer:'HIDDEN',source:solid.source}); } } } for(const cylinder of primitives.cylinders) { if(view==='top'&&cylinder.source.member==='pile')continue; const center=projectPoint(model,cylinder.center,view),r=cylinder.radius*1000; if(view==='top'||view==='footing')entities.push({kind:'circle',center,radius:r,layer:cylinder.source.member==='pile'?'HIDDEN':'STRUCTURE',source:cylinder.source}); else { const bottom=cylinder.bottom*1000,top=cylinder.top*1000; entities.push({kind:'polyline',points:[[center[0]-r,bottom],[center[0]+r,bottom],[center[0]+r,top],[center[0]-r,top]],closed:true,layer:'STRUCTURE',source:cylinder.source}); } } return entities; } export function entityBounds(entities: Entity[]): Bounds { const points: Point[]=entities.flatMap(e=>e.kind==='line'?[e.a,e.b]:e.kind==='polyline'||e.kind==='hatch'?e.points:e.kind==='circle'?[[e.center[0]-e.radius,e.center[1]-e.radius],[e.center[0]+e.radius,e.center[1]+e.radius]] as Point[]:e.kind==='dimension'?(e.axis==='x'?[[e.a[0],e.position],[e.b[0],e.position],e.a,e.b]:[[e.position,e.a[1]],[e.position,e.b[1]],e.a,e.b]) as Point[]:[e.point]); if(!points.length)return{minX:0,minY:0,maxX:1,maxY:1}; return{minX:Math.min(...points.map(p=>p[0])),minY:Math.min(...points.map(p=>p[1])),maxX:Math.max(...points.map(p=>p[0])),maxY:Math.max(...points.map(p=>p[1]))}; }