Skip to content

Commit a4bcb31

Browse files
committed
fix: keep deadlift heels planted through the hip hinge
The deadlift's Lower phase rose onto the balls of the feet at the bottom of the hinge: keeping the sole flat needed 25° of ankle dorsiflexion, but the ROM capped it at 15°, so levelPlantedFeet clamped at the limit and left a ~3.2cm heel lift. Raise the ankle dorsiflexion ROM to 20° (the standard goniometric norm) and ease the Lower-phase knee flexion 25° → 18° so the shin tips only as far as the ankle can absorb. Both soles now stay flat on the floor across the whole clip (0.00cm heel lift, no grounding warnings). Re-tune the superhero landing's right knee (123° → 115°) so its planted foot still meets the floor under the wider ROM, keeping the fixture scorecard green.
1 parent a9f5a37 commit a4bcb31

12 files changed

Lines changed: 94 additions & 79 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"posecode-parser": patch
3+
---
4+
5+
Raise the ankle dorsiflexion ROM to 20° (the standard goniometric norm) so a
6+
weight-bearing hinge can keep its soles flat, and ease the deadlift's Lower-phase
7+
knee flexion to 18° so the planted feet stay grounded through the hinge instead
8+
of rising onto the toes. Re-tune the superhero landing's right knee so its
9+
planted foot still meets the floor under the wider ROM.

packages/posecode-eval/test/eval.test.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -115,26 +115,26 @@ describe("clip-wide constraint diagnostics", () => {
115115
const fixtures = loadFixtures(examplesDir);
116116
const source = (name: string) => fixtures.find((fixture) => fixture.movement === name)!.source;
117117

118-
it("samples between phase endpoints and reports the deadlift/demi-plié heel lifts", () => {
118+
it("keeps the deadlift heels planted through the hinge and still flags the demi-plié lift", () => {
119119
const deadlift = probeMovement(source("deadlift"));
120120
const plie = probeMovement(source("demi-plie"));
121121

122122
expect(deadlift.diagnostics.sampleCount).toBeGreaterThan(deadlift.phases.length);
123-
expect(deadlift.diagnostics.feet.left.maxHeelHeightMeters).toBeGreaterThan(0.03);
124-
expect(deadlift.diagnostics.warnings).toContainEqual(expect.objectContaining({
125-
id: "clip-heel-height:foot_left",
126-
phaseName: "Lower",
127-
kind: "heel-height",
128-
}));
129-
expect(deadlift.diagnostics.warnings).toContainEqual(expect.objectContaining({
130-
id: "clip-grounding-rom-conflict:foot_left",
131-
phaseName: "Lower",
132-
kind: "grounding-rom-conflict",
133-
}));
134-
expect(plie.diagnostics.feet.left.maxHeelHeightMeters).toBeGreaterThan(0.04);
123+
// The hip hinge keeps soft knees (18°) inside the ankle's dorsiflexion ROM
124+
// (20°), so both soles stay flat on the floor across the whole clip.
125+
expect(deadlift.diagnostics.feet.left.maxHeelHeightMeters).toBeLessThan(0.005);
126+
expect(deadlift.diagnostics.warnings).toHaveLength(0);
127+
128+
// The demi-plié still drives the ankle past its dorsiflexion ROM, so it
129+
// remains the exemplar of a reported heel-lift / grounding conflict.
130+
expect(plie.diagnostics.feet.left.maxHeelHeightMeters).toBeGreaterThan(0.02);
135131
expect(plie.diagnostics.warnings).toEqual(expect.arrayContaining([
136132
expect.objectContaining({ id: "clip-heel-height:foot_left", phaseName: "Plié" }),
137-
expect.objectContaining({ id: "clip-sole-angle:foot_left", phaseName: "Plié" }),
133+
expect.objectContaining({
134+
id: "clip-grounding-rom-conflict:foot_left",
135+
phaseName: "Plié",
136+
kind: "grounding-rom-conflict",
137+
}),
138138
]));
139139
});
140140

@@ -154,7 +154,7 @@ describe("clip-wide constraint diagnostics", () => {
154154
});
155155

156156
it("keeps strict diagnostics visible but non-gating in EvalReport and CLI text", () => {
157-
const report = runEval([{ movement: "deadlift", source: source("deadlift") }]);
157+
const report = runEval([{ movement: "demi-plie", source: source("demi-plie") }]);
158158
const movement = report.movements[0]!;
159159
const text = renderReport(report);
160160

packages/posecode-parser/src/rom.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,10 @@ const ROM: Record<string, ActionLimits> = {
5656
extend: { min: 0, max: 5 }, // hyperextension risk limit
5757
},
5858
ankle: {
59-
dorsiflex: { min: 0, max: 15 },
59+
// ~20° dorsiflexion matches the standard goniometric norm (AAOS) and is what
60+
// a weight-bearing hinge/squat needs to keep the sole flat as the shin tips
61+
// forward; the older 15° cap forced planted heels off the floor (deadlift).
62+
dorsiflex: { min: 0, max: 20 },
6063
plantarflex: { min: 0, max: 50 },
6164
},
6265
// --- Pelvis / hip-hinge (trunk-on-thigh angle) ---

packages/posecode-parser/test/rom.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ describe("euler ROM boxes (eulerRomFor)", () => {
7272

7373
it("covers the ankle via dorsiflex/plantarflex and the pelvis via hinge", () => {
7474
// Toes point +Z: dorsiflexion (toes up) is -X, plantarflexion +X.
75-
expect(eulerRomFor("ankle_left")!.x).toEqual({ min: -15, max: 50 });
75+
expect(eulerRomFor("ankle_left")!.x).toEqual({ min: -20, max: 50 });
7676
// The pelvis' torso child points up: the forward hinge is +X.
7777
expect(eulerRomFor("pelvis")!.x).toEqual({ min: 0, max: 120 });
7878
});

packages/posecode-render/test/contacts.test.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,17 @@ describe("levelPlantedFeet", () => {
8686
expect(flat.toeHeight).toBeCloseTo(0, 5);
8787
expect(flat.soleAngleDeg).toBeCloseTo(0, 5);
8888

89-
m.bones.get("knee_left")!.rotation.x = 25 * DEG;
89+
// A tilt deeper than the ankle's dorsiflexion ROM (20°) can't be fully
90+
// levelled: the toe/ball stays the low edge while the heel remains lifted
91+
// and the sole is still visibly pitched.
92+
m.bones.get("knee_left")!.rotation.x = 35 * DEG;
9093
m.root.updateMatrixWorld(true);
9194
groundFigure(m);
9295
levelPlantedFeet(m, ["foot_left"]);
9396
groundFigure(m);
9497
const limited = measureFootContact(m, "left")!;
95-
expect(Math.min(limited.heelHeight, limited.toeHeight)).toBeLessThan(0.02);
96-
expect(limited.heelHeight).toBeGreaterThan(0.02);
98+
expect(limited.toeHeight).toBeLessThan(limited.heelHeight);
99+
expect(limited.heelHeight - limited.toeHeight).toBeGreaterThan(0.02);
97100
expect(limited.soleAngleDeg).toBeGreaterThan(5);
98101
});
99102
});

playground/public/moves/deadlift.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ <h2>The .posecode source</h2>
156156

157157
step &quot;Lower&quot; 1.8s flow:
158158
pelvis: hinge 75
159-
knees: flex 25
159+
knees: flex 18
160160
ankles: plantarflex 0
161161
shoulders: flex 70
162162
elbows: pronate 80

playground/public/moves/superhero-landing.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ <h2>The .posecode source</h2>
167167
spine: flex 50
168168
chest: flex 9
169169
hip_right: flex 84
170-
knee_right: flex 123
170+
knee_right: flex 115
171171
ankle_right: dorsiflex 15
172172
hip_left: extend 12
173173
knee_left: flex 110

0 commit comments

Comments
 (0)