10 KiB
Contratto Fusion 360 → Blender → GLB ⇄ Viewer Three.js
Documento di interfaccia fra l'agent che produce il GLB (Fusion 360 → Blender → glTF/GLB) e l'agent che lo consuma nel viewer web (Three.js + FusionRig.js).
Tutto ciò che è qui deve essere considerato l'unica fonte di verità: se cambia da una parte, va aggiornato in questo file e re-letto dall'altro lato.
1. Pipeline e file di scambio
Fusion 360 ──(add-in / script)──► joints.json (+ STEP/OBJ/STL gerarchia)
│
▼
Blender ──(importa la mesh + ricostruisce empties + scrive userData)──► plotter.glb
│
▼
Web viewer (React + Three.js) ──► FusionRig.js (cinematica live)
Due output:
plotter.glb— geometria + gerarchia +userDataper ogni nodo (vedi §3).joints.json— manifest cinematico esportato da Fusion (vedi §2). Può essere embedded nel GLB comescene.extras.fusion(stringa JSON).
2. Schema joints.json
{
"metadata": {
"units": "mm",
"internalUnit": "cm",
"scaleFactor": 0.01
},
"joints": [ /* Joint */ ],
"asBuiltJoints": [ /* Joint */ ],
"motionLinks": [ /* MotionLink */ ],
"rigidGroups": [ /* RigidGroup */ ]
}
2.1 Joint
{
"name": "Motore asse X", // chiave univoca dentro la sezione
"type": "revolute" | "slider" | "rigid" | ...,
"child": "6627T281_Stepper Motor:1", // occurrenceName foglia
"parent": "asse X:1", // occurrenceName (può essere "ROOT")
"childFullPath": "plotter:1+asse X:1+6627T281_Stepper Motor:1",
"parentFullPath": "plotter:1+asse X:1",
"axis": [-1, 0, 0], // world-space al momento dell'export, normalizzato
"origin": [12.34, 5.67, 0.0], // world-space, in cm
"slideValue": 0.0, // posa corrente (cm) — solo se slider
"rotationValue": 0.0, // posa corrente (rad) — solo se revolute
"slideLimits": {
"isMinimumValueEnabled": true,
"isMaximumValueEnabled": true,
"minimumValue": -1.8,
"maximumValue": 0.0
},
"rotationLimits": { /* idem, valori in rad */ }
}
2.2 MotionLink
{
"name": "Collegamento movimento 8",
"joint1": "Scorrimento 1" | null, // slave (può essere null se Fusion non risolve)
"joint2": "Motore asse Y", // driver (sempre uno dei 5)
"ratio": 1.0 | null, // null = 1
"reversed": true
}
2.3 RigidGroup
{
"name": "Gruppo 1",
"occurrenceNames": ["asse X:1", "carrello X:1", "6627T281_Stepper Motor:1", ...]
}
2.4 Unità (CRITICO)
| Quantità | Unità nel JSON | Unità nel viewer |
|---|---|---|
origin, slideValue, slideLimits.* |
cm | metri (moltiplicare × 0.01) |
axis |
adimensionale, normalizzato | idem |
rotationValue, rotationLimits.* |
rad | rad (nessuna conversione) |
Il GLB deve essere in metri (metadata.units = "meters" lato glTF). Se Blender esporta in cm o mm, il viewer deve scalare la scena oppure, meglio, Blender va configurato per esportare in metri.
3. Convenzioni plotter.glb
3.1 Gerarchia
Ogni occorrenza Fusion = un Object3D (Empty Blender) conservato come nodo separato nel glTF. Niente merge delle geometrie: il viewer ha bisogno di muoverle singolarmente.
3.2 userData per nodo
Per ogni Object3D corrispondente a un'occorrenza Fusion:
{
"userData": {
"fusion_name": "6627T281_Stepper Motor:1", // = Joint.child / RigidGroup.occurrenceNames[i]
"fusion_path": "plotter:1+6627T281_Stepper Motor:1" // = Joint.childFullPath
}
}
Questi due campi devono coincidere byte per byte con i nomi del JSON (compresi :1, spazi, accenti, ?, ecc.). Niente sanitizzazione.
Implementazione consigliata in Blender:
# In ogni Empty, dopo la ricostruzione:
empty.name = nome_pulito_safe # qualunque cosa
empty["fusion_name"] = original_occurrence_name
empty["fusion_path"] = original_full_path
Le custom properties Blender finiscono in
node.extrasnel glTF, cheGLTFLoaderdi Three.js espone suobj.userData.
3.3 Manifest embedded (opzionale ma consigliato)
Lo stesso joints.json può essere embedded nella scena:
bpy.context.scene["fusion"] = json.dumps(manifest)
→ il viewer lo legge da gltf.parser.json.scenes[0].extras.fusion (oppure scene.userData.fusion). Se assente, il viewer si aspetta che l'utente carichi il .json separatamente.
4. I 5 driver pilotati nel viewer
| Chiave UI | Sezione | name (canonico) |
Tipo | child previsto |
axis previsto |
|---|---|---|---|---|---|
A |
asBuiltJoints |
Motore A |
revolute | 6627T331_Stepper Motor (1) (1):1 |
[0, 0, 1] |
X |
joints |
Motore asse X |
revolute | 6627T281_Stepper Motor:1 |
[-1, 0, 0] |
Y |
joints |
Motore asse Y |
revolute | 6627T281_Stepper Motor (1):1 |
[0, 0, 1] |
PEN |
joints |
Asse Penna (spazio finale) |
slider | guida lineare:1 |
[0, 0, 1] |
Z |
joints |
asse Z pneumatico M5? |
slider | TN10*50:1 |
[0, 0, 1] |
Il viewer applica match esatto sul
name; se fallisce, fa fallback fuzzy (token-subset case-insensitive). I token attesi sono infrontend/src/lib/FusionRig.jsconstDRIVERS. Se vengono cambiati i nomi in Fusion, aggiornare quei token in modo coordinato.
5. Regole di simulazione (lato viewer, per documentazione)
Implementate in FusionRig.js. Servono qui solo perché chi produce il GLB capisca cosa il viewer si aspetta:
- Rigid groups → rigid components via union-find: muovere un'occorrenza muove tutta la sua component con lo stesso delta world-space.
- Motion links propagano valore driver → joint slave (
v_slave = v_driver * ratio * (reversed ? -1 : 1)). Link conjoint1: nullvengono ignorati: chi produce il GLB deve evitarli quando possibile (o documentare a parte la coppia mancante). - Posa iniziale =
matrixWorlddi ogni nodo al loader-end. Ogni update parte da zero, niente accumuli. Quindi lo stato neutro nel GLB deve essere la posa "zero" di tutti i driver (A=0, X=0, Y=0, PEN=0, Z=0). - Asse e origine dei joint sono world-space già normalizzati per le transform di assembly: l'add-in Fusion li deve esportare nel frame world dell'assembly esportato.
6. Checklist per chi produce il GLB
Prima di consegnare un nuovo plotter.glb:
- Il GLB è in metri (scena Blender: Scene → Units → Meters, scale 1.0).
- La gerarchia degli Empty Fusion è preservata 1:1 (un Empty per occorrenza, anche per i sotto-assembly).
- Ogni Empty ha
fusion_nameefusion_pathcome custom properties, identici al JSON. - Le mesh sono nodi figli degli Empty (non spostate alla scena root, non joinate).
- Lo stato della scena salvata = posa "tutti i driver a zero".
- (Opzionale)
bpy.context.scene["fusion"] = json.dumps(manifest)con il JSON completo. - Smoke-test: aprire il GLB nel viewer
/lab, espandere[FusionRig] manifest contentsin console, verificare che i 5 driver siano risolti (anche via fuzzy) e cherigidComponentdi ognuno contenga le occorrenze corrette.
7. Output di diagnostica del viewer
All'apertura del file il viewer logga in console:
[FusionRig] manifest contents ← elenco completo dei joint / motionLinks / rigidGroups
[FusionRig] driver "X" risolto via fuzzy → "<nome reale>" (se non c'è match esatto)
[FusionRig] driver "Y" (Motore asse Y) NON trovato. (se serve sistemare i nomi)
[FusionRig] child non risolto per "<joint>" (se userData.fusion_name è sbagliato)
[FusionRig] X (Motore asse X) { axis, origin, child, rigidComponent: [...] }
Se l'agent GLB chiede "perché Y non si muove", la console contiene la risposta in 3 righe.
8. Cose da chiarire / problemi aperti
Aggiornare questa sezione man mano. Mantenere un punto per problema, marcando
[OPEN] / [RESOLVED YYYY-MM-DD].
- [OPEN] I motion-link
joint1: nulldi Fusion vanno risolti per nome (es.Scorrimento 1,Rivoluzione 9)? Oppure l'agent Fusion può ri-esportarli con i nomi corretti? - [OPEN] Confermare che
Motore Aesista davvero inasBuiltJointscon quel nome esatto (problemi di matching nel viewer suggeriscono possibili varianti). - [OPEN] Definire una convenzione per nuove "macchine" (oltre al plotter): file
<machine>.glb+<machine>.joints.json+ variant diDRIVERSnel viewer.
9. Come "centralizzare" la collaborazione fra i due agent
Tre opzioni in ordine crescente di automazione:
-
Repo condiviso +
AGENTS.md/copilot-instructions.md(consigliato, zero infra)- Mettere questo file in un repo Git unico (sotto
docs/) o in un repo dedicatoplotter-contract/che entrambi i progetti includono come submodule o tramitegit read-tree. - In ogni repo aggiungere
AGENTS.mdcon: "Prima di toccare l'export GLB/JSON o il viewer, leggeredocs/FUSION_GLB_CONTRACT.md. Le modifiche al contratto vanno proposte in PR su quel file." - VS Code Copilot Chat carica automaticamente
AGENTS.mdcome instructions.
- Mettere questo file in un repo Git unico (sotto
-
Issue tracker condiviso (GitHub Projects / linear)
- Un solo board con label
contractper task che attraversano i due agent. - Ogni issue cross-side referenzia la riga di
FUSION_GLB_CONTRACT.mdrilevante.
- Un solo board con label
-
MCP server condiviso (massima automazione, più infra)
- Esporre il contratto come tool MCP (
fusion_contract.get_joint("Motore asse X")→ restituisce nome canonico, child atteso, axis, fuzzy tokens). - Entrambi gli agent si connettono allo stesso server MCP e leggono il contratto on-demand.
- Vantaggio: cambia il contratto in un posto, entrambi gli agent ricevono subito la versione nuova.
- Costo: scrivere e mantenere un piccolo server MCP (Python/Node ~150 righe).
- Esporre il contratto come tool MCP (
Raccomandazione: partire dall'opzione 1 (questo file + AGENTS.md in entrambi i repo). Passare a 3 solo se le iterazioni diventano frequenti e i naming change rompono spesso il viewer.