Stance and flight keyframes with single-frame foot pins for the strikes, forward lean, opposite-arm drive, and daylight under both feet in flight — distinct from Cardio's high-knee march, which stays as is. Claude-Session: https://claude.ai/code/session_01PKptrgbx74peTwHGRxBojv
281 lines
13 KiB
Markdown
281 lines
13 KiB
Markdown
# Exercise Library
|
||
|
||
How the exercise library is organized, end to end — from the hand-authored
|
||
source at the repo root, through the render/export pipeline, to the resources
|
||
the app bundles and the Swift code that consumes them.
|
||
|
||
There are **two** locations, and it matters which is which:
|
||
|
||
| | Path | Role |
|
||
|---|---|---|
|
||
| **Source** | `Exercise Library/` (repo root, outside every app target) | Authored + generated reference material. The source of truth. |
|
||
| **Bundle** | `Workouts/Resources/ExerciseMotions/` | Verbatim export of just the two authored files per exercise (plus `skeleton.json`). The only part that ships. |
|
||
|
||
Data flows one way: you edit the **source**, run `render.py --export`, and the
|
||
**bundle** is overwritten. Never hand-edit anything under
|
||
`Workouts/Resources/ExerciseMotions/` — it is regenerated.
|
||
|
||
As of this writing the catalog is **65 exercises**.
|
||
|
||
```
|
||
Exercise Library/ ← authoring source (repo root)
|
||
├── README.md SYSTEM.md COVERAGE.md ← the three governing docs
|
||
├── skeleton.json ← shared rig: bone lengths + joint ROM
|
||
├── render.py kinematics.py ← the render pipeline + the FK math
|
||
├── contact-sheet.png demo-sheet.png ← generated QA sheets
|
||
└── <Exercise Name>/ ← one folder per exercise, 65 of them
|
||
├── info.md ← AUTHORED: the reference page
|
||
├── motion.json ← AUTHORED: the rig script (canonical visual source)
|
||
├── frames/frame-N.svg ← generated: one SVG per key frame
|
||
├── visual.svg ← generated: the primary frame (static contexts)
|
||
├── preview.gif ← generated: the tweened, looping animation
|
||
└── orbit.gif ← generated (on demand): camera sweeps 360°
|
||
|
||
│ python3 render.py --export
|
||
▼
|
||
Workouts/Resources/ExerciseMotions/ ← bundle (flat, unique basenames)
|
||
├── skeleton.json
|
||
├── <Exercise Name>.motion.json ← 65
|
||
└── <Exercise Name>.info.md ← 65
|
||
|
||
│ Bundle.main lookups
|
||
▼
|
||
Workouts/ExerciseFigure/ ← app-side consumers (iOS only)
|
||
├── ExerciseMotion.swift → ExerciseMotionLibrary (decodes rigs; catalog list)
|
||
├── ExerciseInfo.swift → ExerciseInfoLibrary (parses info.md)
|
||
├── MotionSolver.swift → port of kinematics.py
|
||
└── ExerciseFigureView.swift → ExerciseFigureSlot (the animated figure)
|
||
```
|
||
|
||
## The authoring source (`Exercise Library/`)
|
||
|
||
The library lives at the repo root, **deliberately outside** the app targets'
|
||
source folders. Every exercise folder holds same-named files (`info.md`,
|
||
`visual.svg`) that would collide in Xcode's flat resource copy, so the library
|
||
directory itself is never added to the app — only the uniquely-renamed
|
||
`--export` copies ship.
|
||
|
||
### The three governing docs
|
||
|
||
Read these first; they are the authority on their topics and this file does not
|
||
duplicate their depth:
|
||
|
||
- **`README.md`** — the short index: what each per-exercise file is, and the
|
||
authoring order for `info.md`.
|
||
- **`SYSTEM.md`** — the visual system: the anatomical 3D rig, the joint-angle
|
||
conventions (flexion/abduction/rotation, ISB coordinate frame), the full
|
||
`motion.json` schema (frames, pins, camera, zoom, props), the visual language
|
||
(near/far shading, working-part teal, nose tick, mat), and the `render.py`
|
||
command reference. This is the big one.
|
||
- **`COVERAGE.md`** — the **closed-catalog** model: the movement-pattern ×
|
||
modality matrix and the gym-floor census that together define which exercises
|
||
the library *must* contain, plus the considered exclusions. Judge every
|
||
addition or removal against it.
|
||
|
||
### One folder per exercise
|
||
|
||
Folder names are exact app-facing exercise names (`Bench Press`, `Cat-Cow`,
|
||
`Child's Pose`). Each folder has two **authored** files and several
|
||
**generated** ones:
|
||
|
||
| File | Authored? | What it is |
|
||
|---|---|---|
|
||
| `info.md` | ✅ hand-written | The reference page (summary, facts, instructions). Parsed by the app. |
|
||
| `motion.json` | ✅ hand-written | The rig script: key frames of joint angles. **Canonical source for all visuals** and the in-app animation. |
|
||
| `frames/frame-N.svg` | generated | One SVG per key frame. |
|
||
| `visual.svg` | generated | The `primary` frame, for static contexts. |
|
||
| `preview.gif` | generated | The tweened, looping animation. |
|
||
| `orbit.gif` | generated (`--orbit`) | Camera sweeps 360° while the motion loops. |
|
||
|
||
The generated files are **never hand-edited** — `render.py` rewrites them from
|
||
`motion.json`. They exist in-repo as browsable reference/QA; only `visual.svg`
|
||
and the GIFs are for humans, and none of them are exported to the app (the app
|
||
renders live from the rig instead — see below).
|
||
|
||
### Shared / library-level files
|
||
|
||
- **`skeleton.json`** — the shared rig. Named bone-length **profiles**
|
||
(`neutral`, `female`, `male`) plus each joint's degrees of freedom and its
|
||
physiological **range of motion (ROM)**. The app renders `neutral` only.
|
||
Because motions are authored against joint *names* in anatomical
|
||
coordinates, swapping a profile never touches a motion script.
|
||
*(Note: `README.md` still calls this `body.json` — a stale name; the file is
|
||
`skeleton.json`, and `--export` actively deletes any legacy `body.json`.)*
|
||
- **`render.py`** — the renderer (see pipeline section). ~910 lines; needs only
|
||
Pillow for the GIFs/sheets (SVGs are dependency-free).
|
||
- **`kinematics.py`** — the forward-kinematics + projection math, imported by
|
||
`render.py`. The in-app `MotionSolver.swift` is a line-for-line port of this.
|
||
- **`contact-sheet.png`** / **`demo-sheet.png`** — generated QA sheets
|
||
(`--sheet` / `--demo`): every key frame, and rig customizations (profiles,
|
||
flipped camera, theme).
|
||
|
||
## The two authored inputs, in detail
|
||
|
||
### `info.md` — the reference page
|
||
|
||
Uniform hand-authored Markdown in a fixed shape (the app parses the *known
|
||
shape*, not general Markdown). Order: `# Title`, a one-line summary paragraph,
|
||
metadata bullets, then instructional sections.
|
||
|
||
```markdown
|
||
# Bench Press
|
||
|
||
The barbell press — lying on the flat bench, lower the bar to the chest…
|
||
|
||
- **Category:** Main circuit
|
||
- **Type:** Free-weight horizontal press
|
||
- **Targets:** Chest, front delts, triceps
|
||
- **Prescription:** 4 × 6–8
|
||
- **Defaults:** 4 × 6 weighted
|
||
|
||
## Setup
|
||
## Execution (numbered steps)
|
||
## Cues (bullets)
|
||
## Common Mistakes (bullets)
|
||
## Progression (easier → harder)
|
||
```
|
||
|
||
Two metadata bullets carry app behavior beyond display:
|
||
|
||
- **`Type:`** — if it starts "Machine-based", `ExerciseInfo.isMachineBased` is
|
||
true, which gates the machine-settings UI.
|
||
- **`Defaults:`** — the suggested starting plan (`4 × 6 weighted`,
|
||
`3 × 12 bodyweight`, `3 × 30 s`) parsed into sets/reps/loadType/duration for
|
||
an exercise added outside a split. Unrecognized → `nil` (no guessing).
|
||
|
||
### `motion.json` — the rig script
|
||
|
||
The exercise scripted as key frames of anatomical joint angles on the shared
|
||
rig, with IK pins for planted hands/feet, per-exercise camera, and an optional
|
||
equipment `props` layer. **`SYSTEM.md` is the schema authority** — the shape in
|
||
brief:
|
||
|
||
```json
|
||
{
|
||
"name": "Bench Press",
|
||
"primary": 1, // 1-based frame for visual.svg
|
||
"camera": {"yaw": 0, "zoom": 0.7}, // side view; zoom fits tall standing motions
|
||
"working": ["arm_r", "arm_l"], // parts drawn in accent teal
|
||
"props": [ /* scene / cable / bar / dumbbell / pad / roller */ ],
|
||
"frames": [
|
||
{ "hold": 0.4, "tween": 0.9,
|
||
"root": {"pos": [150, 116], "pitch": -90}, // pelvis anchor + trunk orientation
|
||
"spine": [0, 0], "neck": 8, "head": 0,
|
||
"shoulder_r": 15, "elbow_r": 100, // flexion shorthand, or a {…} dict
|
||
"hip_r": 78, "knee_r": 82, "ankle_r": 5,
|
||
"pins": {"foot_r": [196, 148], "hand_r": [93, 100]} } // IK targets
|
||
]
|
||
}
|
||
```
|
||
|
||
Angles are degrees from the neutral standing pose (flexion forward, abduction
|
||
from the midline, rotation external). Tweening happens in angle space so limbs
|
||
swing in natural arcs; the last frame loops back to the first. Props have
|
||
world-space 3D form and orbit with the figure. Canvas is 320×180, ground line
|
||
at y = 152.
|
||
|
||
## The render pipeline
|
||
|
||
`render.py` (+ `kinematics.py` + `skeleton.json`) resolves each `motion.json`
|
||
through 3D forward kinematics and orthographic projection into the generated
|
||
files. Run from inside the library dir:
|
||
|
||
```sh
|
||
cd "Exercise Library"
|
||
python3 render.py # all exercises → frames/*.svg, preview.gif, visual.svg
|
||
python3 render.py "Bird Dog" # just one exercise
|
||
python3 render.py --sheet # + contact-sheet.png (every key frame)
|
||
python3 render.py --demo # + demo-sheet.png (profiles / flipped / theme)
|
||
python3 render.py --orbit "Bird Dog" # orbit.gif: 360° camera sweep
|
||
python3 render.py --figure=female # render with another skeleton profile
|
||
python3 render.py --flip # view from the other side
|
||
python3 render.py --strict # fail on any ROM violation, listing each
|
||
python3 render.py --export # bake app resources (see below)
|
||
python3 render.py --fixtures # regenerate the Swift-solver fixtures (see below)
|
||
```
|
||
|
||
Every key frame is validated against the skeleton ROM; an impossible pose is
|
||
caught mechanically (a warning count, or a hard fail under `--strict`).
|
||
|
||
## The export step
|
||
|
||
`render.py --export` writes `Workouts/Resources/ExerciseMotions/` — and **only**
|
||
three kinds of file:
|
||
|
||
- `skeleton.json` (verbatim copy),
|
||
- `<Exercise Name>.motion.json` (one per folder, renamed to a unique basename),
|
||
- `<Exercise Name>.info.md` (one per folder, likewise).
|
||
|
||
The generated SVGs and GIFs do **not** ship — the app renders the figure live
|
||
from the rig instead. The rename to `<Name>.motion.json` / `<Name>.info.md` is
|
||
what lets Xcode's flat resource copy hold all 65 without collision.
|
||
|
||
## The app-side consumers (`Workouts/ExerciseFigure/`)
|
||
|
||
Compiled into the **iOS target only** — commit b53381e dropped the
|
||
`ExerciseFigure` sources *and* the `ExerciseMotions` resources from the watch
|
||
target, which now shows a timer-only run screen (its own figure-less
|
||
`Workouts Watch App/Views/ExerciseProgressView.swift`). All lookups are
|
||
exact-name `Bundle.main` matches against the exported basenames.
|
||
|
||
- **`ExerciseMotion.swift` → `ExerciseMotionLibrary`** — Codable mirror of the
|
||
rig data; decodes `<Name>.motion.json` + `skeleton.json`.
|
||
- `ExerciseMotionLibrary.exerciseNames` enumerates every bundled
|
||
`*.motion.json`, strips the suffix, and sorts — this is **the picker's
|
||
entire list** and thus the app's catalog.
|
||
- `resources(for:)` returns the motion + neutral profile for one exercise, or
|
||
`nil` if none is bundled.
|
||
- **`ExerciseInfo.swift` → `ExerciseInfoLibrary`** — `ExerciseInfo.parse(...)`
|
||
turns the `info.md` shape into `summary` / `category` / `type` / `targets`
|
||
(split for chips) / `sections` / `defaults`. `ExerciseInfoLibrary.info(for:)`
|
||
loads the bundled page.
|
||
- **`MotionSolver.swift`** — the FK + projection solver, a line-for-line port of
|
||
`kinematics.py`. It is held to the Python pipeline by
|
||
**`WorkoutsTests/Fixtures/figure-fixtures.json`** (projected-geometry
|
||
snapshots the Swift solver must reproduce); regenerate with
|
||
`render.py --fixtures` alongside any pipeline change.
|
||
- **`ExerciseFigureView.swift` → `ExerciseFigureSlot(exerciseName:)`** — the
|
||
public view. Renders the looping, slowly-orbiting figure with `Canvas` +
|
||
`TimelineView`, or empty space when no motion matches. Used by the run
|
||
screen's bottom half (`ExerciseProgressView`) and the reference screen
|
||
(`ExerciseLibraryView`).
|
||
|
||
Other views that surface the catalog: `ExercisePickerView` and
|
||
`ExerciseLibraryView` (Settings → Library) both list
|
||
`ExerciseMotionLibrary.exerciseNames`.
|
||
|
||
### The closed-catalog rule
|
||
|
||
The picker lists **exactly** the bundled names with **no free-text fallback**.
|
||
So the library is the app's closed catalog: a movement pattern or gym station
|
||
with no entry is a hard wall for logging, not a cosmetic gap. That is why
|
||
`COVERAGE.md` exists and why additions are judged against it.
|
||
|
||
## Making a change
|
||
|
||
Adding or editing an exercise is a source-only edit plus a re-export:
|
||
|
||
1. **Add/edit the folder** under `Exercise Library/<Exercise Name>/`: write or
|
||
revise `info.md` and `motion.json`. For a new exercise, first check it against
|
||
`COVERAGE.md` (does a real program train this pattern/station?).
|
||
2. **Render** to preview and validate: `python3 render.py "<Exercise Name>"`
|
||
(or bare `render.py` for all). Fix any ROM warnings; `--strict` to enforce.
|
||
3. **Export** the bundle: `python3 render.py --export`. This is what the app
|
||
sees; the picker picks up a new name automatically (no code change).
|
||
4. **If you changed the pipeline** (`render.py`/`kinematics.py`/`skeleton.json`,
|
||
or anything affecting projected geometry), regenerate the Swift-solver
|
||
fixtures: `python3 render.py --fixtures`, and run `WorkoutsTests` so
|
||
`MotionSolver` stays in lockstep with the Python renderer.
|
||
|
||
A persisted new field in `info.md` or `motion.json` that the app must read also
|
||
needs the matching Codable/parse update in `ExerciseMotion.swift` /
|
||
`ExerciseInfo.swift`.
|
||
|
||
## See also
|
||
|
||
- `Exercise Library/SYSTEM.md` — the rig, joint conventions, full `motion.json`
|
||
schema, visual language, and render command reference.
|
||
- `Exercise Library/COVERAGE.md` — the closed-catalog coverage model.
|
||
- `Exercise Library/README.md` — the per-file index and `info.md` authoring order.
|
||
- `CLAUDE.md` → *Architecture → Starter Data → Exercise library* — the one-paragraph summary.
|