From 411eb6397cbfdb11eb93c121e573192e13abf0ec Mon Sep 17 00:00:00 2001 From: PitresAli Date: Tue, 25 Aug 2026 22:33:27 +0200 Subject: [PATCH] Replace hanging STL dialog with local file picker --- SUB Control/.gitignore | 1 + SUB Control/README.md | 8 +++---- SUB Control/web/app.js | 5 +++- SUB Control/web/index.html | 1 + SUB Control/web/model-viewer.js | 13 +++++++---- SUB Control/web_ui.py | 41 +++++++++++++++++---------------- 6 files changed, 39 insertions(+), 30 deletions(-) diff --git a/SUB Control/.gitignore b/SUB Control/.gitignore index 6dbe475..c7bf4b4 100644 --- a/SUB Control/.gitignore +++ b/SUB Control/.gitignore @@ -3,3 +3,4 @@ __pycache__/ *.py[cod] logs/ model_settings.json +runtime/ diff --git a/SUB Control/README.md b/SUB Control/README.md index 86148ee..78e74d6 100644 --- a/SUB Control/README.md +++ b/SUB Control/README.md @@ -14,10 +14,10 @@ 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**, then drag to orbit, use the mouse wheel to zoom, or switch between shaded and edge-focused -rendering. The chosen file is remembered in `model_settings.json`; only its -local path is stored. STEP/STP CAD solids must first be tessellated/exported as -STL because the embedded renderer intentionally does not include a full CAD -kernel. +rendering. 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. ## Safety first diff --git a/SUB Control/web/app.js b/SUB Control/web/app.js index 4e73fdd..611d18c 100644 --- a/SUB Control/web/app.js +++ b/SUB Control/web/app.js @@ -25,7 +25,10 @@ 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 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'}; +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}}; +$('#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)}; diff --git a/SUB Control/web/index.html b/SUB Control/web/index.html index 116d5ca..ba232fc 100644 --- a/SUB Control/web/index.html +++ b/SUB Control/web/index.html @@ -46,6 +46,7 @@
+
VEHICLENo heartbeatSimpleROV-3 • differential drive
diff --git a/SUB Control/web/model-viewer.js b/SUB Control/web/model-viewer.js index 3cb1692..6c67954 100644 --- a/SUB Control/web/model-viewer.js +++ b/SUB Control/web/model-viewer.js @@ -65,15 +65,18 @@ addEventListener('resize',requestDraw); window.rovModel = { + loadBuffer(buffer, name) { + vertices=parseStl(buffer);stage.classList.add('model-loaded'); + document.querySelector('#model-style').disabled=false; + document.querySelector('#model-file-name').textContent=name; + yaw=-.55;pitch=-.42;zoom=1;requestDraw(); + return {name,triangles:vertices.length/9}; + }, load(payload) { 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 30 * 1024 * 1024: - raise ValueError("The STL is larger than 30 MB; export a lighter mesh") - self.model_path.write_text(json.dumps({"path": str(path)}, indent=2) + "\n", encoding="utf-8") - return self._model_payload(path) + try: + content = base64.b64decode(data, validate=True) + except (ValueError, TypeError) as exc: + raise ValueError("Invalid STL data") from exc + if not content or len(content) > 30 * 1024 * 1024: + raise ValueError("STL must be between 1 byte and 30 MB") + self.model_cache.parent.mkdir(exist_ok=True) + self.model_cache.write_bytes(content) + self.model_cache_name.write_text(safe_name, encoding="utf-8") + return True def saved_model(self): + try: + if self.model_cache.is_file() and self.model_cache.stat().st_size <= 30 * 1024 * 1024: + name = self.model_cache_name.read_text(encoding="utf-8").strip() or "vehicle_model.stl" + return {"name": name, "data": base64.b64encode(self.model_cache.read_bytes()).decode("ascii")} + except (OSError, ValueError): + pass try: path = Path(json.loads(self.model_path.read_text(encoding="utf-8"))["path"]) if path.is_file() and path.suffix.lower() == ".stl" and path.stat().st_size <= 30 * 1024 * 1024: