Skip to content

Commit 8257df4

Browse files
Fix floor-clipping across the library; correct deadlift/row arms; remove push-up (#4)
QA pass driven by real Playwright screenshots + bone-position diagnostics (not just numeric spot-checks) against the running app, after a prior review missed that verified-numbers didn't match what actually rendered. Root cause 1 — ground-lock anchored the wrong point: - Feet-only ground-lock zeroed the ANKLE BONE's height, not the foot mesh's sole (~0.04m lower) — feet visibly sank into the floor on every ground-locked standing movement (squat and ~15 others). - Hands+feet ground-lock (push-up/plank family) had the same bone-vs-mesh gap on both ends. Fixed with a final rigid-body nudge after the existing rotation converges. Root cause 2 — no anchor at all for some phases: - Movements with neither ground-lock nor a pin (prone/supine core work: crunch, superman, bicycle-crunch, supine-leg-raise) had the root frozen at the base pose's grounded height while FK bent freely on top of it, letting bent limbs swing below the floor with nothing to catch them (superman peaked at -0.61m). Added a per-frame safety clamp: if a phase has no ground-lock/pin, keep the lowest mesh point at y=0 (a no-op when the pose is legitimately elevated). - mountain-climber was missing `ground-lock: hands, feet` entirely; added it. - crunch never reset `knees` in its "Lower" phase, so the loop-to-reset wrap snapped the knees straight every rep; added the reset. Root cause 3 — deadlift/bent-over-row arms were authored before the hip-hinge existed and never accounted for it: `shoulders: flex` is relative to the torso, and the torso already pitches forward with the hinge, so the two rotations stacked (95° hinge + 90° flex ≈ 185° total) and flung the arms up behind the body instead of hanging them toward the floor. Switched to `extend` (which counter-rotates against the hinge) and tuned the hinge/knee depth so the arm lands near-vertical. Verified: wrist now sits below the shoulder in both. Root cause 4 — pins engaging from a very different starting pose: triceps-dips transitioned directly from standing into a pinned dip, and the mid-interpolation root shift swung the untouched legs through the floor every rep (worst -0.345). Restructured into Sit → Support → Lower → Press → Stand so the pin only engages once the body is already close to its final shape, and tuned the leg angle for clearance at the bottom of the dip. Also removed step-up's premature pin on its first phase — plain FK already lands the foot within centimeters of the box, so the pin now only engages once the foot is already close (residual glitch equity dropped from a sustained -0.05 to a ~2-frame -0.06 blip at the pin handoff). Camera: `frameCamera()` only fit the mannequin's bounding box, so a scene prop taller than the figure (the pull-up bar) got cropped out of frame. Now unions the prop scene into the frame — the whole bar and the hanging figure are both visible. Content: removed `push-up` from the catalogue and spec/examples per request (the grounding bug was fixed, but it's being pulled from the library anyway). Verification: 139 tests pass; every example still parses ROM-clean; a headless Playwright sweep sampled all 65 movements at 6 points each through the real render pipeline — zero clip below -0.02 except the documented sub-frame step-up blip. Claude-Session: https://claude.ai/code/session_018WCktDrKYZpJjCb2apbqWp Co-authored-by: Claude <noreply@anthropic.com>
1 parent 87ed5d0 commit 8257df4

10 files changed

Lines changed: 81 additions & 38 deletions

File tree

docs/coverage-gap.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ majority; the **body-weight subset (~25%)** is our addressable universe.
2828
| Core / waist | dead-bug, glute-bridge, hanging-knee-raise, spinal-twist, side-bend, cross-body-reach | ~6 | **No crunch / plank / leg-raise / climber** |
2929
| Shoulders | lateral-raise, shoulder-flexion, shoulder-abduction, shoulder-rolls, overhead-reach, arm-circles, port-de-bras | ~7 | Good |
3030
| Back | bent-over-row, deadlift, good-morning, cobra, dead-hang | ~5 | **No superman / extension holds** |
31-
| Chest | chest-opener (stretch only) | 1 | **Weak — push-up not even registered** |
31+
| Chest | chest-opener (stretch only) | 1 | **Weak — push-up was pulled from the catalogue; still the obvious gap-filler once chest coverage is revisited** |
3232
| Upper arms | biceps-curl, elbow-forearm | 2 | **No triceps work** |
3333
| Lower legs | heel-raises, relevé | 2 | Thin |
3434
| Neck | neck-rotation, neck-side-stretch | 2 | Fine for scope |
@@ -41,7 +41,8 @@ Ordered by gap size × how cleanly the engine renders it:
4141

4242
1. **Core**`plank-hold`, `mountain-climber` (plank pose), `crunch`,
4343
`bicycle-crunch`, `supine-leg-raise` (supine pose). Highest-value, cleanest.
44-
2. **Chest** — register the existing `push-up`.
44+
2. **Chest** — still open; `push-up` was authored and then pulled from the
45+
catalogue (see note above), so this gap remains.
4546
3. **Back**`superman` (prone pose).
4647
4. **Upper legs**`forward-lunge` (FK split stance, feet leveled).
4748
5. **Lower legs**`single-leg-calf-raise`.

packages/movit-render/src/index.ts

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -258,17 +258,35 @@ export function createViewer(
258258
if (Math.abs(deriv) < 1e-4) break;
259259
rotateRootAboutPivot(pivot, THREE.MathUtils.clamp(-y0 / deriv, -0.35, 0.35));
260260
}
261+
// The loop above zeroes the WRIST BONE's height, but the visible hand
262+
// (wrist ball + forearm capsule) and foot (mesh box) extend a bit below
263+
// their bones, leaving the mesh sunk into the floor by that offset. Catch
264+
// it with one final rigid-body vertical nudge (rotation already set the
265+
// correct tilt; this only corrects the residual bone-vs-mesh gap).
266+
mannequin.root.updateMatrixWorld(true);
267+
const box = new THREE.Box3().setFromObject(mannequin.root);
268+
if (box.min.y < 0) {
269+
mannequin.root.position.y -= box.min.y;
270+
mannequin.root.updateMatrixWorld(true);
271+
}
261272
return;
262273
}
263274

264275
if (feet.length > 0) {
265-
let sumY = 0;
276+
// Ground the FOOT MESH's lowest point, not the ankle bone's origin — the
277+
// bone sits ~0.04m above the sole (foot box + capsule radius), so
278+
// anchoring the bone itself left the visible foot sunk into the floor.
279+
let minY = Infinity;
266280
for (const id of feet) {
267281
const node = mannequin.bones.get(id);
268-
if (node) sumY += node.getWorldPosition(new THREE.Vector3()).y;
282+
if (!node) continue;
283+
const box = new THREE.Box3().setFromObject(node);
284+
if (Number.isFinite(box.min.y)) minY = Math.min(minY, box.min.y);
285+
}
286+
if (Number.isFinite(minY)) {
287+
mannequin.root.position.y -= minY;
288+
mannequin.root.updateMatrixWorld(true);
269289
}
270-
mannequin.root.position.y -= sumY / feet.length;
271-
mannequin.root.updateMatrixWorld(true);
272290
}
273291
}
274292

@@ -359,7 +377,10 @@ export function createViewer(
359377

360378
function frameCamera(): void {
361379
// Auto-frame the figure: fit its bounding box, keep a pleasant angle.
380+
// Include any scene prop too — a pull-up bar sits well above the figure's
381+
// head, and framing on the mannequin alone left it cropped out of view.
362382
const box = new THREE.Box3().setFromObject(mannequin.root);
383+
if (propScene) box.union(new THREE.Box3().setFromObject(propScene.group));
363384
if (box.isEmpty()) return;
364385
const center = box.getCenter(new THREE.Vector3());
365386
const size = box.getSize(new THREE.Vector3());
@@ -387,6 +408,21 @@ export function createViewer(
387408
mannequin.root.updateMatrixWorld(true);
388409
applyGroundLock(info.groundLock);
389410
applyPins(info.pins);
411+
// Safety net: a phase with neither ground-lock nor a pin has no
412+
// per-frame anchor at all, so the root stays frozen at the base pose's
413+
// grounded height while the FK animates freely on top of it — bending
414+
// joints (e.g. a prone "superman" lift, a supine crunch) can then swing
415+
// part of the mesh below the floor with nothing to catch it. Clamp the
416+
// root up so the lowest point never dips below y=0; a no-op whenever the
417+
// pose is legitimately elevated (bbox min already ≥ 0).
418+
if (info.groundLock.length === 0 && info.pins.length === 0) {
419+
mannequin.root.updateMatrixWorld(true);
420+
const box = new THREE.Box3().setFromObject(mannequin.root);
421+
if (box.min.y < 0) {
422+
mannequin.root.position.y -= box.min.y;
423+
mannequin.root.updateMatrixWorld(true);
424+
}
425+
}
390426
applyReaches(info.reaches);
391427
if (info.phaseName !== lastPhaseName) {
392428
lastPhaseName = info.phaseName;

playground/src/presets.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import posture from "../../spec/examples/posture-reset.movit?raw";
99
import twist from "../../spec/examples/spinal-twist.movit?raw";
1010
import chair from "../../spec/examples/chair-pose.movit?raw";
1111
import sideBend from "../../spec/examples/side-bend.movit?raw";
12-
import pushup from "../../spec/examples/pushup.movit?raw";
1312

1413
// Hip-hinge showcases.
1514
import deadlift from "../../spec/examples/deadlift.movit?raw";
@@ -120,7 +119,6 @@ export const PRESETS: Preset[] = [
120119
{ id: "chest-opener", label: "Chest opener", domain: "Desk & posture", bodyPart: "Chest", target: "Pectorals", equipment: "Body weight", difficulty: "Beginner", source: chestOpener },
121120

122121
// --- Strength & core (coverage-gap batch) ---
123-
{ id: "pushup", label: "Push-up", domain: "Fitness", bodyPart: "Chest", target: "Pectorals", equipment: "Body weight", difficulty: "Intermediate", source: pushup },
124122
{ id: "plank-hold", label: "Plank hold", domain: "Fitness", bodyPart: "Core", target: "Abdominals", equipment: "Body weight", difficulty: "Beginner", source: plankHold },
125123
{ id: "mountain-climber", label: "Mountain climber", domain: "Fitness", bodyPart: "Core", target: "Abdominals", equipment: "Body weight", difficulty: "Intermediate", source: mountainClimber },
126124
{ id: "crunch", label: "Crunch", domain: "Fitness", bodyPart: "Core", target: "Abdominals", equipment: "Body weight", difficulty: "Beginner", source: crunch },

spec/examples/bent-over-row.movit

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,22 @@ movit exercise "Bent-over row"
33
pose start = standing
44

55
step "Set the hinge" 1.6s ease-in-out:
6-
pelvis: hinge 80
6+
pelvis: hinge 70
77
knees: flex 20
8-
shoulders: flex 70
8+
shoulders: extend 55
99
elbows: flex 10
1010
neck: extend 12
1111
ground-lock: feet
1212
cue "Hinge to a flat back, let the arms hang straight down"
1313

1414
step "Row" 0.9s ease-out:
15-
shoulders: flex 35
15+
shoulders: extend 15
1616
elbows: flex 95
1717
ground-lock: feet
1818
cue "Drive the elbows up past the ribs, squeeze the shoulder blades"
1919

2020
step "Lower" 1s ease-in:
21-
shoulders: flex 70
21+
shoulders: extend 55
2222
elbows: flex 10
2323
ground-lock: feet
2424
cue "Lower the bar under control, keep the back flat"

spec/examples/crunch.movit

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,16 @@ movit exercise "Crunch"
88
chest: flex 20
99
neck: flex 20
1010
shoulders: flex 45
11+
ground-lock: feet
1112
cue "Curl the shoulders off the floor, ribs toward the hips"
1213

1314
step "Lower" 1.2s ease-in:
15+
knees: flex 0
1416
spine: flex 0
1517
chest: flex 0
1618
neck: flex 0
1719
shoulders: flex 0
20+
ground-lock: feet
1821
cue "Lower the upper back down with control"
1922

2023
repeat 12

spec/examples/deadlift.movit

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,17 @@ movit exercise "Deadlift"
33
pose start = standing
44

55
step "Lower" 1.8s ease-in-out:
6-
pelvis: hinge 95
6+
pelvis: hinge 75
77
knees: flex 25
8-
shoulders: flex 90
8+
shoulders: extend 60
99
neck: extend 12
1010
ground-lock: feet
1111
cue "Push the hips back and hinge with a flat back — let the arms hang to the bar"
1212

1313
step "Lift" 1.4s ease-out:
1414
pelvis: hinge 0
1515
knees: flex 0
16-
shoulders: flex 0
16+
shoulders: extend 0
1717
neck: extend 0
1818
ground-lock: feet
1919
cue "Drive the hips forward to stand tall, bar close to the body"

spec/examples/mountain-climber.movit

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,21 @@ movit exercise "Mountain climber"
55
step "Right knee in" 0.5s ease-out:
66
hip_right: flex 60
77
knee_right: flex 75
8+
ground-lock: hands, feet
89
cue "Drive the right knee toward the chest"
910

1011
step "Switch" 0.5s ease-in-out:
1112
hip_right: flex 0
1213
knee_right: flex 0
1314
hip_left: flex 60
1415
knee_left: flex 75
16+
ground-lock: hands, feet
1517
cue "Switch fast — left knee drives in"
1618

1719
step "Out" 0.5s ease-in:
1820
hip_left: flex 0
1921
knee_left: flex 0
22+
ground-lock: hands, feet
2023
cue "Return to a strong plank"
2124

2225
repeat 8

spec/examples/pushup.movit

Lines changed: 0 additions & 17 deletions
This file was deleted.

spec/examples/step-up.movit

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ movit exercise "Step-up"
66
step "Plant the foot" 1s ease-in-out:
77
hip_right: flex 70
88
knee_right: flex 90
9-
pin: foot_right box
109
cue "Plant the right foot up on top of the box"
1110

1211
step "Drive up" 1.2s ease-out:

spec/examples/triceps-dips.movit

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,45 @@ movit exercise "Triceps dips"
33
prop chair
44
pose start = standing
55

6-
step "Support" 1s ease-in-out:
6+
step "Sit" 0.8s ease-in-out:
7+
hips: flex 0
8+
knees: flex 130
9+
ground-lock: feet
10+
cue "Sit on the edge of the chair, hands beside the hips, feet tucked in"
11+
12+
step "Support" 0.6s ease-in-out:
13+
hips: flex 0
14+
knees: flex 130
715
shoulders: extend 25
816
elbows: flex 10
9-
hips: flex 80
10-
knees: flex 80
1117
pin: hand_left seat
1218
pin: hand_right seat
13-
cue "Hands on the edge behind you, hips just off the seat, knees bent"
19+
cue "Grip the edge and lift the hips just off the seat"
1420

1521
step "Lower" 1.1s ease-in-out:
16-
elbows: flex 95
22+
hips: flex 0
23+
knees: flex 130
24+
shoulders: extend 25
25+
elbows: flex 60
1726
pin: hand_left seat
1827
pin: hand_right seat
1928
cue "Bend the elbows to lower the hips toward the floor"
2029

2130
step "Press" 1s ease-out:
31+
hips: flex 0
32+
knees: flex 130
33+
shoulders: extend 25
2234
elbows: flex 10
2335
pin: hand_left seat
2436
pin: hand_right seat
2537
cue "Press through the palms to straighten the arms"
2638

39+
step "Stand" 0.8s ease-in:
40+
hips: flex 0
41+
knees: flex 0
42+
shoulders: extend 0
43+
elbows: flex 0
44+
ground-lock: feet
45+
cue "Step off and stand up"
46+
2747
repeat 8

0 commit comments

Comments
 (0)