Files
Presa-Mano/web/pose_3d.js
T

174 lines
5.8 KiB
JavaScript

// 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();
}