diff --git a/BRIDGE_NOTES.md b/BRIDGE_NOTES.md index c22595c..b00cd01 100644 --- a/BRIDGE_NOTES.md +++ b/BRIDGE_NOTES.md @@ -399,3 +399,40 @@ Aggiornerò questa tabella dopo il test visivo. Se qualche driver muove ancora i **Riproducibile su:** `/home/marco/automation_kriz/plotter.glb` (commit GLB attuale). Stantuffo da osservare con driver `PEN ≠ 0`. **Workaround temporaneo lato viewer:** nessuno applicato. Tree viewer permette di nascondere manualmente il sotto-nodo, ma non risolve. + + +--- + +## Risposta agent Fusion del 2026-06-10 - fix geometria fantasma stantuffo + +Confermata la diagnosi: la causa e' lato `ExportKinematicGraph.py`, non lato Blender. `createOBJExportOptions(component, ...)` esporta il Component come `assembly`, includendo automaticamente i body di tutte le sub-occurrence. Lo script Blender e' solo un re-parenter trasparente, non puo' separare body gia' fusi nel file OBJ. + +### Fix implementato + +Modificato `_export_component_mesh` in [ExportKinematicGraph.py](ExportKinematicGraph.py) per esportare **solo i body propri del Component** (`comp.bRepBodies`), che per definizione escludono i body delle sub-occurrence. I sub-component continuano ad avere il loro nodo Empty + mesh separato, ed e' il viewer/Blender che li compone via gerarchia + transform. + +Strategia in tre passi: + +1. **Raccolta body propri visibili** (`_collect_own_visible_bodies`): solo `comp.bRepBodies` filtrati per `isVisible`. +2. **Export OBJ body-per-body**: + - 1 solo body proprio: `createOBJExportOptions(body, path)` diretto. + - N body propri: export di ciascuno in `.__bodyN.obj` temporaneo + concatenazione manuale in `.obj` con riallineamento degli indici `v`/`vt`/`vn` (helper `_concatenate_obj_files`). I temporanei vengono rimossi con i loro `.mtl`. +3. **Fallback STL** sul Component completo: rimane come safety net per i casi in cui l'export OBJ body-per-body fallisce (raro). NB: lo STL del fallback include i discendenti, quindi se vedi geometria fantasma su un nodo specifico controlla nello stats `failed_meshes` -> potrebbe essere caduto sul fallback. + +Effetto collaterale **voluto**: i Component "container" (zero body propri, solo sub-component) non generano piu' alcun `meshFile`. La gerarchia rimane invariata (sono Empty senza mesh) e i loro figli mostrano la geometria attraverso i propri nodi. Nessuna modifica al contratto §2: schema `hierarchy.json`/`joints.json` identico, solo il contenuto degli OBJ cambia. + +### Verifica + +- `py_compile` OK. +- Script sincronizzato in `%APPDATA%\Autodesk\Autodesk Fusion 360\API\Scripts\ExportKinematicGraph\`. + +### Cosa serve da te (viewer) + +1. **Re-export da Fusion** (questa volta serve davvero, e' una pipeline change): + - In Fusion: `Utilities > ADD-INS > Scripts > ExportKinematicGraph > Run` -> cartella `C:\Users\croce\OneDrive\Desktop\export\`. + - `.\build_glb.bat "C:\Users\croce\OneDrive\Desktop\export"` +2. Caricare il nuovo `plotter.glb` su `/home/marco/automation_kriz/frontend/public/models/` (scp come da accordi precedenti). +3. Verifica visiva su `/lab`: muovere `PEN` e confermare che la copia fantasma dello stantuffo dentro `pistone penna:1` e' sparita. +4. Mentre ci sei: ricontrolla la tabella `swapPC` (X/Y/A/PEN/Z) che era ancora `(da verificare)` - una volta caricata la mesh pulita ha senso chiudere anche quel loop e marcare gli esiti. + +Se ne emergono altre di geometrie fantasma su altri nodi, postale qui con il `fullPathName` del padre: il pattern e' lo stesso e dovrebbe essere coperto dal fix. diff --git a/ExportKinematicGraph.py b/ExportKinematicGraph.py index 1654778..14c1a39 100644 --- a/ExportKinematicGraph.py +++ b/ExportKinematicGraph.py @@ -1327,7 +1327,13 @@ def _safe_filename(name, used_set, max_len=80): def _component_has_bodies(comp): - """True se il component contiene almeno una BRepBody esportabile.""" + """True se il component ha almeno una BRepBody PROPRIA visibile. + + `comp.bRepBodies` espone solo i body che appartengono direttamente al + component (non quelli ereditati dalle sub-occurrences), quindi un + component "container" senza body propri ritorna False e non genera + un meshFile — i suoi figli hanno gia' il proprio nodo + mesh. + """ if comp is None: return False try: @@ -1337,7 +1343,6 @@ def _component_has_bodies(comp): for i in range(bodies.count): try: b = bodies.item(i) - # isVisible / isLightBulbOn variano: facciamo best-effort. if _safe_get(b, 'isVisible', True) is False: continue return True @@ -1348,26 +1353,199 @@ def _component_has_bodies(comp): return False -def _export_component_mesh(export_mgr, component, filepath_noext): - """Esporta un Component come mesh. Prova OBJ, fallback STL. - - Ritorna la tupla (path_relativo_estensione_inclusa, formato) oppure - (None, None) se l'export fallisce. - """ - # 1) Tentativo OBJ. - obj_path = filepath_noext + '.obj' +def _collect_own_visible_bodies(comp): + """Ritorna la lista di BRepBody PROPRI del Component (esclude quelli + appartenenti alle sub-occurrences) che siano visibili.""" + out = [] try: - opts = export_mgr.createOBJExportOptions(component, obj_path) - if export_mgr.execute(opts): - return (obj_path, 'obj') + bodies = comp.bRepBodies except Exception: - pass + return out + if bodies is None: + return out + try: + n = bodies.count + except Exception: + return out + for i in range(n): + try: + b = bodies.item(i) + if _safe_get(b, 'isVisible', True) is False: + continue + out.append(b) + except Exception: + continue + return out - # 2) Fallback STL (universalmente supportato per Component). + +def _concatenate_obj_files(input_paths, output_path): + """Concatena piu' OBJ in un unico file riallineando gli indici v/vt/vn. + + Salta le direttive `mtllib` e `usemtl` per evitare riferimenti a .mtl + multipli (Blender importerebbe materiali non risolvibili). Il colore + viene comunque applicato lato JSON (`color`) dallo script Blender. + """ + v_off = vt_off = vn_off = 0 + out_lines = [] + + for path in input_paths: + v_local = vt_local = vn_local = 0 + try: + f = open(path, 'r', encoding='utf-8', errors='replace') + except Exception: + continue + try: + for line in f: + s = line.lstrip() + if not s or s[0] == '#': + continue + # Identifica il keyword (prima della prima whitespace). + # split(None, 1) gestisce sia spazi sia tab. + head = s.split(None, 1) + key = head[0] if head else '' + if key == 'mtllib' or key == 'usemtl': + continue + if key == 'v': + v_local += 1 + out_lines.append(line) + elif key == 'vt': + vt_local += 1 + out_lines.append(line) + elif key == 'vn': + vn_local += 1 + out_lines.append(line) + elif key == 'f': + tokens = s.split() + new_tokens = ['f'] + for tok in tokens[1:]: + parts = tok.split('/') + # v + try: + vi = int(parts[0]) + if vi > 0: + vi += v_off + except Exception: + new_tokens.append(tok) + continue + vt_s = '' + if len(parts) >= 2 and parts[1] != '': + try: + ti = int(parts[1]) + if ti > 0: + ti += vt_off + vt_s = str(ti) + except Exception: + vt_s = parts[1] + vn_s = '' + if len(parts) >= 3 and parts[2] != '': + try: + ni = int(parts[2]) + if ni > 0: + ni += vn_off + vn_s = str(ni) + except Exception: + vn_s = parts[2] + if len(parts) == 1: + new_tokens.append(str(vi)) + elif len(parts) == 2: + new_tokens.append('{0}/{1}'.format(vi, vt_s)) + else: + new_tokens.append('{0}/{1}/{2}'.format(vi, vt_s, vn_s)) + out_lines.append(' '.join(new_tokens) + '\n') + else: + # 'o', 'g', 's', linee non riconosciute: passa attraverso. + out_lines.append(line) + finally: + try: + f.close() + except Exception: + pass + v_off += v_local + vt_off += vt_local + vn_off += vn_local + + with open(output_path, 'w', encoding='utf-8') as f: + f.writelines(out_lines) + + +def _cleanup_tmp_obj(tmp_paths): + """Rimuove gli OBJ temporanei e i loro .mtl associati (se esistono).""" + for p in tmp_paths: + try: + if os.path.exists(p): + os.remove(p) + except Exception: + pass + try: + mtl = p[:-4] + '.mtl' if p.lower().endswith('.obj') else None + if mtl and os.path.exists(mtl): + os.remove(mtl) + except Exception: + pass + + +def _export_component_mesh(export_mgr, component, filepath_noext): + """Esporta un Component come mesh, considerando SOLO i body propri del + component (esclude i body delle sub-occurrences, che hanno il loro nodo + proprio nella gerarchia ed evitano cosi' duplicazioni "fantasma"). + + Strategia: + * 1 solo body proprio: export OBJ diretto del body. + * N body propri: export di ciascun body in OBJ temporaneo + + concatenazione manuale con riallineamento indici. + * Fallback STL: se tutti i tentativi OBJ falliscono, esporta + tutto il Component in STL (qui Fusion include + i discendenti, ma il fallback e' raro e + dichiarato). + + Ritorna la tupla (path_estensione_inclusa, formato) oppure (None, None). + """ + own_bodies = _collect_own_visible_bodies(component) + obj_path = filepath_noext + '.obj' + + # 1) Caso normale: esportiamo solo i body propri. + if own_bodies: + if len(own_bodies) == 1: + try: + opts = export_mgr.createOBJExportOptions(own_bodies[0], obj_path) + if export_mgr.execute(opts): + # Pulizia di un eventuale .mtl generato a fianco. + _cleanup_tmp_obj([]) # no-op: ma rimuoviamo .mtl finale qui sotto. + try: + mtl = filepath_noext + '.mtl' + if os.path.exists(mtl): + os.remove(mtl) + except Exception: + pass + return (obj_path, 'obj') + except Exception: + pass + else: + tmp_paths = [] + for idx, body in enumerate(own_bodies): + tmp = '{0}.__body{1}.obj'.format(filepath_noext, idx) + try: + opts = export_mgr.createOBJExportOptions(body, tmp) + if export_mgr.execute(opts): + tmp_paths.append(tmp) + except Exception: + continue + if tmp_paths: + try: + _concatenate_obj_files(tmp_paths, obj_path) + return (obj_path, 'obj') + except Exception: + pass + finally: + _cleanup_tmp_obj(tmp_paths) + + # 2) Fallback STL sul Component completo (raro, ma manteniamo il safety net). + # NOTA: lo STL del Component include i body delle sub-occurrences, quindi + # in caso di fallback potresti rivedere il pattern "geometria fantasma". + # Loggato dallo stats come 'failed_meshes' a monte se anche questo fallisce. stl_path = filepath_noext + '.stl' try: opts = export_mgr.createSTLExportOptions(component, stl_path) - # Refinement medio: bilancia qualita' / dimensione file. try: opts.meshRefinement = adsk.fusion.MeshRefinementSettings.MeshRefinementMedium except Exception: