diff --git a/SUB Control/README.md b/SUB Control/README.md index 78e74d6..03d9b92 100644 --- a/SUB Control/README.md +++ b/SUB Control/README.md @@ -12,10 +12,9 @@ arming, logging, output limiting, and parameter writes. It does not run a web server, open a browser tab, or require an internet connection. The WebView is frameless and uses the compact title bar drawn by the interface. -The vehicle rail includes a local STL viewer: choose **Load STL**, then drag to -orbit, use the mouse wheel to zoom, or switch between shaded and edge-focused -rendering. A cached STL copy is stored locally under the ignored `runtime/` -directory so the model can be restored on the next launch. +The vehicle rail includes a local STL viewer: choose **Load STL** to show a +fixed orthographic top-view silhouette. A cached STL copy is stored locally +under the ignored `runtime/` directory so the model can be restored on the next launch. STEP/STP CAD solids must first be tessellated/exported as STL because the embedded renderer intentionally does not include a full CAD kernel. diff --git a/SUB Control/web/app.js b/SUB Control/web/app.js index 611d18c..de997c3 100644 --- a/SUB Control/web/app.js +++ b/SUB Control/web/app.js @@ -28,7 +28,6 @@ $('#window-min').onclick=()=>api('minimize_window');$('#window-max').onclick=()= const modelInput=$('#model-input'),modelButton=$('#load-model'); modelButton.onclick=()=>{modelInput.value='';modelInput.click()}; modelInput.onchange=async()=>{const file=modelInput.files?.[0],old=modelButton.textContent;if(!file)return;try{if(!file.name.toLowerCase().endsWith('.stl'))throw new Error('Choose an STL mesh');if(file.size>30*1024*1024)throw new Error('The STL is larger than 30 MB; export a lighter mesh');modelButton.disabled=true;modelButton.textContent='Loading…';await new Promise(requestAnimationFrame);const buffer=await file.arrayBuffer(),result=window.rovModel.loadBuffer(buffer,file.name);modelButton.textContent='Replace STL';toast(`Loaded ${result.name} • ${result.triangles.toLocaleString()} triangles`);if(bridgeReady){const dataUrl=await new Promise((resolve,reject)=>{const reader=new FileReader();reader.onload=()=>resolve(reader.result);reader.onerror=()=>reject(reader.error);reader.readAsDataURL(file)});api('save_model',file.name,String(dataUrl).split(',')[1]).catch(error=>toast(`Model loaded, but could not be saved: ${error.message}`,true))}}catch(error){toast(`STL import failed: ${error.message}`,true)}finally{modelButton.disabled=false;if(modelButton.textContent==='Loading…')modelButton.textContent=old}}; -$('#model-style').onclick=e=>{const edges=window.rovModel.toggleStyle();e.currentTarget.textContent=edges?'Shaded':'Edges'}; $('#refresh').onclick=async()=>populatePorts(await api('refresh_ports'));$('#connect').onclick=async()=>{try{if(connected)await api('disconnect');else await api('connect',$('#port').value,+$('#baud').value,+$('#rate').value)}catch(e){toast(e.message,true)}}; $('#manual').onclick=()=>api('command','manual').catch(e=>toast(e.message,true));$('#arm').onclick=()=>openModal('arm-dialog');$('#confirm-arm').onclick=()=>{api('command','arm').catch(e=>toast(e.message,true));$('#arm-dialog').close()};$('#disarm').onclick=()=>api('command','disarm').catch(e=>toast(e.message,true));$('#logs').onclick=()=>api('open_logs'); $('#gain').oninput=e=>{$('#gain-value').textContent=`${e.target.value}%`;api('set_gain',+e.target.value)}; diff --git a/SUB Control/web/index.html b/SUB Control/web/index.html index ba232fc..c7a86fa 100644 --- a/SUB Control/web/index.html +++ b/SUB Control/web/index.html @@ -45,7 +45,6 @@
-
diff --git a/SUB Control/web/model-viewer.js b/SUB Control/web/model-viewer.js index 6c67954..4ebcf3e 100644 --- a/SUB Control/web/model-viewer.js +++ b/SUB Control/web/model-viewer.js @@ -2,8 +2,8 @@ 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; + let vertices = null, frame = 0; + const renderScale = .62; function parseStl(buffer) { const view = new DataView(buffer); @@ -37,39 +37,44 @@ 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)); + 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(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;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()} + 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,cx=width/2,cy=height/2; + maskCtx.fillStyle='#fff'; + for(let i=0;i{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(); + requestDraw(); return {name,triangles:vertices.length/9}; }, load(payload) { @@ -77,7 +82,6 @@ const binary=atob(payload.data),bytes=new Uint8Array(binary.length); for(let i=0;i