84 lines
5.0 KiB
JavaScript
84 lines
5.0 KiB
JavaScript
(() => {
|
|
const canvas = document.querySelector('#model-canvas');
|
|
const stage = document.querySelector('.rov-stage');
|
|
const ctx = canvas.getContext('2d');
|
|
let vertices = null, yaw = -.55, pitch = -.42, zoom = 1, edgesOnly = false;
|
|
let dragging = false, lastX = 0, lastY = 0, frame = 0;
|
|
|
|
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) {
|
|
const stride = Math.max(1, Math.ceil(binaryCount / 14000));
|
|
for (let t = 0; t < binaryCount; t += stride) {
|
|
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');
|
|
const stride = Math.max(1, Math.ceil(matches.length / 42000));
|
|
for (let i = 0; i + 2 < matches.length; i += 3 * stride) {
|
|
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 rotated(x,y,z) {
|
|
const cy=Math.cos(yaw),sy=Math.sin(yaw),cp=Math.cos(pitch),sp=Math.sin(pitch);
|
|
const x1=x*cy+z*sy,z1=-x*sy+z*cy;
|
|
return [x1,y*cp-z1*sp,y*sp+z1*cp];
|
|
}
|
|
|
|
function draw() {
|
|
frame=0;if(!vertices)return;
|
|
const rect=canvas.getBoundingClientRect(),dpr=Math.min(devicePixelRatio||1,2);
|
|
const width=Math.max(1,Math.round(rect.width*dpr)),height=Math.max(1,Math.round(rect.height*dpr));
|
|
if(canvas.width!==width||canvas.height!==height){canvas.width=width;canvas.height=height}
|
|
ctx.setTransform(dpr,0,0,dpr,0,0);ctx.clearRect(0,0,rect.width,rect.height);
|
|
const size=Math.min(rect.width,rect.height)*.39*zoom,cx=rect.width/2,cy=rect.height/2;
|
|
const tris=[];
|
|
for(let i=0;i<vertices.length;i+=9){const a=rotated(vertices[i],vertices[i+1],vertices[i+2]),b=rotated(vertices[i+3],vertices[i+4],vertices[i+5]),c=rotated(vertices[i+6],vertices[i+7],vertices[i+8]);tris.push({a,b,c,z:(a[2]+b[2]+c[2])/3})}
|
|
tris.sort((a,b)=>a.z-b.z);
|
|
ctx.lineJoin='round';ctx.lineWidth=.65;
|
|
for(const t of tris){const ax=cx+t.a[0]*size,ay=cy-t.a[1]*size,bx=cx+t.b[0]*size,by=cy-t.b[1]*size,cx2=cx+t.c[0]*size,cy2=cy-t.c[1]*size;const nz=(bx-ax)*(cy2-ay)-(by-ay)*(cx2-ax),light=Math.max(0,Math.min(1,.25+Math.abs(nz)/(size*size)*5));ctx.beginPath();ctx.moveTo(ax,ay);ctx.lineTo(bx,by);ctx.lineTo(cx2,cy2);ctx.closePath();ctx.fillStyle=edgesOnly?'rgba(79,96,102,.035)':`rgba(${55+light*80},${67+light*82},${72+light*84},.88)`;ctx.strokeStyle=edgesOnly?'rgba(183,210,216,.72)':'rgba(171,193,198,.25)';ctx.fill();ctx.stroke()}
|
|
}
|
|
function requestDraw(){if(!frame)frame=requestAnimationFrame(draw)}
|
|
|
|
canvas.addEventListener('pointerdown',e=>{dragging=true;lastX=e.clientX;lastY=e.clientY;canvas.setPointerCapture(e.pointerId)});
|
|
canvas.addEventListener('pointermove',e=>{if(!dragging)return;yaw+=(e.clientX-lastX)*.012;pitch=Math.max(-1.45,Math.min(1.45,pitch+(e.clientY-lastY)*.012));lastX=e.clientX;lastY=e.clientY;requestDraw()});
|
|
canvas.addEventListener('pointerup',()=>dragging=false);
|
|
canvas.addEventListener('wheel',e=>{e.preventDefault();zoom=Math.max(.55,Math.min(2.2,zoom*(e.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-style').disabled=false;
|
|
document.querySelector('#model-file-name').textContent=name;
|
|
yaw=-.55;pitch=-.42;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);
|
|
},
|
|
toggleStyle() { edgesOnly=!edgesOnly;requestDraw();return edgesOnly; }
|
|
};
|
|
})();
|