Give machine props world-space 3D form that rotates with the camera

Scene shapes, cable anchors, bar angles, pad perpendiculars, and roller
offsets all resolve in the authored view exactly as before, then rotate
about the world-vertical axis through the root anchor - the same
resolve-then-rotate pattern as the figure's pins and the mat - so at the
authored yaw every exercise renders bit-identically to today, and under
an orbiting camera the equipment turns with the figure while staying
welded to its hands and feet. Scene lines gain an optional depth plane
(z) and slab extrusion (depth) so seats, backrests, and platforms keep
form edge-on; the rect shape is retired (re-authored as slab lines).
All 14 machines' props re-authored with depths and verified at eight
orbit angles. The fixture snapshots move into the pipeline as
render.py --fixtures and now cover orbit-presentation samples with
resolved prop primitives for a spread of prop flavors; the in-app
renderer resolves props in MotionSolver (lockstep with resolve_props)
and the view just draws primitives.

Claude-Session: https://claude.ai/code/session_01HJDQQDA9QdP8zByg43H5v3
This commit is contained in:
2026-07-06 22:15:45 -04:00
parent ed906535b6
commit 81186c51b1
92 changed files with 723 additions and 403 deletions
+34 -17
View File
@@ -132,8 +132,8 @@ 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": "line", "pts": [[134, 123], [96, 36]], "w": 9, "depth": 8},
{"kind": "line", "pts": [[160, 64], [160, 120]], "w": 16, "z": -14},
{"kind": "circle", "c": [142, 77], "r": 3.5, "fill": true, "color": "prop"}
]},
{"type": "cable", "from": [190, 8], "to": ["hand_r", "hand_l"]},
@@ -143,23 +143,37 @@ scale drawings of the machine.
]
```
- **`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). Scene shapes
are view-locked billboards — they don't rotate with `--orbit`.
- **`cable`** — a thin line from a fixed anchor `from` to a moving joint `to`;
the machine's pulley line.
Props are authored in the authored view but have **world-space 3D form**:
under an orbiting camera the whole equipment layer rotates about the
world-vertical axis through the root anchor, exactly like the figure and the
mat. Everything resolves in the *authored* view — scene points, cable
anchors, bar angles, pad perpendiculars, roller offsets — and the resolved
constructs are then rotated; joint-attached items keep following the rotated
figure's hands and feet, so equipment stays welded to the body. At the
authored yaw nothing moves, so the authored look is exact.
- **`scene`** — static shapes: `line` (polyline, stroke width `w`) and
`circle` (`fill: false` for an outline). Points are canvas `[x, y]` (or
`[x, y, z]` where needed); a shape-level `"z"` sets its depth plane
(positive toward the camera), and a line with `"depth"` is a **slab**
extruded that half-width through its plane, it stays a plain line edge-on
and opens into a swept quad as the camera orbits (seats, backrests,
platforms). 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` (`[x, y]` or
`[x, y, z]`) to a moving joint `to`; the machine's pulley line.
- **`bar` / `dumbbell` / `pad`** — a segment centered on the joint(s) in
`at` (a single joint, or the midpoint of a list). Joints are the extremities
(`hand_r`, `foot_l`, …) plus the mid joints (`elbow_r`, `knee_l`, …), so a
machine pad can ride a knee (`["knee_r", "knee_l"]`) or span a shin
(`["knee_r", "foot_r"]`).
`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.
`bar` lies at a fixed authored-view `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`. Under orbit the segment rotates with the scene
and foreshortens naturally. `plateR` puts filled discs on both ends
(dumbbells default to 4.5). A prop whose limb is hidden that frame simply
isn't drawn.
- **`roller`** — a machine roller pad seen end-on: a filled disc riding the
limb's lower bone near the joint in `at`, on the `side` (+1/1) of the bone
it presses — a leg extension's instep roller (`side: 1`), a leg curl's
@@ -179,6 +193,7 @@ python3 render.py --figure=female # render with another skeleton profile
python3 render.py --flip # view from the other side (camera + 180°)
python3 render.py --strict # fail on any ROM violation, listing each
python3 render.py --export # bake app resources into Workouts/Resources/ExerciseMotions
python3 render.py --fixtures # regenerate WorkoutsTests/Fixtures/figure-fixtures.json
```
`render.py` needs only Pillow (for GIFs/sheets; the SVGs have no dependency).
@@ -192,6 +207,8 @@ the `--export` copies ship: `skeleton.json` plus uniquely-named
and the exercise-library reference screen (`ExerciseInfo.swift` parses the
info pages). The in-app solver is a line-for-line port of `kinematics.py`,
held to it by `WorkoutsTests/Fixtures/figure-fixtures.json` — projected
geometry snapshots the Swift solver must reproduce; regenerate them alongside
any pipeline change. Re-run `python3 render.py --export` after editing any
motion or info page; the library stays the source of truth.
geometry snapshots (figure and prop primitives, including orbit-presentation
samples) the Swift solver must reproduce; regenerate with
`python3 render.py --fixtures` alongside any pipeline change. Re-run
`python3 render.py --export` after editing any motion or info page; the
library stays the source of truth.