(() => { 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;ia.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