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:
@@ -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 `<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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user