perf: Optimize STL chunk transfer to 1 MB and primitive spatial hashing for instant auto-loading

This commit is contained in:
2026-08-26 00:50:40 +02:00
parent c8f287cb3c
commit 1abd4f74ac
3 changed files with 5 additions and 4 deletions
+1 -1
View File
@@ -30,7 +30,7 @@ const modelInput=$('#model-input'),modelButton=$('#load-model');
modelButton.onclick=()=>{modelInput.value='';modelInput.click()}; modelButton.onclick=()=>{modelInput.value='';modelInput.click()};
function bytesToBase64(bytes){let value='';for(let i=0;i<bytes.length;i+=0x8000)value+=String.fromCharCode(...bytes.subarray(i,i+0x8000));return btoa(value)} function bytesToBase64(bytes){let value='';for(let i=0;i<bytes.length;i+=0x8000)value+=String.fromCharCode(...bytes.subarray(i,i+0x8000));return btoa(value)}
async function cacheModel(file){const token=await api('begin_model_save',file.name,file.size),chunk=196608;for(let offset=0;offset<file.size;offset+=chunk){const bytes=new Uint8Array(await file.slice(offset,offset+chunk).arrayBuffer());await api('append_model_chunk',token,bytesToBase64(bytes));if(offset% (chunk*4)===0)await new Promise(requestAnimationFrame)}await api('finish_model_save',token)} async function cacheModel(file){const token=await api('begin_model_save',file.name,file.size),chunk=196608;for(let offset=0;offset<file.size;offset+=chunk){const bytes=new Uint8Array(await file.slice(offset,offset+chunk).arrayBuffer());await api('append_model_chunk',token,bytesToBase64(bytes));if(offset% (chunk*4)===0)await new Promise(requestAnimationFrame)}await api('finish_model_save',token)}
async function loadCachedModel(info){const bytes=new Uint8Array(info.size),chunk=196608;for(let offset=0;offset<info.size;offset+=chunk){const response=await api('read_saved_model_chunk',offset,chunk),part=atob(response.data);for(let i=0;i<part.length;i++)bytes[offset+i]=part.charCodeAt(i);if(offset%(chunk*4)===0)await new Promise(requestAnimationFrame)}return window.rovModel.loadBuffer(bytes.buffer,info.name)} async function loadCachedModel(info){const bytes=new Uint8Array(info.size),chunk=1048576;for(let offset=0;offset<info.size;offset+=chunk){const response=await api('read_saved_model_chunk',offset,chunk),part=atob(response.data);for(let i=0;i<part.length;i++)bytes[offset+i]=part.charCodeAt(i);if(offset%(chunk*4)===0)await new Promise(requestAnimationFrame)}return window.rovModel.loadBuffer(bytes.buffer,info.name)}
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)cacheModel(file).then(()=>toast('STL saved for the next launch')).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}}; 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)cacheModel(file).then(()=>toast('STL saved for the next launch')).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)}}; $('#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'); $('#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');
+2 -1
View File
@@ -283,7 +283,8 @@
const numTriangles = Math.floor(rawTriangles.length / 9); const numTriangles = Math.floor(rawTriangles.length / 9);
const posMap = new Map(); const posMap = new Map();
const getVertexIndex = (x, y, z) => { const getVertexIndex = (x, y, z) => {
const key = `${Math.round(x * 10000)},${Math.round(y * 10000)},${Math.round(z * 10000)}`; const ix = (x * 2000) | 0, iy = (y * 2000) | 0, iz = (z * 2000) | 0;
const key = (ix * 73856093 ^ iy * 19349663 ^ iz * 83492791) >>> 0;
let idx = posMap.get(key); let idx = posMap.get(key);
if (idx === undefined) { if (idx === undefined) {
idx = posMap.size; idx = posMap.size;
+2 -2
View File
@@ -139,8 +139,8 @@ class ControlApi:
pass pass
return None return None
def read_saved_model_chunk(self, offset, size=196608): def read_saved_model_chunk(self, offset, size=1048576):
offset, size = int(offset), min(int(size), 196608) offset, size = int(offset), min(int(size), 1048576)
if offset < 0 or size < 1 or not self.model_cache.is_file(): if offset < 0 or size < 1 or not self.model_cache.is_file():
raise ValueError("Saved model is unavailable") raise ValueError("Saved model is unavailable")
with self.model_cache.open("rb") as stream: with self.model_cache.open("rb") as stream: