fusion: export body-per-body sui body propri (fix geometria fantasma stantuffo)

createOBJExportOptions(component, ...) includeva i body delle sub-occurrences,

duplicando la geometria nei nodi padre. Ora esportiamo solo comp.bRepBodies

(body propri del Component): per N>1 body, concateniamo gli OBJ riallineando

gli indici v/vt/vn. Container senza body propri non generano meshFile. Fallback STL invariato.
This commit is contained in:
marco
2026-06-10 18:03:10 +02:00
parent 6372af25c0
commit c3e506c22d
2 changed files with 231 additions and 16 deletions

View File

@@ -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`. **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. **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 `<name>.__bodyN.obj` temporaneo + concatenazione manuale in `<name>.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.

View File

@@ -1327,7 +1327,13 @@ def _safe_filename(name, used_set, max_len=80):
def _component_has_bodies(comp): 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: if comp is None:
return False return False
try: try:
@@ -1337,7 +1343,6 @@ def _component_has_bodies(comp):
for i in range(bodies.count): for i in range(bodies.count):
try: try:
b = bodies.item(i) b = bodies.item(i)
# isVisible / isLightBulbOn variano: facciamo best-effort.
if _safe_get(b, 'isVisible', True) is False: if _safe_get(b, 'isVisible', True) is False:
continue continue
return True return True
@@ -1348,26 +1353,199 @@ def _component_has_bodies(comp):
return False return False
def _export_component_mesh(export_mgr, component, filepath_noext): def _collect_own_visible_bodies(comp):
"""Esporta un Component come mesh. Prova OBJ, fallback STL. """Ritorna la lista di BRepBody PROPRI del Component (esclude quelli
appartenenti alle sub-occurrences) che siano visibili."""
Ritorna la tupla (path_relativo_estensione_inclusa, formato) oppure out = []
(None, None) se l'export fallisce.
"""
# 1) Tentativo OBJ.
obj_path = filepath_noext + '.obj'
try: try:
opts = export_mgr.createOBJExportOptions(component, obj_path) bodies = comp.bRepBodies
if export_mgr.execute(opts):
return (obj_path, 'obj')
except Exception: 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' stl_path = filepath_noext + '.stl'
try: try:
opts = export_mgr.createSTLExportOptions(component, stl_path) opts = export_mgr.createSTLExportOptions(component, stl_path)
# Refinement medio: bilancia qualita' / dimensione file.
try: try:
opts.meshRefinement = adsk.fusion.MeshRefinementSettings.MeshRefinementMedium opts.meshRefinement = adsk.fusion.MeshRefinementSettings.MeshRefinementMedium
except Exception: except Exception: