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.
676 lines
24 KiB
Python
676 lines
24 KiB
Python
# -*- 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()
|
|
)
|