Restore orbit and zoom for silhouette model

This commit is contained in:
2026-08-25 22:55:23 +02:00
parent 8fed9dc61e
commit ed205945d9
3 changed files with 25 additions and 9 deletions
+4 -3
View File
@@ -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. 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 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 The vehicle rail includes a local STL viewer: choose **Load STL** to start with
fixed orthographic top-view silhouette. A cached STL copy is stored locally an orthographic top-view silhouette, then drag to orbit or use the mouse wheel
under the ignored `runtime/` directory so the model can be restored on the next launch. 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 STEP/STP CAD solids must first be tessellated/exported as STL because the
embedded renderer intentionally does not include a full CAD kernel. embedded renderer intentionally does not include a full CAD kernel.
+20 -5
View File
@@ -2,7 +2,8 @@
const canvas = document.querySelector('#model-canvas'); const canvas = document.querySelector('#model-canvas');
const stage = document.querySelector('.rov-stage'); const stage = document.querySelector('.rov-stage');
const ctx = canvas.getContext('2d'); 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; const renderScale = .62;
function parseStl(buffer) { function parseStl(buffer) {
@@ -45,13 +46,21 @@
ctx.setTransform(1,0,0,1,0,0);ctx.clearRect(0,0,width,height); ctx.setTransform(1,0,0,1,0,0);ctx.clearRect(0,0,width,height);
const mask=document.createElement('canvas'),maskCtx=mask.getContext('2d'); const mask=document.createElement('canvas'),maskCtx=mask.getContext('2d');
mask.width=width;mask.height=height; 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'; maskCtx.fillStyle='#fff';
for(let i=0;i<vertices.length;i+=9){ 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.beginPath(); maskCtx.beginPath();
maskCtx.moveTo(cx+vertices[i]*size,cy-vertices[i+1]*size); maskCtx.moveTo(cx+a[0]*size,cy-a[1]*size);
maskCtx.lineTo(cx+vertices[i+3]*size,cy-vertices[i+4]*size); maskCtx.lineTo(cx+b[0]*size,cy-b[1]*size);
maskCtx.lineTo(cx+vertices[i+6]*size,cy-vertices[i+7]*size); maskCtx.lineTo(cx+c[0]*size,cy-c[1]*size);
maskCtx.closePath();maskCtx.fill(); maskCtx.closePath();maskCtx.fill();
} }
// A filtered copy creates only the exterior contour; triangle boundaries // A filtered copy creates only the exterior contour; triangle boundaries
@@ -68,12 +77,18 @@
ctx.drawImage(surface,0,0); ctx.drawImage(surface,0,0);
} }
function requestDraw(){if(!frame)frame=requestAnimationFrame(draw)} 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); addEventListener('resize',requestDraw);
window.rovModel = { window.rovModel = {
loadBuffer(buffer, name) { loadBuffer(buffer, name) {
vertices=parseStl(buffer);stage.classList.add('model-loaded'); vertices=parseStl(buffer);stage.classList.add('model-loaded');
document.querySelector('#model-file-name').textContent=name; document.querySelector('#model-file-name').textContent=name;
yaw=0;pitch=0;zoom=1;
requestDraw(); requestDraw();
return {name,triangles:vertices.length/9}; return {name,triangles:vertices.length/9};
}, },
+1 -1
View File
@@ -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} .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}} @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} .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}