From ed205945d9866941ee9d633131ed460dd1693ac0 Mon Sep 17 00:00:00 2001 From: PitresAli Date: Tue, 25 Aug 2026 22:55:23 +0200 Subject: [PATCH] Restore orbit and zoom for silhouette model --- SUB Control/README.md | 7 ++++--- SUB Control/web/model-viewer.js | 25 ++++++++++++++++++++----- SUB Control/web/styles.css | 2 +- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/SUB Control/README.md b/SUB Control/README.md index 03d9b92..202d282 100644 --- a/SUB Control/README.md +++ b/SUB Control/README.md @@ -12,9 +12,10 @@ 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** 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. +The vehicle rail includes a local STL viewer: choose **Load STL** to start with +an orthographic top-view silhouette, then drag to orbit or use the mouse wheel +to zoom. 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/model-viewer.js b/SUB Control/web/model-viewer.js index 4ebcf3e..39c6625 100644 --- a/SUB Control/web/model-viewer.js +++ b/SUB Control/web/model-viewer.js @@ -2,7 +2,8 @@ const canvas = document.querySelector('#model-canvas'); const stage = document.querySelector('.rov-stage'); const ctx = canvas.getContext('2d'); - let vertices = null, frame = 0; + let vertices = null, frame = 0, yaw = 0, pitch = 0, zoom = 1; + let dragging = false, lastX = 0, lastY = 0; const renderScale = .62; function parseStl(buffer) { @@ -45,13 +46,21 @@ 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; + 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'; for(let i=0;i{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}; }, diff --git a/SUB Control/web/styles.css b/SUB Control/web/styles.css index 7b6d6b5..90852a8 100644 --- a/SUB Control/web/styles.css +++ b/SUB Control/web/styles.css @@ -9,4 +9,4 @@ .modal{width:min(620px,calc(100vw - 40px));padding:27px;border:1px solid #ffffff24;border-radius:24px;background:#f7f8f5;color:#1c2021;box-shadow:0 35px 100px #000b}.modal::backdrop{background:#071012b5;backdrop-filter:blur(8px)}.modal h2{margin:5px 0 17px;font-size:25px}.modal-close{position:absolute;top:18px;right:18px;width:34px;height:34px;border:0;border-radius:50%;background:#e8ebe6;color:#555;cursor:pointer;font-size:20px}.form-grid{display:grid;gap:13px}.form-grid.two{grid-template-columns:1fr 1fr}.form-grid.three{grid-template-columns:repeat(3,1fr)}.form-grid label{display:flex;flex-direction:column;gap:6px;color:#737976;font-size:10px}.form-grid input,.form-grid select{width:100%;height:40px;padding:0 11px}.note{color:#7a807d;font-size:11px;line-height:1.5}.modal-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:22px}.modal-actions.wide{grid-column:1/-1}.motor-modal{width:min(760px,calc(100vw - 40px))}.motor-head,.motor-row{display:grid;grid-template-columns:70px 1fr 110px 94px;align-items:center;gap:9px}.motor-head{padding:8px 10px;color:#969b99;font-size:9px;letter-spacing:.1em}.motor-list{max-height:390px;overflow:auto}.motor-row{min-height:47px;padding:5px 10px;border-top:1px solid #e3e6e1}.motor-row select{height:34px;padding:0 9px}.motor-row .button{height:32px;padding:0 10px}.toast{position:fixed;z-index:20;left:50%;bottom:28px;padding:11px 16px;border-radius:12px;background:#202725;color:#fff;box-shadow:0 10px 35px #0006;transform:translate(-50%,25px);opacity:0;pointer-events:none;transition:.2s}.toast.show{transform:translate(-50%,0);opacity:1}.toast.error{background:#77352f} @media(max-width:950px){.app-shell{grid-template-columns:225px minmax(0,1fr)}.vehicle-rail{padding-left:18px;padding-right:18px}.drive-grid{grid-template-columns:minmax(330px,1fr) 190px}.workspace{padding-left:22px;padding-right:22px}.connection select{width:120px}}@media(max-height:700px){.rov-stage{height:240px}.drive-grid{padding:12px 0}.joystick{--size:min(32vh,250px)}.workspace{padding-top:18px}} .control-card{border-radius:14px}.command-bar{border-radius:12px}.button,.icon-button,.connection select,select,input{border-radius:9px}.modal{border-radius:16px}.brand-mark{border-radius:7px} -.rov-stage #model-canvas{display:none;position:absolute;inset:0;width:100%;height:100%;z-index:3;image-rendering:auto}.rov-stage.model-loaded #default-model{display:none}.rov-stage.model-loaded #model-canvas{display:block}.model-tools{position:absolute;z-index:5;left:50%;bottom:5px;display:flex;gap:5px;transform:translateX(-50%);opacity:.72;transition:opacity .18s}.rov-stage:hover .model-tools{opacity:1}.model-tools button{height:27px;padding:0 9px;border:1px solid #ffffff18;border-radius:7px;background:#20282bcc;color:#aab3b5;font-size:8px;letter-spacing:.06em;cursor:pointer;backdrop-filter:blur(7px)}.model-tools button:hover{color:#fff;background:#303a3e}.model-tools button:disabled{opacity:.35;cursor:default} +.rov-stage #model-canvas{display:none;position:absolute;inset:0;width:100%;height:100%;z-index:3;image-rendering:auto;cursor:grab;touch-action:none}.rov-stage #model-canvas:active{cursor:grabbing}.rov-stage.model-loaded #default-model{display:none}.rov-stage.model-loaded #model-canvas{display:block}.model-tools{position:absolute;z-index:5;left:50%;bottom:5px;display:flex;gap:5px;transform:translateX(-50%);opacity:.72;transition:opacity .18s}.rov-stage:hover .model-tools{opacity:1}.model-tools button{height:27px;padding:0 9px;border:1px solid #ffffff18;border-radius:7px;background:#20282bcc;color:#aab3b5;font-size:8px;letter-spacing:.06em;cursor:pointer;backdrop-filter:blur(7px)}.model-tools button:hover{color:#fff;background:#303a3e}.model-tools button:disabled{opacity:.35;cursor:default}