# 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.
