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
13 KiB
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 forinfo.md.SYSTEM.md— the visual system: the anatomical 3D rig, the joint-angle conventions (flexion/abduction/rotation, ISB coordinate frame), the fullmotion.jsonschema (frames, pins, camera, zoom, props), the visual language (near/far shading, working-part teal, nose tick, mat), and therender.pycommand 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 rendersneutralonly. Because motions are authored against joint names in anatomical coordinates, swapping a profile never touches a motion script. (Note:README.mdstill calls thisbody.json— a stale name; the file isskeleton.json, and--exportactively deletes any legacybody.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 byrender.py. The in-appMotionSolver.swiftis 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.
# 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.isMachineBasedis 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:
{
"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:
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.exerciseNamesenumerates 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, ornilif none is bundled.
ExerciseInfo.swift→ExerciseInfoLibrary—ExerciseInfo.parse(...)turns theinfo.mdshape intosummary/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 ofkinematics.py. It is held to the Python pipeline byWorkoutsTests/Fixtures/figure-fixtures.json(projected-geometry snapshots the Swift solver must reproduce); regenerate withrender.py --fixturesalongside any pipeline change.ExerciseFigureView.swift→ExerciseFigureSlot(exerciseName:)— the public view. Renders the looping, slowly-orbiting figure withCanvas+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:
- Add/edit the folder under
Exercise Library/<Exercise Name>/: write or reviseinfo.mdandmotion.json. For a new exercise, first check it againstCOVERAGE.md(does a real program train this pattern/station?). - Render to preview and validate:
python3 render.py "<Exercise Name>"(or barerender.pyfor all). Fix any ROM warnings;--strictto enforce. - Export the bundle:
python3 render.py --export. This is what the app sees; the picker picks up a new name automatically (no code change). - 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 runWorkoutsTestssoMotionSolverstays 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, fullmotion.jsonschema, visual language, and render command reference.Exercise Library/COVERAGE.md— the closed-catalog coverage model.Exercise Library/README.md— the per-file index andinfo.mdauthoring order.CLAUDE.md→ Architecture → Starter Data → Exercise library — the one-paragraph summary.