Files
Pier_v1/도면작성/project-views.ts
T

100 lines
8.6 KiB
TypeScript

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<points.length;i++){const a=points[i],b=points[(i+1)%points.length],da=(a.y-y)*(above?1:-1),db=(b.y-y)*(above?1:-1);
if(da>=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<PierModel,Entity[]>();
/** 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)<Math.abs(b.y-el)?a:b);
rows.push({point:project(p),name,el});
}
rows.sort((a,b)=>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;
}