Files
syncro_multi_agente/FUSION_GLB_CONTRACT.md

224 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`
```jsonc
{
"metadata": {
"units": "mm",
"internalUnit": "cm",
"scaleFactor": 0.01
},
"joints": [ /* Joint */ ],
"asBuiltJoints": [ /* Joint */ ],
"motionLinks": [ /* MotionLink */ ],
"rigidGroups": [ /* RigidGroup */ ]
}
```
### 2.1 `Joint`
```jsonc
{
"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`
```jsonc
{
"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`
```jsonc
{
"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:
```jsonc
{
"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:
```python
# 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:
```python
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.