Update UI layout, window controls, and vehicle styling
This commit is contained in:
+269
-22
@@ -1,42 +1,289 @@
|
||||
const $=s=>document.querySelector(s), $$=s=>[...document.querySelectorAll(s)];
|
||||
const control={forward:0,turn:0,vertical:0}, keys=new Set(); let bridgeReady=false, connected=false, batteryConfig={}, servoSignature='', lastAxisSend=0, axisTimer=0;
|
||||
const api=(name,...args)=>window.pywebview.api[name](...args);
|
||||
addEventListener('pointermove',e=>{const button=e.target.closest?.('button:not(.battery-card), .stick');if(!button)return;const rect=button.getBoundingClientRect();button.style.setProperty('--glow-x',`${e.clientX-rect.left}px`);button.style.setProperty('--glow-y',`${e.clientY-rect.top}px`)});
|
||||
document.addEventListener('click',e=>{const btn=e.target.closest('.num-step');if(!btn)return;const pill=btn.closest('.number-pill');if(!pill)return;const input=pill.querySelector('input');if(!input)return;const step=+input.step||1,min=input.min!==''?+input.min:-Infinity,max=input.max!==''?+input.max:Infinity;let val=+input.value||0;if(btn.classList.contains('down'))val=Math.max(min,+(val-step).toFixed(4));else val=Math.min(max,+(val+step).toFixed(4));input.value=val;input.dispatchEvent(new Event('input',{bubbles:true}));input.dispatchEvent(new Event('change',{bubbles:true}))});
|
||||
function toast(message,error=false){const el=$('#toast');el.textContent=message;el.className=`toast show${error?' error':''}`;clearTimeout(toast.t);toast.t=setTimeout(()=>el.className='toast',2600)}
|
||||
window.showToast=toast;
|
||||
function populatePorts(ports){const p=$('#port'),old=p.value;p.innerHTML='';if(!ports.length)p.add(new Option('No COM ports',''));ports.forEach(x=>p.add(new Option(x,x)));if(ports.includes(old))p.value=old}
|
||||
function sendAxes(){if(!bridgeReady)return;const transmit=()=>{lastAxisSend=performance.now();axisTimer=0;api('set_axes',control.forward,control.turn,control.vertical).catch(e=>toast(e.message,true))},wait=50-(performance.now()-lastAxisSend);if(wait<=0)transmit();else{clearTimeout(axisTimer);axisTimer=setTimeout(transmit,wait)}}
|
||||
function updateReadouts(){ $('#surge-readout').textContent=Math.round(control.forward*100);$('#yaw-readout').textContent=Math.round(control.turn*100);$('#heave-readout').textContent=Math.round(control.vertical*100)}
|
||||
|
||||
const joy=$('#joystick'),stick=$('#stick');let joyDrag=false,returnAnim=0;
|
||||
function setJoy(x,y,send=true){const r=joy.clientWidth*.34,n=Math.hypot(x,y);if(n>1){x/=n;y/=n}control.turn=x;control.forward=-y;stick.style.transform=`translate(calc(-50% + ${x*r}px),calc(-50% + ${y*r}px))`;updateReadouts();if(send)sendAxes()}
|
||||
function joyEvent(e){const b=joy.getBoundingClientRect();setJoy((e.clientX-b.left-b.width/2)/(b.width*.34),(e.clientY-b.top-b.height/2)/(b.height*.34))}
|
||||
joy.addEventListener('pointerdown',e=>{joyDrag=true;cancelAnimationFrame(returnAnim);joy.setPointerCapture(e.pointerId);joyEvent(e)});joy.addEventListener('pointermove',e=>{if(joyDrag)joyEvent(e)});joy.addEventListener('pointerup',()=>{joyDrag=false;easeJoy()});
|
||||
function easeJoy(){const sx=control.turn,sy=-control.forward,start=performance.now();function frame(now){const p=Math.min(1,(now-start)/400),smooth=p*p*(3-2*p);setJoy(sx*(1-smooth),sy*(1-smooth));if(p<1)returnAnim=requestAnimationFrame(frame)}returnAnim=requestAnimationFrame(frame)}
|
||||
window.setJoyControl=(x,y)=>{control.turn=x;control.forward=-y;updateReadouts();sendAxes()};
|
||||
function releaseHandle(el){el.classList.remove('pressed');el.classList.add('released');setTimeout(()=>el.classList.remove('released'),520)}
|
||||
|
||||
const heave=$('#heave'),hknob=$('#heave-knob'),hfill=$('#heave-fill');let heaveDrag=false,heaveAnim=0;
|
||||
function setHeave(v,send=true){v=Math.max(-1,Math.min(1,v));control.vertical=v;hknob.style.top=`${50-v*43}%`;hfill.style.top=v>=0?`${50-v*43}%`:'50%';hfill.style.height=`${Math.abs(v)*43}%`;updateReadouts();if(send)sendAxes()}
|
||||
function heaveEvent(e){const b=heave.getBoundingClientRect();setHeave((b.top+b.height/2-e.clientY)/(b.height*.43))}
|
||||
heave.addEventListener('pointerdown',e=>{heaveDrag=true;cancelAnimationFrame(heaveAnim);heave.setPointerCapture(e.pointerId);heaveEvent(e)});heave.addEventListener('pointermove',e=>{if(heaveDrag)heaveEvent(e)});heave.addEventListener('pointerup',()=>{heaveDrag=false;easeHeave()});
|
||||
heave.addEventListener('pointerdown',e=>{heaveDrag=true;hknob.classList.remove('released');hknob.classList.add('pressed');cancelAnimationFrame(heaveAnim);heave.setPointerCapture(e.pointerId);heaveEvent(e)});heave.addEventListener('pointermove',e=>{if(heaveDrag)heaveEvent(e)});heave.addEventListener('pointerup',()=>{heaveDrag=false;releaseHandle(hknob);easeHeave()});heave.addEventListener('pointercancel',()=>{heaveDrag=false;releaseHandle(hknob);easeHeave()});
|
||||
function easeHeave(){const sv=control.vertical,start=performance.now();function frame(now){const p=Math.min(1,(now-start)/400),smooth=p*p*(3-2*p);setHeave(sv*(1-smooth));if(p<1)heaveAnim=requestAnimationFrame(frame)}heaveAnim=requestAnimationFrame(frame)}
|
||||
|
||||
function applyKeys(){const turn=(keys.has('d')||keys.has('e')?1:0)-(keys.has('a')||keys.has('q')?1:0),forward=(keys.has('w')?1:0)-(keys.has('s')?1:0),vertical=(keys.has('r')?1:0)-(keys.has('f')?1:0);if(turn||forward){cancelAnimationFrame(returnAnim);setJoy(turn,-forward)}else if(!joyDrag)easeJoy();if(vertical){cancelAnimationFrame(heaveAnim);setHeave(vertical)}else if(!heaveDrag)easeHeave()}
|
||||
addEventListener('keydown',e=>{if(/^(INPUT|SELECT)$/.test(e.target.tagName))return;const k=e.key.toLowerCase();if('wasdqerf'.includes(k)&&!keys.has(k)){keys.add(k);applyKeys()}});addEventListener('keyup',e=>{const k=e.key.toLowerCase();if(keys.delete(k))applyKeys()});
|
||||
addEventListener('blur',()=>{keys.clear();if(!joyDrag)easeJoy();if(!heaveDrag)easeHeave()});
|
||||
function applyKeys(){const turn=(keys.has('d')||keys.has('e')?1:0)-(keys.has('a')||keys.has('q')?1:0),forward=(keys.has('w')?1:0)-(keys.has('s')?1:0),vertical=(keys.has('r')?1:0)-(keys.has('f')?1:0);if(turn||forward){window.joystick3D?.setPos(turn,-forward)}else{window.joystick3D?.easeBack()}if(vertical){cancelAnimationFrame(heaveAnim);setHeave(vertical)}else if(!heaveDrag)easeHeave()}
|
||||
addEventListener('keydown',e=>{if(/^(INPUT|SELECT)$/.test(e.target.tagName))return;if(e.key==='F2'){e.preventDefault();if(!e.repeat)startArmHold();return}const shortcuts={F1:'#manual',F3:'#disarm'};if(shortcuts[e.key]){e.preventDefault();if(!e.repeat)$(shortcuts[e.key]).click();return}const k=e.key.toLowerCase();if('wasdqerf'.includes(k)&&!keys.has(k)){keys.add(k);applyKeys()}});addEventListener('keyup',e=>{if(e.key==='F2'){e.preventDefault();stopArmHold(true);return}const k=e.key.toLowerCase();if(keys.delete(k))applyKeys()});
|
||||
addEventListener('blur',()=>{keys.clear();window.joystick3D?.easeBack();if(!heaveDrag)easeHeave()});
|
||||
|
||||
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');
|
||||
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}};
|
||||
$('#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)};
|
||||
$('#battery-form').onsubmit=async e=>{e.preventDefault();const data=Object.fromEntries(new FormData(e.target));try{batteryConfig=await api('save_battery',data);$('#battery-modal').close();toast('Battery settings saved')}catch(err){toast(err.message,true)}};
|
||||
function openModal(id){
|
||||
const el=typeof id==='string'?document.getElementById(id):id;
|
||||
if(!el)return;
|
||||
el.classList.remove('closing');
|
||||
if(!el.open)el.showModal();
|
||||
}
|
||||
function closeModal(dialog){
|
||||
if(!dialog||!dialog.open||dialog.classList.contains('closing'))return;
|
||||
dialog.classList.add('closing');
|
||||
let done=false;
|
||||
const finish=()=>{
|
||||
if(done)return;
|
||||
done=true;
|
||||
dialog.removeEventListener('animationend',finish);
|
||||
dialog.classList.remove('closing');
|
||||
dialog.close();
|
||||
};
|
||||
dialog.addEventListener('animationend',finish,{once:true});
|
||||
setTimeout(finish,300);
|
||||
}
|
||||
$$('[data-open]').forEach(b=>b.onclick=()=>openModal(b.dataset.open));
|
||||
$$('[data-close]').forEach(b=>b.onclick=()=>closeModal(b.closest('dialog')));
|
||||
$$('dialog').forEach(d=>{
|
||||
d.addEventListener('cancel',e=>{e.preventDefault();closeModal(d)});
|
||||
d.addEventListener('click',e=>{
|
||||
const r=d.getBoundingClientRect();
|
||||
if(e.clientX<r.left||e.clientX>r.right||e.clientY<r.top||e.clientY>r.bottom)closeModal(d);
|
||||
});
|
||||
});
|
||||
$('#window-min').onclick=()=>api('minimize_window');$('#window-max').onclick=async()=>{const max=await api('toggle_maximize_window');document.body.classList.toggle('maximized',!!max)};$('#window-close').onclick=()=>{if(document.body.classList.contains('window-exiting'))return;document.body.classList.add('window-exiting');setTimeout(()=>api('close_window'),360)};$('.topbar').ondblclick=async e=>{if(!e.target.closest('button,select,input')){const max=await api('toggle_maximize_window');document.body.classList.toggle('maximized',!!max)}};
|
||||
const modelInput=$('#model-input'), modelButton=$('#load-model'), rovStage=$('.rov-stage');
|
||||
|
||||
async function handleStlFile(file){
|
||||
if(!file)return;
|
||||
try{
|
||||
if(!file.name.toLowerCase().endsWith('.stl'))throw new Error('Choose an STL mesh (*.stl)');
|
||||
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…';
|
||||
const buffer=await file.arrayBuffer();
|
||||
const result=window.rovModel.loadBuffer(buffer,file.name);
|
||||
modelButton.textContent='Replace STL';
|
||||
toast(`Loaded ${result.name} • ${result.triangles.toLocaleString()} triangles`);
|
||||
if(bridgeReady&&window.pywebview?.api?.save_model){
|
||||
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(e=>console.warn(e));
|
||||
}
|
||||
}catch(error){
|
||||
toast(`STL import failed: ${error.message}`,true);
|
||||
}finally{
|
||||
modelButton.disabled=false;
|
||||
if(modelButton.textContent==='Loading…')modelButton.textContent='Load STL';
|
||||
}
|
||||
}
|
||||
|
||||
modelButton.onclick=async()=>{
|
||||
let loaded=false;
|
||||
if(window.pywebview?.api?.choose_model){
|
||||
try{
|
||||
const model=await api('choose_model');
|
||||
if(model&&model.data){
|
||||
const res=window.rovModel.load(model);
|
||||
modelButton.textContent='Replace STL';
|
||||
toast(`Loaded ${res.name} • ${res.triangles.toLocaleString()} triangles`);
|
||||
loaded=true;
|
||||
}
|
||||
}catch(err){
|
||||
console.warn('choose_model error:', err);
|
||||
}
|
||||
}
|
||||
if(!loaded&&modelInput){
|
||||
modelInput.value='';
|
||||
modelInput.click();
|
||||
}
|
||||
};
|
||||
|
||||
if(modelInput){
|
||||
modelInput.onchange=()=>{
|
||||
const file=modelInput.files?.[0];
|
||||
if(file)handleStlFile(file);
|
||||
};
|
||||
}
|
||||
|
||||
if(rovStage){
|
||||
rovStage.addEventListener('dragover',e=>{e.preventDefault();e.stopPropagation();rovStage.classList.add('drag-over')});
|
||||
rovStage.addEventListener('dragleave',e=>{e.preventDefault();e.stopPropagation();rovStage.classList.remove('drag-over')});
|
||||
rovStage.addEventListener('drop',e=>{
|
||||
e.preventDefault();e.stopPropagation();rovStage.classList.remove('drag-over');
|
||||
const file=e.dataTransfer?.files?.[0];
|
||||
if(file)handleStlFile(file);
|
||||
});
|
||||
}
|
||||
$('#refresh').onclick=async e=>{const button=e.currentTarget;button.classList.remove('spinning');void button.offsetWidth;button.classList.add('spinning');setTimeout(()=>button.classList.remove('spinning'),650);if(bridgeReady)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=async()=>{try{await api('command','manual');toast('Manual mode command sent')}catch(e){toast(e.message,true)}};
|
||||
const armButton=$('#arm');
|
||||
let armHoldTimer=null, armStart=0, armCharged=false;
|
||||
const ARM_HOLD_MS=1000;
|
||||
|
||||
function updateArmProgress(){
|
||||
if(!armStart)return;
|
||||
const elapsed=performance.now()-armStart;
|
||||
const progress=Math.min(1,elapsed/ARM_HOLD_MS);
|
||||
armButton.style.setProperty('--arm-progress',`${progress*100}%`);
|
||||
if(progress>=1){
|
||||
armCharged=true;
|
||||
stopArmHold(false);
|
||||
armButton.classList.add('charged');
|
||||
setTimeout(()=>armButton.classList.remove('charged'),600);
|
||||
api('command','arm').then(()=>toast('Vehicle armed')).catch(e=>toast(e.message,true));
|
||||
return;
|
||||
}
|
||||
armHoldTimer=requestAnimationFrame(updateArmProgress);
|
||||
}
|
||||
|
||||
function startArmHold(e){
|
||||
if(armStart||armCharged)return;
|
||||
if(e&&e.button!==undefined&&e.button!==0)return;
|
||||
armStart=performance.now();
|
||||
armCharged=false;
|
||||
armButton.classList.add('charging');
|
||||
armButton.style.setProperty('--arm-progress','0%');
|
||||
armHoldTimer=requestAnimationFrame(updateArmProgress);
|
||||
}
|
||||
|
||||
function stopArmHold(wasCancelled=true){
|
||||
if(!armStart)return;
|
||||
const elapsed=performance.now()-armStart;
|
||||
cancelAnimationFrame(armHoldTimer);
|
||||
armHoldTimer=null;
|
||||
armStart=0;
|
||||
armButton.classList.remove('charging');
|
||||
armButton.style.setProperty('--arm-progress','0%');
|
||||
if(wasCancelled&&!armCharged&&elapsed<ARM_HOLD_MS&&elapsed>70){
|
||||
toast('Hold for 1s to Arm vehicle');
|
||||
}
|
||||
armCharged=false;
|
||||
}
|
||||
|
||||
armButton.addEventListener('pointerdown',startArmHold);
|
||||
armButton.addEventListener('pointerup',()=>stopArmHold(true));
|
||||
armButton.addEventListener('pointerleave',()=>stopArmHold(true));
|
||||
armButton.addEventListener('pointercancel',()=>stopArmHold(true));
|
||||
armButton.addEventListener('contextmenu',e=>e.preventDefault());
|
||||
|
||||
$('#disarm').onclick=async()=>{try{await api('command','disarm');toast('Disarm command sent')}catch(e){toast(e.message,true)}};
|
||||
$('#logs').onclick=()=>api('open_logs');
|
||||
function setGain(value){const gain=$('#gain');gain.value=Math.max(+gain.min,Math.min(+gain.max,value));$('#gain-value').textContent=`${gain.value}%`;if(bridgeReady)api('set_gain',+gain.value)}
|
||||
$('#gain').oninput=e=>setGain(+e.target.value);$('#gain-down').onclick=()=>setGain(+$('#gain').value-5);$('#gain-up').onclick=()=>setGain(+$('#gain').value+5);
|
||||
$('#battery-form').onsubmit=async e=>{e.preventDefault();const data=Object.fromEntries(new FormData(e.target));try{batteryConfig=await api('save_battery',data);closeModal($('#battery-modal'));toast('Battery settings saved')}catch(err){toast(err.message,true)}};
|
||||
function fillBatteryForm(){for(const [k,v] of Object.entries(batteryConfig)){const el=$(`#battery-form [name=${k}]`);if(el)el.value=v}}
|
||||
function renderMotors(servos){const signature=JSON.stringify(servos);if(signature===servoSignature)return;servoSignature=signature;const list=$('#motor-list');list.innerHTML='';for(let i=1;i<=16;i++){const s=servos[String(i)]||{},row=document.createElement('div');row.className='motor-row';const opts=['<option value="0">Unassigned</option>',...Array.from({length:12},(_,n)=>`<option value="${n+1}" ${s.motor===n+1?'selected':''}>Motor ${n+1}</option>`)].join('');row.innerHTML=`<strong>SERVO${i}</strong><select aria-label="SERVO${i} function">${opts}</select><span>${s.direction==null?'—':s.direction===-1?'Reversed':'Normal'}</span><button class="button subtle">Reverse</button>`;row.querySelector('select').onchange=e=>api('set_servo_function',i,+e.target.value).catch(x=>toast(x.message,true));row.querySelector('button').onclick=()=>api('reverse_motor',i).catch(x=>toast(x.message,true));list.append(row)}}
|
||||
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)}
|
||||
function render(s){connected=s.connected;$('#connect').textContent=connected?'Disconnect':'Connect';const linkText=s.link==='LINK OK'?'connected':(s.link?s.link.toLowerCase():'disconnected');$('#link').textContent=linkText;$('.link-state').classList.toggle('online',s.link==='LINK OK'||s.link==='connected');$('#vehicle').textContent=s.vehicle?s.vehicle.toLowerCase():'no heartbeat';$('#status').textContent=s.status?s.status.toLowerCase():'ready';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 modelLoadPromise=null;
|
||||
async function loadDefaultModel(){
|
||||
if(modelLoadPromise)return modelLoadPromise;
|
||||
modelLoadPromise=(async()=>{
|
||||
const statusEl=$('#model-status');
|
||||
|
||||
// 1. Direct high-speed HTTP fetch (instant, ~20ms)
|
||||
for(const url of [
|
||||
'/model.stl',
|
||||
'/model',
|
||||
'/runtime/vehicle_model.stl',
|
||||
'/assets/Main%20Assembly%20V1%20(2).stl',
|
||||
'../runtime/vehicle_model.stl',
|
||||
'../assets/Main%20Assembly%20V1%20(2).stl'
|
||||
]){
|
||||
try{
|
||||
const res=await fetch(url);
|
||||
if(res.ok){
|
||||
if(statusEl)statusEl.textContent='Loading mesh…';
|
||||
const buf=await res.arrayBuffer();
|
||||
if(window.rovModel){
|
||||
const result=window.rovModel.loadBuffer(buf,'Main Assembly V1 (2).stl');
|
||||
$('#load-model').textContent='Replace STL';
|
||||
console.log('[Direct Fetch] Model loaded:', result);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}catch(err){
|
||||
console.warn(`[Direct Fetch] ${url} failed:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Python Bridge Chunked Stream (fallback)
|
||||
if(window.pywebview?.api?.get_saved_model_info){
|
||||
try{
|
||||
const info=await api('get_saved_model_info');
|
||||
if(info&&info.size){
|
||||
const total=info.size, chunk=262144;
|
||||
const bytes=new Uint8Array(total);
|
||||
let loaded=0;
|
||||
if(statusEl){
|
||||
statusEl.className='model-status';
|
||||
statusEl.textContent=`Streaming ${info.name} (0%)…`;
|
||||
}
|
||||
while(loaded<total){
|
||||
const reqSize=Math.min(chunk,total-loaded);
|
||||
const resp=await api('read_saved_model_chunk',loaded,reqSize);
|
||||
const part=atob(resp.data);
|
||||
for(let i=0;i<part.length;i++)bytes[loaded+i]=part.charCodeAt(i);
|
||||
loaded+=part.length;
|
||||
const pct=Math.round((loaded/total)*100);
|
||||
if(statusEl)statusEl.textContent=`Streaming ${pct}% (${(loaded/(1024*1024)).toFixed(1)}MB)…`;
|
||||
if(loaded%(chunk*4)===0)await new Promise(r=>setTimeout(r,0));
|
||||
}
|
||||
if(window.rovModel){
|
||||
const res=window.rovModel.loadBuffer(bytes.buffer,info.name);
|
||||
$('#load-model').textContent='Replace STL';
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}catch(e){
|
||||
console.warn('[Bridge Stream] Failed:', e);
|
||||
if(statusEl){
|
||||
statusEl.className='model-status error';
|
||||
statusEl.textContent=`Bridge Error: ${e.message}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})();
|
||||
return modelLoadPromise;
|
||||
}
|
||||
|
||||
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){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();
|
||||
async function boot(){
|
||||
if(booted||!window.pywebview?.api)return;
|
||||
booted=true;
|
||||
bridgeReady=true;
|
||||
loadDefaultModel();
|
||||
try{
|
||||
const init=await api('initial_state');
|
||||
batteryConfig=init.battery_config;
|
||||
populatePorts(init.ports);
|
||||
fillBatteryForm();
|
||||
render(init.state);
|
||||
poll();
|
||||
setInterval(async()=>{
|
||||
if(!connected){
|
||||
try{populatePorts(await api('refresh_ports'))}catch(_){}
|
||||
}
|
||||
},2000);
|
||||
}catch(e){
|
||||
toast(`Python bridge: ${e.message}`,true);
|
||||
}
|
||||
}
|
||||
function waitForBridge(){
|
||||
if(window.pywebview?.api)boot();
|
||||
else setTimeout(waitForBridge,50);
|
||||
}
|
||||
addEventListener('pywebviewready',boot);
|
||||
if(document.readyState==='loading'){
|
||||
addEventListener('DOMContentLoaded',()=>{loadDefaultModel();waitForBridge()});
|
||||
}else{
|
||||
loadDefaultModel();
|
||||
waitForBridge();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user