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
+32 -1
View File
@@ -21,7 +21,8 @@ struct SplitDocumentCodableTests {
loadType: 0,
durationSeconds: 0,
weightLastUpdated: Self.stableTimestamp,
weightReminderWeeks: 4
weightReminderWeeks: 4,
category: ExerciseCategory.warmup.rawValue
)
let original = SplitDocument(
schemaVersion: SplitDocument.currentSchemaVersion,
@@ -42,4 +43,34 @@ struct SplitDocumentCodableTests {
#expect(decoded == original)
#expect(decoded.relativePath == "Splits/\(original.id).json")
}
/// A pre-category exercise (no `category` key) must still decode the field is
/// optional and deliberately not schema-bumped, defaulting to the main circuit.
@Test func decodesLegacyExerciseWithoutCategory() throws {
let json = """
{
"schemaVersion": 1,
"id": "01HZZZZZZZZZZZZZZZZZZZZZZZ",
"name": "Push Day",
"color": "blue",
"systemImage": "dumbbell.fill",
"order": 0,
"createdAt": "2023-11-14T22:13:20Z",
"updatedAt": "2023-11-14T22:13:20Z",
"exercises": [{
"id": "01HZZZZZZZZZZZZZZZZZZZZZZY",
"name": "Bench Press",
"order": 0,
"sets": 4,
"reps": 10,
"weight": 135,
"loadType": 1,
"durationSeconds": 0,
"weightReminderWeeks": 4
}]
}
"""
let decoded = try DocumentCoder.decode(SplitDocument.self, from: Data(json.utf8))
#expect(decoded.exercises.first?.category == nil)
}
}