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
+25 -6
View File
@@ -19,7 +19,26 @@ import Foundation
/// exported by `render.py --export` into `Resources/ExerciseMotions/` `skeleton.json`
/// plus one `<Exercise Name>.motion.json` per library entry.
/// Bone lengths for one figure profile (`neutral` is the only one the app renders).
/// The figure build the exercise animation renders. A presentation-only choice of
/// skeleton profile (bone proportions) the motion scripts are identical across builds,
/// so switching never touches an exercise's animation, only its dimensions. The raw value
/// is the `skeleton.json` profile key; persisted via `@AppStorage("figureProfile")`.
enum FigureProfile: String, CaseIterable {
case neutral
case feminine = "female"
case masculine = "male"
var displayName: String {
switch self {
case .neutral: "Neutral"
case .feminine: "Feminine"
case .masculine: "Masculine"
}
}
}
/// Bone lengths for one figure profile (`neutral`, `female`, or `male`; the app picks
/// one per the `figureProfile` setting see `FigureProfile`).
struct SkeletonProfile: Codable {
let headR: Double
let neck: Double
@@ -270,10 +289,10 @@ enum ExerciseMotionLibrary {
.sorted()
}()
/// The motion script plus the neutral skeleton profile for `exerciseName`, or `nil`
/// when no bundled motion matches (most exercises have none the caller keeps
/// its space empty).
static func resources(for exerciseName: String) -> Resources? {
/// The motion script plus the requested skeleton `profile` for `exerciseName`, or
/// `nil` when no bundled motion matches (most exercises have none the caller keeps
/// its space empty). An unknown profile key falls back to `neutral`.
static func resources(for exerciseName: String, profile: FigureProfile = .neutral) -> Resources? {
guard
let motionURL = Bundle.main.url(forResource: exerciseName, withExtension: "motion.json"),
let skeletonURL = Bundle.main.url(forResource: "skeleton", withExtension: "json"),
@@ -281,7 +300,7 @@ enum ExerciseMotionLibrary {
let skeletonData = try? Data(contentsOf: skeletonURL),
let motion = try? JSONDecoder().decode(ExerciseMotion.self, from: motionData),
let skeleton = try? JSONDecoder().decode(Skeleton.self, from: skeletonData),
let profile = skeleton.profiles["neutral"]
let profile = skeleton.profiles[profile.rawValue] ?? skeleton.profiles[FigureProfile.neutral.rawValue]
else { return nil }
return Resources(motion: motion, profile: profile)
}