Skip to content

Commit da0fb02

Browse files
committed
Fix mobile viewer and hand orientation
1 parent cdcd978 commit da0fb02

42 files changed

Lines changed: 681 additions & 119 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/heroic-owls-land.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,7 @@
44
"posecode-mcp": patch
55
---
66

7-
Strengthen Posecode motion authoring and playback with strict contact validation, grounded multi-contact solving, continuous sparse transitions, and more lifelike canonical movement guidance.
7+
Strengthen Posecode motion authoring and playback with strict contact validation,
8+
grounded multi-contact solving, continuous sparse transitions, explicit forearm
9+
roll guidance, natural relaxed hands, a mobile-safe phase rail, and more lifelike
10+
canonical movements.

packages/posecode-eval/src/checks.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ import {
2121
kneeFlexionDeg,
2222
lowestPoint,
2323
palmFloorAngleDeg,
24+
palmForwardAngleDeg,
25+
palmInwardAngleDeg,
26+
palmUpAngleDeg,
2427
phaseMaxLandmarkSpeed,
2528
propPenetrationDepth,
2629
segmentTiltDeg,
@@ -535,6 +538,81 @@ export const MOVEMENT_CHECKS: MovementChecks[] = [
535538
(v) => v > 1.0,
536539
"wrist > 1.0m",
537540
),
541+
phaseCheck(
542+
"left-palm-up",
543+
"Curl",
544+
(p) => palmUpAngleDeg(p, "left"),
545+
(v) => v < 55,
546+
"< 55° from palm-up at peak flexion",
547+
),
548+
phaseCheck(
549+
"right-palm-up",
550+
"Curl",
551+
(p) => palmUpAngleDeg(p, "right"),
552+
(v) => v < 55,
553+
"< 55° from palm-up at peak flexion",
554+
),
555+
phaseCheck(
556+
"left-palm-forward-at-bottom",
557+
"Lower",
558+
(p) => palmForwardAngleDeg(p, "left"),
559+
(v) => v < 25,
560+
"< 25° from forward",
561+
),
562+
phaseCheck(
563+
"right-palm-forward-at-bottom",
564+
"Lower",
565+
(p) => palmForwardAngleDeg(p, "right"),
566+
(v) => v < 25,
567+
"< 25° from forward",
568+
),
569+
],
570+
},
571+
{
572+
movement: "jab-cross",
573+
checks: [
574+
phaseCheck(
575+
"jab-palm-down",
576+
"Jab",
577+
(p) => palmFloorAngleDeg(p, "left"),
578+
(v) => v < 35,
579+
"< 35° from palm-down",
580+
),
581+
phaseCheck(
582+
"cross-palm-down",
583+
"Cross",
584+
(p) => palmFloorAngleDeg(p, "right"),
585+
(v) => v < 35,
586+
"< 35° from palm-down",
587+
),
588+
phaseCheck(
589+
"lead-guard-palm-in",
590+
"Recoil cross",
591+
(p) => palmInwardAngleDeg(p, "left"),
592+
(v) => v < 20,
593+
"< 20° from inward",
594+
),
595+
phaseCheck(
596+
"rear-guard-palm-in",
597+
"Recoil cross",
598+
(p) => palmInwardAngleDeg(p, "right"),
599+
(v) => v < 20,
600+
"< 20° from inward",
601+
),
602+
phaseCheck(
603+
"lead-guard-high",
604+
"Recoil cross",
605+
(p) => heightOf(p, "wrist_left"),
606+
(v) => v > 1.45,
607+
"> 1.45m",
608+
),
609+
phaseCheck(
610+
"rear-guard-high",
611+
"Recoil cross",
612+
(p) => heightOf(p, "wrist_right"),
613+
(v) => v > 1.45,
614+
"> 1.45m",
615+
),
538616
],
539617
},
540618
{

packages/posecode-eval/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ export {
2929
kneeFlexionDeg,
3030
lowestPoint,
3131
palmFloorAngleDeg,
32+
palmForwardAngleDeg,
33+
palmInwardAngleDeg,
34+
palmUpAngleDeg,
3235
phaseMaxLandmarkSpeed,
3336
segmentTiltDeg,
3437
soleUpAngleDeg,

packages/posecode-eval/src/metrics.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,31 @@ export function palmFloorAngleDeg(pose: PhasePose, side: "left" | "right"): numb
107107
return angleBetweenDeg(rotateByQuat([0, 0, 1], q), [0, -1, 0]);
108108
}
109109

110+
/** Angle between the palm face normal and world-up (0 = palm facing up). */
111+
export function palmUpAngleDeg(pose: PhasePose, side: "left" | "right"): number {
112+
const q = pose.boneQuaternions.get(`wrist_${side}`);
113+
if (!q) return 180;
114+
return angleBetweenDeg(rotateByQuat([0, 0, 1], q), [0, 1, 0]);
115+
}
116+
117+
/** Angle between the palm face normal and character-forward (+Z at zero yaw). */
118+
export function palmForwardAngleDeg(pose: PhasePose, side: "left" | "right"): number {
119+
const q = pose.boneQuaternions.get(`wrist_${side}`);
120+
if (!q) return 180;
121+
const forward: Vec3 = [Math.sin(pose.rootYaw), 0, Math.cos(pose.rootYaw)];
122+
return angleBetweenDeg(rotateByQuat([0, 0, 1], q), forward);
123+
}
124+
125+
/** Angle between a palm and the body's lateral midline direction. */
126+
export function palmInwardAngleDeg(pose: PhasePose, side: "left" | "right"): number {
127+
const q = pose.boneQuaternions.get(`wrist_${side}`);
128+
if (!q) return 180;
129+
const inward = side === "left"
130+
? sub(bone(pose, "shoulder_right"), bone(pose, "shoulder_left"))
131+
: sub(bone(pose, "shoulder_left"), bone(pose, "shoulder_right"));
132+
return angleBetweenDeg(rotateByQuat([0, 0, 1], q), inward);
133+
}
134+
110135
/** Angle between the semantic fist's knuckle direction and floor-down. */
111136
export function fistFloorAngleDeg(pose: PhasePose, side: "left" | "right"): number {
112137
const q = pose.boneQuaternions.get(`wrist_${side}`);

packages/posecode-language/src/hover.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,13 @@ export function getHover(
5757
if (bone && token !== "hold") {
5858
const rom = romFor(bone, token);
5959
if (rom) {
60+
const detail = KEYWORD_DOCS[token];
6061
return md(
61-
`**${boneType(bone)} · ${token}**: configured range **${rom.min}${rom.max}°**. Angles beyond this are clamped with a diagnostic.`,
62+
`**${boneType(bone)} · ${token}**: configured range **${rom.min}${rom.max}°**. Angles beyond this are clamped with a diagnostic.${detail ? ` ${detail}` : ""}`,
6263
);
6364
}
6465
}
65-
return md(`Action **${token}**.`);
66+
return md(`Action **${token}**.${KEYWORD_DOCS[token] ? ` ${KEYWORD_DOCS[token]}` : ""}`);
6667
}
6768

6869
if (JOINT_NAMES.includes(token)) {

packages/posecode-language/src/vocab.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,6 @@ export const KEYWORD_DOCS: Record<string, string> = {
7474
travel: "Moves the figure across the floor: `travel: 0.4 0` (world x z metres from the start spot). Absolute, carried across phases. Standing poses only.",
7575
cue: "A short coaching cue shown while this phase plays.",
7676
hold: "Reset every rotation channel on this joint to its neutral / rest angle: `<joint>: hold neutral`.",
77+
pronate: "Rolls the forearm toward palm-down. With upright arms at the sides, about 80° faces the palm inward toward the thigh; final world facing also depends on the arm pose.",
78+
supinate: "Rolls the forearm in the palm-up direction; final world facing also depends on the shoulder and elbow pose.",
7779
};

packages/posecode-language/test/language.test.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ describe("getCompletions", () => {
5959

6060
it("suggests joints (and child keywords) at the start of an indented line", () => {
6161
const l = onLine(" ", 4);
62-
expect(l).toEqual(expect.arrayContaining(["knees", "elbows"]));
62+
expect(l).toEqual(expect.arrayContaining(["knees", "elbows", "forearms"]));
6363
expect(l).toContain("cue");
6464
});
6565

@@ -87,6 +87,15 @@ describe("getCompletions", () => {
8787
expect(chestActions).not.toContain("rotate-out");
8888
});
8989

90+
it("offers and explains anatomical forearm rotation", () => {
91+
expect(onLine(" forearms: ", 14)).toEqual(
92+
expect.arrayContaining(["pronate", "supinate"]),
93+
);
94+
const line = " forearms: pronate 80";
95+
const hover = getHover(line, 0, line.indexOf("pronate") + 1);
96+
expect(hover?.contents).toContain("thigh");
97+
});
98+
9099
it("suggests timing modes inside a step header", () => {
91100
expect(onLine(' step "y" 2s ', 14)).toEqual(
92101
expect.arrayContaining(["flow", "settle", "linear"]),

packages/posecode-mcp/src/guide.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,13 @@ posecode <kind> "<Name>" # kind = exercise | stretch | posture
6060
\`\`\`
6161
6262
Joints: neck head spine chest pelvis, and (singular or plural) shoulders elbows
63-
wrists hips knees ankles. Actions (degrees are absolute targets): flex/extend,
63+
forearms wrists hips knees ankles. \`forearms\` aliases the elbow bones for palm
64+
roll. Actions (degrees are absolute targets): flex/extend,
6465
abduct/adduct, rotate-in/rotate-out (shoulder/hip), twist-left/twist-right
65-
(axial joints), dorsiflex/plantarflex, hold neutral, and hinge (pelvis only).
66+
(axial joints), supinate/pronate (forearm roll), dorsiflex/plantarflex, hold
67+
neutral, and hinge (pelvis only). With upright arms at the sides,
68+
\`forearms: pronate 80\` faces the palms inward toward the thighs.
69+
At zero degrees, \`pronate 0\` and \`supinate 0\` are the same absolute target.
6670
Use only joint/action pairs and declared prop anchors accepted by the validator.
6771
Author the gross pose before reach; a parsed reach is not proof of contact.
6872
Keep cues, sides, and declared contacts consistent through every phase. Floor

packages/posecode-parser/src/joints.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ const FINGERS_RIGHT: BoneId[] = [
7070
const GROUPS: Record<string, BoneId[]> = {
7171
shoulders: ["shoulder_left", "shoulder_right"],
7272
elbows: ["elbow_left", "elbow_right"],
73+
// Anatomical authoring alias: forearm axial rotation lives on the elbow
74+
// bone in the rig, but `forearms: pronate 80` is clearer to authors.
75+
forearms: ["elbow_left", "elbow_right"],
7376
wrists: ["wrist_left", "wrist_right"],
7477
hips: ["hip_left", "hip_right"],
7578
knees: ["knee_left", "knee_right"],
@@ -196,8 +199,10 @@ const ACTIONS: Record<string, ActionAxis> = {
196199
// toward the person's left (+X), and -Y turns it right (-X).
197200
"twist-left": { axis: "y", sign: 1 },
198201
"twist-right": { axis: "y", sign: -1 },
199-
supinate: { axis: "y", sign: 1 },
200-
pronate: { axis: "y", sign: -1 },
202+
// From the palm-forward driver rest, pronation turns each palm toward its
203+
// own thigh. The left-side mirror in clamp.ts supplies the opposite sign.
204+
supinate: { axis: "y", sign: -1 },
205+
pronate: { axis: "y", sign: 1 },
201206
// The foot points FORWARD (+Z): lifting the toes toward the shin
202207
// (dorsiflexion) is a -X rotation, pointing them is +X.
203208
dorsiflex: { axis: "x", sign: -1 },

packages/posecode-parser/src/types.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,10 @@ export interface ReachTarget {
4747

4848
/**
4949
* A contact pin: translate the whole figure so `effector` sits on a fixed
50-
* `anchor` (a prop anchor, a landmark, or `floor`). Unlike a reach (which moves
51-
* the limb to a target) a pin moves the BODY, so the figure can hang from a bar,
52-
* rise onto a box, or lower into a dip while the contact stays put.
50+
* world `anchor` (a declared prop anchor or `floor`). Unlike a reach (which
51+
* moves the limb to a target) a pin moves the BODY, so a body landmark cannot
52+
* serve as its anchor: that landmark would move with the same root. Pins let the
53+
* figure hang from a bar, rise onto a box, or keep one floor support fixed.
5354
*/
5455
export interface PinTarget {
5556
effector: string;

0 commit comments

Comments
 (0)