Add Warm-Up and Stretching activity types

Adds WorkoutActivityType.warmUp (HealthKit .preparationAndRecovery) and
.stretching (.flexibility), and retags the six starter splits that were all
mislabeled as Functional Strength:

- Warm-Up:    Upper Body Warm-Up, Lower Body Warm-Up, Morning Wake-Up
- Stretching: Morning Mobility, Full Body Stretch, Evening Stretch

The split editor's activity picker surfaces them automatically (CaseIterable).
Older app versions decode the new raw values as the default type — additive and
not schema-gated, so no quarantine.
This commit is contained in:
2026-07-09 15:45:31 -04:00
parent f01e149269
commit e12fe31152
308 changed files with 5594 additions and 990 deletions
+22 -12
View File
@@ -108,15 +108,24 @@ never touches a motion script** — proportions are the skeleton's problem.
## The visual language
- **Near vs far** — the rule that never bends: within each limb pair the
member **nearer the camera** draws dark (`#3a3f4b`) and in front, the far
member light (`#a9afba`) and behind. Working limbs keep the split: near =
teal `#0d9488`, far = light teal `#86cfc5`. Opposite-limb moves (bird dog,
dead bug) read as visibly opposite: one dark-teal limb, one light-teal
limb. When a view leaves depths tied (face-on machines), the canvas-right
member takes the dark ink. Draw order is by camera depth (far parts first),
so a twist genuinely passes the near arm in front of the chest; the head
paints last, filled opaque, so overhead arms pass behind the face.
- **Near vs far** — a smooth depth gradient, resolved per joint: every drawn
joint is toned by its own camera depth between the near, dark end (`#3a3f4b`;
teal `#0d9488` when working) and the far, light end (`#a9afba`; light teal
`#86cfc5`), and each bone is stroked with a gradient between its two joints'
tones, so the ink flows along a limb that reaches toward or away from the
camera instead of the whole bone snapping to one flat tone; the stroke width
tracks it too (near 6 → far 5). A joint reaches full contrast once it sits
`SHADE_SPAN_FRAC` of the shoulder/pelvis half-width in front of (or behind)
its limb pair's central depth plane, so a straight limb in a side view is
still fully dark (near) or light (far) — the per-joint average matches the
old flat per-limb tone — while as the two members cross (mid-orbit, or
face-on machines) they fade to a shared mid-tone instead of the ink snapping.
Opposite-limb moves (bird dog, dead bug) still read as opposite: one leaning
dark-teal, one light-teal. The **binary** near/far pick (canvas-right wins a
depth tie) stays underneath, driving the far offset and draw order — depth-
sorted, far parts first, so a twist genuinely passes the near arm in front of
the chest; the head paints last, filled opaque, so overhead arms pass behind
the face.
- **The far offset** — in profile views both members of a pair project onto
the same line; the far member is nudged by the profile's `farOffset` so it
stays distinguishable. The nudge scales with how side-on the view is and
@@ -223,9 +232,10 @@ flat resource copy, so the library itself never enters the app bundle. Only
the `--export` copies ship: `skeleton.json` plus uniquely-named
`<Name>.motion.json` and `<Name>.info.md` files in
`Workouts/Resources/ExerciseMotions/`, consumed by the in-app renderer
(`Workouts/ExerciseFigure/`, compiled into both the iOS and watch targets)
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`,
(`Workouts/ExerciseFigure/`, compiled into the iOS target only — the watch's
run screen is timer-only, so its figure sources and these resources were
dropped from the watch target) 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 (figure and prop primitives, including orbit-presentation
samples) the Swift solver must reproduce; regenerate with