diff --git a/SUB Control/web/app.js b/SUB Control/web/app.js index 21098b1..4e73fdd 100644 --- a/SUB Control/web/app.js +++ b/SUB Control/web/app.js @@ -25,7 +25,7 @@ addEventListener('blur',()=>{keys.clear();if(!joyDrag)easeJoy();if(!heaveDrag)ea function openModal(id){document.getElementById(id)?.showModal()}$$('[data-open]').forEach(b=>b.onclick=()=>openModal(b.dataset.open));$$('[data-close]').forEach(b=>b.onclick=()=>b.closest('dialog').close()); $('#window-min').onclick=()=>api('minimize_window');$('#window-max').onclick=()=>api('toggle_maximize_window');$('#window-close').onclick=()=>api('close_window');$('.window-drag').ondblclick=()=>api('toggle_maximize_window'); -$('#load-model').onclick=async()=>{try{const model=await api('choose_model');if(model)window.rovModel.load(model)}catch(e){toast(e.message,true)}};$('#model-style').onclick=e=>{const edges=window.rovModel.toggleStyle();e.currentTarget.textContent=edges?'Shaded':'Edges'}; +$('#load-model').onclick=async e=>{const button=e.currentTarget,old=button.textContent;try{button.disabled=true;button.textContent='Loading…';const model=await api('choose_model');if(!model)return;await new Promise(requestAnimationFrame);const result=window.rovModel.load(model);button.textContent='Replace STL';toast(`Loaded ${result.name} • ${result.triangles.toLocaleString()} triangles`)}catch(error){toast(`STL import failed: ${error.message}`,true)}finally{button.disabled=false;if(button.textContent==='Loading…')button.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)}; @@ -35,6 +35,6 @@ function renderMotors(servos){const signature=JSON.stringify(servos);if(signatur function render(s){connected=s.connected;$('#connect').textContent=connected?'Disconnect':'Connect';$('#link').textContent=s.link;$('.link-state').classList.toggle('online',s.link==='LINK OK');$('#vehicle').textContent=s.vehicle;$('#status').textContent=s.status;const b=s.battery;if(b){$('#battery-title').textContent=`${b.voltage.toFixed(2)} V`;$('#battery-percent').textContent=`${Math.round(b.percent)}% • ${b.cells}S`;$('#battery-fill').style.width=`${b.percent}%`;$('#battery-fill').style.background=b.low?'var(--red)':'var(--acid)'}else{$('#battery-title').textContent='—';$('#battery-percent').textContent='Waiting';$('#battery-fill').style.width='0'}renderMotors(s.servos)} async function poll(){if(!bridgeReady)return;try{render(await api('poll'))}catch(e){}setTimeout(poll,250)} let booted=false; -async function boot(){if(booted||!window.pywebview?.api)return;booted=true;bridgeReady=true;try{const init=await api('initial_state');batteryConfig=init.battery_config;populatePorts(init.ports);fillBatteryForm();render(init.state);const saved=await api('saved_model');if(saved)window.rovModel.load(saved);poll();setInterval(async()=>{if(!connected)try{populatePorts(await api('refresh_ports'))}catch(_){}},2000)}catch(e){booted=false;bridgeReady=false;toast(`Python bridge: ${e.message}`,true);setTimeout(waitForBridge,500)}} +async function boot(){if(booted||!window.pywebview?.api)return;booted=true;bridgeReady=true;try{const init=await api('initial_state');batteryConfig=init.battery_config;populatePorts(init.ports);fillBatteryForm();render(init.state);const saved=await api('saved_model');if(saved){try{window.rovModel.load(saved);$('#load-model').textContent='Replace STL'}catch(error){toast(`Saved STL failed: ${error.message}`,true)}}poll();setInterval(async()=>{if(!connected)try{populatePorts(await api('refresh_ports'))}catch(_){}},2000)}catch(e){booted=false;bridgeReady=false;toast(`Python bridge: ${e.message}`,true);setTimeout(waitForBridge,500)}} function waitForBridge(){if(window.pywebview?.api)boot();else setTimeout(waitForBridge,50)} addEventListener('pywebviewready',boot);if(document.readyState==='loading')addEventListener('DOMContentLoaded',waitForBridge);else waitForBridge(); diff --git a/SUB Control/web/model-viewer.js b/SUB Control/web/model-viewer.js index 7f0fa42..3cb1692 100644 --- a/SUB Control/web/model-viewer.js +++ b/SUB Control/web/model-viewer.js @@ -66,13 +66,14 @@ window.rovModel = { load(payload) { - try { - const bytes=Uint8Array.from(atob(payload.data),c=>c.charCodeAt(0)); - vertices=parseStl(bytes.buffer);stage.classList.add('model-loaded'); - document.querySelector('#model-style').disabled=false; - document.querySelector('#model-file-name').textContent=payload.name; - yaw=-.55;pitch=-.42;zoom=1;requestDraw(); - } catch (error) { window.showToast?.(error.message,true); } + 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