Author the machine exercise circuit with props and corrected hip machine motions

Fourteen Planet Fitness machine exercises join the rig library, each with
authored motion, info page, and schematic equipment via the new props layer
(scene shapes, cables, bars, pads) rendered in lockstep by render.py and the
in-app figure renderer. Abductor and Adductor are authored face-on with the
real seated machine motion — knees-bent legs swinging apart/together against
knee pads — replacing the earlier middle-split depiction. The watch target
now bundles the figure renderer and motion rigs too.

Claude-Session: https://claude.ai/code/session_01LEoff8bXGBS83tK1c55Mf7
This commit is contained in:
2026-07-06 16:29:31 -04:00
parent 888852cc2e
commit fce8fa4c17
130 changed files with 4116 additions and 82 deletions
+55 -8
View File
@@ -52,6 +52,48 @@ changing data, never artwork.
natural arcs and bone lengths never distort. The last frame tweens back to
the first (looping). Asymmetric timing carries technique: leg raises lower
slowly (`tween` 1.4 s down, 0.6 s up).
- **Face-on figures** — `gaze` is optional: a frame without it faces the
viewer and draws no nose tick. Used by exercises whose motion is lateral
(abductor/adductor), where a side view would hide the movement.
## The props layer
Machines and free weights are data too: an optional top-level `"props"` array
adds an equipment layer around the figure. `scene` shapes and `cable`s draw
*behind* the figure in a recessive gray; joint-attached items (`bar`,
`dumbbell`, `pad`) draw *over* the limbs in a darker gray and follow the
resolved hand/foot positions every frame — a pinned foot pressing a `pad`
carries the platform with it through the tween for free. The figure stays the
hero: props are schematic silhouettes (a seat, a backrest, one handle), never
scale drawings of the machine.
```json
"props": [
{"type": "scene", "shapes": [
{"kind": "line", "pts": [[134, 123], [96, 36]], "w": 9},
{"kind": "rect", "x": 54, "y": 104, "w": 40, "h": 8, "r": 3},
{"kind": "circle", "c": [142, 77], "r": 3.5, "fill": true, "color": "prop"}
]},
{"type": "cable", "from": [190, 8], "to": ["hand_r", "hand_l"]},
{"type": "bar", "at": ["hand_r", "hand_l"], "halfLen": 26, "plateR": 0},
{"type": "dumbbell", "at": "hand_r"},
{"type": "pad", "at": ["foot_r", "foot_l"], "angle": 88, "halfLen": 20, "w": 6}
]
```
- **`scene`** — static shapes in canvas coordinates: `line` (polyline, stroke
width `w`), `circle` (`fill: false` for an outline), `rect` (filled, corner
radius `r`). A shape may set `"color": "prop"` to use the darker
attached-item gray (e.g. a fixed handle the hands rest on).
- **`cable`** — a thin line from a fixed anchor `from` to a moving joint `to`;
the machine's pulley line.
- **`bar` / `dumbbell` / `pad`** — a segment centered on the joint(s) in
`at` (a single extremity, or the midpoint of `["hand_r", "hand_l"]`).
`bar` lies at a fixed world `angle` (default 0 = horizontal — in side view a
two-handed bar is drawn horizontal by convention); `dumbbell` and `pad`
default to perpendicular to the lower bone (forearm/shin), or take an
explicit `angle`. `plateR` puts filled discs on both ends (dumbbells default
to 4.5). A prop whose limb is hidden that frame simply isn't drawn.
- The same math is the plan for the app: a small SwiftUI renderer consumes
`body.json` + `motion.json` and tweens angles on the lower half of the
exercise screen (the paged timer flow occupies only the top half).
@@ -61,9 +103,12 @@ changing data, never artwork.
- **Right vs left limb** — the one rule that never bends: the figure's
**right**-side limbs are dark (`#3a3f4b`), its **left**-side limbs are light
(`#a9afba`) and drawn *behind* the body. Working limbs keep the split:
right = teal `#0d9488`, left = light teal `#86cfc5`. Every render embeds a
small `R —` / `L —` legend, so opposite-limb moves (bird dog, dead bug) are
visibly opposite: one dark-teal limb, one light-teal limb.
right = teal `#0d9488`, left = light teal `#86cfc5`. Opposite-limb moves
(bird dog, dead bug) read as visibly opposite: one dark-teal limb, one
light-teal limb. `render.py` reference renders embed a small `R —` / `L —`
legend for the rig author; the in-app renderer deliberately omits it — the
labels are only anatomically true for right-facing figures, and the
contrast alone carries the meaning.
- **Facing / front-of-torso** — the head carries a **nose tick** (`gaze`
angle); the belly is on that side. Prone noses point at the floor, supine
at the ceiling. The head is drawn last, filled opaque, so overhead arms
@@ -83,7 +128,7 @@ python3 render.py --sheet # + contact-sheet.png of every key frame
python3 render.py --demo # + demo-sheet.png: profile / flip / theme variants
python3 render.py --figure=female # render with another body profile
python3 render.py --flip # mirror the figure (faces the other way)
python3 render.py --export # copy body.json + <Name>.motion.json app resources
python3 render.py --export # copy body.json + <Name>.motion.json + <Name>.info.md app resources
```
`render.py` needs only Pillow (for GIFs/sheets; the SVGs have no dependency).
@@ -91,7 +136,9 @@ 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
flat resource copy, so the library itself never enters the app bundle. Only
the `--export` copies ship: `body.json` plus uniquely-named
`<Name>.motion.json` files in `Workouts/Resources/ExerciseMotions/`, consumed
by the in-app SwiftUI renderer (`Workouts/ExerciseFigure/`). Re-run
`python3 render.py --export` after editing any motion; the library stays the
source of truth.
`<Name>.motion.json` and `<Name>.info.md` files in
`Workouts/Resources/ExerciseMotions/`, consumed by the in-app SwiftUI renderer
(`Workouts/ExerciseFigure/`) and the exercise-library reference screen
(`ExerciseInfo.swift` parses the info pages). Re-run
`python3 render.py --export` after editing any motion or info page; the
library stays the source of truth.