# 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 └── / ← 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 ├── .motion.json ← 65 └── .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), - `.motion.json` (one per folder, renamed to a unique basename), - `.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 `.motion.json` / `.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 `.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//`: 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 ""` (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.