Files
syncro_multi_agente/FUSION_GLB_CONTRACT.md

10 KiB
Raw Blame History

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 + userData per ogni nodo (vedi §3).
  • joints.json — manifest cinematico esportato da Fusion (vedi §2). Può essere embedded nel GLB come scene.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 */ }
}
{
  "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.extras nel glTF, che GLTFLoader di Three.js espone su obj.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 in frontend/src/lib/FusionRig.js const DRIVERS. 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:

  1. Rigid groups → rigid components via union-find: muovere un'occorrenza muove tutta la sua component con lo stesso delta world-space.
  2. Motion links propagano valore driver → joint slave (v_slave = v_driver * ratio * (reversed ? -1 : 1)). Link con joint1: null vengono ignorati: chi produce il GLB deve evitarli quando possibile (o documentare a parte la coppia mancante).
  3. Posa iniziale = matrixWorld di 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).
  4. 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_name e fusion_path come 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 contents in console, verificare che i 5 driver siano risolti (anche via fuzzy) e che rigidComponent di 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: null di 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 A esista davvero in asBuiltJoints con 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 di DRIVERS nel viewer.

9. Come "centralizzare" la collaborazione fra i due agent

Tre opzioni in ordine crescente di automazione:

  1. Repo condiviso + AGENTS.md / copilot-instructions.md (consigliato, zero infra)

    • Mettere questo file in un repo Git unico (sotto docs/) o in un repo dedicato plotter-contract/ che entrambi i progetti includono come submodule o tramite git read-tree.
    • In ogni repo aggiungere AGENTS.md con: "Prima di toccare l'export GLB/JSON o il viewer, leggere docs/FUSION_GLB_CONTRACT.md. Le modifiche al contratto vanno proposte in PR su quel file."
    • VS Code Copilot Chat carica automaticamente AGENTS.md come instructions.
  2. Issue tracker condiviso (GitHub Projects / linear)

    • Un solo board con label contract per task che attraversano i due agent.
    • Ogni issue cross-side referenzia la riga di FUSION_GLB_CONTRACT.md rilevante.
  3. 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).

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.