feat(pose_3d): IK 2-bone su URDF dopo Apply/Jog/GoPose (polso modello segue il target)
This commit is contained in:
1 parent
7f60238be9
commit
38848ad53c
1 file changed
+125
-1
+125
-1
@@ -74,6 +74,8 @@ const state = {
|
|||||||
precise: true,
|
precise: true,
|
||||||
saved_poses: {},
|
saved_poses: {},
|
||||||
live_max_step_m: 0.15,
|
live_max_step_m: 0.15,
|
||||||
|
robot: null, // URDF robot loaded via urdf-loader (null se non caricato)
|
||||||
|
arm_links_m: null, // {upper_m, fore_m} computati da URDF al load
|
||||||
};
|
};
|
||||||
|
|
||||||
function checkWorkspace(side, position_m) {
|
function checkWorkspace(side, position_m) {
|
||||||
@@ -142,7 +144,10 @@ async function loadG1Urdf(scene) {
|
|||||||
// alziamo il robot di ~0.793m cosi' i piedi toccano la griglia Z=0.
|
// alziamo il robot di ~0.793m cosi' i piedi toccano la griglia Z=0.
|
||||||
robot.position.z = 0.793;
|
robot.position.z = 0.793;
|
||||||
scene.add(robot);
|
scene.add(robot);
|
||||||
console.log('[pose_3d] G1 URDF caricato:', robot.name || 'g1');
|
state.robot = robot;
|
||||||
|
state.arm_links_m = measureArmLinkLengths(robot);
|
||||||
|
console.log('[pose_3d] G1 URDF caricato:', robot.name || 'g1',
|
||||||
|
'arm links (m):', state.arm_links_m);
|
||||||
resolve(robot);
|
resolve(robot);
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
@@ -151,6 +156,121 @@ async function loadG1Urdf(scene) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function measureArmLinkLengths(robot) {
|
||||||
|
// Misura le lunghezze dei link "upper arm" e "forearm" leggendo le posizioni
|
||||||
|
// dei joint right_shoulder_pitch -> right_elbow -> right_wrist_pitch
|
||||||
|
// (proxy per la lunghezza dell'avambraccio fino al polso).
|
||||||
|
// Default fallback: ~0.25 m upper, ~0.25 m forearm se i joint non si trovano.
|
||||||
|
const get = (n) => robot.joints && robot.joints[n] ? robot.joints[n] : null;
|
||||||
|
const shp = get('right_shoulder_pitch');
|
||||||
|
const elb = get('right_elbow');
|
||||||
|
const wrp = get('right_wrist_pitch');
|
||||||
|
if (!shp || !elb || !wrp) return { upper_m: 0.25, fore_m: 0.25 };
|
||||||
|
// World positions
|
||||||
|
const wShoulder = new THREE.Vector3();
|
||||||
|
const wElbow = new THREE.Vector3();
|
||||||
|
const wWrist = new THREE.Vector3();
|
||||||
|
shp.getWorldPosition(wShoulder);
|
||||||
|
elb.getWorldPosition(wElbow);
|
||||||
|
wrp.getWorldPosition(wWrist);
|
||||||
|
return {
|
||||||
|
upper_m: wShoulder.distanceTo(wElbow),
|
||||||
|
fore_m: wElbow.distanceTo(wWrist),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function solveTwoBoneIK(shoulderWorldPos, wristTargetWorldPos, L1, L2, poleHintDir) {
|
||||||
|
// Calcola angolo gomito e direzione shoulder->elbow per posizionare
|
||||||
|
// l'end-effector (polso) in `wristTargetWorldPos`.
|
||||||
|
// Restituisce {elbow_angle, shoulder_pitch, shoulder_roll, shoulder_yaw}
|
||||||
|
// basati su convenzione G1 (Unitree): pitch attorno a Y, roll attorno a X, yaw attorno a Z del frame spalla.
|
||||||
|
// I valori sono approssimati, calibrabili in seguito.
|
||||||
|
const sw = new THREE.Vector3().subVectors(wristTargetWorldPos, shoulderWorldPos);
|
||||||
|
let d = sw.length();
|
||||||
|
const dmax = (L1 + L2) * 0.999;
|
||||||
|
const dmin = Math.abs(L1 - L2) * 1.001;
|
||||||
|
if (d > dmax) d = dmax;
|
||||||
|
if (d < dmin) d = dmin;
|
||||||
|
sw.normalize();
|
||||||
|
|
||||||
|
// Angolo del gomito (law of cosines), 0 = teso, pi = ripiegato
|
||||||
|
const cosE = (L1*L1 + L2*L2 - d*d) / (2*L1*L2);
|
||||||
|
const elbow_angle = Math.PI - Math.acos(Math.max(-1, Math.min(1, cosE)));
|
||||||
|
|
||||||
|
// Direzione "verso la target" in coordinate base del polso shoulder
|
||||||
|
// Per il G1 right arm in zero pose: shoulder X = down (verso il basso), Z = forward.
|
||||||
|
// Approssimazione: shoulder_pitch ruota attorno a Y (sinistra-destra), shoulder_roll attorno a X (avanti-indietro).
|
||||||
|
// - pitch: angolo nel piano XZ (front/back vs down)
|
||||||
|
// - roll: angolo fuori dal piano (verso medial/lateral)
|
||||||
|
// - yaw: tipicamente 0 per drag puro (rotazione attorno a all'asse del braccio)
|
||||||
|
|
||||||
|
// pitch: alzare/abbassare il braccio = ruotare attorno a Y dello shoulder frame
|
||||||
|
// In zero pose dx, braccio giu' -> world (0, 0, -1). Vogliamo il braccio puntato lungo `sw`.
|
||||||
|
// pitch sposta nel piano X-Z (vista frontale: avanti/dietro)
|
||||||
|
// roll sposta lateralmente
|
||||||
|
const x = sw.x; // forward axis (X base = +x)
|
||||||
|
const y = sw.y; // lateral (sinistra +)
|
||||||
|
const z = sw.z; // up (+z)
|
||||||
|
|
||||||
|
// pitch: angolo nel piano XZ. In zero pose (-Z), pitch=0. pitch>0 alza in avanti (+X).
|
||||||
|
// arm pointing direction: from shoulder to wrist, in world frame.
|
||||||
|
// Se z=-1 (giù) e x=0,y=0 -> pitch=0, roll=0
|
||||||
|
// Se x=+1 (avanti) e z=0 -> pitch = +pi/2
|
||||||
|
const horiz = Math.sqrt(x*x + y*y);
|
||||||
|
const shoulder_pitch = Math.atan2(x, -z); // 0 = giù, +pi/2 = avanti
|
||||||
|
// roll: angolo dalla verticale lungo Y (asse laterale)
|
||||||
|
const shoulder_roll = Math.atan2(y, Math.sqrt(x*x + z*z)); // 0 = nel piano sagittale
|
||||||
|
// yaw: lasciato a 0 per ora (rotazione attorno al braccio); puo' essere
|
||||||
|
// tunato dal quaternion target del polso
|
||||||
|
const shoulder_yaw = 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
elbow_angle,
|
||||||
|
shoulder_pitch,
|
||||||
|
shoulder_roll,
|
||||||
|
shoulder_yaw,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyIKToUrdf(side, target_position, target_quaternion) {
|
||||||
|
// Aggiorna i joint dell'URDF G1 per posizionare il polso `side` al target.
|
||||||
|
// No-op se l'URDF non e' caricato.
|
||||||
|
if (!state.robot || !state.arm_links_m) return;
|
||||||
|
const robot = state.robot;
|
||||||
|
// Posizione spalla in world frame (legge dall'URDF live)
|
||||||
|
const shoulderName = `${side}_shoulder_pitch`;
|
||||||
|
const shJoint = robot.joints && robot.joints[shoulderName];
|
||||||
|
if (!shJoint) {
|
||||||
|
console.warn('[pose_3d] joint non trovato:', shoulderName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const shoulderWorld = new THREE.Vector3();
|
||||||
|
shJoint.getWorldPosition(shoulderWorld);
|
||||||
|
// Target in world (i target sono gia' in scene/world frame)
|
||||||
|
const wristWorld = new THREE.Vector3(target_position[0], target_position[1], target_position[2]);
|
||||||
|
|
||||||
|
const { upper_m, fore_m } = state.arm_links_m;
|
||||||
|
const ik = solveTwoBoneIK(shoulderWorld, wristWorld, upper_m, fore_m);
|
||||||
|
|
||||||
|
// Per il braccio sinistro, alcuni segni si invertono (mirror)
|
||||||
|
const sign = side === 'right' ? 1 : -1;
|
||||||
|
|
||||||
|
try {
|
||||||
|
robot.setJointValue(`${side}_shoulder_pitch`, ik.shoulder_pitch);
|
||||||
|
robot.setJointValue(`${side}_shoulder_roll`, ik.shoulder_roll * sign);
|
||||||
|
robot.setJointValue(`${side}_shoulder_yaw`, ik.shoulder_yaw);
|
||||||
|
robot.setJointValue(`${side}_elbow`, ik.elbow_angle);
|
||||||
|
// Wrist orientation: per ora teniamo 0 (raggiunge la posizione, l'orientazione
|
||||||
|
// non e' guidata dal target quaternion). Estensione futura: convertire il
|
||||||
|
// quaternion target a roll/pitch/yaw del polso URDF.
|
||||||
|
robot.setJointValue(`${side}_wrist_roll`, 0);
|
||||||
|
robot.setJointValue(`${side}_wrist_pitch`, 0);
|
||||||
|
robot.setJointValue(`${side}_wrist_yaw`, 0);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[pose_3d] setJointValue err:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ----- scene init -----
|
// ----- scene init -----
|
||||||
function initScene() {
|
function initScene() {
|
||||||
const canvas = $('p3Canvas');
|
const canvas = $('p3Canvas');
|
||||||
@@ -620,6 +740,7 @@ async function onApplyClick() {
|
|||||||
// commit locale: il target diventa il nuovo current (simulato)
|
// commit locale: il target diventa il nuovo current (simulato)
|
||||||
state.current[side] = { position: tgt.position, quaternion: tgt.quaternion };
|
state.current[side] = { position: tgt.position, quaternion: tgt.quaternion };
|
||||||
state.target[side] = null;
|
state.target[side] = null;
|
||||||
|
applyIKToUrdf(side, tgt.position, tgt.quaternion);
|
||||||
updateRealWrists();
|
updateRealWrists();
|
||||||
syncGhostsToTargetOrReal();
|
syncGhostsToTargetOrReal();
|
||||||
updateReadout();
|
updateReadout();
|
||||||
@@ -737,6 +858,7 @@ function bindJogAutoApply() {
|
|||||||
} else {
|
} else {
|
||||||
$('p3Status').textContent = `✓ Jog: raggiunto (residual ${(r.position_error_mm||0).toFixed(1)} mm)`;
|
$('p3Status').textContent = `✓ Jog: raggiunto (residual ${(r.position_error_mm||0).toFixed(1)} mm)`;
|
||||||
$('p3Status').className = 'p3-status ok';
|
$('p3Status').className = 'p3-status ok';
|
||||||
|
applyIKToUrdf(side, tgt.position, tgt.quaternion);
|
||||||
state.target[side] = null;
|
state.target[side] = null;
|
||||||
syncGhostsToTargetOrReal();
|
syncGhostsToTargetOrReal();
|
||||||
}
|
}
|
||||||
@@ -798,6 +920,8 @@ async function onGoPose() {
|
|||||||
$('p3Status').textContent = `✓ Posa "${name}" applicata` +
|
$('p3Status').textContent = `✓ Posa "${name}" applicata` +
|
||||||
(dry_run ? ' (simulato)' : '');
|
(dry_run ? ' (simulato)' : '');
|
||||||
$('p3Status').className = 'p3-status ok';
|
$('p3Status').className = 'p3-status ok';
|
||||||
|
const pose = state.saved_poses[name];
|
||||||
|
if (pose) applyIKToUrdf(pose.side, pose.position, pose.quaternion);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showError(e.message);
|
showError(e.message);
|
||||||
|
|||||||
Reference in new issue
Block a user