diff --git a/packages/posecode-eval/test/eval.test.ts b/packages/posecode-eval/test/eval.test.ts index 4eaa61a..99f8d94 100644 --- a/packages/posecode-eval/test/eval.test.ts +++ b/packages/posecode-eval/test/eval.test.ts @@ -74,6 +74,15 @@ describe("metrics", () => { const pullUp = movement("pull-up"); expect(Math.min(...pullUp.phases.map((p) => headPropClearance(pullUp, p)))).toBeGreaterThan(-0.01); + + // A dead bug moves opposite limbs while the torso remains planted. This + // catches the old silent `ground-lock: back` no-op, where whole-figure + // floor reconciliation could move the torso with whichever limb was lowest. + const deadBug = movement("dead-bug"); + for (const id of ["pelvis", "chest"]) { + const heights = deadBug.phases.map((p) => p.bones.get(id)![1]); + expect(Math.max(...heights) - Math.min(...heights)).toBeLessThan(0.01); + } }); }); diff --git a/packages/posecode-language/src/vocab.ts b/packages/posecode-language/src/vocab.ts index f0a5523..9094ae2 100644 --- a/packages/posecode-language/src/vocab.ts +++ b/packages/posecode-language/src/vocab.ts @@ -22,7 +22,7 @@ export const KINDS = ["exercise", "stretch", "posture"]; /** Recognised start poses (`pose start = ...`). */ export const POSES = ["neutral", "standing", "plank", "supine", "prone", "seated"]; -/** Effectors that can be ground-locked. */ +/** Floor contacts that can be ground-locked. */ export const EFFECTORS = [...GROUND_LOCK_EFFECTOR_NAMES]; /** Reach/pin effectors (groups + per-side aliases), sourced from the parser. */ @@ -50,7 +50,7 @@ export const KEYWORD_DOCS: Record = { snap: "Timing mode: fast, near-immediate arrival — an accent.", linear: "Timing mode: constant velocity — intentionally mechanical.", repeat: "How many times the movement loops.", - "ground-lock": "Pins grouped or per-side effectors to the floor: `feet`, `foot_left` (also written `left foot`), `hands`, or `forearms`. Planted feet auto-level unless the ankle is intentionally plantarflexed.", + "ground-lock": "Keeps grouped, per-side, or axial contacts on the floor: `feet`, `foot_left` (also `left foot`), `hands`, `forearms`, or `back` for supine work. Planted feet auto-level unless intentionally plantarflexed.", reach: "Drives an effector to a target via ROM-constrained IK: `reach: hand_left ankle_left`, `reach: hands floor`.", pin: "Moves the body so an effector sits on an anchor: `pin: hands bar` (hang, pull up, step up, dip).", diff --git a/packages/posecode-language/test/language.test.ts b/packages/posecode-language/test/language.test.ts index be727e3..b3fc2ee 100644 --- a/packages/posecode-language/test/language.test.ts +++ b/packages/posecode-language/test/language.test.ts @@ -75,7 +75,15 @@ describe("getCompletions", () => { it("suggests effectors after `ground-lock: `", () => { expect(onLine(" ground-lock: ", 17)).toEqual( - expect.arrayContaining(["hands", "feet", "hand_left", "hand_right", "foot_left", "foot_right"]), + expect.arrayContaining([ + "hands", + "feet", + "hand_left", + "hand_right", + "foot_left", + "foot_right", + "back", + ]), ); }); diff --git a/packages/posecode-mcp/src/guide.ts b/packages/posecode-mcp/src/guide.ts index 3f1b74e..69b3d1f 100644 --- a/packages/posecode-mcp/src/guide.ts +++ b/packages/posecode-mcp/src/guide.ts @@ -37,7 +37,7 @@ posecode "" # kind = exercise | stretch | posture pose start = # neutral | standing | plank step "" : # mode = flow | settle | drive | snap | linear : - ground-lock: # feet/hands/forearms, or one side: foot_left / left foot + ground-lock: # feet/hands/forearms/back, or foot_left / left foot cue "" repeat \`\`\` @@ -47,4 +47,7 @@ wrists hips knees ankles. Actions (degrees are absolute targets): flex/extend, abduct/adduct, rotate-in/rotate-out, dorsiflex/plantarflex, hold neutral, and hinge (hips only, closed-chain hip flexion: torso tips over planted feet with a neutral spine; use for deadlift / forward fold instead of hips: flex). -Stay within healthy range of motion; the renderer hard-clamps anything beyond.`; +Stay within healthy range of motion; the renderer hard-clamps anything beyond. +Use ground-lock: feet when standing, hands and feet in a high plank, forearms +and feet in a forearm plank, and back for supine floor work such as a dead bug. +Do not invent other contact names.`; diff --git a/packages/posecode-parser/src/joints.ts b/packages/posecode-parser/src/joints.ts index 72aa4b5..cb0fe66 100644 --- a/packages/posecode-parser/src/joints.ts +++ b/packages/posecode-parser/src/joints.ts @@ -121,9 +121,10 @@ const EFFECTOR_GROUPS: Record = { export const EFFECTOR_NAMES = [...Object.keys(EFFECTOR_GROUPS), ...EFFECTOR_SIDES]; /** - * Effectors accepted by `ground-lock:`. Ground locking has historically + * Contacts accepted by `ground-lock:`. Ground locking has historically * supported the symmetric hand/forearm/foot groups; per-side aliases let a * movement keep one support planted while the opposite limb moves freely. + * `back` is an axial surface contact for supine floor work. */ export const GROUND_LOCK_EFFECTOR_NAMES = [ "hands", @@ -135,6 +136,7 @@ export const GROUND_LOCK_EFFECTOR_NAMES = [ "feet", "foot_left", "foot_right", + "back", ] as const; const GROUND_LOCK_EFFECTOR_SET = new Set(GROUND_LOCK_EFFECTOR_NAMES); diff --git a/packages/posecode-parser/src/parser.ts b/packages/posecode-parser/src/parser.ts index c4a0bdf..9a65872 100644 --- a/packages/posecode-parser/src/parser.ts +++ b/packages/posecode-parser/src/parser.ts @@ -232,7 +232,7 @@ function parseStepChild(ln: Line, current: AstStep | null): ParseError | null { if (head === "ground-lock") { if (!current) return { line: ln.line, message: "`ground-lock` outside of a step" }; if (t[1]?.type !== "colon") { - return { line: ln.line, message: "expected `ground-lock: `" }; + return { line: ln.line, message: "expected `ground-lock: `" }; } const words = t.slice(2).filter((tok) => tok.type === "word").map((tok) => tok.value); const effectors: string[] = []; @@ -255,7 +255,7 @@ function parseStepChild(ln: Line, current: AstStep | null): ParseError | null { effectors.push(value); } if (effectors.length === 0) { - return { line: ln.line, message: "`ground-lock` requires at least one effector" }; + return { line: ln.line, message: "`ground-lock` requires at least one contact" }; } current.groundLock = effectors; current.groundLockLine = ln.line; diff --git a/packages/posecode-parser/src/types.ts b/packages/posecode-parser/src/types.ts index 242714e..cb229e0 100644 --- a/packages/posecode-parser/src/types.ts +++ b/packages/posecode-parser/src/types.ts @@ -67,7 +67,7 @@ export interface Phase { durationSec: number; easing: Easing; targets: JointTarget[]; - /** Grouped or per-side floor effectors pinned for this phase, e.g. ["feet"] or ["foot_right"]. */ + /** Grouped, per-side, or axial floor contacts held this phase, e.g. ["foot_right"] or ["back"]. */ groundLock: string[]; /** Reach-IK goals active during this phase. */ reaches: ReachTarget[]; diff --git a/packages/posecode-parser/test/parse.test.ts b/packages/posecode-parser/test/parse.test.ts index 709bae9..91dd60e 100644 --- a/packages/posecode-parser/test/parse.test.ts +++ b/packages/posecode-parser/test/parse.test.ts @@ -263,6 +263,29 @@ describe("reach/pin effectors", () => { }); }); +describe("ground-lock contacts", () => { + it("accepts back as a supine floor contact", () => { + const result = parse(`posecode exercise "Dead bug" + rig humanoid + pose start = supine + step "Extend" 1s settle: + ground-lock: back`); + expect(result.errors).toEqual([]); + expect(result.ir?.phases[0]?.groundLock).toEqual(["back"]); + }); + + it("rejects unknown contacts instead of silently ignoring them", () => { + const result = parse(`posecode posture "Floor" + rig humanoid + step "Hold" 1s linear: + ground-lock: shoulders`); + expect(result.ir).toBeNull(); + expect(result.errors).toEqual([ + expect.objectContaining({ line: 4, message: expect.stringContaining("shoulders") }), + ]); + }); +}); + describe("clip directive", () => { const doc = (clipLine: string): string => [ diff --git a/packages/posecode-render/src/groundlock.ts b/packages/posecode-render/src/groundlock.ts index 89ede25..0bea147 100644 --- a/packages/posecode-render/src/groundlock.ts +++ b/packages/posecode-render/src/groundlock.ts @@ -3,8 +3,8 @@ * render loop and the headless eval harness (posecode-eval) use the identical * solver. * - * "Ground-lock" = keep the declared grouped or per-side effectors planted while the - * body moves, tuned per support type: + * "Ground-lock" = keep the declared grouped, per-side, or axial contacts + * planted while the body moves, tuned per support type: * * - **Hands + feet (push-up / plank):** pivot the whole rigid body about the * foot line (the toes stay planted) until the hands reach the floor. As the @@ -23,6 +23,8 @@ * the heels). Only feet near the floor anchor (a swing leg in a curl or * march must stay free), and only the average delta is corrected so * symmetric spreads (jumping jacks) don't fight the lock. + * - **Back (dead bug / supine floor work):** translate the body vertically so + * the pelvis-to-ribcage surface stays on the floor while the limbs move. * * Both paths ground the visible MESH (bounding boxes), not just bone origins: * an ankle bone sits ~0.04m above the sole, so anchoring bones alone left the @@ -97,8 +99,14 @@ export function applyGroundLock( const hands = ids.filter((id) => id.startsWith("wrist")); const forearms = ids.filter((id) => id.startsWith("elbow")); const feet = ids.filter((id) => id.startsWith("ankle")); + const back = ids.filter((id) => id === "pelvis" || id === "spine" || id === "chest"); const upperSupports = forearms.length > 0 ? forearms : hands; + if (back.length > 0) { + dropOwnMeshesToFloor(m, back); + return; + } + if (upperSupports.length > 0 && feet.length > 0) { // Plant the feet FIRST: drop the body so the foot mesh rests on the floor, // so the pivot the body then rotates about is itself at floor level. The @@ -146,6 +154,37 @@ export function applyGroundLock( } } +/** + * Drop only the meshes owned by the selected bones onto the floor. `Box3` on + * a torso bone would include its child limbs, so an overhead arm could + * otherwise lift a supine person's back. Bone child subtrees are deliberately + * excluded; non-bone groups (capsules/ellipsoids) remain part of the surface. + */ +function dropOwnMeshesToFloor(m: Mannequin, boneIds: string[]): void { + const boneNodes = new Set(m.bones.values()); + const box = new THREE.Box3(); + const childBox = new THREE.Box3(); + let found = false; + + for (const id of boneIds) { + const bone = m.bones.get(id); + if (!bone) continue; + for (const child of bone.children) { + if (boneNodes.has(child)) continue; + childBox.setFromObject(child); + if (!childBox.isEmpty()) { + box.union(childBox); + found = true; + } + } + } + + if (found && Number.isFinite(box.min.y)) { + m.root.position.y -= box.min.y; + m.root.updateMatrixWorld(true); + } +} + /** * Drop the whole body vertically so the lowest FOOT-mesh point rests on the * floor. Grounds the visible sole (bounding box), not the ankle bone, which diff --git a/packages/posecode-render/src/index.ts b/packages/posecode-render/src/index.ts index 3e09f12..64ba663 100644 --- a/packages/posecode-render/src/index.ts +++ b/packages/posecode-render/src/index.ts @@ -4,8 +4,9 @@ * `createViewer(canvas)` sets up a Three.js studio scene with the procedural * mannequin and returns a controller. `load(ir)` builds a timeline from a parsed * PosecodeIR; the render loop applies forward kinematics each frame, then keeps - * ground-locked contacts (hands/feet) planted via floating-root solving. The - * camera auto-frames the figure and eases smoothly when a new movement loads. + * ground-locked contacts (hands/forearms/feet/back) planted via floating-root + * solving. The camera auto-frames the figure and eases smoothly when a new + * movement loads. */ import * as THREE from "three"; @@ -1055,6 +1056,11 @@ function contactBoneIds(info: { ids.add("elbow_left"); ids.add("elbow_right"); } + if (effector === "back") { + ids.add("pelvis"); + ids.add("spine"); + ids.add("chest"); + } }; for (const group of info.groundLock) addEffector(group); for (const contact of [...info.grips, ...info.pins]) addEffector(contact.effector); diff --git a/packages/posecode-render/src/mannequin.ts b/packages/posecode-render/src/mannequin.ts index 5191b79..e9e086d 100644 --- a/packages/posecode-render/src/mannequin.ts +++ b/packages/posecode-render/src/mannequin.ts @@ -240,6 +240,8 @@ export function buildMannequin(material?: THREE.Material, proportions?: Proporti feet: ["ankle_left", "ankle_right"], foot_left: ["ankle_left"], foot_right: ["ankle_right"], + // Axial surface used by supine floor work (dead bug, crunch, leg raise). + back: ["pelvis", "spine", "chest"], }, collision: proportions?.collision ?? DEFAULT_COLLISION, }; diff --git a/packages/posecode-render/test/render.test.ts b/packages/posecode-render/test/render.test.ts index ef5d544..a2b219f 100644 --- a/packages/posecode-render/test/render.test.ts +++ b/packages/posecode-render/test/render.test.ts @@ -35,6 +35,7 @@ describe("mannequin", () => { expect(m.effectors.forearms).toEqual(["elbow_left", "elbow_right"]); expect(m.effectors.feet).toEqual(["ankle_left", "ankle_right"]); expect(m.effectors.foot_right).toEqual(["ankle_right"]); + expect(m.effectors.back).toEqual(["pelvis", "spine", "chest"]); }); }); @@ -466,6 +467,32 @@ describe("ground-lock (shared solver)", () => { expect(Math.abs(soleY)).toBeLessThan(0.01); }); + it("plants the torso surface for a supine back lock", () => { + const m = buildMannequin(); + const spec = poseFor("supine"); + m.root.position.set(...spec.root!.position!); + const [rx, ry, rz] = spec.root!.rotationDeg!; + m.root.rotation.set(rx * DEG, ry * DEG, rz * DEG); + // Put the limbs into an asymmetric dead-bug phase before solving contact. + m.bones.get("shoulder_right")!.rotation.x = -150 * DEG; + m.bones.get("hip_left")!.rotation.x = -20 * DEG; + m.bones.get("knee_left")!.rotation.x = 5 * DEG; + m.root.updateMatrixWorld(true); + + applyGroundLock(m, ["back"]); + + // Measure meshes owned by the pelvis/spine/chest only. Descendant limbs + // are intentionally excluded: moving an arm must not lift the back. + const boneNodes = new Set(m.bones.values()); + const backBox = new THREE.Box3(); + for (const id of ["pelvis", "spine", "chest"]) { + for (const child of m.bones.get(id)!.children) { + if (!boneNodes.has(child)) backBox.union(new THREE.Box3().setFromObject(child)); + } + } + expect(backBox.min.y).toBeCloseTo(0, 3); + }); + it("is a no-op when no effectors are ground-locked", () => { const m = posedRaw( [ diff --git a/playground/public/llm-guide.html b/playground/public/llm-guide.html index 2a829aa..7c84dc9 100644 --- a/playground/public/llm-guide.html +++ b/playground/public/llm-guide.html @@ -133,7 +133,7 @@

Grammar

step "<Phase name>" <Ns> <mode>: # mode = flow | settle | drive | snap | linear <joint>: <action> <degrees> reach: <effector> <target> # optional: drive a hand/foot to a target via IK - ground-lock: <effectors> # groups, foot_left/foot_right, or natural left foot/right foot + ground-lock: <contacts> # groups/back, foot_left, or natural left foot turn: <degrees> # optional: face this yaw by phase end (standing only) travel: <x> <z> # optional: move to this x z (metres) by phase end cue "<short coaching cue>" @@ -149,8 +149,8 @@

Rules

previous value.

  1. Stay within healthy range of motion (e.g. knee flex ≤ 144°, elbow flex ≤

154°, shoulder flex ≤ 180°). The renderer hard-clamps anything beyond it.

-
  1. Add a one-line cue per phase. Use ground-lock for whatever touches the
-

floor (feet when standing; hands and feet in a plank).

+
  1. Add a one-line cue per phase. Ground contacts are a closed vocabulary:
+

use ground-lock: feet when standing, ground-lock: hands, feet in a high plank, ground-lock: forearms, feet in a forearm plank, and ground-lock: back for supine floor work such as a dead bug. Do not invent other contact names.

  1. repeat the rep count.

Timing modes

  • flow: carry momentum through an interior pose; use for continuous dance,
@@ -209,7 +209,7 @@

Reaching, props, lying poses & hands

  • Pins: pin: <effector> <anchor> moves the whole BODY so the effector sits

on the anchor (vs reach, which moves just the limb). Same effectors as reach, including hands / feet. Use it for hanging and climbing: pin: hands bar + flexing the elbows = a pull-up; pin: foot_right box + straightening the leg = a step-up; pin: hands bars (dip bars) + bending the elbows = a triceps dip.

  • Lying / seated: pose start = supine | prone | seated for floor and mat
-

work (glute bridge, dead bug, cobra, seated forward fold).

+

work (glute bridge, dead bug, cobra, seated forward fold). In a supine exercise whose torso stays down, add ground-lock: back to each phase.

  • Hands: fingers: flex 80 makes a fist; curl individual fingers for shapes

(index_right: flex 95). Single-DOF per finger, good for grip and rough gesture, not exact sign language.

Authoring by domain

diff --git a/playground/public/moves/dead-bug.html b/playground/public/moves/dead-bug.html index a5a1000..04233b3 100644 --- a/playground/public/moves/dead-bug.html +++ b/playground/public/moves/dead-bug.html @@ -163,12 +163,14 @@

The .posecode source

shoulders: flex 90 hips: flex 90 knees: flex 90 + ground-lock: back cue "Arms reach to the ceiling, knees stacked over the hips" step "Extend right arm & left leg" 1.2s flow: shoulder_right: flex 150 hip_left: flex 20 knee_left: flex 5 + ground-lock: back cue "Lower the right arm overhead and the left leg toward the floor" step "Switch" 1.2s flow: @@ -178,12 +180,14 @@

The .posecode source

shoulder_left: flex 150 hip_right: flex 20 knee_right: flex 5 + ground-lock: back cue "Return and switch: left arm and right leg reach out" step "Return" 1s settle: shoulder_left: flex 90 hip_right: flex 90 knee_right: flex 90 + ground-lock: back cue "Bring everything back to the start position" repeat 4 diff --git a/playground/public/spec.html b/playground/public/spec.html index 5422e85..99db4bc 100644 --- a/playground/public/spec.html +++ b/playground/public/spec.html @@ -142,7 +142,10 @@

1. Grammar

timingMode = "flow" | "settle" | "drive" | "snap" | "linear" ; child = jointTarget | groundLock | reach | pin | turn | travel | cue ; jointTarget= joint ":" action [ NUMBER ] ; -groundLock = "ground-lock" ":" effector { "," effector } ; +groundLock = "ground-lock" ":" floorContact { "," floorContact } ; +floorContact = "hands" | "forearms" | "feet" | "back" + | ( "hand" | "elbow" | "foot" ) "_" ( "left" | "right" ) + | ( "left" | "right" ) ( "hand" | "forearm" | "elbow" | "foot" ) ; reach = "reach" ":" effector target ; (* effector → world target via ROM-constrained IK *) pin = "pin" ":" effector anchor ; (* move the BODY so effector sits on anchor *) turn = "turn" ":" NUMBER ; (* face this yaw (deg) by phase end *) @@ -209,8 +212,8 @@

5. Rendering model

C1-continuous quaternion splines between keyframes, shaped by the destination phase's timing mode.

  1. Grounding: the figure is dropped so its lowest point rests on the floor

(a bounding-box drop), which grounds standing, plank, and the lying/seated poses alike.

-
  1. Ground-lock IK: effectors listed in ground-lock (hands, forearms,
-

feet, or the per-side aliases hand_left|hand_right, elbow_left|elbow_right, foot_left|foot_right) are pinned to their planted floor position so they stay put while the body moves. Unsupported effector names are line-anchored validation errors.

+
  1. Ground-lock: contacts listed in ground-lock (hands, forearms,
+

feet, or the per-side aliases hand_left|hand_right, elbow_left|elbow_right, foot_left|foot_right) stay planted while the body moves. back holds the pelvis-to-ribcage surface on the floor for supine work such as dead bugs. Unsupported contact names are line-anchored validation errors.

  1. Reach-IK: a reach: line drives an effector (`hand_left|hand_right|

foot_left|foot_right, or the groups hands/feet for both sides) to a world target via Cyclic Coordinate Descent (CCD) over the arm/leg chain. A target is a body landmark bone (e.g. ankle_left), the keyword floor, or a prop anchor (bar, seat, wall`). The solve is ROM-constrained: each iteration clamps every chain joint into its §4 Range-of-Motion limits (expressed as a per-axis box in the bone's local Euler frame), so a reach toward an unsafe or unreachable target settles on the closest *healthy* pose; solved angles obey the same hard limits as authored ones.

  1. Props: prop chair|wall|bar|box|dip-bars adds a scene object at a
@@ -239,7 +242,7 @@

6. Intermediate Representation (IR)

durationSec: number; easing: "flow" | "settle" | "drive" | "snap" | "linear"; targets: { boneId: string; euler: { x: number; y: number; z: number } }[]; - groundLock: string[]; // ["hands","feet"] or ["foot_right"] + groundLock: string[]; // ["hands","feet"], ["foot_right"], or ["back"] cue?: string; }[]; } diff --git a/spec/SPEC.md b/spec/SPEC.md index d459cc2..a35e85a 100644 --- a/spec/SPEC.md +++ b/spec/SPEC.md @@ -34,7 +34,10 @@ step = "step" STRING DURATION timingMode ":" { child } ; timingMode = "flow" | "settle" | "drive" | "snap" | "linear" ; child = jointTarget | groundLock | reach | pin | turn | travel | cue ; jointTarget= joint ":" action [ NUMBER ] ; -groundLock = "ground-lock" ":" effector { "," effector } ; +groundLock = "ground-lock" ":" floorContact { "," floorContact } ; +floorContact = "hands" | "forearms" | "feet" | "back" + | ( "hand" | "elbow" | "foot" ) "_" ( "left" | "right" ) + | ( "left" | "right" ) ( "hand" | "forearm" | "elbow" | "foot" ) ; reach = "reach" ":" effector target ; (* effector → world target via ROM-constrained IK *) pin = "pin" ":" effector anchor ; (* move the BODY so effector sits on anchor *) turn = "turn" ":" NUMBER ; (* face this yaw (deg) by phase end *) @@ -145,11 +148,12 @@ research §5.1 normative tables. Selected ceilings (degrees): 2. **Grounding**: the figure is dropped so its lowest point rests on the floor (a bounding-box drop), which grounds standing, plank, and the lying/seated poses alike. -3. **Ground-lock IK**: effectors listed in `ground-lock` (`hands`, `forearms`, +3. **Ground-lock**: contacts listed in `ground-lock` (`hands`, `forearms`, `feet`, or the per-side aliases `hand_left|hand_right`, - `elbow_left|elbow_right`, `foot_left|foot_right`) are pinned to their planted - floor position so they stay put while the body moves. Unsupported effector - names are line-anchored validation errors. + `elbow_left|elbow_right`, `foot_left|foot_right`) stay planted while the body + moves. `back` holds the pelvis-to-ribcage surface on the floor for supine + work such as dead bugs. Unsupported contact names are line-anchored + validation errors. 4. **Reach-IK**: a `reach:` line drives an effector (`hand_left|hand_right| foot_left|foot_right`, or the groups `hands`/`feet` for both sides) to a world **target** via Cyclic Coordinate Descent (CCD) over the arm/leg chain. @@ -226,7 +230,7 @@ interface PosecodeIR { durationSec: number; easing: "flow" | "settle" | "drive" | "snap" | "linear"; targets: { boneId: string; euler: { x: number; y: number; z: number } }[]; - groundLock: string[]; // ["hands","feet"] or ["foot_right"] + groundLock: string[]; // ["hands","feet"], ["foot_right"], or ["back"] cue?: string; }[]; } diff --git a/spec/examples/dead-bug.posecode b/spec/examples/dead-bug.posecode index b99fbd6..c9a84b7 100644 --- a/spec/examples/dead-bug.posecode +++ b/spec/examples/dead-bug.posecode @@ -6,12 +6,14 @@ posecode exercise "Dead bug" shoulders: flex 90 hips: flex 90 knees: flex 90 + ground-lock: back cue "Arms reach to the ceiling, knees stacked over the hips" step "Extend right arm & left leg" 1.2s flow: shoulder_right: flex 150 hip_left: flex 20 knee_left: flex 5 + ground-lock: back cue "Lower the right arm overhead and the left leg toward the floor" step "Switch" 1.2s flow: @@ -21,12 +23,14 @@ posecode exercise "Dead bug" shoulder_left: flex 150 hip_right: flex 20 knee_right: flex 5 + ground-lock: back cue "Return and switch: left arm and right leg reach out" step "Return" 1s settle: shoulder_left: flex 90 hip_right: flex 90 knee_right: flex 90 + ground-lock: back cue "Bring everything back to the start position" repeat 4 diff --git a/spec/llm-authoring.md b/spec/llm-authoring.md index d28f1f8..95876ee 100644 --- a/spec/llm-authoring.md +++ b/spec/llm-authoring.md @@ -20,7 +20,7 @@ posecode "" # kind = exercise | stretch | posture step "" : # mode = flow | settle | drive | snap | linear : reach: # optional: drive a hand/foot to a target via IK - ground-lock: # groups, foot_left/foot_right, or natural left foot/right foot + ground-lock: # groups/back, foot_left, or natural left foot turn: # optional: face this yaw by phase end (standing only) travel: # optional: move to this x z (metres) by phase end cue "" @@ -53,8 +53,11 @@ wrists hips knees ankles`. Plural names move both sides symmetrically; use previous value. 3. Stay within healthy range of motion (e.g. knee flex ≤ 144°, elbow flex ≤ 154°, shoulder flex ≤ 180°). The renderer hard-clamps anything beyond it. -4. Add a one-line `cue` per phase. Use `ground-lock` for whatever touches the - floor (feet when standing; hands and feet in a plank). +4. Add a one-line `cue` per phase. Ground contacts are a closed vocabulary: + use `ground-lock: feet` when standing, `ground-lock: hands, feet` in a high + plank, `ground-lock: forearms, feet` in a forearm plank, and + `ground-lock: back` for supine floor work such as a dead bug. Do not invent + other contact names. 5. `repeat` the rep count. ## Timing modes @@ -152,7 +155,8 @@ posecode exercise "Deadlift" flexing the elbows = a pull-up; `pin: foot_right box` + straightening the leg = a step-up; `pin: hands bars` (dip bars) + bending the elbows = a triceps dip. - **Lying / seated**: `pose start = supine | prone | seated` for floor and mat - work (glute bridge, dead bug, cobra, seated forward fold). + work (glute bridge, dead bug, cobra, seated forward fold). In a supine + exercise whose torso stays down, add `ground-lock: back` to each phase. - **Hands**: `fingers: flex 80` makes a fist; curl individual fingers for shapes (`index_right: flex 95`). Single-DOF per finger, good for grip and rough gesture, not exact sign language.