# Three.js — come usare plotter.glb ## Caricamento e accesso a joints/motion links ```javascript import * as THREE from 'three'; import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; const loader = new GLTFLoader(); loader.load('plotter.glb', (gltf) => { const root = gltf.scene; scene.add(root); // ---------------------------------------------------------- // 1) Dati Fusion (joints, asBuiltJoints, motionLinks) // Salvati in scene.extras.fusion come stringa JSON // ---------------------------------------------------------- const sceneIndex = gltf.parser.json.scene ?? 0; const sceneJson = gltf.parser.json.scenes[sceneIndex]; let fusion = null; try { const raw = sceneJson?.extras?.fusion; fusion = raw ? JSON.parse(raw) : null; } catch (e) { console.warn('Fusion extras non parsabile', e); } if (fusion) { console.log('Joints:', fusion.joints.length); console.log('AsBuilt:', fusion.asBuiltJoints.length); console.log('MotionLinks:', fusion.motionLinks.length); console.log('Metadata:', fusion.metadata); } // ---------------------------------------------------------- // 2) Indice nome -> Object3D per trovare i nodi del joint // Ogni Empty ha userData.fusion_fullPathName // ---------------------------------------------------------- const byFullPath = new Map(); root.traverse((obj) => { const fp = obj.userData?.fusion_fullPathName; if (fp) byFullPath.set(fp, obj); }); // ---------------------------------------------------------- // 3) Esempio: applicare un joint slider // ---------------------------------------------------------- const j = fusion.joints.find(jt => jt.type === 'slider'); if (j) { const child = byFullPath.get(j.childFullPath); if (child) { const axis = new THREE.Vector3(...j.axis).normalize(); // Sposta il child lungo l'asse di 100 mm = 0.1 m child.position.addScaledVector(axis, 0.1); } } // ---------------------------------------------------------- // 4) Esempio: applicare un joint revolute // ---------------------------------------------------------- const r = fusion.joints.find(jt => jt.type === 'revolute'); if (r) { const child = byFullPath.get(r.childFullPath); if (child) { const axis = new THREE.Vector3(...r.axis).normalize(); const angle = Math.PI / 4; // 45 gradi // Rotazione attorno all'asse, applicata in local space child.rotateOnAxis(axis, angle); } } }); ``` ## Struttura di `scene.extras.fusion` ```jsonc { "metadata": { "units": "mm", "scaleFactor": 0.01, ... }, "joints": [ { "name": "joint_carrello_y", "type": "slider", // rigid | revolute | slider | cylindrical | pin_slot | planar | ball "parent": "CARRELLO_X", "child": "CARRELLO_Y", "parentFullPath": "TELAIO:1+CARRELLO_X:1", "childFullPath": "TELAIO:1+CARRELLO_X:1+CARRELLO_Y:1", "origin": [x, y, z], // in metri "axis": [x, y, z], // direzione unitaria "secondaryAxis": [...], // solo pin_slot / planar "slideLimits": { "minimumValue": 0, "maximumValue": 0.22, ... }, // metri "rotationLimits": { ... }, // radianti "slideValue": 0.05, // metri (posizione corrente) "rotationValue": 0.5, // radianti "isSuppressed": false, "isLightBulbOn": true, "isLocked": false } ], "asBuiltJoints": [ /* stessa shape di joints */ ], "motionLinks": [ { "name": "ML1", "joint1": "joint_motor", "joint2": "joint_belt", "ratio": 2.5, "reversed": false, "isSuppressed": false } ] } ``` ## Note - **Unità:** GLB e dati joint sono in **metri**. - **Asse Y-up:** convenzione glTF (cambiata da Blender al momento dell'export). - **Nodi Fusion:** ogni Empty Blender ha `userData.fusion_fullPathName`, `userData.fusion_componentName`, `userData.fusion_id` (visibili in `Object3D.userData` lato Three.js). - **Mesh:** sono parentate sotto gli Empty corrispondenti. Per muovere un sotto-assieme, sposta l'Empty (Three.js eredita le trasformazioni come ti aspetti). - **Motion links:** la logica di "vincolo" tra joints (ratio, reversed) la devi implementare tu nel render loop: leggi `slideValue/rotationValue` di joint1, calcola joint2 = joint1 × ratio (con segno), applica a `byFullPath.get(joint2.childFullPath)`.