fusion: nuovo script ExportKinematicGraph_ATL (10 fix per export ATL)
Standalone add-in che importa ExportKinematicGraph come 'base' e ridefinisce solo le funzioni joint con: snap axis (1e-9), limits None se entrambi enable false, origin fallback su entityOne/Two per AsBuilt revolute, disambiguazione nomi duplicati (#2, #3...), flag _orphan, _token per matching, joint1Token/joint2Token sui motionLinks, glbNodeName troncato a 63 char. Log diagnostico + metadata.fixes nel JSON. Script plotter NON toccato.
This commit is contained in:
675
ExportKinematicGraph_ATL.py
Normal file
675
ExportKinematicGraph_ATL.py
Normal file
@@ -0,0 +1,675 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
ExportKinematicGraph_ATL.py
|
||||
---------------------------
|
||||
Variante "ATL" dell'add-in Fusion 360 che esporta hierarchy + joints per
|
||||
il viewer Three.js.
|
||||
|
||||
Perche' esiste un secondo script?
|
||||
Il modello ATL di Olimpic Sail ha caratteristiche che hanno fatto
|
||||
emergere alcune lacune nell'export del plotter (vedi BRIDGE_NOTES
|
||||
sezione "Findings dal viewer ATL (2026-06-18)"):
|
||||
* nomi joint duplicati,
|
||||
* `origin` mancante sugli AsBuiltJoint revolute,
|
||||
* jitter numerico negli assi,
|
||||
* limits 0..0 ambigui quando i flag enable sono false,
|
||||
* motionLinks con joint1/joint2 None (limite API),
|
||||
* nodi orfani con parent/child None.
|
||||
L'agente viewer ha chiesto esplicitamente di NON toccare
|
||||
`ExportKinematicGraph.py` (che e' stabile per il plotter) e di
|
||||
affiancare un secondo script dedicato all'ATL.
|
||||
|
||||
Strategia di implementazione:
|
||||
Importa il modulo `ExportKinematicGraph` come `base` (helper colore,
|
||||
mesh, hierarchy, walk component sono identici). Sovrascrive solo:
|
||||
* estrazione joint / asBuiltJoint / motionLink con fix mirati,
|
||||
* deduplica nomi (`Rivoluzione 5` -> `Rivoluzione 5`, `Rivoluzione 5#2`, ...),
|
||||
* snap del rumore numerico sull'axis,
|
||||
* normalizzazione limits a None quando i flag sono entrambi disabilitati,
|
||||
* origin fallback per revolute AsBuilt da entityOne/entityTwo,
|
||||
* scarto / flag esplicito per joint orfani,
|
||||
* emissione di `entityToken` (hex) accanto al `name` per fare matching anche con nomi duplicati o link rotti.
|
||||
|
||||
Output:
|
||||
<cartella scelta>/
|
||||
export/
|
||||
meshes/*.obj
|
||||
hierarchy.json
|
||||
joints.json
|
||||
`joints.json` segna `generator: 'ExportKinematicGraph_ATL.py'`.
|
||||
|
||||
Come usare in Fusion:
|
||||
Utilities -> ADD-INS -> Scripts -> Green "+" -> "Create from existing
|
||||
script" e selezionare questo file. Eseguire. Cartella di destinazione
|
||||
consigliata: `C:\\Users\\croce\\OneDrive\\Desktop\\export_ATL\\`.
|
||||
"""
|
||||
|
||||
import adsk.core
|
||||
import adsk.fusion
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Caricamento modulo base (ExportKinematicGraph plotter)
|
||||
# =============================================================================
|
||||
#
|
||||
# Lo script ATL riusa gli helper del plotter via import. Cerco il modulo
|
||||
# in:
|
||||
# 1) cartella dello script ATL (caso "sviluppo nel repo, entrambi i
|
||||
# file affiancati in `export grafo fusion/`")
|
||||
# 2) cartella sorella `..\ExportKinematicGraph\` (caso "installato come
|
||||
# due add-in separati in %APPDATA%\Autodesk\.../API/Scripts/")
|
||||
# 3) percorso fisso del repo (fallback hard-coded)
|
||||
#
|
||||
# Se il modulo non si trova, lo script abortisce con un messaggio chiaro.
|
||||
|
||||
def _locate_base_module():
|
||||
here = os.path.dirname(os.path.abspath(__file__))
|
||||
candidates = [
|
||||
here,
|
||||
os.path.normpath(os.path.join(here, '..', 'ExportKinematicGraph')),
|
||||
r'C:\Users\croce\OneDrive\Desktop\export grafo fusion',
|
||||
]
|
||||
for path in candidates:
|
||||
if os.path.isfile(os.path.join(path, 'ExportKinematicGraph.py')):
|
||||
if path not in sys.path:
|
||||
sys.path.insert(0, path)
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
_BASE_PATH = _locate_base_module()
|
||||
if _BASE_PATH is None:
|
||||
raise ImportError(
|
||||
"Impossibile trovare ExportKinematicGraph.py. Cercato in: "
|
||||
"cartella script, ..\\ExportKinematicGraph\\, repo "
|
||||
"'export grafo fusion'."
|
||||
)
|
||||
|
||||
# Forziamo reload se il base e' gia' stato importato in una sessione
|
||||
# Fusion precedente, cosi' eventuali modifiche al plotter sono raccolte.
|
||||
import importlib
|
||||
import ExportKinematicGraph as base # noqa: E402
|
||||
importlib.reload(base)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Logging diagnostico (stdout + UI palette)
|
||||
# =============================================================================
|
||||
|
||||
_LOG_LINES = []
|
||||
|
||||
|
||||
def _log(msg):
|
||||
line = '[ATL-export] ' + str(msg)
|
||||
_LOG_LINES.append(line)
|
||||
try:
|
||||
# Fusion redirige stdout alla TextCommandPalette in alcune build.
|
||||
print(line)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Utility numeriche (snap, token)
|
||||
# =============================================================================
|
||||
|
||||
_EPS_AXIS = 1e-9 # snap a zero sotto questa soglia
|
||||
_EPS_UNIT = 1e-9 # snap a +/-1 se molto vicino
|
||||
|
||||
|
||||
def _snap_axis(axis):
|
||||
"""Snap del rumore numerico in un vettore axis.
|
||||
|
||||
- componenti < EPS_AXIS in modulo -> 0.0
|
||||
- componenti molto vicine a +/-1 -> +/-1.0
|
||||
Ritorna None se l'input e' None o vuoto.
|
||||
"""
|
||||
if not axis:
|
||||
return axis
|
||||
out = []
|
||||
for v in axis:
|
||||
try:
|
||||
fv = float(v)
|
||||
except Exception:
|
||||
out.append(v)
|
||||
continue
|
||||
if abs(fv) < _EPS_AXIS:
|
||||
fv = 0.0
|
||||
elif abs(fv - 1.0) < _EPS_UNIT:
|
||||
fv = 1.0
|
||||
elif abs(fv + 1.0) < _EPS_UNIT:
|
||||
fv = -1.0
|
||||
out.append(fv)
|
||||
return out
|
||||
|
||||
|
||||
def _token_of(entity):
|
||||
"""Ritorna `entity.entityToken` se disponibile, altrimenti None.
|
||||
|
||||
Il token e' una stringa opaca univoca per entita' Fusion: utile come
|
||||
matching key quando i `name` sono duplicati o quando i motion link
|
||||
perdono il riferimento al `name` del joint.
|
||||
"""
|
||||
if entity is None:
|
||||
return None
|
||||
try:
|
||||
tok = getattr(entity, 'entityToken', None)
|
||||
if tok:
|
||||
return str(tok)
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Limits: normalizzazione
|
||||
# =============================================================================
|
||||
|
||||
def _normalize_limits(limits_dict):
|
||||
"""Se entrambi i flag min/max enabled sono False, ritorna None.
|
||||
|
||||
Cosi' il consumer interpreta correttamente "joint libero, no limiti"
|
||||
invece di un range 0..0 (che il viewer attualmente filtra via,
|
||||
facendo sparire lo slider).
|
||||
|
||||
Mantiene il dict originale se almeno uno dei due limiti e' attivo,
|
||||
cosi' `isMinimumValueEnabled=True, isMaximumValueEnabled=False` resta
|
||||
valido (es: solo lower bound).
|
||||
"""
|
||||
if limits_dict is None:
|
||||
return None
|
||||
enabled_min = bool(limits_dict.get('isMinimumValueEnabled'))
|
||||
enabled_max = bool(limits_dict.get('isMaximumValueEnabled'))
|
||||
if not enabled_min and not enabled_max:
|
||||
return None
|
||||
return limits_dict
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Origin per AsBuiltJoint: fallback su entityOne/entityTwo
|
||||
# =============================================================================
|
||||
|
||||
def _point3d_to_list(pt):
|
||||
"""Wrap di point_to_list che accetta gia' liste / tuple."""
|
||||
if pt is None:
|
||||
return None
|
||||
if isinstance(pt, (list, tuple)):
|
||||
try:
|
||||
return [float(pt[0]), float(pt[1]), float(pt[2])]
|
||||
except Exception:
|
||||
return None
|
||||
return base.point_to_list(pt)
|
||||
|
||||
|
||||
def _origin_from_entity(entity):
|
||||
"""Estrae il centro geometrico da una BRepFace cilindrica o BRepEdge
|
||||
circolare. Best-effort, tutto protetto da try/except: l'API espone
|
||||
`Surface.origin` per cilindri/coni e `Curve3D.center` per cerchi/archi.
|
||||
"""
|
||||
if entity is None:
|
||||
return None
|
||||
# BRepFace.geometry -> Surface (Cylinder, Cone, Sphere, Torus, Plane, ...)
|
||||
try:
|
||||
geom = getattr(entity, 'geometry', None)
|
||||
if geom is not None:
|
||||
# Cylinder/Cone/Torus/Sphere espongono `origin`.
|
||||
origin = getattr(geom, 'origin', None)
|
||||
if origin is not None:
|
||||
return base.point_to_list(origin)
|
||||
# Circle3D/Ellipse3D/Arc3D espongono `center`.
|
||||
center = getattr(geom, 'center', None)
|
||||
if center is not None:
|
||||
return base.point_to_list(center)
|
||||
except Exception:
|
||||
pass
|
||||
# BRepVertex
|
||||
try:
|
||||
pt = getattr(entity, 'geometry', None)
|
||||
if pt is not None and hasattr(pt, 'x'):
|
||||
return base.point_to_list(pt)
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def _origin_for_as_built(joint):
|
||||
"""Pipeline a tre stadi per ricavare l'origine di un AsBuiltJoint.
|
||||
|
||||
1) `joint.geometry.origin` (raro per AsBuilt, ma proviamo).
|
||||
2) Centro geometrico di `joint.entityOne` (di solito una BRepFace
|
||||
cilindrica per i revolute, o un BRepEdge circolare).
|
||||
3) Centro di `joint.entityTwo` come last resort.
|
||||
|
||||
Ritorna `(origin_list, source)` dove `source` e':
|
||||
'geometry' | 'entityOne' | 'entityTwo' | None
|
||||
Cosi' loggo la provenienza nelle stats.
|
||||
"""
|
||||
pt = base._joint_geometry_point(getattr(joint, 'geometry', None))
|
||||
if pt:
|
||||
return pt, 'geometry'
|
||||
for ent_attr, label in (('entityOne', 'entityOne'), ('entityTwo', 'entityTwo')):
|
||||
ent = getattr(joint, ent_attr, None)
|
||||
pt = _origin_from_entity(ent)
|
||||
if pt:
|
||||
return pt, label
|
||||
return None, None
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Disambiguazione nomi (fix #2)
|
||||
# =============================================================================
|
||||
|
||||
def _disambiguate_names(records, name_key='name', dup_marker='#'):
|
||||
"""Modifica in-place la lista `records` rendendo univoci i `name`.
|
||||
|
||||
Strategia: scan in ordine. La prima occorrenza tiene il name originale,
|
||||
dalla seconda in poi viene appeso `#2`, `#3`, ... Conta i collision
|
||||
per loggarli.
|
||||
|
||||
Ritorna il numero di rinominazioni effettuate.
|
||||
"""
|
||||
counts = {}
|
||||
renamed = 0
|
||||
for rec in records:
|
||||
if rec is None:
|
||||
continue
|
||||
orig = rec.get(name_key)
|
||||
if not orig:
|
||||
continue
|
||||
counts[orig] = counts.get(orig, 0) + 1
|
||||
if counts[orig] == 1:
|
||||
continue
|
||||
new = '{0}{1}{2}'.format(orig, dup_marker, counts[orig])
|
||||
rec['_originalName'] = orig
|
||||
rec[name_key] = new
|
||||
renamed += 1
|
||||
return renamed
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Serializzazione joint con fix ATL
|
||||
# =============================================================================
|
||||
|
||||
def _enrich_axis(motion_info):
|
||||
"""Applica snap a `axis` e `secondaryAxis` in-place."""
|
||||
motion_info['axis'] = _snap_axis(motion_info.get('axis'))
|
||||
motion_info['secondaryAxis'] = _snap_axis(motion_info.get('secondaryAxis'))
|
||||
return motion_info
|
||||
|
||||
|
||||
def _serialize_joint_atl(j):
|
||||
"""Variante di base._serialize_joint con: token, axis snappato,
|
||||
limits normalizzati."""
|
||||
try:
|
||||
data = base._serialize_joint(j)
|
||||
if data is None:
|
||||
return None
|
||||
# Snap axis.
|
||||
data['axis'] = _snap_axis(data.get('axis'))
|
||||
data['secondaryAxis'] = _snap_axis(data.get('secondaryAxis'))
|
||||
# Normalize limits.
|
||||
data['rotationLimits'] = _normalize_limits(data.get('rotationLimits'))
|
||||
data['slideLimits'] = _normalize_limits(data.get('slideLimits'))
|
||||
# Token del joint (sopravvive a rename).
|
||||
data['_token'] = _token_of(j)
|
||||
# Flag orfano (parent + child entrambi None).
|
||||
if data.get('parent') is None and data.get('child') is None:
|
||||
data['_orphan'] = True
|
||||
return data
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _serialize_as_built_joint_atl(j):
|
||||
"""Variante di base._serialize_as_built_joint con: token, axis
|
||||
snappato, limits normalizzati, origin con fallback su entity."""
|
||||
try:
|
||||
data = base._serialize_as_built_joint(j)
|
||||
if data is None:
|
||||
return None
|
||||
# Snap axis.
|
||||
data['axis'] = _snap_axis(data.get('axis'))
|
||||
data['secondaryAxis'] = _snap_axis(data.get('secondaryAxis'))
|
||||
# Normalize limits.
|
||||
data['rotationLimits'] = _normalize_limits(data.get('rotationLimits'))
|
||||
data['slideLimits'] = _normalize_limits(data.get('slideLimits'))
|
||||
# Origin con fallback (fondamentale per i revolute).
|
||||
if data.get('origin') is None:
|
||||
origin, source = _origin_for_as_built(j)
|
||||
if origin is not None:
|
||||
data['origin'] = origin
|
||||
data['_originSource'] = source
|
||||
# Token del joint.
|
||||
data['_token'] = _token_of(j)
|
||||
# Flag orfano.
|
||||
if data.get('parent') is None and data.get('child') is None:
|
||||
data['_orphan'] = True
|
||||
return data
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _export_joints_atl(design, root_comp):
|
||||
"""Come base.export_joints ma con _serialize_joint_atl."""
|
||||
result = []
|
||||
seen = set()
|
||||
for comp in base._iter_all_components(design, root_comp):
|
||||
joints = base._safe_get(comp, 'joints')
|
||||
if joints is None:
|
||||
continue
|
||||
try:
|
||||
n = joints.count
|
||||
except Exception:
|
||||
n = 0
|
||||
for i in range(n):
|
||||
try:
|
||||
j = joints.item(i)
|
||||
except Exception:
|
||||
continue
|
||||
try:
|
||||
key = j.entityToken
|
||||
except Exception:
|
||||
key = id(j)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
data = _serialize_joint_atl(j)
|
||||
if data is not None:
|
||||
result.append(data)
|
||||
return result
|
||||
|
||||
|
||||
def _export_as_built_joints_atl(design, root_comp):
|
||||
"""Come base.export_as_built_joints ma con _serialize_as_built_joint_atl."""
|
||||
result = []
|
||||
seen = set()
|
||||
for comp in base._iter_all_components(design, root_comp):
|
||||
ab_joints = base._safe_get(comp, 'asBuiltJoints')
|
||||
if ab_joints is None:
|
||||
continue
|
||||
try:
|
||||
n = ab_joints.count
|
||||
except Exception:
|
||||
n = 0
|
||||
for i in range(n):
|
||||
try:
|
||||
j = ab_joints.item(i)
|
||||
except Exception:
|
||||
continue
|
||||
try:
|
||||
key = j.entityToken
|
||||
except Exception:
|
||||
key = id(j)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
data = _serialize_as_built_joint_atl(j)
|
||||
if data is not None:
|
||||
result.append(data)
|
||||
return result
|
||||
|
||||
|
||||
def _export_motion_links_atl(design):
|
||||
"""Come base.export_motion_links ma emette anche i token delle
|
||||
entita' originali per il matching manuale quando joint1/joint2
|
||||
sono None (limite API noto).
|
||||
"""
|
||||
result = []
|
||||
root_comp = base._safe_get(design, 'rootComponent')
|
||||
if root_comp is None:
|
||||
return result
|
||||
seen = set()
|
||||
for comp in base._iter_all_components(design, root_comp):
|
||||
motion_links = base._safe_get(comp, 'motionLinks')
|
||||
if motion_links is None:
|
||||
continue
|
||||
try:
|
||||
n = motion_links.count
|
||||
except Exception:
|
||||
n = 0
|
||||
for i in range(n):
|
||||
try:
|
||||
ml = motion_links.item(i)
|
||||
except Exception:
|
||||
continue
|
||||
try:
|
||||
key = ml.entityToken
|
||||
except Exception:
|
||||
key = id(ml)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
try:
|
||||
j1 = base._safe_get(ml, 'jointOne')
|
||||
j2 = base._safe_get(ml, 'jointTwo')
|
||||
# Anche entityOne/entityTwo (proxy interno) possono dare
|
||||
# un token utile quando il name e' perso.
|
||||
e1 = base._safe_get(ml, 'entityOne')
|
||||
e2 = base._safe_get(ml, 'entityTwo')
|
||||
result.append({
|
||||
'name': base._safe_get(ml, 'name'),
|
||||
'joint1': base._safe_get(j1, 'name'),
|
||||
'joint2': base._safe_get(j2, 'name'),
|
||||
'joint1Token': _token_of(j1) or _token_of(e1),
|
||||
'joint2Token': _token_of(j2) or _token_of(e2),
|
||||
'ratio': base._safe_get(ml, 'ratio'),
|
||||
'reversed': base._safe_get(ml, 'isReversed'),
|
||||
'isSuppressed': base._safe_get(ml, 'isSuppressed'),
|
||||
'isLightBulbOn': base._safe_get(ml, 'isLightBulbOn'),
|
||||
'currentValue': base._safe_get(ml, 'currentValue'),
|
||||
})
|
||||
except Exception:
|
||||
continue
|
||||
return result
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Enrich hierarchy: glbNodeName + log
|
||||
# =============================================================================
|
||||
|
||||
# Blender 4.x tronca i nomi degli Object a 63 caratteri (limite Python
|
||||
# bpy.types.ID.name). Qui emettiamo `glbNodeName` previsto = name[:63]
|
||||
# cosi' il consumer puo' fare lookup senza indovinare.
|
||||
_BLENDER_NAME_MAX = 63
|
||||
|
||||
|
||||
def _annotate_hierarchy(nodes):
|
||||
"""Aggiunge `glbNodeName` a ogni nodo (fix #10)."""
|
||||
for n in nodes:
|
||||
nm = n.get('name')
|
||||
if not nm:
|
||||
continue
|
||||
n['glbNodeName'] = nm if len(nm) <= _BLENDER_NAME_MAX else nm[:_BLENDER_NAME_MAX]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Folder dialog
|
||||
# =============================================================================
|
||||
|
||||
def _ask_destination_folder(ui, default_dir=None):
|
||||
dialog = ui.createFolderDialog()
|
||||
dialog.title = 'Scegli la cartella di destinazione export ATL'
|
||||
if default_dir:
|
||||
try:
|
||||
dialog.initialDirectory = default_dir
|
||||
except Exception:
|
||||
pass
|
||||
if dialog.showDialog() != adsk.core.DialogResults.DialogOK:
|
||||
return None
|
||||
return dialog.folder
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Entry point
|
||||
# =============================================================================
|
||||
|
||||
def run(context):
|
||||
"""Entry point richiamato da Fusion 360.
|
||||
|
||||
L'utente sceglie una cartella PARENT. Lo script crea (se serve) la
|
||||
sottocartella `export/` al suo interno e ci scrive:
|
||||
meshes/*.obj
|
||||
hierarchy.json
|
||||
joints.json
|
||||
"""
|
||||
ui = None
|
||||
try:
|
||||
app = adsk.core.Application.get()
|
||||
ui = app.userInterface
|
||||
|
||||
product = app.activeProduct
|
||||
if not isinstance(product, adsk.fusion.Design):
|
||||
if ui:
|
||||
ui.messageBox('Nessun design Fusion attivo.')
|
||||
return
|
||||
design = product
|
||||
|
||||
root_comp = design.rootComponent
|
||||
doc_name = base._safe_get(app.activeDocument, 'name', 'Untitled')
|
||||
|
||||
units = None
|
||||
try:
|
||||
units = design.unitsManager.defaultLengthUnits
|
||||
except Exception:
|
||||
units = None
|
||||
|
||||
api_version = None
|
||||
try:
|
||||
api_version = adsk.core.Application.get().version
|
||||
except Exception:
|
||||
api_version = None
|
||||
|
||||
parent_dir = _ask_destination_folder(ui)
|
||||
if not parent_dir:
|
||||
return
|
||||
|
||||
if os.path.basename(os.path.normpath(parent_dir)).lower() == 'export':
|
||||
out_dir = parent_dir
|
||||
else:
|
||||
out_dir = os.path.join(parent_dir, 'export')
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
|
||||
_log('body-per-body mode: ON (eredita da ExportKinematicGraph)')
|
||||
_log('base module path: ' + str(_BASE_PATH))
|
||||
|
||||
# --- 1) Mesh + hierarchy (riusiamo la pipeline plotter) ---------------
|
||||
nodes, mesh_stats = base.export_meshes_and_hierarchy(
|
||||
design, root_comp, out_dir
|
||||
)
|
||||
_annotate_hierarchy(nodes)
|
||||
_log('hierarchy nodes: {0}, mesh ok: {1}, mesh fail: {2}'.format(
|
||||
len(nodes), mesh_stats['exported_meshes'], mesh_stats['failed_meshes']
|
||||
))
|
||||
|
||||
hierarchy_payload = {
|
||||
'metadata': {
|
||||
'documentName': doc_name,
|
||||
'units': units,
|
||||
'internalUnit': 'cm',
|
||||
'scaleFactor': 0.01,
|
||||
'exportedAt': datetime.utcnow().isoformat() + 'Z',
|
||||
'apiVersion': api_version,
|
||||
'generator': 'ExportKinematicGraph_ATL.py',
|
||||
'profile': 'ATL',
|
||||
'blenderNameMax': _BLENDER_NAME_MAX,
|
||||
},
|
||||
'nodes': nodes,
|
||||
}
|
||||
with open(os.path.join(out_dir, 'hierarchy.json'), 'w', encoding='utf-8') as f:
|
||||
json.dump(hierarchy_payload, f, indent=2, ensure_ascii=False,
|
||||
default=str)
|
||||
|
||||
# --- 2) Joints con fix ATL --------------------------------------------
|
||||
joints_list = _export_joints_atl(design, root_comp)
|
||||
as_built_list = _export_as_built_joints_atl(design, root_comp)
|
||||
motion_links = _export_motion_links_atl(design)
|
||||
rigid_groups = base.export_rigid_groups(design, root_comp)
|
||||
|
||||
# Disambiguazione nomi: i joint e gli asBuilt vivono in spazi nomi
|
||||
# separati per Fusion, ma il viewer matcha per nome unico globale.
|
||||
# Disambiguiamo i duplicati cross-section.
|
||||
joints_renamed = _disambiguate_names(joints_list)
|
||||
ab_renamed = _disambiguate_names(as_built_list)
|
||||
|
||||
# Conteggio orfani / origin recuperati.
|
||||
orphans_joints = sum(1 for x in joints_list if x.get('_orphan'))
|
||||
orphans_ab = sum(1 for x in as_built_list if x.get('_orphan'))
|
||||
origin_recovered = sum(1 for x in as_built_list
|
||||
if x.get('_originSource') in ('entityOne', 'entityTwo'))
|
||||
revolutes_no_origin = sum(1 for x in as_built_list
|
||||
if x.get('type') == 'revolute' and x.get('origin') is None)
|
||||
|
||||
_log('joints: {0} (renamed {1}, orphans {2})'.format(
|
||||
len(joints_list), joints_renamed, orphans_joints))
|
||||
_log('asBuiltJoints: {0} (renamed {1}, orphans {2}, origin recovered {3}, revolute senza origin {4})'.format(
|
||||
len(as_built_list), ab_renamed, orphans_ab, origin_recovered, revolutes_no_origin))
|
||||
_log('motionLinks: {0}'.format(len(motion_links)))
|
||||
_log('rigidGroups: {0}'.format(len(rigid_groups)))
|
||||
|
||||
joints_payload = {
|
||||
'metadata': {
|
||||
'documentName': doc_name,
|
||||
'units': units,
|
||||
'internalUnit': 'cm',
|
||||
'scaleFactor': 0.01,
|
||||
'exportedAt': datetime.utcnow().isoformat() + 'Z',
|
||||
'apiVersion': api_version,
|
||||
'generator': 'ExportKinematicGraph_ATL.py',
|
||||
'profile': 'ATL',
|
||||
'fixes': {
|
||||
'duplicateNames': {'renamedJoints': joints_renamed,
|
||||
'renamedAsBuilt': ab_renamed},
|
||||
'axisSnap': {'epsilon': _EPS_AXIS},
|
||||
'limitsNormalized': True,
|
||||
'orphans': {'joints': orphans_joints,
|
||||
'asBuilt': orphans_ab},
|
||||
'asBuiltOrigin': {'recovered': origin_recovered,
|
||||
'revoluteStillMissing': revolutes_no_origin},
|
||||
'motionLinkTokens': True,
|
||||
},
|
||||
},
|
||||
'joints': joints_list,
|
||||
'asBuiltJoints': as_built_list,
|
||||
'motionLinks': motion_links,
|
||||
'rigidGroups': rigid_groups,
|
||||
}
|
||||
with open(os.path.join(out_dir, 'joints.json'), 'w', encoding='utf-8') as f:
|
||||
json.dump(joints_payload, f, indent=2, ensure_ascii=False,
|
||||
default=str)
|
||||
|
||||
# Salva il log accanto per debug.
|
||||
with open(os.path.join(out_dir, 'export_atl.log'), 'w', encoding='utf-8') as f:
|
||||
f.write('\n'.join(_LOG_LINES) + '\n')
|
||||
|
||||
if ui:
|
||||
ui.messageBox(
|
||||
'Export ATL completato.\n\n'
|
||||
'Cartella: {0}\n\n'
|
||||
'Nodi gerarchia: {1}\n'
|
||||
'Mesh esportate: {2} (fallite: {3})\n\n'
|
||||
'Joints: {4} (rinominati {5}, orfani {6})\n'
|
||||
'As-Built Joints: {7} (rinominati {8}, origin recuperati {9}, revolute senza origin {10})\n'
|
||||
'Motion Links: {11}\n'
|
||||
'Rigid Groups: {12}\n\n'
|
||||
'Log dettagliato: export_atl.log'.format(
|
||||
out_dir,
|
||||
len(nodes),
|
||||
mesh_stats['exported_meshes'],
|
||||
mesh_stats['failed_meshes'],
|
||||
len(joints_list), joints_renamed, orphans_joints,
|
||||
len(as_built_list), ab_renamed, origin_recovered, revolutes_no_origin,
|
||||
len(motion_links),
|
||||
len(rigid_groups),
|
||||
)
|
||||
)
|
||||
|
||||
except Exception:
|
||||
if ui:
|
||||
ui.messageBox(
|
||||
'Errore durante l\'export ATL:\n\n' + traceback.format_exc()
|
||||
)
|
||||
Reference in New Issue
Block a user