Add the exercise reference library, animated exercise figures, and exercise categories

Exercise Library/ holds per-exercise reference docs (setup, cues,
mistakes, progressions) with SVG visuals and a Python-rendered motion
pipeline; Workouts/ExerciseFigure renders the bundled *.motion.json
rigs as animated stick figures on the exercise screen. Exercises gain
a warm-up/main-circuit category, timed exercises display hold time via
planSummary, and a completed exercise reopens to a check screen instead
of its timers.
This commit is contained in:
2026-07-06 01:15:52 -04:00
parent ddd5631ef2
commit 7274f155e9
79 changed files with 2521 additions and 11 deletions
+24 -1
View File
@@ -64,12 +64,13 @@ final class Exercise {
var durationTotalSeconds: Int = 0
var weightLastUpdated: Date?
var weightReminderTimeIntervalWeeks: Int = 2
var categoryRaw: Int = ExerciseCategory.main.rawValue
var split: Split?
init(id: String, name: String, order: Int, sets: Int, reps: Int, weight: Int,
loadType: Int, durationTotalSeconds: Int, weightLastUpdated: Date?,
weightReminderTimeIntervalWeeks: Int) {
weightReminderTimeIntervalWeeks: Int, categoryRaw: Int = ExerciseCategory.main.rawValue) {
self.id = id
self.name = name
self.order = order
@@ -80,6 +81,7 @@ final class Exercise {
self.durationTotalSeconds = durationTotalSeconds
self.weightLastUpdated = weightLastUpdated
self.weightReminderTimeIntervalWeeks = weightReminderTimeIntervalWeeks
self.categoryRaw = categoryRaw
}
var loadTypeEnum: LoadType {
@@ -87,6 +89,27 @@ final class Exercise {
set { loadType = newValue.rawValue }
}
var categoryEnum: ExerciseCategory {
get { ExerciseCategory(rawValue: categoryRaw) ?? .main }
set { categoryRaw = newValue.rawValue }
}
/// One-line plan summary for list rows duration exercises show their hold time
/// ("3 × 45 sec"), unloaded ones drop the weight ("3 × 12 reps"), weighted ones
/// keep the full "3 × 10 × 40 lb".
func planSummary(weightUnit: WeightUnit) -> String {
switch loadTypeEnum {
case .duration:
let m = durationMinutes, s = durationSeconds
let hold = m > 0 && s > 0 ? "\(m)m \(s)s" : (m > 0 ? "\(m) min" : "\(s) sec")
return "\(sets) × \(hold)"
case .none:
return "\(sets) × \(reps) reps"
case .weight:
return "\(sets) × \(reps) × \(weightUnit.format(weight))"
}
}
/// Minutes component of the total duration (for min:sec pickers).
var durationMinutes: Int {
get { durationTotalSeconds / 60 }