From f161894d53d73d7e9b671dc950e9863e24a361f5 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Thu, 21 May 2026 15:54:29 +0200 Subject: [PATCH] feat(pose_3d): scene Three.js base (torso, polsi, UCS, grid) --- web/pose_3d.js | 173 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 web/pose_3d.js diff --git a/web/pose_3d.js b/web/pose_3d.js new file mode 100644 index 0000000..c82ae17 --- /dev/null +++ b/web/pose_3d.js @@ -0,0 +1,173 @@ +// Tab "Pose 3D" — viewport Three.js, gizmo translate/rotate, drag dei polsi. +// Vanilla JS, ES modules. + +import * as THREE from 'three'; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; +import { TransformControls } from 'three/addons/controls/TransformControls.js'; + +const $ = (id) => document.getElementById(id); +const POLL_MS = 1000; + +// ----- helpers geometria ----- +function makeUCS(size = 0.1) { + // 3 linee X(rosso) Y(verde) Z(blu) dalla origine + const g = new THREE.Group(); + const mat = (color) => new THREE.LineBasicMaterial({ color, linewidth: 2 }); + const make = (dir, color) => { + const geom = new THREE.BufferGeometry().setFromPoints([ + new THREE.Vector3(0, 0, 0), + new THREE.Vector3(dir.x * size, dir.y * size, dir.z * size), + ]); + return new THREE.Line(geom, mat(color)); + }; + g.add(make(new THREE.Vector3(1, 0, 0), 0xe74c3c)); + g.add(make(new THREE.Vector3(0, 1, 0), 0x3ecf8e)); + g.add(make(new THREE.Vector3(0, 0, 1), 0x4da3ff)); + return g; +} + +function makeWristMarker(color) { + const group = new THREE.Group(); + const sphere = new THREE.Mesh( + new THREE.SphereGeometry(0.025, 16, 12), + new THREE.MeshStandardMaterial({ color, metalness: 0.2, roughness: 0.5 }), + ); + group.add(sphere); + group.add(makeUCS(0.07)); + group.userData.sphere = sphere; + return group; +} + +// ----- state ----- +const state = { + three: null, // {scene, camera, renderer, orbit, transform} + arms: { right: null, left: null }, // wrist groups (real) + ghosts: { right: null, left: null }, // wrist groups (target preview) + links: { right: null, left: null }, // line shoulder→wrist + current: { right: null, left: null }, // {position, quaternion} + target: { right: null, left: null }, // null = nessuna modifica locale + active_side: 'right', + mode: 'editor', + gizmo_mode: 'translate', + precise: true, + saved_poses: {}, + live_max_step_m: 0.15, +}; + +// Posizioni torso e spalle hardcoded (riferimento visivo, non IK) +const TORSO_POS = new THREE.Vector3(0, 0, 0.40); +const SHOULDER = { + right: new THREE.Vector3(0, -0.20, 0.60), + left: new THREE.Vector3(0, 0.20, 0.60), +}; + +// ----- scene init ----- +function initScene() { + const canvas = $('p3Canvas'); + const renderer = new THREE.WebGLRenderer({ canvas, antialias: true }); + const rect = canvas.parentElement.getBoundingClientRect(); + renderer.setSize(rect.width, rect.height, false); + renderer.setPixelRatio(window.devicePixelRatio || 1); + + const scene = new THREE.Scene(); + scene.background = new THREE.Color(0x0d1014); + + const camera = new THREE.PerspectiveCamera(45, rect.width / rect.height, 0.05, 20); + camera.position.set(1, -1, 0.8); + camera.up.set(0, 0, 1); // Z up (coerente col base frame del robot) + camera.lookAt(0, 0, 0.3); + + scene.add(new THREE.HemisphereLight(0xffffff, 0x202030, 0.6)); + const dir = new THREE.DirectionalLight(0xffffff, 0.8); + dir.position.set(2, 2, 3); + scene.add(dir); + + // Grid + base UCS + const grid = new THREE.GridHelper(2, 20, 0x444444, 0x222222); + grid.rotation.x = Math.PI / 2; // piano XY (z=0) + scene.add(grid); + const baseUCS = makeUCS(0.2); + scene.add(baseUCS); + + // Torso + const torso = new THREE.Mesh( + new THREE.BoxGeometry(0.20, 0.40, 0.50), + new THREE.MeshStandardMaterial({ color: 0x4a5560, transparent: true, opacity: 0.7 }), + ); + torso.position.copy(TORSO_POS); + scene.add(torso); + + // Arms + for (const side of ['right', 'left']) { + const wrist = makeWristMarker(side === 'right' ? 0x4da3ff : 0xff5e7e); + wrist.position.copy(SHOULDER[side]); // posizione di partenza, sarà aggiornata dal polling + wrist.userData.side = side; + scene.add(wrist); + state.arms[side] = wrist; + + // Linea shoulder → wrist + const lineGeom = new THREE.BufferGeometry().setFromPoints([ + SHOULDER[side].clone(), + wrist.position.clone(), + ]); + const line = new THREE.Line(lineGeom, new THREE.LineBasicMaterial({ color: 0xaaaaaa })); + scene.add(line); + state.links[side] = line; + + // Ghost (preview target), inizialmente nascosto + const ghost = makeWristMarker(side === 'right' ? 0x4da3ff : 0xff5e7e); + ghost.userData.sphere.material = ghost.userData.sphere.material.clone(); + ghost.userData.sphere.material.transparent = true; + ghost.userData.sphere.material.opacity = 0.4; + ghost.visible = false; + scene.add(ghost); + state.ghosts[side] = ghost; + } + + const orbit = new OrbitControls(camera, renderer.domElement); + orbit.target.set(0, 0, 0.3); + orbit.update(); + + const transform = new TransformControls(camera, renderer.domElement); + transform.setMode('translate'); + // Disable orbit while dragging gizmo + transform.addEventListener('dragging-changed', (e) => { orbit.enabled = !e.value; }); + scene.add(transform); + + state.three = { scene, camera, renderer, orbit, transform }; + + // Resize handler + const onResize = () => { + const r = canvas.parentElement.getBoundingClientRect(); + renderer.setSize(r.width, r.height, false); + camera.aspect = r.width / r.height; + camera.updateProjectionMatrix(); + }; + window.addEventListener('resize', onResize); + + // Render loop + function animate() { + requestAnimationFrame(animate); + renderer.render(scene, camera); + } + animate(); +} + +// ----- init triggered on first activation ----- +let inited = false; +function ensureInit() { + if (inited) return; + if (!$('p3Canvas')) return; + inited = true; + initScene(); +} + +// Observe quando la tab diventa visibile per inizializzare +const tabObserver = new MutationObserver(() => { + if ($('tab-pose-3d')?.classList.contains('active')) ensureInit(); +}); +if ($('tab-pose-3d')) { + tabObserver.observe($('tab-pose-3d'), { attributes: true, attributeFilter: ['class'] }); + // Se è già attivo all'avvio + if ($('tab-pose-3d').classList.contains('active')) ensureInit(); +}