Files
argo/SUB Control/web/model-viewer.js
T

105 lines
5.6 KiB
JavaScript

(() => {
const canvas = document.querySelector('#model-canvas');
const stage = document.querySelector('.rov-stage');
const ctx = canvas.getContext('2d');
let vertices = null, frame = 0, yaw = 0, pitch = 0, zoom = 1;
let dragging = false, lastX = 0, lastY = 0;
// Still below native resolution, but high enough for clean assembly contours.
const renderScale = .82;
function parseStl(buffer) {
const view = new DataView(buffer);
let raw = [];
const binaryCount = buffer.byteLength >= 84 ? view.getUint32(80, true) : 0;
const isBinary = binaryCount > 0 && 84 + binaryCount * 50 <= buffer.byteLength;
if (isBinary) {
for (let t = 0; t < binaryCount; t++) {
const offset = 84 + t * 50 + 12;
for (let v = 0; v < 3; v++) {
raw.push(view.getFloat32(offset + v * 12, true));
raw.push(view.getFloat32(offset + v * 12 + 4, true));
raw.push(view.getFloat32(offset + v * 12 + 8, true));
}
}
} else {
const text = new TextDecoder().decode(buffer);
const matches = [...text.matchAll(/vertex\s+([-+\deE.]+)\s+([-+\deE.]+)\s+([-+\deE.]+)/gi)];
if (matches.length < 3) throw new Error('This file does not contain a readable STL mesh');
for (let i = 0; i + 2 < matches.length; i += 3) {
for (let j = 0; j < 3; j++) raw.push(+matches[i+j][1], +matches[i+j][2], +matches[i+j][3]);
}
}
if (raw.length < 9) throw new Error('The STL mesh is empty');
let minX=Infinity,minY=Infinity,minZ=Infinity,maxX=-Infinity,maxY=-Infinity,maxZ=-Infinity;
for(let i=0;i<raw.length;i+=3){minX=Math.min(minX,raw[i]);maxX=Math.max(maxX,raw[i]);minY=Math.min(minY,raw[i+1]);maxY=Math.max(maxY,raw[i+1]);minZ=Math.min(minZ,raw[i+2]);maxZ=Math.max(maxZ,raw[i+2])}
const cx=(minX+maxX)/2,cy=(minY+maxY)/2,cz=(minZ+maxZ)/2,scale=2/Math.max(maxX-minX,maxY-minY,maxZ-minZ,1e-9);
for(let i=0;i<raw.length;i+=3){raw[i]=(raw[i]-cx)*scale;raw[i+1]=(raw[i+1]-cy)*scale;raw[i+2]=(raw[i+2]-cz)*scale}
return new Float32Array(raw);
}
function draw() {
frame=0;if(!vertices)return;
const rect=canvas.getBoundingClientRect();
const width=Math.max(1,Math.round(rect.width*renderScale)),height=Math.max(1,Math.round(rect.height*renderScale));
if(canvas.width!==width||canvas.height!==height){canvas.width=width;canvas.height=height}
ctx.setTransform(1,0,0,1,0,0);ctx.clearRect(0,0,width,height);
const mask=document.createElement('canvas'),maskCtx=mask.getContext('2d');
mask.width=width;mask.height=height;
const size=Math.min(width,height)*.43*zoom,cx=width/2,cy=height/2;
const cosY=Math.cos(yaw),sinY=Math.sin(yaw),cosP=Math.cos(pitch),sinP=Math.sin(pitch);
const project=(x,y,z)=>{
const rx=x*cosY+z*sinY,rz=-x*sinY+z*cosY;
return [rx,y*cosP-rz*sinP];
};
maskCtx.fillStyle='#fff';
maskCtx.beginPath();
for(let i=0;i<vertices.length;i+=9){
const a=project(vertices[i],vertices[i+1],vertices[i+2]);
const b=project(vertices[i+3],vertices[i+4],vertices[i+5]);
const c=project(vertices[i+6],vertices[i+7],vertices[i+8]);
maskCtx.moveTo(cx+a[0]*size,cy-a[1]*size);
maskCtx.lineTo(cx+b[0]*size,cy-b[1]*size);
maskCtx.lineTo(cx+c[0]*size,cy-c[1]*size);
maskCtx.closePath();
}
// Rasterize the union once. Filling faces individually creates faint
// antialiased seams that resemble a wireframe on dense assemblies.
maskCtx.fill();
// A filtered copy creates only the exterior contour; triangle boundaries
// never enter the final image because the source is a single solid mask.
ctx.save();
ctx.filter='drop-shadow(0 0 1.2px rgba(8,15,17,.9)) drop-shadow(0 4px 5px rgba(0,0,0,.28))';
ctx.drawImage(mask,0,0);ctx.restore();
const surface=document.createElement('canvas'),surfaceCtx=surface.getContext('2d');
surface.width=width;surface.height=height;
const gradient=surfaceCtx.createLinearGradient(0,0,width,height);
gradient.addColorStop(0,'#ffffff');gradient.addColorStop(.58,'#f4f5f2');gradient.addColorStop(1,'#dfe2de');
surfaceCtx.fillStyle=gradient;surfaceCtx.fillRect(0,0,width,height);
surfaceCtx.globalCompositeOperation='destination-in';surfaceCtx.drawImage(mask,0,0);
ctx.drawImage(surface,0,0);
}
function requestDraw(){if(!frame)frame=requestAnimationFrame(draw)}
canvas.addEventListener('pointerdown',event=>{dragging=true;lastX=event.clientX;lastY=event.clientY;canvas.setPointerCapture(event.pointerId)});
canvas.addEventListener('pointermove',event=>{if(!dragging)return;yaw+=(event.clientX-lastX)*.012;pitch=Math.max(-1.45,Math.min(1.45,pitch+(event.clientY-lastY)*.012));lastX=event.clientX;lastY=event.clientY;requestDraw()});
canvas.addEventListener('pointerup',()=>dragging=false);
canvas.addEventListener('pointercancel',()=>dragging=false);
canvas.addEventListener('wheel',event=>{event.preventDefault();zoom=Math.max(.55,Math.min(2.2,zoom*(event.deltaY>0?.9:1.1)));requestDraw()},{passive:false});
addEventListener('resize',requestDraw);
window.rovModel = {
loadBuffer(buffer, name) {
vertices=parseStl(buffer);stage.classList.add('model-loaded');
document.querySelector('#model-file-name').textContent=name;
yaw=0;pitch=0;zoom=1;
requestDraw();
return {name,triangles:vertices.length/9};
},
load(payload) {
if (!payload?.data) throw new Error('The selected STL returned no mesh data');
const binary=atob(payload.data),bytes=new Uint8Array(binary.length);
for(let i=0;i<binary.length;i++)bytes[i]=binary.charCodeAt(i);
return this.loadBuffer(bytes.buffer,payload.name);
}
};
})();