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