Skip to content

Commit 83494a6

Browse files
authored
Merge pull request #323 from whuang37/sim-hand-convention-fix
Sim Hand Pose Migration to Angular Representation
2 parents b52c72c + 2040254 commit 83494a6

17 files changed

Lines changed: 751 additions & 1413 deletions

demos/sim_hand_poses/README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Simulator Hand Poses Demo
2+
3+
Small developer demo for inspecting and authoring simulator hand poses.
4+
5+
The demo shows both simulator hands, exposes per-joint semantic rotation
6+
controls, and displays the resolved raw joint transforms produced by forward
7+
kinematics. It is useful when tuning preset hand poses, checking gesture
8+
recognition behavior, or copying rotation JSON into simulator pose data.
9+
10+
## Controls
11+
12+
- Use the simulator hand pose controls to switch between built-in poses.
13+
- Use the sidebar sliders to edit the currently displayed pose rotations.
14+
- Rotation sliders are shown in degrees from `-180` to `180`.
15+
- The runtime hand rotation API still uses radians.
16+
- The controls mirror the active simulator hand and update as the displayed
17+
pose changes.
18+
- `Reset` returns the edited hand rotations to neutral.
19+
20+
## JSON Views
21+
22+
- `Raw JSON` shows the currently displayed WebXR-style joint transforms after
23+
FK resolution.
24+
- `Rotations JSON` shows the semantic rotation data for the active hand.
25+
- Use `Copy` to copy either JSON view.
26+
27+
## AI Prompt
28+
29+
The prompt bubble can generate semantic hand rotations from a text
30+
description when AI support is configured. Generated rotations are applied to
31+
both hands and can then be inspected or adjusted with the sliders.

demos/sim_hand_poses/main.js

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,14 @@ Rotations are applied onto a flat neutral hand pose.
2222
Rotations are applied through forward kinematics.
2323
Format: {"joint-name":[x,y,z]} where x/y/z are euler angle radians.
2424
For long fingers:
25-
x: flexion/extension. Negative curls toward palm, positive extends away.
26-
y: abduction/adduction. Negative spreads toward thumb, positive away.
27-
z: twist. Negative twists away from thumb, positive toward thumb.
28-
Prefer not to change the thumb metacarpal joint.
25+
x: flexion/extension. Positive flexes toward palm, negative extends away.
26+
y: abduction/adduction. Positive spreads away from the middle-finger axis, negative adducts toward it.
27+
z: axial roll. Positive rolls toward thumb, negative rolls away from thumb.
28+
For the middle finger, y is radial/ulnar deviation: positive moves toward index/thumb, negative toward ring/pinky.
29+
For the thumb:
30+
x: positive flexes across palm, negative extends/repositions.
31+
y: positive abducts away from palm, negative adducts back toward palm.
32+
z: positive rolls into opposition/internal rotation, negative repositions/external rotation away.
2933
Include every non-tip WebXR joint listed below. Use [0,0,0] for neutral joints:
3034
${ROTATION_JOINT_NAMES.join(', ')}
3135
`;
@@ -73,6 +77,15 @@ function cleanRotationsForJson(rotations) {
7377
return cleanRotations;
7478
}
7579

80+
function copyRotations(target, source) {
81+
for (const jointName of ROTATION_JOINT_NAMES) {
82+
const rotation = source[jointName] ?? [0, 0, 0];
83+
target[jointName][0] = rotation[0];
84+
target[jointName][1] = rotation[1];
85+
target[jointName][2] = rotation[2];
86+
}
87+
}
88+
7689
function cleanJointsForJson(joints) {
7790
return joints.map((joint) => ({
7891
t: joint.t.map(toFixedNumber),
@@ -380,6 +393,17 @@ class ManualSimHandScene extends xb.Script {
380393
}
381394
}
382395

396+
class DisplayedPoseSync extends xb.Script {
397+
constructor(syncDisplayedPose) {
398+
super();
399+
this._syncDisplayedPose = syncDisplayedPose;
400+
}
401+
402+
update() {
403+
this._syncDisplayedPose();
404+
}
405+
}
406+
383407
class GestureHUD extends xb.Script {
384408
init() {
385409
this._active = {
@@ -498,19 +522,35 @@ async function start() {
498522

499523
let updateJsonViews = () => {};
500524

525+
const getActiveHandRotations = () => {
526+
const activeHandIndex =
527+
xb.core.simulator.controls.simulatorControllerState
528+
.currentControllerIndex;
529+
return activeHandIndex === 0
530+
? xb.core.simulator.hands.leftHandCurrentRotations
531+
: xb.core.simulator.hands.rightHandCurrentRotations;
532+
};
533+
534+
const getDisplayedJoints = (bones) =>
535+
bones.map((bone) => ({
536+
t: bone.position.toArray(),
537+
r: bone.quaternion.toArray(),
538+
s: bone.scale.toArray(),
539+
}));
540+
501541
const applyHandRotations = () => {
502542
xb.core.simulator.hands.setLeftHandRotations(handRotations);
503543
xb.core.simulator.hands.setRightHandRotations(handRotations);
504544
updateJsonViews();
505545
};
506546

507-
const syncControlsToRotations = () => {
547+
const syncControlsToRotations = (rotations = getActiveHandRotations()) => {
508548
for (const input of document.querySelectorAll(
509549
'.manual-sim-hand-slider input[type="range"]'
510550
)) {
511551
const axisIndex = ['x', 'y', 'z'].indexOf(input.dataset.axis);
512552
const degrees = Math.round(
513-
handRotations[input.dataset.joint][axisIndex] / DEG_TO_RAD
553+
(rotations[input.dataset.joint]?.[axisIndex] ?? 0) / DEG_TO_RAD
514554
);
515555
input.value = String(degrees);
516556
input.nextElementSibling.value = String(degrees);
@@ -519,27 +559,36 @@ async function start() {
519559

520560
updateJsonViews = createSidebar(
521561
(jointName, axis, value) => {
562+
copyRotations(handRotations, getActiveHandRotations());
522563
handRotations[jointName][['x', 'y', 'z'].indexOf(axis)] = value;
523564
applyHandRotations();
524565
},
525566
() => {
526567
for (const rotation of Object.values(handRotations)) {
527568
rotation.fill(0);
528569
}
529-
syncControlsToRotations();
570+
syncControlsToRotations(handRotations);
530571
applyHandRotations();
531572
},
532573
() => ({
533574
raw: {
534-
left: cleanJointsForJson(xb.core.simulator.hands.leftHandTargetJoints),
575+
left: cleanJointsForJson(
576+
getDisplayedJoints(xb.core.simulator.hands.leftHandBones)
577+
),
535578
right: cleanJointsForJson(
536-
xb.core.simulator.hands.rightHandTargetJoints
579+
getDisplayedJoints(xb.core.simulator.hands.rightHandBones)
537580
),
538581
},
539-
rotations: cleanRotationsForJson(handRotations),
582+
rotations: cleanRotationsForJson(getActiveHandRotations()),
540583
})
541584
);
542585

586+
const syncDisplayedPose = () => {
587+
syncControlsToRotations();
588+
updateJsonViews();
589+
};
590+
xb.add(new DisplayedPoseSync(syncDisplayedPose));
591+
543592
createPromptBubble(async (description) => {
544593
xb.core.options.ai.gemini.config = {
545594
responseMimeType: 'application/json',
@@ -558,7 +607,7 @@ async function start() {
558607
handRotations[jointName][1] = generatedRotation[1];
559608
handRotations[jointName][2] = generatedRotation[2];
560609
}
561-
syncControlsToRotations();
610+
syncControlsToRotations(handRotations);
562611
applyHandRotations();
563612
});
564613
}

0 commit comments

Comments
 (0)