Fix figure IK snapping and gate the library on a fail-hard motion checker

Three solver defects made limbs teleport, twist, or windmill: write-back
angles wrapped at ±180 and lerped the long way around; branch flips landed
on configurations the anatomical write-back cannot represent, silently
pulling pinned extremities off their pins; and the degenerate straight-limb
bend plane fell back to the camera axis instead of the anatomical anterior.
solve_limb now verifies each branch reproduces the solved end before
accepting it, resolve unwraps written-back angles toward the pose they
replace, and the degenerate plane comes from the parent's anterior axis.

render.py --check replays every exercise's full tween loop and fails hard
on six invariants (pin fidelity, continuity, wraps, authored-vs-resolved
drift, ground penetration, resolved ROM); --export refuses to ship a
failing exercise. All 66 motions re-authored or retouched to pass: honest
authored angles where pins used to override them silently, grounded feet
on the seated machines, a vertical bench-press bar path, straight-armed
child's pose, a butterfly stretch seated on the mat, and FK arms where
pins forced impossible reaches. MotionSolver.swift mirrors the solver
changes line for line, held by regenerated fixtures.

Claude-Session: https://claude.ai/code/session_01PKptrgbx74peTwHGRxBojv
This commit is contained in:
2026-07-12 00:37:23 -04:00
parent 400601283e
commit 79e75a9127
303 changed files with 3114 additions and 2272 deletions
+17
View File
@@ -221,10 +221,27 @@ python3 render.py --orbit "Bird Dog" # orbit.gif: camera sweeps 360° while loo
python3 render.py --figure=female # render with another skeleton profile
python3 render.py --flip # view from the other side (camera + 180°)
python3 render.py --strict # fail on any ROM violation, listing each
python3 render.py --check # simulate every tick; fail hard on solver inconsistencies
python3 render.py --export # bake app resources into Workouts/Resources/ExerciseMotions
python3 render.py --fixtures # regenerate WorkoutsTests/Fixtures/figure-fixtures.json
```
`--check` goes further than `--strict`'s authored-frame ROM gate: it's compute-only (no
frames/GIFs written) and replays the exact pipeline `render_exercise` drives — resolve
every key frame, build the tween timeline, re-resolve every tick — at the flat, unpitched
camera with no mat (elevation and the mat are presentation-only), watching for the ways
the solver has historically swallowed inconsistencies: **pin fidelity** (a pinned hand or
foot drifting off its canvas target as it tweens), **pin-unreachable** (an authored pin
farther than the limb can physically reach, reported as its own authoring-error class),
**continuity** (any drawn joint teleporting between adjacent ticks), **wrap** (an angle
DoF swinging more than 180° between adjacent key frames, including the loop closure —
an unnaturally long lerp arc), **authored-vs-resolved drift** (a pin silently overriding
the authored pose by more than 45°), and **resolved ROM** (the ROM gate re-run on
post-IK angles, with slack). Violations print per exercise, grouped and capped to a
worst-instance-plus-count per class/joint, ending in a pass/fail tally; **`--export`
always runs the full `--check` across the folders it's about to copy first and aborts
if anything fails** — there's no override, bad geometry must never ship.
`render.py` needs only Pillow (for GIFs/sheets; the SVGs have no dependency).
The library lives at the repo root, outside every target's source folders —
same-named files per entry (`info.md`, `visual.svg`) would collide in Xcode's