Files
hwacheon-network-viewer/web/geometry.js
T

87 lines
5.9 KiB
JavaScript

import {vec,hex} from './math.js';
export const palette={junction:'#99c4ab',circular:'#64bdb9',rectangular:'#d7b47a',trapezoidal:'#b8a2d8',outfall:'#8bb5d4',storage:'#b8a2d8',pump:'#d8bd75',weir:'#d29a88',grid:'#35474a'};
export function layerOf(f){return ['junction','conduit','outfall'].includes(f.type)?f.type:'other';}
export function physicalPoint(n,origin){return n.xy?[n.xy[0]-origin[0],n.xy[1]-origin[1],n.invert-origin[2]]:null;}
// Inlet/outlet rings are vertical in the original XY frame. The cross-section
// centre is raised by half the full input height above the respective invert.
export function conduitPath(link,origin){
if(!link.pathXY||!link.renderable)return [];
const lengths=[0];for(let i=1;i<link.pathXY.length;i++)lengths.push(lengths.at(-1)+Math.hypot(link.pathXY[i][0]-link.pathXY[i-1][0],link.pathXY[i][1]-link.pathXY[i-1][1]));
const total=lengths.at(-1);if(total<=1e-9)return [];
return link.pathXY.map((p,i)=>[p[0]-origin[0],p[1]-origin[1],link.invertStart-origin[2]+(link.invertEnd-link.invertStart)*lengths[i]/total]);
}
class Batch {
constructor(mode){this.mode=mode;this.data=[];this.count=0;}
vertex(base,offset,normal,color,id,kind=0,slice=0){this.data.push(...base,...offset,...normal,...color,id,kind,slice);this.count++;}
triangle(bases,offsets,color,id,kind=0,slices=[0,0,0]){
const p=bases.map((b,i)=>vec.add(b,offsets[i]));const n=vec.norm(vec.cross(vec.sub(p[1],p[0]),vec.sub(p[2],p[0])));
for(let i=0;i<3;i++)this.vertex(bases[i],offsets[i],n,color,id,kind,slices[i]);
}
quad(bases,offsets,color,id,kind=0,slices=[0,0,0,0]){
for(const tri of [[0,1,2],[0,2,3]])this.triangle(tri.map(i=>bases[i]),tri.map(i=>offsets[i]),color,id,kind,tri.map(i=>slices[i]));
}
}
function vessel(batch,point,height,radius,color,id,steps=14,kind=1){
const top=[point[0],point[1],point[2]+height];
for(let i=0;i<steps;i++){
const a=i/steps*Math.PI*2,b=(i+1)/steps*Math.PI*2,u=[radius*Math.cos(a),radius*Math.sin(a),0],v=[radius*Math.cos(b),radius*Math.sin(b),0];
batch.quad([point,point,top,top],[u,v,v,u],color,id,kind);
batch.triangle([top,top,top],[[0,0,0],u,v],color,id,kind);
batch.triangle([point,point,point],[[0,0,0],v,u],color,id,kind);
}
}
function tube(batch,link,origin,color,id){
const path=conduitPath(link,origin),section=link.section;
let ring=section.shape==='CIRCULAR'?Array.from({length:12},(_,i)=>{const t=i/12*Math.PI*2;return[section.width/2*Math.cos(t),section.height/2*(1+Math.sin(t)),Math.sin(t)];}):[[-section.width/2,0,-1],[section.width/2,0,-1],[section.width/2,section.height,1],[-section.width/2,section.height,1]];
const open=section.shape==='TRAPEZOIDAL'||section.shape==='RECT_OPEN';
if(open){
const h=section.height,w=section.width,left=section.sideSlopes?.[0]??0,right=section.sideSlopes?.[1]??0;
ring=[[-w/2-left*h,h,1],[-w/2,0,-1],[w/2,0,-1],[w/2+right*h,h,1]];
}
for(let k=1;k<path.length;k++){
const a=path[k-1],b=path[k],dx=b[0]-a[0],dy=b[1]-a[1],len=Math.hypot(dx,dy);if(len<1e-9)continue;
const nx=-dy/len,ny=dx/len;
for(let barrel=0;barrel<section.barrels;barrel++){
// Barrel separation is schematic when counts exceed one; the supplied file has one throughout.
const lateral=(barrel-(section.barrels-1)/2)*(section.topWidth??section.width)*1.1;
for(let i=0;i<ring.length-(open?1:0);i++){
const r=ring[i],s=ring[(i+1)%ring.length],u=[nx*(r[0]+lateral),ny*(r[0]+lateral),r[1]],v=[nx*(s[0]+lateral),ny*(s[0]+lateral),s[1]];
batch.quad([a,b,b,a],[u,u,v,v],color,id,2,[r[2],r[2],s[2],s[2]]);
}
}
}
return path;
}
export function buildGeometry(network,facilities){
const batches={};for(const layer of ['junction','conduit','outfall','other','grid'])batches[layer]={mesh:new Batch('triangles'),lines:new Batch('lines'),points:new Batch('points')};
const origin=network.meta.origin,nodes=new Map(network.nodes.map(n=>[n.id,n]));
for(const f of facilities){
const group=batches[layerOf(f)],id=f.pickId,color=hex(palette[f.type==='conduit'?(f.section?.shape==='CIRCULAR'?'circular':f.section?.shape==='TRAPEZOIDAL'?'trapezoidal':'rectangular'):f.type]);
if(f.xy){
const point=physicalPoint(f,origin);const height=f.maxDepth??1;
if(f.type==='junction')vessel(group.mesh,point,height,.6,color,id);
else if(f.type==='storage')vessel(group.mesh,point,height,1.7,color,id,6,1);
else vessel(group.mesh,point,1,.8,color,id,4,3);
// Five-pixel minimum marker is a position glyph, not a physical manhole diameter.
group.points.vertex([point[0],point[1],point[2]+height],[0,0,0],[0,0,1],color,id,3);
}else if(f.type==='conduit'&&f.renderable){
const path=tube(group.mesh,f,origin,color,id);
for(let i=1;i<path.length;i++)for(const p of [path[i-1],path[i]])group.lines.vertex(p,[0,0,f.section.height/2],[0,0,1],color,id,4);
}else if(['pump','weir'].includes(f.type)&&f.renderable){
const na=nodes.get(f.fromNode),nb=nodes.get(f.toNode),pa=physicalPoint(na,origin),pb=physicalPoint(nb,origin);
const z=f.type==='weir'?f.crestElevation-origin[2]:Math.max(pa[2],pb[2]);
const a=[pa[0],pa[1],z],b=[pb[0],pb[1],z],mid=vec.scale(vec.add(a,b),.5);
// Separate glyphs allow parallel pumps to be selected; spacing is a symbolic display offset.
const peers=facilities.filter(p=>p.type===f.type&&p.fromNode===f.fromNode&&p.toNode===f.toNode),slot=peers.indexOf(f)-(peers.length-1)/2;
const direction=vec.norm([b[0]-a[0],b[1]-a[1],0]);mid[0]+=-direction[1]*slot*3;mid[1]+=direction[0]*slot*3;
vessel(group.mesh,mid,1.3,f.type==='pump'?.8:1.1,color,id,f.type==='pump'?8:4,3);
group.points.vertex([mid[0],mid[1],z+1.3],[0,0,0],[0,0,1],color,id,3);
for(const p of [a,mid,mid,b])group.lines.vertex(p,[0,0,0],[0,0,1],color,id,4);
f.symbolPoint=[mid[0],mid[1],z+1.3];
}
}
const bound=network.meta.bounds,extent=Math.max(bound.maxX-bound.minX,bound.maxY-bound.minY)*.72,grid=batches.grid.lines;
for(let n=-Math.ceil(extent/100)*100;n<=extent;n+=100)for(const pair of [[[-extent,n,-1],[extent,n,-1]],[[n,-extent,-1],[n,extent,-1]]])for(const p of pair)grid.vertex(p,[0,0,0],[0,0,1],hex(palette.grid),0,4);
return batches;
}