Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/calm-feet-land.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"posecode-parser": patch
"posecode-render": patch
"posecode-mcp": patch
---

Add a ballet first-position start pose, keep superhero landing contacts stable
with an inward planted fist, and prevent visible foot friction in demi-plié.
34 changes: 34 additions & 0 deletions packages/posecode-eval/src/checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,33 @@ export const MOVEMENT_CHECKS: MovementChecks[] = [
},
],
},
{
// A demi-plié keeps the turnout established at the hips while both feet
// remain planted. Checking each ankle independently catches the symmetric
// outward/inward skate that a center-only ground-lock metric cannot see.
movement: "demi-plie",
checks: [
(result) => {
if (result.phases.length < 2) {
return { id: "demi-plie-feet-stay-planted", pass: false, detail: "plié phases missing" };
}
let maxDrift = 0;
for (let i = 1; i < result.phases.length; i++) {
for (const side of ["left", "right"] as const) {
maxDrift = Math.max(
maxDrift,
footSkateDistance(result.phases[i - 1]!, result.phases[i]!, side),
);
}
}
return {
id: "demi-plie-feet-stay-planted",
pass: maxDrift < 0.015,
detail: `${maxDrift.toFixed(3)}m maximum ankle drift (want < 0.015m)`,
};
},
],
},
{
// Three-point landing: the front sole, rear knee, and opposite fist must
// form distinct supports while the torso stays above the floor. Contact
Expand All @@ -339,6 +366,13 @@ export const MOVEMENT_CHECKS: MovementChecks[] = [
(v) => v >= 65 && v <= 100,
"65–100° character-forward pitch",
),
phaseCheck(
"planted-fist-palm-inward",
"Hold the landing",
(p) => palmInwardAngleDeg(p, "left"),
(v) => v < 30,
"< 30° from inward",
),
(result) => {
const p = phase(result, "Hold the landing");
if (!p) return { id: "exact-three-supports", pass: false, detail: "phase \"Hold the landing\" not found" };
Expand Down
10 changes: 10 additions & 0 deletions packages/posecode-eval/test/eval.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,4 +286,14 @@ describe("fixture scorecard", () => {
expect(movement.passed).toBeLessThan(movement.total);
expect(failed.some((check) => check.id.startsWith("contact-position:"))).toBe(true);
});

it("rejects a superhero landing whose planted fist faces away from the body", () => {
const fixtures = loadFixtures(examplesDir);
const source = fixtures.find((fixture) => fixture.movement === "superhero-landing")!.source;
const outward = source.replace("elbow_left: pronate 80", "elbow_left: supinate 80");
const check = runEval([{ movement: "superhero-landing", source: outward }])
.movements[0]!.checks.find((item) => item.id === "planted-fist-palm-inward");

expect(check).toEqual(expect.objectContaining({ pass: false }));
});
});
2 changes: 1 addition & 1 deletion packages/posecode-language/src/vocab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export const KEYWORD_DOCS: Record<string, string> = {
posecode: 'Document header: `posecode <kind> "<name>"`.',
rig: "Selects the rig (currently `humanoid`).",
prop: "Adds a scene object: `prop chair | wall | bar | box | dip-bars`. Supplies declared reach, pin, and grip anchors.",
pose: "Sets the starting pose: `pose start = standing | neutral | plank | supine | prone | seated`.",
pose: "Sets the starting pose: `pose start = standing | first-position | neutral | plank | supine | prone | seated`.",
start: "Used in `pose start = <pose>`.",
clip: 'Optional mocap clip: `clip "walk"`. A renderer with a matching retargeted animation plays it crossfaded over the procedural pose; others ignore it.',
step: 'A movement phase: `step "<name>" <Ns> <mode>:` where mode is flow | settle | drive | snap | linear.',
Expand Down
2 changes: 1 addition & 1 deletion packages/posecode-mcp/src/guide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ say that Posecode cannot yet represent the missing capability.
posecode <kind> "<Name>" # kind = exercise | stretch | posture
rig humanoid
prop <type> # optional: chair | wall | bar | box | dip-bars
pose start = <pose> # neutral | standing | plank | supine | prone | seated
pose start = <pose> # neutral | standing | first-position | plank | supine | prone | seated
step "<Phase>" <Ns> <mode>: # mode = flow | settle | drive | snap | linear
<joint>: <action> <degrees>
ground-lock: <contacts> # repeat feet/hands/forearms/back or side-specific supports
Expand Down
1 change: 1 addition & 0 deletions packages/posecode-parser/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type RigName = (typeof RIG_NAMES)[number];
export const START_POSE_NAMES = [
"neutral",
"standing",
"first-position",
"plank",
"supine",
"prone",
Expand Down
14 changes: 14 additions & 0 deletions packages/posecode-render/src/poses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,19 @@ const STANDING: PoseSpec = {
joints: { ...RELAXED_FOREARMS },
};

// Ballet first position: legs externally rotated from the hips while the
// straight-leg chain stays vertical. Seeding turnout in the base pose means a
// demi-plié can keep it constant instead of visibly twisting planted feet on
// every descent and rise.
const FIRST_POSITION: PoseSpec = {
root: { position: [0, 0, 0], rotationDeg: [0, 0, 0] },
joints: {
...RELAXED_FOREARMS,
hip_left: [0, 30, 0],
hip_right: [0, -30, 0],
},
};

// Lying face-up. Rotating the standing figure -90° about X lays it on its back:
// the original front (+Z) ends up facing the ceiling (+Y) and the head points
// toward -Z. groundFigure() (bounding-box drop) then rests the back on the floor.
Expand Down Expand Up @@ -81,6 +94,7 @@ const SEATED: PoseSpec = {
const POSES: Record<string, PoseSpec> = {
neutral: NEUTRAL,
standing: STANDING,
"first-position": FIRST_POSITION,
plank: PLANK,
supine: SUPINE,
prone: PRONE,
Expand Down
44 changes: 44 additions & 0 deletions packages/posecode-render/test/render.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { describe, it, expect } from "vitest";
import fs from "node:fs";
import * as THREE from "three";
import { buildMannequin } from "../src/mannequin.js";
import { buildTimeline } from "../src/timeline.js";
Expand Down Expand Up @@ -473,6 +474,49 @@ describe("ground-lock (shared solver)", () => {
return m;
}

it("keeps the canonical demi-plié turned out without foot friction", () => {
const source = fs.readFileSync(
new URL("../../../spec/examples/demi-plie.posecode", import.meta.url),
"utf8",
);
const { ir, errors } = parse(source);
expect(errors).toEqual([]);
const timeline = buildTimeline(ir!);
const m = buildMannequin();

expect(timeline.basePose.joints?.hip_left?.[1]).toBe(30);
expect(timeline.basePose.joints?.hip_right?.[1]).toBe(-30);

timeline.sample(0, m.bones);
groundFigure(m);
const basePosition = m.root.position.clone();
const baseRotation = m.root.quaternion.clone();
const anchors = new Map(
["ankle_left", "ankle_right"].map((id) => [
id,
m.bones.get(id)!.getWorldPosition(new THREE.Vector3()),
]),
);

let maxDrift = 0;
for (let t = 0; t < timeline.duration; t += 0.025) {
m.root.position.copy(basePosition);
m.root.quaternion.copy(baseRotation);
const info = timeline.sample(t, m.bones);
m.root.updateMatrixWorld(true);
applyGroundLock(m, info.groundLock, anchors);
levelPlantedFeet(m, info.groundLock);
m.root.updateMatrixWorld(true);
for (const id of ["ankle_left", "ankle_right"]) {
const point = m.bones.get(id)!.getWorldPosition(new THREE.Vector3());
const anchor = anchors.get(id)!;
maxDrift = Math.max(maxDrift, Math.hypot(point.x - anchor.x, point.z - anchor.z));
}
}

expect(maxDrift).toBeLessThan(0.01);
});

it("drops the body and plants the foot mesh for a feet-only squat", () => {
const m = posedRaw(
[
Expand Down
6 changes: 3 additions & 3 deletions playground/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -177,14 +177,14 @@ <h1 class="headline">
<div class="studio-hud"><span id="hero-phase">Drop into the landing</span></div>
</div>

<!-- Overlapping code chip: an excerpt from the contact phase on screen -->
<!-- Contact-phase excerpt, kept below the viewer so it never hides the motion. -->
<pre class="code-chip" aria-hidden="true"># contact-phase excerpt
<span class="t-kw">posecode</span> <span class="t-kind">posture</span> <span class="t-str">"Superhero Three-Point Landing"</span>
<span class="t-kw">pose</span> start <span class="t-punct">=</span> <span class="t-atom">standing</span>

<span class="t-kw">step</span> <span class="t-str">"Make three-point contact"</span> <span class="t-num">0.3s</span> <span class="t-atom">settle</span><span class="t-punct">:</span>
<span class="t-kw">pin</span><span class="t-punct">:</span> <span class="t-atom">knee_left floor</span>
<span class="t-kw">reach</span><span class="t-punct">:</span> <span class="t-atom">foot_right floor</span>
<span class="t-kw">ground-lock</span><span class="t-punct">:</span> <span class="t-atom">foot_right</span>
<span class="t-kw">reach</span><span class="t-punct">:</span> <span class="t-atom">knee_left floor</span>
<span class="t-kw">reach</span><span class="t-punct">:</span> <span class="t-atom">fist_left floor</span>
<span class="t-kw">cue</span> <span class="t-str">"Set all three contacts"</span></pre>
</div>
Expand Down
4 changes: 2 additions & 2 deletions playground/public/llm-guide.html
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ <h2>Grammar</h2>
<pre class="code-block"><code>posecode &lt;kind&gt; &quot;&lt;Name&gt;&quot; # kind = exercise | stretch | posture
rig humanoid
prop &lt;type&gt; # optional: chair | wall | bar | box | dip-bars (repeatable)
pose start = &lt;pose&gt; # neutral | standing | plank | supine | prone | seated
pose start = &lt;pose&gt; # neutral | standing | first-position | plank | supine | prone | seated
step &quot;&lt;Phase name&gt;&quot; &lt;Ns&gt; &lt;mode&gt;: # mode = flow | settle | drive | snap | linear
&lt;joint&gt;: &lt;action&gt; &lt;degrees&gt;
reach: &lt;effector&gt; &lt;target&gt; # limb IK to a landmark, floor, or declared prop anchor
Expand Down Expand Up @@ -209,7 +209,7 @@ <h2>Reaching, props, lying poses &amp; hands</h2>
ground-lock: feet
cue &quot;Hinge and reach toward the ankles&quot;</code></pre>
<p>A hand or fist sent to the floor is also oriented onto its palm or knuckles. That explicit surface contact can adjust forearm/wrist roll within ROM, so do not fight it with a contradictory palm-facing cue.</p>
<ul><li><strong>Props</strong>: <code>prop chair | wall | bar | box | dip-bars</code> (top level). The chair sits behind the figure (sit-to-stand, box squat), the wall behind that (wall sit), the bar overhead, the box in front (step-ups), and the dip bars either side at hip-press height (<code>grip: hands bars</code> + elbow flex = triceps dips).</li><li><strong>Pins</strong>: <code>pin: &lt;effector&gt; &lt;anchor&gt;</code> moves the whole BODY so the effector sits on the anchor (vs <code>reach</code>, which moves just the limb). Use one primary pin for body translation: <code>pin: foot_right box</code> can support a step-up, and <code>pin: pelvis floor</code> can keep the pelvis on the mat. Use <code>grip</code>, not several simultaneous hand pins, for a bar or rails.</li><li><strong>Grips</strong>: <code>grip: hands bar</code> or <code>grip: hands bars</code> is the dedicated two-hand prop contact. It assigns separate left/right anchors, solves both arms, and closes the fingers. Declare <code>prop bar</code> or <code>prop dip-bars</code> first. Prefer this to multiple pins for a pull-up, hang, or dip.</li><li><strong>Lying / seated</strong>: <code>pose start = supine | prone | seated</code> for floor and mat work (glute bridge, dead bug, cobra, seated forward fold). In a supine exercise whose torso stays down, add <code>ground-lock: back</code> to each phase.</li><li><strong>Hands</strong>: <code>fingers: flex 80</code> makes a fist; curl individual fingers for shapes (<code>index_right: flex 95</code>). Single-DOF per finger, good for grip and rough gesture, not exact sign language.</li></ul>
<ul><li><strong>Props</strong>: <code>prop chair | wall | bar | box | dip-bars</code> (top level). The chair sits behind the figure (sit-to-stand, box squat), the wall behind that (wall sit), the bar overhead, the box in front (step-ups), and the dip bars either side at hip-press height (<code>grip: hands bars</code> + elbow flex = triceps dips).</li><li><strong>Pins</strong>: <code>pin: &lt;effector&gt; &lt;anchor&gt;</code> moves the whole BODY so the effector sits on the anchor (vs <code>reach</code>, which moves just the limb). Use one primary pin for body translation: <code>pin: foot_right box</code> can support a step-up, and <code>pin: pelvis floor</code> can keep the pelvis on the mat. Use <code>grip</code>, not several simultaneous hand pins, for a bar or rails.</li><li><strong>Grips</strong>: <code>grip: hands bar</code> or <code>grip: hands bars</code> is the dedicated two-hand prop contact. It assigns separate left/right anchors, solves both arms, and closes the fingers. Declare <code>prop bar</code> or <code>prop dip-bars</code> first. Prefer this to multiple pins for a pull-up, hang, or dip.</li><li><strong>Dance turnout</strong>: <code>pose start = first-position</code> for ballet movements that must keep the legs externally rotated without twisting planted feet</li><li><strong>Lying / seated</strong>: <code>pose start = supine | prone | seated</code> for floor and mat work (glute bridge, dead bug, cobra, seated forward fold). In a supine exercise whose torso stays down, add <code>ground-lock: back</code> to each phase.</li><li><strong>Hands</strong>: <code>fingers: flex 80</code> makes a fist; curl individual fingers for shapes (<code>index_right: flex 95</code>). Single-DOF per finger, good for grip and rough gesture, not exact sign language.</li></ul>
<h2>Three-point superhero landing</h2>
<p>A landing is defined by its contacts, not by a dramatic cue. Keep the existing front-foot support planted while reach constraints blend the rear knee and same-side fist down. Once the knee has arrived, hand the whole-body anchor to that knee and solve the foot and fist independently. Repeat the three contacts through the hold; never combine a whole-root pin with <code>ground-lock</code> in one step.</p>
<pre class="code-block" data-lang="posecode"><code>posecode posture &quot;Superhero Three-Point Landing&quot;
Expand Down
20 changes: 10 additions & 10 deletions playground/public/moves/demi-plie.html
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,11 @@ <h2>Movement phases</h2>
<ol class="steps">
<li>
<span class="step-name">Plié<span class="step-dur">2.2s · settle</span></span>
<span class="step-cue">Turn out from the hips and bend the knees over the toes: heels down</span>
<span class="step-cue">Lower straight down with the knees tracking the turned-out toes and both heels anchored</span>
</li>
<li>
<span class="step-name">Straighten<span class="step-dur">2.2s · settle</span></span>
<span class="step-cue">Press the floor away and rise to a tall first position</span>
<span class="step-cue">Press the floor away and rise without changing the turnout or moving the feet</span>
</li>
</ol>

Expand All @@ -150,29 +150,29 @@ <h2>The .posecode source</h2>
angles, not 3D transforms.</p>
<pre class="code-block"><code>posecode exercise &quot;Demi-plié&quot;
rig humanoid
pose start = standing
pose start = first-position

step &quot;Plié&quot; 2.2s settle:
hips: flex 20
hips: rotate-out 30
hips: flex 31.5
hips: abduct 17
knees: flex 55
ankles: dorsiflex 15
shoulders: abduct 70
elbows: flex 15
spine: extend 4
ground-lock: feet
cue &quot;Turn out from the hips and bend the knees over the toes: heels down&quot;
cue &quot;Lower straight down with the knees tracking the turned-out toes and both heels anchored&quot;

step &quot;Straighten&quot; 2.2s settle:
hips: flex 0
hips: rotate-out 0
hips: abduct 0
knees: flex 0
ankles: plantarflex 0
ankles: dorsiflex 0
shoulders: abduct 0
elbows: flex 0
spine: flex 0
spine: extend 0
ground-lock: feet
cue &quot;Press the floor away and rise to a tall first position&quot;
cue &quot;Press the floor away and rise without changing the turnout or moving the feet&quot;

repeat 4
</code></pre>
Expand Down
16 changes: 8 additions & 8 deletions playground/public/moves/superhero-landing.html
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ <h2>Movement phases</h2>
</li>
<li>
<span class="step-name">Make three-point contact<span class="step-dur">0.3s · settle</span></span>
<span class="step-cue">Set the left knee and left fist on the floor beside the planted right foot</span>
<span class="step-cue">Keep the right foot planted as the left knee and left fist settle onto the floor</span>
</li>
<li>
<span class="step-name">Hold the landing<span class="step-dur">0.8s · linear</span></span>
Expand All @@ -168,12 +168,12 @@ <h2>The .posecode source</h2>
knee_right: flex 123
ankle_right: dorsiflex 15
hip_left: extend 12
knee_left: flex 105
knee_left: flex 110
ankle_left: plantarflex 28
shoulder_left: flex 98
shoulder_left: abduct 2
elbow_left: flex 8
elbow_left: supinate 80
elbow_left: pronate 80
fingers_left: flex 80
shoulder_right: extend 24
shoulder_right: abduct 22
Expand All @@ -185,14 +185,14 @@ <h2>The .posecode source</h2>

step &quot;Make three-point contact&quot; 0.3s settle:
neck: extend 25
pin: knee_left floor
reach: foot_right floor
ground-lock: foot_right
reach: knee_left floor
reach: fist_left floor
cue &quot;Set the left knee and left fist on the floor beside the planted right foot&quot;
cue &quot;Keep the right foot planted as the left knee and left fist settle onto the floor&quot;

step &quot;Hold the landing&quot; 0.8s linear:
pin: knee_left floor
reach: foot_right floor
ground-lock: foot_right
reach: knee_left floor
reach: fist_left floor
cue &quot;Hold the three contacts with the free right arm swept behind you&quot;

Expand Down
Loading