diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0556ef5..b196ca8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,12 +1,18 @@
**July 2026**
+A new Progress tab shows weekly goal streaks, workout trends, per-exercise weight progression, achievements, and your full workout history.
+
+A new Meditation exercise and matching starter routine let you log timed sits, recorded in Apple Health as Mind & Body sessions.
+
Finishing an exercise from the watch no longer risks crashing the workout screen on the iPhone.
Deleting a workout no longer crashes the app.
While you rest between exercises, the workout screen now previews the next exercise's figure and name so you can see what's coming up.
-Spoken cues can now announce the next exercise during a rest and count you into every set with "in 1, 2, 3, GO!".
+Spoken cues can now announce the next exercise during a rest and count you into every set with "in 3, 2, 1, GO!".
+
+Spoken cues now also count timed sets out, announcing the exercise's end with the same "in 3, 2, 1" countdown.
A new Narration setting picks whether spoken cues give the coming-up countdown, the full setup and form read, or both.
diff --git a/Exercise Library/COVERAGE.md b/Exercise Library/COVERAGE.md
index d6a4df5..6ee7d97 100644
--- a/Exercise Library/COVERAGE.md
+++ b/Exercise Library/COVERAGE.md
@@ -40,6 +40,7 @@ it is a considered exclusion, not a gap.
| Core flexion | Abdominal | — | Crunch, Reverse Crunch, Leg Raises |
| Mobility / warm-up | — | — | Cat-Cow, March in Place, Leg Swings, Neck Rolls, Arm Circles, Torso Twist, Standing Side Bend, Hip Circles |
| Static stretch | — | — | Standing Forward Fold, Standing Quad Stretch, Standing Calf Stretch, Chest Opener, Overhead Triceps Stretch, Kneeling Hip Flexor Stretch, Butterfly Stretch, Cobra Stretch, Child's Pose |
+| Mindfulness / breath | — | — | Meditation |
## Gym-floor census
@@ -60,6 +61,7 @@ entries) a user logging by that station's name will find.
| Floor / mat | Bodyweight Squat, Push-Up, Crunch, Glute Bridge, Plank, Side Plank, Bird Dog, Dead Bug, Hollow Body Hold, Leg Raises, Reverse Crunch, Cat-Cow |
| Open floor (warm-up / mobility) | March in Place, Leg Swings, Neck Rolls, Arm Circles, Torso Twist, Standing Side Bend, Hip Circles, Standing Calf Raise |
| Stretching mat (cool-down) | Standing Forward Fold, Standing Quad Stretch, Standing Calf Stretch, Chest Opener, Overhead Triceps Stretch, Kneeling Hip Flexor Stretch, Butterfly Stretch, Cobra Stretch, Child's Pose |
+| Quiet corner (mindfulness) | Meditation |
## Considered exclusions (the "most gyms" line)
diff --git a/Exercise Library/Meditation/frames/frame-1.svg b/Exercise Library/Meditation/frames/frame-1.svg
new file mode 100644
index 0000000..4b79dd9
--- /dev/null
+++ b/Exercise Library/Meditation/frames/frame-1.svg
@@ -0,0 +1,42 @@
+
diff --git a/Exercise Library/Meditation/frames/frame-2.svg b/Exercise Library/Meditation/frames/frame-2.svg
new file mode 100644
index 0000000..19a7cd9
--- /dev/null
+++ b/Exercise Library/Meditation/frames/frame-2.svg
@@ -0,0 +1,41 @@
+
diff --git a/Exercise Library/Meditation/info.md b/Exercise Library/Meditation/info.md
new file mode 100644
index 0000000..f50ebb0
--- /dev/null
+++ b/Exercise Library/Meditation/info.md
@@ -0,0 +1,41 @@
+# Meditation
+
+Sit tall with the legs crossed, rest the hands on the knees, and follow the
+breath. A timed stillness practice that lowers stress, settles the nervous
+system, and closes a session — or a day — with focus.
+
+- **Category:** Mindfulness
+- **Type:** Seated meditation (timed hold)
+- **Targets:** Breath, focus, posture
+- **Prescription:** Sit 5–10 min, attention on the breath, returning to it whenever the mind wanders
+- **Defaults:** 1 × 300 s
+
+## Setup
+
+Sit on the floor (or a cushion) with the legs crossed — lotus, half-lotus, or a
+simple cross, whatever the hips allow. Stack the spine tall, rest the hands on
+the knees, and soften the gaze or close the eyes.
+
+## Execution
+
+1. Lengthen up through the crown of the head, letting the shoulders drop.
+2. Breathe slowly through the nose — a long inhale, a longer exhale.
+3. Rest the attention on the breath moving in and out.
+4. When the mind wanders, notice it without judgment and return to the breath.
+
+## Cues
+
+- Sit on the front edge of a cushion to tilt the pelvis and free the spine.
+- Let the breath deepen on its own — observe it rather than forcing it.
+- Keep the face, jaw, and hands soft; effort lives only in the tall spine.
+
+## Common Mistakes
+
+- Slumping through the lower back as the sit goes on.
+- Chasing every thought instead of letting it pass.
+- Straining into a full lotus the hips aren't ready for — a simple cross works.
+
+## Progression
+
+A few minutes seated comfortably → longer sits with a steady tall spine →
+half-lotus or full lotus as hip mobility allows.
diff --git a/Exercise Library/Meditation/motion.json b/Exercise Library/Meditation/motion.json
new file mode 100644
index 0000000..6501a12
--- /dev/null
+++ b/Exercise Library/Meditation/motion.json
@@ -0,0 +1,30 @@
+{
+ "name": "Meditation",
+ "primary": 1,
+ "camera": {"yaw": 45},
+ "working": ["spine"],
+ "frames": [
+ {
+ "hold": 1.2, "tween": 2.6,
+ "root": {"pos": [160, 143], "pitch": 2},
+ "spine": [6, 2],
+ "neck": 4, "head": -8,
+ "shoulder_r": {"flexion": 30, "abduction": 8, "rotation": -25}, "elbow_r": 100,
+ "shoulder_l": {"flexion": 30, "abduction": 8, "rotation": -25}, "elbow_l": 100,
+ "hip_r": {"flexion": 88, "abduction": 45, "rotation": 50}, "knee_r": 146, "ankle_r": 0,
+ "hip_l": {"flexion": 88, "abduction": 45, "rotation": 50}, "knee_l": 146, "ankle_l": 0,
+ "pins": {"hand_r": [174, 95], "hand_l": [176, 96]}
+ },
+ {
+ "hold": 0.8, "tween": 2.8,
+ "root": {"pos": [160, 142], "pitch": 1},
+ "spine": [3, -2],
+ "neck": 2, "head": -6,
+ "shoulder_r": {"flexion": 33, "abduction": 8, "rotation": -25}, "elbow_r": 96,
+ "shoulder_l": {"flexion": 33, "abduction": 8, "rotation": -25}, "elbow_l": 96,
+ "hip_r": {"flexion": 88, "abduction": 45, "rotation": 50}, "knee_r": 146, "ankle_r": 0,
+ "hip_l": {"flexion": 88, "abduction": 45, "rotation": 50}, "knee_l": 146, "ankle_l": 0,
+ "pins": {"hand_r": [174, 93], "hand_l": [176, 94]}
+ }
+ ]
+}
diff --git a/Exercise Library/Meditation/preview.gif b/Exercise Library/Meditation/preview.gif
new file mode 100644
index 0000000..6c7a48d
Binary files /dev/null and b/Exercise Library/Meditation/preview.gif differ
diff --git a/Exercise Library/Meditation/visual.svg b/Exercise Library/Meditation/visual.svg
new file mode 100644
index 0000000..4b79dd9
--- /dev/null
+++ b/Exercise Library/Meditation/visual.svg
@@ -0,0 +1,42 @@
+
diff --git a/README.md b/README.md
index b7c613d..efea401 100644
--- a/README.md
+++ b/README.md
@@ -64,9 +64,13 @@ your own iCloud Drive.
planned reps and weight, and the rest and finish pages offer a small adjust
pill to correct what you actually did (in 2.5 lb / 1.25 kg steps) without
breaking the flow; the completed-exercise page shows the recorded sets.
-- **Progress tracking** — weight-progression charts per exercise across past
- sessions, plotting the top-set weight you actually lifted; workout volume
- sums the recorded sets too.
+- **Progress tab** — per-goal adherence tracks (a 12-week strip per goal with
+ weekly streaks and per-schedule this-week dots), this-week totals with
+ vs-last-week deltas, trend charts (workouts, volume, active time, energy over
+ 7D–All ranges), weight-progression charts per exercise plotting the top-set
+ weight you actually lifted, a derived achievements gallery, and full workout
+ history — everything recomputed from your workout files, nothing separately
+ stored.
- **Apple Watch companion** — starting a workout on the iPhone launches the watch
app straight into it. The watch lists your in-progress workouts; pick one, pick an
exercise, and run it as a paged flow: a **Ready?** lead-in, count-up work phases,
diff --git a/SCHEMA.md b/SCHEMA.md
index d1ebec6..2f3c3f5 100644
--- a/SCHEMA.md
+++ b/SCHEMA.md
@@ -154,7 +154,7 @@ veto seed resurrection forever).
`0` none · `1` weight · `2` duration
**WorkoutActivityType** (Int — on `SplitDocument.activityType`):
-`0` traditionalStrength · `1` functionalStrength · `2` hiit · `3` coreTraining · `4` cardio · `5` cycling
+`0` traditionalStrength · `1` functionalStrength · `2` hiit · `3` coreTraining · `4` cardio · `5` cycling · `6` warmUp · `7` stretching · `8` mindAndBody
**MetricSource** (String — on `WorkoutMetrics.source`):
`watch`, `phoneEstimate`
diff --git a/Scripts/generate_starter_splits.swift b/Scripts/generate_starter_splits.swift
index 78dbd56..fbc6bf1 100644
--- a/Scripts/generate_starter_splits.swift
+++ b/Scripts/generate_starter_splits.swift
@@ -183,6 +183,11 @@ let seeds: [Sp] = [
Sp(name: "Cardio", color: "red", icon: "figure.run", activity: 4, ex: [
Ex(name: "Cardio", sets: 1, reps: 0, load: 2, dur: 1200),
]),
+ // A single timed sit in a `.mindAndBody` (→ HealthKit `.mindAndBody`) split,
+ // same one-block shape as Cardio.
+ Sp(name: "Meditation", color: "purple", icon: "figure.mind.and.body", activity: 8, ex: [
+ Ex(name: "Meditation", sets: 1, reps: 0, load: 2, dur: 300),
+ ]),
]
// MARK: - Emit
diff --git a/Shared/Connectivity/WCPayload.swift b/Shared/Connectivity/WCPayload.swift
index 281e8f7..c843c6f 100644
--- a/Shared/Connectivity/WCPayload.swift
+++ b/Shared/Connectivity/WCPayload.swift
@@ -7,14 +7,17 @@ import Foundation
/// WatchConnectivity allows) keyed by stable ULIDs — no name/date reconciliation.
enum WCPayload {
static let typeKey = "type"
- static let splitsKey = "splits"
+ // PINNED VALUES: the constant names became `routinesKey` / `editingRoutineIDKey`,
+ // but their string VALUES stay `"splits"` / `"editingSplitID"` so an updated phone
+ // still interops with a not-yet-updated watch over WatchConnectivity.
+ static let routinesKey = "splits"
static let workoutsKey = "workouts"
static let workoutKey = "workout"
static let restSecondsKey = "restSeconds"
static let doneCountdownSecondsKey = "doneCountdownSeconds"
static let weightUnitKey = "weightUnit"
static let editingWorkoutIDKey = "editingWorkoutID"
- static let editingSplitIDKey = "editingSplitID"
+ static let editingRoutineIDKey = "editingSplitID"
static let workoutUpdateType = "workoutUpdate" // watch → phone (one workout)
static let requestSyncType = "requestSync" // watch → phone (please push state)
@@ -23,31 +26,31 @@ enum WCPayload {
// MARK: - Phone → Watch (application context: latest-state-wins)
- /// `editingWorkoutID` / `editingSplitID` are an exclusive-edit lock: while the phone
- /// has a workout's exercise (or a split) open in an editor, the watch parks any
+ /// `editingWorkoutID` / `editingRoutineID` are an exclusive-edit lock: while the phone
+ /// has a workout's exercise (or a routine) open in an editor, the watch parks any
/// matching run and locks re-entry, so only one device owns the run at a time. They're
/// part of the same latest-wins context — absent keys mean "not editing" (lock clear).
- static func encodeState(splits: [SplitDocument], workouts: [WorkoutDocument], restSeconds: Int, doneCountdownSeconds: Int, weightUnit: String, editingWorkoutID: String?, editingSplitID: String?) -> [String: Any] {
+ static func encodeState(routines: [RoutineDocument], workouts: [WorkoutDocument], restSeconds: Int, doneCountdownSeconds: Int, weightUnit: String, editingWorkoutID: String?, editingRoutineID: String?) -> [String: Any] {
var dict: [String: Any] = [:]
- if let s = try? DocumentCoder.encoder.encode(splits) { dict[splitsKey] = s }
+ if let s = try? DocumentCoder.encoder.encode(routines) { dict[routinesKey] = s }
if let w = try? DocumentCoder.encoder.encode(workouts) { dict[workoutsKey] = w }
dict[restSecondsKey] = restSeconds
dict[doneCountdownSecondsKey] = doneCountdownSeconds
dict[weightUnitKey] = weightUnit
if let editingWorkoutID { dict[editingWorkoutIDKey] = editingWorkoutID }
- if let editingSplitID { dict[editingSplitIDKey] = editingSplitID }
+ if let editingRoutineID { dict[editingRoutineIDKey] = editingRoutineID }
return dict
}
- /// `nil` means the payload carried splits that failed to decode (a schema mismatch
+ /// `nil` means the payload carried routines that failed to decode (a schema mismatch
/// between the two builds) — distinct from an absent key or a legitimately empty
/// list, so the receiver can surface it instead of silently applying nothing.
- static func decodeSplits(_ dict: [String: Any]) -> [SplitDocument]? {
- guard let data = dict[splitsKey] as? Data else { return [] }
- return try? DocumentCoder.decoder.decode([SplitDocument].self, from: data)
+ static func decodeRoutines(_ dict: [String: Any]) -> [RoutineDocument]? {
+ guard let data = dict[routinesKey] as? Data else { return [] }
+ return try? DocumentCoder.decoder.decode([RoutineDocument].self, from: data)
}
- /// See `decodeSplits` — `nil` is a decode failure, not an empty list.
+ /// See `decodeRoutines` — `nil` is a decode failure, not an empty list.
static func decodeWorkouts(_ dict: [String: Any]) -> [WorkoutDocument]? {
guard let data = dict[workoutsKey] as? Data else { return [] }
return try? DocumentCoder.decoder.decode([WorkoutDocument].self, from: data)
@@ -61,7 +64,7 @@ enum WCPayload {
static func decodeEditingWorkoutID(_ dict: [String: Any]) -> String? { dict[editingWorkoutIDKey] as? String }
- static func decodeEditingSplitID(_ dict: [String: Any]) -> String? { dict[editingSplitIDKey] as? String }
+ static func decodeEditingRoutineID(_ dict: [String: Any]) -> String? { dict[editingRoutineIDKey] as? String }
// MARK: - Watch → Phone (a single updated workout)
diff --git a/Shared/HealthKit/HealthKitMapping.swift b/Shared/HealthKit/HealthKitMapping.swift
index 5cc6d06..d26ca71 100644
--- a/Shared/HealthKit/HealthKitMapping.swift
+++ b/Shared/HealthKit/HealthKitMapping.swift
@@ -26,6 +26,7 @@ extension WorkoutActivityType {
case .cycling: .cycling
case .warmUp: .preparationAndRecovery
case .stretching: .flexibility
+ case .mindAndBody: .mindAndBody
}
}
}
diff --git a/Shared/Model/Documents.swift b/Shared/Model/Documents.swift
index bfe945e..6e6c3f8 100644
--- a/Shared/Model/Documents.swift
+++ b/Shared/Model/Documents.swift
@@ -5,16 +5,16 @@ import IndieSync
/// entities so the wire format can evolve without dragging the cache schema.
///
/// One document = one aggregate root:
-/// • `SplitDocument` embeds its `[ExerciseDocument]` → `Splits/.json`
+/// • `RoutineDocument` embeds its `[ExerciseDocument]` → `Splits/.json`
/// • `WorkoutDocument` embeds its `[WorkoutLogDocument]` → `Workouts/YYYY/MM/.json`
///
/// `schemaVersion` lets us migrate old files on read without forcing a rewrite
/// at sync time, and forward-gates files written by a newer app version.
/// These documents are also the wire format for the iPhone↔Watch bridge.
-// MARK: - Split
+// MARK: - Routine
-struct SplitDocument: Codable, Sendable, Equatable, Identifiable {
+struct RoutineDocument: Codable, Sendable, Equatable, Identifiable {
var schemaVersion: Int
var id: String // ULID
var name: String
@@ -29,7 +29,7 @@ struct SplitDocument: Codable, Sendable, Equatable, Identifiable {
/// older app dropping it on rewrite (reverting to the default) is preferable to
/// quarantining the user's whole routine.
var activityType: Int?
- /// Per-split rest length (seconds) and flow-mode toggle. Both optional and
+ /// Per-routine rest length (seconds) and flow-mode toggle. Both optional and
/// deliberately NOT schema-bumped, same rationale as `activityType`: an older
/// app dropping them on rewrite reverts to the global default rest / no-flow,
/// which is preferable to quarantining the user's whole routine.
@@ -46,6 +46,9 @@ struct SplitDocument: Codable, Sendable, Equatable, Identifiable {
// quarantine rather than rewrite.
static let currentSchemaVersion = 3
+ // PINNED: the iCloud directory name stays "Splits/" even though the type is now
+ // `RoutineDocument`. Existing user files live under Splits/ — the directory name
+ // is on-disk data, not a symbol, and must not change on the rename.
var relativePath: String { "Splits/\(id).json" }
}
@@ -87,8 +90,8 @@ struct SetEntry: Codable, Sendable, Equatable {
struct WorkoutDocument: Codable, Sendable, Equatable, Identifiable {
var schemaVersion: Int
var id: String // ULID (chronological)
- var splitID: String?
- var splitName: String?
+ var routineID: String?
+ var routineName: String?
var start: Date
var end: Date?
var status: String // WorkoutStatus raw value
@@ -109,13 +112,35 @@ struct WorkoutDocument: Codable, Sendable, Equatable, Identifiable {
/// period. See `WorkoutMergePlanner`.
var deletedLogIDs: [String: Date]? = nil
- /// Snapshot of the split's rest length / flow flag at plan time (a running
- /// workout has no live link to its split, same reason sets/reps/weight are
+ /// Snapshot of the routine's rest length / flow flag at plan time (a running
+ /// workout has no live link to its routine, same reason sets/reps/weight are
/// snapshotted per log). Optional and not schema-bumped, same rationale as
- /// `SplitDocument.restSeconds`/`autoAdvance`.
+ /// `RoutineDocument.restSeconds`/`autoAdvance`.
var restSeconds: Int? = nil
var autoAdvance: Bool? = nil
+ // PINNED CODING KEYS: the Swift properties `routineID`/`routineName` were renamed
+ // from `splitID`/`splitName`, but their on-disk (and iPhone↔Watch wire) JSON keys
+ // stay `"splitID"`/`"splitName"` so existing files and a not-yet-updated peer still
+ // decode. Every stored property must be enumerated here (the computed `relativePath`
+ // and static members are not coded); all others keep their default key names.
+ enum CodingKeys: String, CodingKey {
+ case schemaVersion
+ case id
+ case routineID = "splitID"
+ case routineName = "splitName"
+ case start
+ case end
+ case status
+ case createdAt
+ case updatedAt
+ case logs
+ case metrics
+ case deletedLogIDs
+ case restSeconds
+ case autoAdvance
+ }
+
// Bumped 1→2 when `metrics` was added: the captured HR/calorie data is
// irreplaceable, so the forward-gate must quarantine these files on older apps
// rather than let them strip `metrics` on rewrite.
@@ -180,7 +205,7 @@ extension WorkoutDocument {
/// End the workout now, keeping progress: mark every not-completed log as skipped, then
/// recompute so it resolves to `.completed` (with `end` stamped). This is the
/// "End Workout → Save" operation, shared by the in-workout menu and the
- /// start-a-new-split prompt.
+ /// start-a-new-routine prompt.
mutating func endKeepingProgress() {
for i in logs.indices where (WorkoutStatus(rawValue: logs[i].status) ?? .notStarted) != .completed {
logs[i].transition(to: .skipped)
@@ -222,7 +247,7 @@ struct WorkoutLogDocument: Codable, Sendable, Equatable, Identifiable {
}
extension WorkoutLogDocument {
- /// Build a fresh plan-time log from a split's exercise. Snapshots the plan
+ /// Build a fresh plan-time log from a routine's exercise. Snapshots the plan
/// fields (sets/reps/weight/duration) *and* the machine comfort settings, so a
/// running workout carries the exercise's settings as its own editable copy —
/// `nil` stays `nil` (not a machine exercise), and `[]` stays `[]` (machine
@@ -337,9 +362,48 @@ struct WorkoutMetrics: Codable, Sendable, Equatable {
var recordedAt: Date
}
+// MARK: - Schedule
+
+/// The plan layer: one routine + a recurrence + an optional goal tag, persisted as
+/// `Schedules/.json`. The schedule is the join object between routines and
+/// goals — scheduling never touches the routine file (starter routines are
+/// immutable fixed-ULID seeds).
+struct ScheduleDocument: Codable, Sendable, Equatable, Identifiable {
+ var schemaVersion: Int
+ var id: String // ULID
+ var routineID: String
+ var routineName: String // denormalized display fallback (routine may be deleted)
+ var goal: String? // GoalKind raw value; nil = unassigned
+ var recurrence: String // ScheduleRecurrence raw value
+ var weekdays: [Int]? // Calendar weekday numbers 1 (Sun) – 7 (Sat); .fixedDays only
+ var timesPerWeek: Int? // .frequency only
+ var date: Date? = nil // the one-off day; .once only
+ var order: Int
+ var createdAt: Date
+ var updatedAt: Date
+
+ static let currentSchemaVersion = 1
+
+ var relativePath: String { "Schedules/\(id).json" }
+}
+
+extension ScheduleDocument {
+ /// The tagged goal, or nil when unassigned / an unknown raw value.
+ var goalKind: GoalKind? { goal.flatMap(GoalKind.init(rawValue:)) }
+
+ /// The recurrence, defaulting to `.daily` on an unknown raw value.
+ var recurrenceEnum: ScheduleRecurrence { ScheduleRecurrence(rawValue: recurrence) ?? .daily }
+
+ /// Human-readable recurrence line ("Jul 12, 2026" / "Daily" / "Mon & Thu" / "2× per week").
+ var recurrenceSummary: String {
+ recurrenceEnum.summary(weekdays: weekdays, timesPerWeek: timesPerWeek, date: date)
+ }
+}
+
// MARK: - Forward-compatibility gate
// `VersionedDocument` (the `isReadable` quarantine gate for files written by a
// newer app version), `Tombstone`, and `DocumentCoder` all live in IndieSync now.
-extension SplitDocument: VersionedDocument {}
+extension RoutineDocument: VersionedDocument {}
extension WorkoutDocument: VersionedDocument {}
+extension ScheduleDocument: VersionedDocument {}
diff --git a/Shared/Model/Entities.swift b/Shared/Model/Entities.swift
index 5c07e04..96e6aa2 100644
--- a/Shared/Model/Entities.swift
+++ b/Shared/Model/Entities.swift
@@ -11,10 +11,10 @@ import SwiftData
// SwiftData PersistentIdentifier). Computed helpers preserve the API the views
// used against the old Core Data classes.
-// MARK: - Split
+// MARK: - Routine
@Model
-final class Split {
+final class Routine {
@Attribute(.unique) var id: String = ULID.make()
var name: String = ""
var color: String = "indigo"
@@ -27,7 +27,7 @@ final class Split {
var restSeconds: Int?
var autoAdvance: Bool?
- @Relationship(deleteRule: .cascade, inverse: \Exercise.split)
+ @Relationship(deleteRule: .cascade, inverse: \Exercise.routine)
var exercises: [Exercise] = []
init(id: String, name: String, color: String, systemImage: String, order: Int,
@@ -42,7 +42,7 @@ final class Split {
self.jsonRelativePath = jsonRelativePath
}
- static let unnamed = "Unnamed Split"
+ static let unnamed = "Unnamed Routine"
var exercisesArray: [Exercise] { exercises.sorted { $0.order < $1.order } }
@@ -70,7 +70,7 @@ final class Exercise {
// machine exercise; empty → a machine exercise with nothing recorded yet.
var machineSettings: [MachineSetting]?
- var split: Split?
+ var routine: Routine?
init(id: String, name: String, order: Int, sets: Int, reps: Int, weight: Double,
loadType: Int, durationTotalSeconds: Int, machineSettings: [MachineSetting]? = nil) {
@@ -124,8 +124,8 @@ final class Exercise {
@Model
final class Workout {
@Attribute(.unique) var id: String = ULID.make()
- var splitID: String?
- var splitName: String?
+ var routineID: String?
+ var routineName: String?
var start: Date = Date()
var end: Date?
var statusRaw: String = WorkoutStatus.notStarted.rawValue
@@ -156,11 +156,11 @@ final class Workout {
@Relationship(deleteRule: .cascade, inverse: \WorkoutLog.workout)
var logs: [WorkoutLog] = []
- init(id: String, splitID: String?, splitName: String?, start: Date, end: Date?,
+ init(id: String, routineID: String?, routineName: String?, start: Date, end: Date?,
statusRaw: String, createdAt: Date, updatedAt: Date, jsonRelativePath: String) {
self.id = id
- self.splitID = splitID
- self.splitName = splitName
+ self.routineID = routineID
+ self.routineName = routineName
self.start = start
self.end = end
self.statusRaw = statusRaw
@@ -174,6 +174,14 @@ final class Workout {
set { statusRaw = newValue.rawValue }
}
+ /// True while this workout is an untouched draft: created by a start tap, but no
+ /// exercise has ever left `.notStarted`. Peeking into a routine must leave no
+ /// trace, so the start paths discard a pristine draft when the user backs out of
+ /// it instead of persisting a phantom "in progress" workout.
+ var isPristineDraft: Bool {
+ status == .notStarted && logs.allSatisfy { $0.status == .notStarted }
+ }
+
var statusName: String { status.displayName }
var logsArray: [WorkoutLog] { logs.sorted { $0.order < $1.order } }
@@ -298,3 +306,54 @@ final class WorkoutLog {
set { durationTotalSeconds = durationMinutes * 60 + newValue }
}
}
+
+// MARK: - Schedule
+
+@Model
+final class Schedule {
+ @Attribute(.unique) var id: String = ULID.make()
+ var routineID: String = ""
+ var routineName: String = ""
+ var goalRaw: String?
+ var recurrenceRaw: String = ScheduleRecurrence.daily.rawValue
+ var weekdays: [Int]?
+ var timesPerWeek: Int?
+ var date: Date?
+ var order: Int = 0
+ var createdAt: Date = Date()
+ var updatedAt: Date = Date()
+ var jsonRelativePath: String = ""
+
+ init(id: String, routineID: String, routineName: String, goalRaw: String?,
+ recurrenceRaw: String, weekdays: [Int]?, timesPerWeek: Int?, date: Date? = nil,
+ order: Int, createdAt: Date, updatedAt: Date, jsonRelativePath: String) {
+ self.id = id
+ self.routineID = routineID
+ self.routineName = routineName
+ self.goalRaw = goalRaw
+ self.recurrenceRaw = recurrenceRaw
+ self.weekdays = weekdays
+ self.timesPerWeek = timesPerWeek
+ self.date = date
+ self.order = order
+ self.createdAt = createdAt
+ self.updatedAt = updatedAt
+ self.jsonRelativePath = jsonRelativePath
+ }
+
+ var goalKind: GoalKind? {
+ get { goalRaw.flatMap(GoalKind.init(rawValue:)) }
+ set { goalRaw = newValue?.rawValue }
+ }
+
+ var recurrenceEnum: ScheduleRecurrence {
+ get { ScheduleRecurrence(rawValue: recurrenceRaw) ?? .daily }
+ set { recurrenceRaw = newValue.rawValue }
+ }
+
+ /// Human-readable recurrence line — shares its formatting with `ScheduleDocument`
+ /// via `ScheduleRecurrence.summary(weekdays:timesPerWeek:date:)`.
+ var recurrenceSummary: String {
+ recurrenceEnum.summary(weekdays: weekdays, timesPerWeek: timesPerWeek, date: date)
+ }
+}
diff --git a/Shared/Model/Enums.swift b/Shared/Model/Enums.swift
index f98eb75..557beb7 100644
--- a/Shared/Model/Enums.swift
+++ b/Shared/Model/Enums.swift
@@ -35,9 +35,9 @@ enum LoadType: Int, CaseIterable, Codable, Sendable {
}
}
-/// The kind of training a split represents — used to tag the watch's Apple Health
+/// The kind of training a routine represents — used to tag the watch's Apple Health
/// workout so it lands in the right category (and credits the rings correctly).
-/// Persisted as its raw `Int` on `SplitDocument`; the `HKWorkoutActivityType`
+/// Persisted as its raw `Int` on `RoutineDocument`; the `HKWorkoutActivityType`
/// mapping lives in `Shared/HealthKit/HealthKitMapping.swift` to keep this enum
/// HealthKit-free.
enum WorkoutActivityType: Int, CaseIterable, Codable, Sendable {
@@ -49,6 +49,7 @@ enum WorkoutActivityType: Int, CaseIterable, Codable, Sendable {
case cycling = 5
case warmUp = 6
case stretching = 7
+ case mindAndBody = 8
var displayName: String {
switch self {
@@ -60,6 +61,7 @@ enum WorkoutActivityType: Int, CaseIterable, Codable, Sendable {
case .cycling: "Cycling"
case .warmUp: "Warm-Up"
case .stretching: "Stretching"
+ case .mindAndBody: "Mind & Body"
}
}
@@ -73,6 +75,7 @@ enum WorkoutActivityType: Int, CaseIterable, Codable, Sendable {
case .cycling: "figure.outdoor.cycle"
case .warmUp: "figure.arms.open"
case .stretching: "figure.flexibility"
+ case .mindAndBody: "figure.mind.and.body"
}
}
}
@@ -119,3 +122,126 @@ enum WeightUnit: String, CaseIterable, Codable, Sendable {
/// Render a stored weight value with the current unit's label, e.g. "135 lb".
func format(_ value: Int) -> String { "\(value) \(abbreviation)" }
}
+
+// MARK: - Goals
+
+/// The predefined practice-goal categories. Deliberately a small, closed set —
+/// schedules reference them by raw value, so raw values are stable persisted data.
+enum GoalKind: String, CaseIterable, Codable, Sendable, Identifiable {
+ // Declaration order IS the initial board order (before any user reorder):
+ // the day reads top-to-bottom — morning mobility, then cardio, then strength,
+ // with mindfulness closing it out.
+ case mobility, cardio, strength, mindfulness
+
+ var id: String { rawValue }
+
+ var displayName: String {
+ switch self {
+ case .strength: "Strength"
+ // Deliberately double-barreled: the same practice serves performance
+ // ("endurance") and health ("cardio") motivations — the name excludes neither.
+ case .cardio: "Endurance/Cardio"
+ case .mobility: "Mobility"
+ case .mindfulness: "Mindfulness"
+ }
+ }
+
+ var systemImage: String {
+ switch self {
+ case .strength: "dumbbell.fill"
+ case .cardio: "figure.run"
+ case .mobility: "figure.flexibility"
+ case .mindfulness: "brain.head.profile"
+ }
+ }
+
+ /// A hue name `Color.color(from:)` understands. Four distinct hues so the goals
+ /// read apart at a glance.
+ var colorName: String {
+ switch self {
+ case .strength: "red"
+ case .cardio: "orange"
+ case .mobility: "teal"
+ case .mindfulness: "purple"
+ }
+ }
+}
+
+extension GoalKind {
+ static let orderDefaultsKey = "goalKindOrder"
+
+ /// All cases in the user's chosen order: stored raw values first (unknown ones
+ /// dropped, duplicates collapsed), then any cases missing from the stored list in
+ /// declaration order — so newly added kinds appear automatically.
+ static func orderedCases(defaults: UserDefaults = .standard) -> [GoalKind] {
+ let stored = defaults.stringArray(forKey: orderDefaultsKey) ?? []
+ var ordered: [GoalKind] = []
+ var seen = Set()
+ for raw in stored {
+ if let kind = GoalKind(rawValue: raw), seen.insert(kind).inserted {
+ ordered.append(kind)
+ }
+ }
+ for kind in GoalKind.allCases where !seen.contains(kind) {
+ ordered.append(kind)
+ }
+ return ordered
+ }
+
+ /// Persist the display order as `[String]` of raw values.
+ static func saveOrder(_ kinds: [GoalKind], defaults: UserDefaults = .standard) {
+ defaults.set(kinds.map(\.rawValue), forKey: orderDefaultsKey)
+ }
+}
+
+// MARK: - Schedules
+
+/// How often a schedule expects its routine. Raw values are persisted in ScheduleDocument.
+enum ScheduleRecurrence: String, CaseIterable, Codable, Sendable {
+ case once, daily, fixedDays, frequency
+
+ var displayName: String {
+ switch self {
+ case .once: "Once"
+ case .daily: "Daily"
+ case .fixedDays: "Fixed Days"
+ case .frequency: "Times per Week"
+ }
+ }
+}
+
+extension ScheduleRecurrence {
+ /// Human-readable recurrence description shared by `ScheduleDocument` and the
+ /// `Schedule` cache entity, so both render identically. `weekdays` uses Calendar
+ /// weekday numbers (1 = Sun … 7 = Sat); `timesPerWeek` is the frequency count;
+ /// `date` is the one-off day (`.once` only). A degenerate case (fixed days with no
+ /// weekdays, a nil count or date) falls back to the recurrence's own `displayName`.
+ func summary(weekdays: [Int]?, timesPerWeek: Int?, date: Date?) -> String {
+ switch self {
+ case .once:
+ return date?.formatDate() ?? displayName // "Jul 12, 2026" / "Once"
+ case .daily:
+ return displayName // "Daily"
+ case .fixedDays:
+ // A fixed Gregorian calendar's `shortWeekdaySymbols` is 0-indexed from
+ // Sunday, so weekday number `n` maps to index `n - 1` regardless of locale.
+ let symbols = Calendar(identifier: .gregorian).shortWeekdaySymbols
+ let names = (weekdays ?? []).sorted().compactMap { n -> String? in
+ (1...7).contains(n) ? symbols[n - 1] : nil
+ }
+ return names.isEmpty ? displayName : Self.joined(names)
+ case .frequency:
+ return "\(timesPerWeek ?? 0)× per week"
+ }
+ }
+
+ /// Join weekday short names conversationally: "Mon", "Mon & Thu", "Mon, Wed & Fri".
+ private static func joined(_ names: [String]) -> String {
+ switch names.count {
+ case 0: return ""
+ case 1: return names[0]
+ case 2: return "\(names[0]) & \(names[1])"
+ default: return names.dropLast().joined(separator: ", ") + " & " + (names.last ?? "")
+ }
+ }
+}
diff --git a/Shared/Model/Mappers.swift b/Shared/Model/Mappers.swift
index ade5a21..a9f6a72 100644
--- a/Shared/Model/Mappers.swift
+++ b/Shared/Model/Mappers.swift
@@ -21,14 +21,14 @@ extension ExerciseDocument {
}
}
-extension SplitDocument {
- init(from split: Split) {
- self.init(schemaVersion: Self.currentSchemaVersion, id: split.id, name: split.name,
- color: split.color, systemImage: split.systemImage, order: split.order,
- createdAt: split.createdAt, updatedAt: split.updatedAt,
- exercises: split.exercisesArray.map(ExerciseDocument.init(from:)),
- activityType: split.activityTypeRaw,
- restSeconds: split.restSeconds, autoAdvance: split.autoAdvance)
+extension RoutineDocument {
+ init(from routine: Routine) {
+ self.init(schemaVersion: Self.currentSchemaVersion, id: routine.id, name: routine.name,
+ color: routine.color, systemImage: routine.systemImage, order: routine.order,
+ createdAt: routine.createdAt, updatedAt: routine.updatedAt,
+ exercises: routine.exercisesArray.map(ExerciseDocument.init(from:)),
+ activityType: routine.activityTypeRaw,
+ restSeconds: routine.restSeconds, autoAdvance: routine.autoAdvance)
}
}
@@ -46,8 +46,8 @@ extension WorkoutLogDocument {
extension WorkoutDocument {
init(from workout: Workout) {
- self.init(schemaVersion: Self.currentSchemaVersion, id: workout.id, splitID: workout.splitID,
- splitName: workout.splitName, start: workout.start, end: workout.end,
+ self.init(schemaVersion: Self.currentSchemaVersion, id: workout.id, routineID: workout.routineID,
+ routineName: workout.routineName, start: workout.start, end: workout.end,
status: workout.statusRaw, createdAt: workout.createdAt, updatedAt: workout.updatedAt,
logs: workout.logsArray.map(WorkoutLogDocument.init(from:)),
metrics: workout.metrics,
@@ -86,11 +86,22 @@ extension WorkoutDocument {
logs: [])
}
+extension ScheduleDocument {
+ init(from schedule: Schedule) {
+ self.init(schemaVersion: Self.currentSchemaVersion, id: schedule.id,
+ routineID: schedule.routineID, routineName: schedule.routineName,
+ goal: schedule.goalRaw, recurrence: schedule.recurrenceRaw,
+ weekdays: schedule.weekdays, timesPerWeek: schedule.timesPerWeek,
+ date: schedule.date,
+ order: schedule.order, createdAt: schedule.createdAt, updatedAt: schedule.updatedAt)
+ }
+}
+
// MARK: - Document → Cache (upsert)
enum CacheMapper {
- static func fetchSplit(id: String, in context: ModelContext) -> Split? {
- var d = FetchDescriptor(predicate: #Predicate { $0.id == id })
+ static func fetchRoutine(id: String, in context: ModelContext) -> Routine? {
+ var d = FetchDescriptor(predicate: #Predicate { $0.id == id })
d.fetchLimit = 1
return try? context.fetch(d).first
}
@@ -101,30 +112,36 @@ enum CacheMapper {
return try? context.fetch(d).first
}
- // MARK: Split
+ static func fetchSchedule(id: String, in context: ModelContext) -> Schedule? {
+ var d = FetchDescriptor(predicate: #Predicate { $0.id == id })
+ d.fetchLimit = 1
+ return try? context.fetch(d).first
+ }
- static func upsertSplit(_ doc: SplitDocument, relativePath: String, into context: ModelContext) {
- let split: Split
- if let existing = fetchSplit(id: doc.id, in: context) {
- split = existing
+ // MARK: Routine
+
+ static func upsertRoutine(_ doc: RoutineDocument, relativePath: String, into context: ModelContext) {
+ let routine: Routine
+ if let existing = fetchRoutine(id: doc.id, in: context) {
+ routine = existing
} else {
- split = Split(id: doc.id, name: doc.name, color: doc.color, systemImage: doc.systemImage,
+ routine = Routine(id: doc.id, name: doc.name, color: doc.color, systemImage: doc.systemImage,
order: doc.order, createdAt: doc.createdAt, updatedAt: doc.updatedAt,
jsonRelativePath: relativePath)
- context.insert(split)
+ context.insert(routine)
}
- split.name = doc.name
- split.color = doc.color
- split.systemImage = doc.systemImage
- split.order = doc.order
- split.createdAt = doc.createdAt
- split.updatedAt = doc.updatedAt
- split.jsonRelativePath = relativePath
- split.activityTypeRaw = doc.activityType ?? 0
- split.restSeconds = doc.restSeconds
- split.autoAdvance = doc.autoAdvance
+ routine.name = doc.name
+ routine.color = doc.color
+ routine.systemImage = doc.systemImage
+ routine.order = doc.order
+ routine.createdAt = doc.createdAt
+ routine.updatedAt = doc.updatedAt
+ routine.jsonRelativePath = relativePath
+ routine.activityTypeRaw = doc.activityType ?? 0
+ routine.restSeconds = doc.restSeconds
+ routine.autoAdvance = doc.autoAdvance
- let existing = Dictionary(split.exercises.map { ($0.id, $0) }, uniquingKeysWith: { a, _ in a })
+ let existing = Dictionary(routine.exercises.map { ($0.id, $0) }, uniquingKeysWith: { a, _ in a })
var keep = Set()
for ed in doc.exercises {
keep.insert(ed.id)
@@ -132,11 +149,11 @@ enum CacheMapper {
apply(ed, to: e)
} else {
let e = makeExercise(ed)
- e.split = split
+ e.routine = routine
context.insert(e)
}
}
- for e in Array(split.exercises) where !keep.contains(e.id) {
+ for e in Array(routine.exercises) where !keep.contains(e.id) {
context.delete(e)
}
}
@@ -165,14 +182,14 @@ enum CacheMapper {
if let existing = fetchWorkout(id: doc.id, in: context) {
workout = existing
} else {
- workout = Workout(id: doc.id, splitID: doc.splitID, splitName: doc.splitName,
+ workout = Workout(id: doc.id, routineID: doc.routineID, routineName: doc.routineName,
start: doc.start, end: doc.end, statusRaw: doc.status,
createdAt: doc.createdAt, updatedAt: doc.updatedAt,
jsonRelativePath: relativePath)
context.insert(workout)
}
- workout.splitID = doc.splitID
- workout.splitName = doc.splitName
+ workout.routineID = doc.routineID
+ workout.routineName = doc.routineName
workout.start = doc.start
workout.end = doc.end
workout.statusRaw = doc.status
@@ -228,4 +245,31 @@ enum CacheMapper {
l.setEntries = d.setEntries
l.logUpdatedAt = d.updatedAt
}
+
+ // MARK: Schedule
+
+ static func upsertSchedule(_ doc: ScheduleDocument, relativePath: String, into context: ModelContext) {
+ let schedule: Schedule
+ if let existing = fetchSchedule(id: doc.id, in: context) {
+ schedule = existing
+ } else {
+ schedule = Schedule(id: doc.id, routineID: doc.routineID, routineName: doc.routineName,
+ goalRaw: doc.goal, recurrenceRaw: doc.recurrence, weekdays: doc.weekdays,
+ timesPerWeek: doc.timesPerWeek, date: doc.date, order: doc.order,
+ createdAt: doc.createdAt, updatedAt: doc.updatedAt,
+ jsonRelativePath: relativePath)
+ context.insert(schedule)
+ }
+ schedule.routineID = doc.routineID
+ schedule.routineName = doc.routineName
+ schedule.goalRaw = doc.goal
+ schedule.recurrenceRaw = doc.recurrence
+ schedule.weekdays = doc.weekdays
+ schedule.timesPerWeek = doc.timesPerWeek
+ schedule.date = doc.date
+ schedule.order = doc.order
+ schedule.createdAt = doc.createdAt
+ schedule.updatedAt = doc.updatedAt
+ schedule.jsonRelativePath = relativePath
+ }
}
diff --git a/Shared/Persistence/WorkoutsModelContainer.swift b/Shared/Persistence/WorkoutsModelContainer.swift
index b677d5b..81ab4c3 100644
--- a/Shared/Persistence/WorkoutsModelContainer.swift
+++ b/Shared/Persistence/WorkoutsModelContainer.swift
@@ -6,14 +6,16 @@ import SwiftData
/// safe; `SyncEngine` repopulates it from the container on next launch.
enum WorkoutsModelContainer {
/// Bump whenever the cache schema changes — wipes and rebuilds from files.
- /// 2: added health-metric columns to `Workout` and `activityTypeRaw` to `Split`.
+ /// 2: added health-metric columns to `Workout` and `activityTypeRaw` to `Routine`.
/// 3: added `categoryRaw` to `Exercise`.
/// 4: removed the weight-reminder columns and `categoryRaw` from `Exercise` and
/// the `completed` column from `WorkoutLog`; added `machineSettings` to both.
/// 5: `weight` went Int → Double on `Exercise` and `WorkoutLog`; added
/// `setEntries` and `logUpdatedAt` to `WorkoutLog`.
/// 6: added `deletedLogIDs` to `Workout` (per-log merge deletion tombstones).
- static let currentSchemaVersion = 6
+ /// 7: renamed `Split` entity to `Routine`.
+ /// 8: added `Schedule` cache entity.
+ static let currentSchemaVersion = 8
private static let schemaVersionKey = "Workouts.persistenceSchemaVersion"
private static let identityTokenKey = "Workouts.iCloudIdentityToken"
@@ -30,7 +32,7 @@ enum WorkoutsModelContainer {
}
static func make() -> ModelContainer {
- let schema = Schema([Split.self, Exercise.self, Workout.self, WorkoutLog.self])
+ let schema = Schema([Routine.self, Exercise.self, Workout.self, WorkoutLog.self, Schedule.self])
ensureStoreDirectoryExists()
wipeIfNeeded()
wipeIfAccountChanged()
diff --git a/Shared/Screenshots/ScreenshotSeed.swift b/Shared/Screenshots/ScreenshotSeed.swift
index 7ea298c..78c1bb8 100644
--- a/Shared/Screenshots/ScreenshotSeed.swift
+++ b/Shared/Screenshots/ScreenshotSeed.swift
@@ -16,23 +16,24 @@ enum ScreenshotSeed {
}
/// Insert a believable in-progress workout, a few finished sessions (so charts have
- /// a trend), and the starter splits. Returns the in-progress workout to display.
+ /// a trend), and the starter routines. Returns the in-progress workout to display.
@MainActor
@discardableResult
static func populate(_ context: ModelContext) -> Workout? {
// Idempotent: clear any prior seed so repeated launches don't stack duplicates
// (cascade deletes take the child exercises/logs with them).
try? context.delete(model: Workout.self)
- try? context.delete(model: Split.self)
+ try? context.delete(model: Routine.self)
+ try? context.delete(model: Schedule.self)
let cal = Calendar(identifier: .gregorian)
let today = Date()
func daysAgo(_ n: Int) -> Date { cal.date(byAdding: .day, value: -n, to: today) ?? today }
- // ---- Splits (with exercises) ------------------------------------------
+ // ---- Routines (with exercises) ------------------------------------------
// Names match the bundled Exercise Library rigs so every screen shows its
// animated figure (an unknown name would leave the figure slot empty).
- let splits: [(String, String, String, [(String, Int, Int, Int)])] = [
+ let routines: [(String, String, String, [(String, Int, Int, Int)])] = [
("Upper Body", "blue", "figure.strengthtraining.traditional", [
("Lat Pull Down", 4, 10, 110), ("Seated Row", 4, 10, 90),
("Shoulder Press", 4, 10, 40), ("Arm Curl", 3, 12, 40),
@@ -47,44 +48,106 @@ enum ScreenshotSeed {
]),
]
- for (sIndex, s) in splits.enumerated() {
- let split = Split(id: ULID.make(), name: s.0, color: s.1, systemImage: s.2,
+ var created: [Routine] = []
+ for (sIndex, s) in routines.enumerated() {
+ let routine = Routine(id: ULID.make(), name: s.0, color: s.1, systemImage: s.2,
order: sIndex, createdAt: today, updatedAt: today, jsonRelativePath: "")
- context.insert(split)
+ context.insert(routine)
+ created.append(routine)
for (eIndex, e) in s.3.enumerated() {
let isDuration = e.0 == "Plank"
let ex = Exercise(id: ULID.make(), name: e.0, order: eIndex, sets: e.1, reps: e.2,
weight: Double(e.3), loadType: (isDuration ? LoadType.duration : .weight).rawValue,
durationTotalSeconds: isDuration ? 45 : 0)
- ex.split = split
+ ex.routine = routine
context.insert(ex)
}
}
- let upper = splits[0]
+ // ---- Schedules (the Today board's rows, spanning two goal groups) -------
+ let schedulePlans: [(routine: Routine, goal: GoalKind, recurrence: ScheduleRecurrence,
+ weekdays: [Int]?, timesPerWeek: Int?)] = [
+ (created[0], .strength, .fixedDays, [2, 5], nil), // Upper Body · Mon & Thu
+ (created[1], .strength, .frequency, nil, 2), // Lower Body · 2× per week
+ (created[2], .mobility, .daily, nil, nil), // Core · daily
+ ]
+ // Schedules predate the history below, so the Progress tab's adherence
+ // tracks cover the seeded weeks instead of starting "today".
+ let scheduledSince = daysAgo(70)
+ for (i, plan) in schedulePlans.enumerated() {
+ context.insert(Schedule(
+ id: ULID.make(), routineID: plan.routine.id, routineName: plan.routine.name,
+ goalRaw: plan.goal.rawValue, recurrenceRaw: plan.recurrence.rawValue,
+ weekdays: plan.weekdays, timesPerWeek: plan.timesPerWeek, order: i,
+ createdAt: scheduledSince, updatedAt: scheduledSince, jsonRelativePath: ""))
+ }
- // ---- Past finished sessions (Lat Pull Down trend for the chart) --------
- let liftTrend: [Double] = [95, 100, 105, 110]
- for (i, w) in liftTrend.enumerated() {
- let date = daysAgo((liftTrend.count - i) * 4)
- let workout = Workout(id: ULID.make(), splitID: nil, splitName: upper.0, start: date,
- end: date.addingTimeInterval(2400), statusRaw: WorkoutStatus.completed.rawValue,
- createdAt: date, updatedAt: date, jsonRelativePath: "")
+ let upper = routines[0]
+ let upperID = created[0].id
+
+ // ---- Past finished sessions ---------------------------------------------
+ // Ten weeks of believable history across all three routines so Progress has
+ // living goal tracks, streaks, trends, and history: Upper Body lands on its
+ // Mon & Thu slots (one missed), Lower Body twice a week (one missed), Core
+ // most mornings — unbroken in the last four weeks, so the Mobility streak is
+ // alive. Upper's Lat Pull Down ramps toward 110 for the progression chart.
+ // All deterministic — patterns, not randomness.
+ func addCompleted(routine: Routine, spec: [(String, Int, Int, Int)],
+ on day: Date, hour: Int, lengthMinutes: Int, latPullDown: Double?) {
+ let start = cal.date(bySettingHour: hour, minute: 15, second: 0, of: day) ?? day
+ let end = start.addingTimeInterval(Double(lengthMinutes) * 60)
+ let workout = Workout(id: ULID.make(), routineID: routine.id, routineName: routine.name,
+ start: start, end: end, statusRaw: WorkoutStatus.completed.rawValue,
+ createdAt: start, updatedAt: end, jsonRelativePath: "")
context.insert(workout)
- for (eIndex, e) in upper.3.enumerated() {
- let weight = e.0 == "Lat Pull Down" ? w : Double(e.3)
+ for (eIndex, e) in spec.enumerated() {
+ let isDuration = e.0 == "Plank"
+ let weight = e.0 == "Lat Pull Down" ? (latPullDown ?? Double(e.3)) : Double(e.3)
let log = WorkoutLog(id: ULID.make(), exerciseName: e.0, order: eIndex, sets: e.1,
- reps: e.2, weight: weight, loadType: LoadType.weight.rawValue,
- durationTotalSeconds: 0, currentStateIndex: e.1,
- statusRaw: WorkoutStatus.completed.rawValue, notes: nil, date: date,
- setEntries: actualEntries(sets: e.1, reps: e.2, weight: weight, at: date))
+ reps: e.2, weight: isDuration ? 0 : weight,
+ loadType: (isDuration ? LoadType.duration : .weight).rawValue,
+ durationTotalSeconds: isDuration ? 45 : 0, currentStateIndex: e.1,
+ statusRaw: WorkoutStatus.completed.rawValue, notes: nil, date: start,
+ setEntries: isDuration ? nil
+ : actualEntries(sets: e.1, reps: e.2, weight: weight, at: start))
log.workout = workout
context.insert(log)
}
}
+ var upperSlot = 0, lowerSlot = 0
+ for d in stride(from: 69, through: 1, by: -1) {
+ let day = daysAgo(d)
+ let weekday = cal.component(.weekday, from: day)
+
+ // Upper Body — Mon & Thu; the 5th slot is the believable miss.
+ if weekday == 2 || weekday == 5 {
+ upperSlot += 1
+ if upperSlot != 5 {
+ let lift = min(110, 87.5 + 2.5 * Double((upperSlot + 1) / 2))
+ addCompleted(routine: created[0], spec: routines[0].3,
+ on: day, hour: 17, lengthMinutes: 40, latPullDown: lift)
+ }
+ }
+ // Lower Body — Tue & Sat, its 2×-per-week rhythm; the 4th slot missed.
+ if weekday == 3 || weekday == 7 {
+ lowerSlot += 1
+ if lowerSlot != 4 {
+ addCompleted(routine: created[1], spec: routines[1].3,
+ on: day, hour: 18, lengthMinutes: 35, latPullDown: nil)
+ }
+ }
+ // Core — daily mornings; Sundays skipped beyond the last four weeks.
+ if d <= 28 || weekday != 1 {
+ addCompleted(routine: created[2], spec: routines[2].3,
+ on: day, hour: 6, lengthMinutes: 15, latPullDown: nil)
+ }
+ }
+
// ---- The current in-progress session -----------------------------------
- let workout = Workout(id: ULID.make(), splitID: nil, splitName: upper.0, start: today,
+ // Carries the routine's id so the Today board's schedule row shows it as
+ // today's in-progress run.
+ let workout = Workout(id: ULID.make(), routineID: upperID, routineName: upper.0, start: today,
end: nil, statusRaw: WorkoutStatus.inProgress.rawValue,
createdAt: today, updatedAt: today, jsonRelativePath: "")
context.insert(workout)
diff --git a/Shared/Utils/Color+Extensions.swift b/Shared/Utils/Color+Extensions.swift
index a9d593d..56e8493 100644
--- a/Shared/Utils/Color+Extensions.swift
+++ b/Shared/Utils/Color+Extensions.swift
@@ -43,7 +43,7 @@ extension Color {
}
}
-// Canonical palettes for splits (single source of truth).
+// Canonical palettes for routines (single source of truth).
let availableColors = ["red", "orange", "yellow", "green", "mint", "teal", "cyan", "blue", "indigo", "purple", "pink", "brown"]
let availableIcons = ["dumbbell.fill", "figure.strengthtraining.traditional", "figure.run", "figure.hiking", "figure.cooldown", "figure.boxing", "figure.wrestling", "figure.gymnastics", "figure.handball", "figure.core.training", "heart.fill", "bolt.fill"]
diff --git a/UX-REDESIGN.md b/UX-REDESIGN.md
new file mode 100644
index 0000000..d4cfe53
--- /dev/null
+++ b/UX-REDESIGN.md
@@ -0,0 +1,240 @@
+# UX Redesign — Goals, Schedules & the Today Board
+
+Design direction agreed 2026-07-09/10. This restructures the app from a
+single-screen history list into a four-tab app built on three independent
+layers — routines, schedules, goals — with a **Today board** as the entry
+surface, and rides a full visual refresh along with it. The sync architecture
+is untouched; the data-model additions are two small new document aggregates
+(Goals, Schedules) on the existing machinery.
+
+## The three layers
+
+```
+ROUTINES the content — what "Lower Body" or "Evening Meditation" consists of
+SCHEDULES the plan — "Lower Body · every Mon & Thu · → Strength"
+GOALS the meaning — stable categories everything rolls up to
+```
+
+**You plan with schedules, act on routines, observe by goals.** The three are
+deliberately independent:
+
+- **Goals outlive plans.** Switching from a 3-day to a 5-day program rewrites
+ schedules, but the Strength *track* in Progress continues unbroken — history
+ rolls up to the goal, not to the plan that produced it. The goal is the
+ ledger category; a schedule is how you're currently trying to fill it.
+- **Goals are optional.** A schedule with no goal tag still appears on the
+ board and logs normally; it rolls up as uncategorized. Pure-scheduling use
+ works; goals can be adopted later.
+- **Scheduling never touches routine files.** The schedule is the join object
+ (it references `routineID` and `goalID` from its own document), so
+ scheduling an immutable fixed-ULID starter routine never forks the seed.
+
+### Terminology: "Routine" replaces "Split" (user-facing)
+
+"Split" is lifting jargon; the entity is now general — a meditation session, a
+mobility flow, a run. UI strings say **Routine** everywhere; starter splits
+become starter routines. Code-level type names (`Split`, `SplitDocument`, …)
+stay as-is for now — a rename is churn with no user value; revisit only if it
+starts to confuse.
+
+### Goals
+
+Just **name + color/icon + order** — no cadence, no machinery. Examples:
+Strength, Endurance, Morning Movement, Mindfulness. Persisted as
+`Goals/.json` (standard three-place data model). Workouts additionally
+**stamp the goal at completion** (denormalized `goalID`/`goalName`, same
+pattern as the existing `splitID`/`splitName`), so history keeps the
+attribution it had when logged even if schedules are re-tagged later.
+
+### Schedules
+
+A schedule = **one routine + a recurrence + an optional goal tag**, persisted
+as `Schedules/.json`. Recurrence flavors:
+
+1. **Fixed days** — "every Mon & Thu". Deterministic; the board just shows it.
+2. **Frequency** — "2×/week, whenever". The planner picks the days.
+3. **Daily** — e.g. the morning routine or evening meditation.
+
+Plus an optional **time window** (e.g. morning, default cutoff ~11 am,
+tunable) with a per-schedule setting for whether the window gates the streak:
+on = only in-window starts feed the streak, though the row stays startable and
+completable all day; off = any-time completion counts.
+
+**Pool rotation is emergent, not a feature**: three 1×/week frequency
+schedules (Legs, Upper, Core) under Strength rotate naturally via the
+planner's recency pressure. Schedules stay minimal.
+
+**Missed fixed days get a catch-up nudge**: the schedule reappears next day as
+due-with-a-reason ("missed Mon · catch up?"), planner-placed on the best
+make-up day before the next fixed slot (recovery-aware in Phase 2), with an
+explicit skip. Skipping just waits for the next fixed slot.
+
+Example of the model's reach: *Goal: Mindfulness ← Schedule: "Meditation ·
+daily · evening window" ← Routine: one duration-based Meditation exercise*
+(`LoadType.duration` already exists; add a `mindAndBody` case to
+`WorkoutActivityType` — optional-int wire field, no schema bump — mapping to
+HealthKit's mind-and-body session on the watch).
+
+## Information architecture
+
+Four tabs: **Home · Progress · Library · Settings**.
+
+| Tab | Contents | Provenance |
+|---|---|---|
+| Home | The Today board (new) | New surface |
+| Progress | Per-goal tracks + trends + full history + achievements | Absorbs `WorkoutLogsView`, promotes `WeightProgressionChartView` / `ExerciseProgressView` |
+| Library | Exercise library | Promotes `ExerciseLibraryView` out of Settings |
+| Settings | Slimmed settings + goal/schedule editing + About | Existing, minus Library |
+
+Routine management stays reachable from Home — setup, not a daily
+destination. The `SplitDetailView`/`ExerciseListView` duplication is
+consolidated during this restructure (ExerciseListView is a pure duplicate).
+The watch app is untouched: it remains a thin remote; nothing changes the wire
+format until Phase 2's optional location field.
+
+## Home — the Today board
+
+Home is **the today-projection of the user's schedules**: a flat, stable,
+time-ordered checklist where each row wears a small colored goal tag. It is
+the jumping board for the day, a guide, and a motivational aid — in that
+order. Boxes fill up as the day goes on.
+
+```
+Thursday, July 10
+┌──────────────────────────────────┐
+│ ✓ Mobility ·7:04· [Morning] 🔥12│ done — stays in place, streak ticked
+│ ● Lower Body [Strength] │ fixed schedule, due today
+│ Mon & Thu · ▶ Start │
+│ · Zone-2 Ride [Endurance] │ flexible — ambient status row
+│ 1 of 2 this week · 3 days left │
+└──────────────────────────────────┘
+This week ▪▪▪▫▫▫▫ + one encouraging line
+```
+
+- **Row states**: *due* (start button, streak at stake — fixed schedules on
+ their day, flexible ones when the planner says so, catch-ups after a miss) →
+ *in progress* (resume affordance, always wins attention) → *done* (✓, time,
+ bumped streak). Done rows stay in place — the agenda's shape is stable and
+ the day visibly fills up. Flexible schedules not due today render as
+ **ambient** compact rows (week progress, on-pace, next suggested day),
+ tappable to start anyway. Never blocked, just not shouting.
+- **Due rows carry reason chips** from the planner ("missed Mon", "5 days
+ since legs · you're usually here Thursdays") — never a black box. An
+ override is a non-event and becomes signal.
+- **Rest is declarable per row**: "make it a rest day" checks a due row off
+ guilt-free; adherence treats a declared rest as met, and the board stays
+ completable. (The planner can also propose rest when everything scores low.)
+- **Sprinkles**: per-row streak flames, the week strip vs. usual pace, a
+ clean-sweep moment when the last due row completes, then a small tomorrow
+ preview.
+- **Degrades gracefully**: no schedules → bootstrap proposal card; one
+ schedule → a one-row board, i.e. a classic single-suggestion dashboard.
+
+## The planner — `NextSplitPlanner`
+
+A **pure, unit-testable scorer** (precedent: `SeedReconcilePlanner`). With
+explicit schedules, its scope narrows to where inference is genuinely needed:
+
+- **Placing flexible (frequency) schedules** — which of the remaining days
+ this week, driven by recency pressure vs. days left.
+- **Catch-up placement** — the best make-up day for a missed fixed slot
+ before its next occurrence.
+- **Ordering emergent rotations** — several flexible schedules under one goal
+ rotate via recency pressure.
+- **(Phase 2) Recovery guard** — overlap between a routine's target muscles
+ and muscles trained in the last ~48 h downweights it, using the bundled
+ library's `Targets:` metadata (already parsed by `ExerciseInfo`). Because it
+ looks across *all* recent workouts it is also the cross-goal coordinator
+ ("no heavy legs the morning after a long run").
+- **(Phase 2) Location affinity** — boost routines historically done at the
+ current place; missing/denied location contributes nothing.
+
+Weekday affinity remains a minor learned signal for flexible schedules.
+**Cold start:** with little history, flexible placement falls back to even
+spacing and says so. Deterministic, hand-tuned weights, fixture-history unit
+tests.
+
+### Location capture (Phase 2)
+
+- First workout start after the update requests **when-in-use,
+ reduced-accuracy** location (place-level is all we need). Denial is
+ respected silently forever.
+- A coarse fix is stamped on `WorkoutDocument` as an **optional field** — no
+ schema bump; older decoders ignore it (same rationale as `activityType`).
+- Places are **derived at read time** by proximity clustering (~150 m);
+ clusters are never persisted — consistent with the rebuildable-cache
+ philosophy.
+
+## Streaks & adherence
+
+- **Computed per schedule**: did "Lower Body" hit Mon & Thu this week; did
+ "Meditation" happen daily. Declared rests count as met. Streak state is
+ always **derived** from workout documents — never persisted.
+- **Rolled up per goal**: a goal's streak = consecutive weeks in which *all*
+ its member schedules hit their recurrence. Precise diagnostics ("Upper is
+ what's slipping") with an honest rollup.
+- Week-based schedules are forgiving by construction: a missed day breaks
+ nothing until the week itself is missed.
+
+## Progress
+
+- **Per-goal tracks first**: each goal's adherence over time, fed by
+ per-schedule adherence; then aggregate trends — weekly volume, workout
+ frequency; muscle-group balance (Phase 2). Time ranges per the
+ trend-visualization pattern (7D/30D/90D/1Y/All).
+- **Per-exercise drill-down**: the existing weight-progression and exercise
+ progress views, promoted and restyled; later pinnable to a goal as an
+ optional **outcome metric** (observed, never promised — goals themselves
+ stay practice-anchored).
+- **History**: the current `WorkoutLogsView` list lives here.
+- **Achievements section**: a badge grid within Progress. All achievements are
+ **pure derivations** (per-goal streak milestones, PR counts, volume/session
+ milestones) — recomputable after any cache rebuild. Persisting earned-dates
+ is deferred until we want one-time celebration moments.
+
+## Visual redesign
+
+Rides Phase 1 rather than following it: a shared component set — card, stat
+tile, board row (due / ambient / done variants), goal tag, badge, reason chip
+— with the iOS 26 glass treatment, applied as each surface is built. Existing
+screens (run flow, routine editing) are restyled to the same system as they're
+touched. Theming continues through `Color+Extensions`.
+
+## Phasing
+
+- **Phase 1 — goals + schedules + shell + design system**: Goal and Schedule
+ document aggregates (documents + cache entities + mappers + editing UI +
+ activity-type **bootstrap** that proposes initial goals/schedules from
+ existing routines); 4-tab `TabView`; Today board v1 (due/ambient/done rows,
+ catch-up nudges, resume, declared rest); planner v1 (recency + even-spacing
+ placement, weekday affinity); Progress tab (per-goal tracks + history +
+ promoted charts + achievements grid); Library promoted; Settings slimmed;
+ "Routine" terminology pass; view-duplication consolidation; component set.
+- **Phase 2 — the senses**: location capture + place affinity, recovery guard
+ (cross-goal coordination), muscle-balance chart, outcome-metric attachments,
+ `mindAndBody` activity type + watch session mapping, richer achievements.
+- **Phase 3 — the library grows**: external exercise ingestion — exercises
+ become user documents (new aggregate alongside the bundled library; real
+ schema work). Library UI is designed now to accommodate this.
+
+## Open questions
+
+- Tab icons/names final pass (Home/Progress/Library/Settings working titles).
+- **How declared rest / explicit skip persists.** Purely local UI state won't
+ survive device switches or cache rebuilds. Candidate: a `WorkoutDocument`
+ with `status: .skipped` for that schedule's routine (the status already
+ exists) — cross-device and derivation-friendly. Decide before Phase 1 build.
+ Related: distinguish *abandoned* (never started / walked away) from an
+ *explicit skip*, possibly carrying an "excuse" ("sick day") that adherence
+ treats as met. The board's past-day rows currently show an undifferentiated
+ "Skipped" tag as the placeholder for this.
+- Where goal/schedule editing lives: Settings vs. from the board (long-press
+ a row / "edit plan" affordance) vs. both.
+- Clean-sweep celebration + tomorrow-preview treatment (how loud, how playful).
+- Default morning cutoff: fixed ~11 am vs. learned from the user's own
+ distribution.
+- Whether the watch should eventually show the Today board / row states
+ (would need the computed projection pushed in the application context —
+ cheap; revisit after Phase 1).
+- How far the "practice tracker" identity goes (app is named *Workouts*;
+ mindfulness fits the model today — does the branding follow eventually?).
diff --git a/Workouts Watch App/Connectivity/WatchConnectivityBridge.swift b/Workouts Watch App/Connectivity/WatchConnectivityBridge.swift
index b8b5b4e..2c17381 100644
--- a/Workouts Watch App/Connectivity/WatchConnectivityBridge.swift
+++ b/Workouts Watch App/Connectivity/WatchConnectivityBridge.swift
@@ -34,9 +34,9 @@ final class WatchConnectivityBridge: NSObject {
/// Exclusive-edit lock pushed by the phone. While set, the watch parks the matching
/// run (popping out of its progress view) and blocks re-entry, so the phone owns the
/// edit and the watch can't clobber it with a stale optimistic write. `editingWorkoutID`
- /// matches a run by its workout id; `editingSplitID` matches any run by its `splitID`.
+ /// matches a run by its workout id; `editingRoutineID` matches any run by its `routineID`.
private(set) var editingWorkoutID: String?
- private(set) var editingSplitID: String?
+ private(set) var editingRoutineID: String?
/// Monotonic sequence stamped on each live-run frame we send. Bumped to stay ahead of any
/// frame we *receive*, so the two devices share one increasing per-run sequence and either
@@ -101,16 +101,16 @@ final class WatchConnectivityBridge: NSObject {
}
/// Apply the last application context the system holds for us (settings, edit
- /// locks, and the authoritative splits + workouts sets). Idempotent, so it's safe
+ /// locks, and the authoritative routines + workouts sets). Idempotent, so it's safe
/// to call both eagerly at launch and again when activation completes.
private func applyReceivedContext() {
guard let session else { return }
let ctx = session.receivedApplicationContext
Self.log.info("applyReceivedContext: activation=\(session.activationState.rawValue) keys=\(ctx.keys.sorted().joined(separator: ","), privacy: .public)")
- applyState(WCPayload.decodeSplits(ctx), workouts: WCPayload.decodeWorkouts(ctx))
+ applyState(WCPayload.decodeRoutines(ctx), workouts: WCPayload.decodeWorkouts(ctx))
applySettings(ctx)
editingWorkoutID = WCPayload.decodeEditingWorkoutID(ctx)
- editingSplitID = WCPayload.decodeEditingSplitID(ctx)
+ editingRoutineID = WCPayload.decodeEditingRoutineID(ctx)
}
/// Apply a decoded state push. `nil` (decode failure — the phone runs a build with
@@ -118,14 +118,14 @@ final class WatchConnectivityBridge: NSObject {
/// against a bogus empty set nor silently show stale data forever. The upsert/prune
/// itself is delegated to the pure, session-free `WatchCacheApplier` seam so the
/// apply/prune contract is unit-testable.
- private func applyState(_ splits: [SplitDocument]?, workouts: [WorkoutDocument]?) {
- guard WatchCacheApplier.apply(splits: splits, workouts: workouts, into: context) else {
- Self.log.error("applyState: payload failed to decode (splits=\(splits == nil ? "failed" : "ok", privacy: .public), workouts=\(workouts == nil ? "failed" : "ok", privacy: .public)) — phone/watch build mismatch?")
+ private func applyState(_ routines: [RoutineDocument]?, workouts: [WorkoutDocument]?) {
+ guard WatchCacheApplier.apply(routines: routines, workouts: workouts, into: context) else {
+ Self.log.error("applyState: payload failed to decode (routines=\(routines == nil ? "failed" : "ok", privacy: .public), workouts=\(workouts == nil ? "failed" : "ok", privacy: .public)) — phone/watch build mismatch?")
schemaMismatch = true
return
}
schemaMismatch = false
- Self.log.info("applyState: applied \(splits?.count ?? 0) splits, \(workouts?.count ?? 0) workouts")
+ Self.log.info("applyState: applied \(routines?.count ?? 0) routines, \(workouts?.count ?? 0) workouts")
lastSyncDate = Date()
onWorkoutsChanged?()
}
@@ -277,7 +277,7 @@ final class WatchConnectivityBridge: NSObject {
// MARK: - Cache apply/prune seam
/// The pure, session-free core of the phone→watch state apply: it upserts the authoritative
-/// splits/workouts into the watch's SwiftData cache and prunes anything the phone no longer
+/// routines/workouts into the watch's SwiftData cache and prunes anything the phone no longer
/// sends. Split out of `WatchConnectivityBridge` (which wraps `WCSession`) so the apply/prune
/// contract — including the authoritative-empty prune and the nil-decode skip — is unit-testable
/// against an in-memory `ModelContext` without a live WatchConnectivity session.
@@ -288,31 +288,31 @@ enum WatchCacheApplier {
/// applied, `false` when skipped, so the caller can log / stamp `lastSyncDate` accordingly.
@MainActor
@discardableResult
- static func apply(splits: [SplitDocument]?, workouts: [WorkoutDocument]?, into context: ModelContext) -> Bool {
- guard let splits, let workouts else { return false }
- apply(splits: splits, workouts: workouts, into: context)
+ static func apply(routines: [RoutineDocument]?, workouts: [WorkoutDocument]?, into context: ModelContext) -> Bool {
+ guard let routines, let workouts else { return false }
+ apply(routines: routines, workouts: workouts, into: context)
return true
}
- /// Upsert every split/workout the phone sent, then prune anything it *didn't*. Both sets are
+ /// Upsert every routine/workout the phone sent, then prune anything it *didn't*. Both sets are
/// authoritative, so an authoritative empty push clears rows the phone no longer has (a deleted
- /// split; a run discarded/deleted on the phone, completed-and-aged-out, or otherwise dropped
+ /// routine; a run discarded/deleted on the phone, completed-and-aged-out, or otherwise dropped
/// from the ~24h window). On first launch the cache is empty, so the prune is a harmless no-op.
- /// The watch never originates a split/workout, so pruning can't lose local-only data.
+ /// The watch never originates a routine/workout, so pruning can't lose local-only data.
@MainActor
- static func apply(splits: [SplitDocument], workouts: [WorkoutDocument], into context: ModelContext) {
- var liveSplitIDs = Set()
- for s in splits {
- CacheMapper.upsertSplit(s, relativePath: s.relativePath, into: context)
- liveSplitIDs.insert(s.id)
+ static func apply(routines: [RoutineDocument], workouts: [WorkoutDocument], into context: ModelContext) {
+ var liveRoutineIDs = Set()
+ for s in routines {
+ CacheMapper.upsertRoutine(s, relativePath: s.relativePath, into: context)
+ liveRoutineIDs.insert(s.id)
}
var liveWorkoutIDs = Set()
for w in workouts {
CacheMapper.upsertWorkout(w, relativePath: w.relativePath, into: context)
liveWorkoutIDs.insert(w.id)
}
- if let allSplits = try? context.fetch(FetchDescriptor()) {
- for s in allSplits where !liveSplitIDs.contains(s.id) { context.delete(s) }
+ if let allRoutines = try? context.fetch(FetchDescriptor()) {
+ for s in allRoutines where !liveRoutineIDs.contains(s.id) { context.delete(s) }
}
if let allWorkouts = try? context.fetch(FetchDescriptor()) {
for w in allWorkouts where !liveWorkoutIDs.contains(w.id) { context.delete(w) }
@@ -359,21 +359,21 @@ extension WatchConnectivityBridge: WCSessionDelegate {
}
nonisolated func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String: Any]) {
- let splits = WCPayload.decodeSplits(applicationContext)
+ let routines = WCPayload.decodeRoutines(applicationContext)
let workouts = WCPayload.decodeWorkouts(applicationContext)
let rest = WCPayload.decodeRestSeconds(applicationContext)
let done = WCPayload.decodeDoneCountdownSeconds(applicationContext)
let unit = WCPayload.decodeWeightUnit(applicationContext)
let editingWorkoutID = WCPayload.decodeEditingWorkoutID(applicationContext)
- let editingSplitID = WCPayload.decodeEditingSplitID(applicationContext)
+ let editingRoutineID = WCPayload.decodeEditingRoutineID(applicationContext)
Task { @MainActor in
- self.applyState(splits, workouts: workouts)
+ self.applyState(routines, workouts: workouts)
if let rest { UserDefaults.standard.set(rest, forKey: WCPayload.restSecondsKey) }
if let done { UserDefaults.standard.set(done, forKey: WCPayload.doneCountdownSecondsKey) }
if let unit { UserDefaults.standard.set(unit, forKey: WCPayload.weightUnitKey) }
// Absent keys mean "not editing" — set unconditionally so the lock clears.
self.editingWorkoutID = editingWorkoutID
- self.editingSplitID = editingSplitID
+ self.editingRoutineID = editingRoutineID
}
}
}
diff --git a/Workouts Watch App/Views/ActiveWorkoutGateView.swift b/Workouts Watch App/Views/ActiveWorkoutGateView.swift
index 7da6768..b62e8a4 100644
--- a/Workouts Watch App/Views/ActiveWorkoutGateView.swift
+++ b/Workouts Watch App/Views/ActiveWorkoutGateView.swift
@@ -11,7 +11,7 @@ import SwiftData
/// Root of the watch app. The watch only runs the workouts that are currently
/// active on the phone — there's no history browsing here. An "active" workout is a
/// cached one that isn't finished (`notStarted` or `inProgress`); a workout is
-/// created on the phone as `notStarted` the moment a split is picked, and flips to
+/// created on the phone as `notStarted` the moment a routine is picked, and flips to
/// `completed` once every exercise is done, at which point it drops off this list.
///
/// The root shows every active workout (most-recent first); picking one opens its
@@ -20,7 +20,7 @@ struct ActiveWorkoutGateView: View {
@Environment(WatchConnectivityBridge.self) private var bridge
@Query(sort: \Workout.start, order: .reverse) private var workouts: [Workout]
- @Query private var splits: [Split]
+ @Query private var routines: [Routine]
/// Navigated workouts (depth 1). Held here — rather than relying on implicit
/// `NavigationLink` destinations — so we can pop back to the gate the moment the phone
@@ -31,17 +31,17 @@ struct ActiveWorkoutGateView: View {
workouts.filter { $0.status == .inProgress || $0.status == .notStarted }
}
- private func split(for workout: Workout) -> Split? {
- guard let id = workout.splitID else { return nil }
- return splits.first { $0.id == id }
+ private func routine(for workout: Workout) -> Routine? {
+ guard let id = workout.routineID else { return nil }
+ return routines.first { $0.id == id }
}
- /// True while the phone has this workout's exercise — or the split it came from — open
+ /// True while the phone has this workout's exercise — or the routine it came from — open
/// in an editor. The watch parks such a run and blocks re-entry so the phone owns the
/// edit and the watch can't forward a stale optimistic write over it.
private func isLockedForEditing(_ workout: Workout) -> Bool {
if let id = bridge.editingWorkoutID, id == workout.id { return true }
- if let splitID = bridge.editingSplitID, splitID == workout.splitID { return true }
+ if let routineID = bridge.editingRoutineID, routineID == workout.routineID { return true }
return false
}
@@ -89,17 +89,17 @@ struct ActiveWorkoutGateView: View {
// pop back to the gate so re-entry rebuilds a fresh working copy. Also pop if the run
// we're inside was pruned (discarded/deleted on the phone, or aged out of the push).
.onChange(of: bridge.editingWorkoutID) { _, _ in popIfNavigatedRunUnavailable() }
- .onChange(of: bridge.editingSplitID) { _, _ in popIfNavigatedRunUnavailable() }
+ .onChange(of: bridge.editingRoutineID) { _, _ in popIfNavigatedRunUnavailable() }
.onChange(of: workouts.map(\.id)) { _, _ in popIfNavigatedRunUnavailable() }
}
@ViewBuilder
private func row(for workout: Workout) -> some View {
if isLockedForEditing(workout) {
- ActiveWorkoutRow(workout: workout, split: split(for: workout), editingOnPhone: true)
+ ActiveWorkoutRow(workout: workout, routine: routine(for: workout), editingOnPhone: true)
} else {
NavigationLink(value: ActiveWorkoutRoute(workoutID: workout.id)) {
- ActiveWorkoutRow(workout: workout, split: split(for: workout))
+ ActiveWorkoutRow(workout: workout, routine: routine(for: workout))
}
}
}
@@ -135,7 +135,7 @@ struct ActiveWorkoutGateView: View {
private struct ActiveWorkoutRow: View {
let workout: Workout
- let split: Split?
+ let routine: Routine?
/// When true, the phone is editing this run — render it dimmed and non-tappable.
var editingOnPhone: Bool = false
@@ -144,13 +144,13 @@ private struct ActiveWorkoutRow: View {
var body: some View {
HStack(spacing: 10) {
- Image(systemName: editingOnPhone ? "pencil" : (split?.systemImage ?? "figure.strengthtraining.traditional"))
+ Image(systemName: editingOnPhone ? "pencil" : (routine?.systemImage ?? "figure.strengthtraining.traditional"))
.font(.title3)
- .foregroundStyle(split.map { Color.color(from: $0.color) } ?? .workTint)
+ .foregroundStyle(routine.map { Color.color(from: $0.color) } ?? .workTint)
.frame(width: 26)
VStack(alignment: .leading, spacing: 2) {
- Text(workout.splitName ?? Split.unnamed)
+ Text(workout.routineName ?? Routine.unnamed)
.font(.headline)
.lineLimit(1)
diff --git a/Workouts Watch App/Views/ExerciseProgressView.swift b/Workouts Watch App/Views/ExerciseProgressView.swift
index c2b7d0d..db4349e 100644
--- a/Workouts Watch App/Views/ExerciseProgressView.swift
+++ b/Workouts Watch App/Views/ExerciseProgressView.swift
@@ -163,11 +163,11 @@ struct ExerciseProgressView: View {
/// page-index math below depends on it.
private var showsReady: Bool { debugInitialPage == nil && !enteredViaFlow }
- /// This split runs as a continuous flow — finishing one exercise rests, then
+ /// This routine runs as a continuous flow — finishing one exercise rests, then
/// auto-advances into the next — rather than returning to the list per exercise.
private var autoAdvance: Bool { doc.autoAdvance == true }
- /// Rest length for this run: the split's per-split override, else the global default.
+ /// Rest length for this run: the routine's per-routine override, else the global default.
/// Governs both between-set rests and (in flow mode) the between-exercise rest.
private var effectiveRest: Int { max(1, doc.restSeconds ?? restSeconds) }
diff --git a/Workouts Watch App/Views/RunFlowView.swift b/Workouts Watch App/Views/RunFlowView.swift
index b9e3b53..32ce9b0 100644
--- a/Workouts Watch App/Views/RunFlowView.swift
+++ b/Workouts Watch App/Views/RunFlowView.swift
@@ -8,10 +8,10 @@
import SwiftUI
/// Thin coordinator that lets a single pushed run walk across exercises in a flow-mode
-/// split (`autoAdvance`). It owns which log is on screen and swaps it when the running
+/// routine (`autoAdvance`). It owns which log is on screen and swaps it when the running
/// `ExerciseProgressView` finishes an exercise; `.id(currentLogID)` rebuilds the child
/// fresh for each exercise, so the delicate per-exercise run/mirror machinery stays
-/// exactly as it is. For a non-flow split it's transparent — one exercise, unchanged.
+/// exactly as it is. For a non-flow routine it's transparent — one exercise, unchanged.
struct RunFlowView: View {
@Environment(WatchConnectivityBridge.self) private var bridge
diff --git a/Workouts Watch App/Views/WorkoutLogListView.swift b/Workouts Watch App/Views/WorkoutLogListView.swift
index 979a8b5..3d2080a 100644
--- a/Workouts Watch App/Views/WorkoutLogListView.swift
+++ b/Workouts Watch App/Views/WorkoutLogListView.swift
@@ -35,16 +35,16 @@ struct WorkoutLogListView: View {
_doc = State(initialValue: WorkoutDocument(fromLive: workout) ?? .deletedPlaceholder)
}
- /// The split this workout came from (read-only on the watch), used to offer
+ /// The routine this workout came from (read-only on the watch), used to offer
/// additional exercises that aren't logged yet. Fetched imperatively — *not* via
- /// `@Query` — so the list body never observes the live `Split` or traverses its
+ /// `@Query` — so the list body never observes the live `Routine` or traverses its
/// `exercises` relationship during a render. Doing so (a `@Query`-observed model
/// whose to-many relationship is read in `body`) drove a SwiftData re-render loop
/// that hung the watch. `availableExercises` is therefore only ever evaluated from
/// the picker sheet's closure, not from `body`.
- private var split: Split? {
- guard let splitID = doc.splitID else { return nil }
- return CacheMapper.fetchSplit(id: splitID, in: modelContext)
+ private var routine: Routine? {
+ guard let routineID = doc.routineID else { return nil }
+ return CacheMapper.fetchRoutine(id: routineID, in: modelContext)
}
private var sortedLogs: [WorkoutLogDocument] {
@@ -52,9 +52,9 @@ struct WorkoutLogListView: View {
}
private var availableExercises: [Exercise] {
- guard let split else { return [] }
+ guard let routine else { return [] }
let existingNames = Set(doc.logs.map { $0.exerciseName })
- return split.exercisesArray.filter { !existingNames.contains($0.name) }
+ return routine.exercisesArray.filter { !existingNames.contains($0.name) }
}
var body: some View {
@@ -74,10 +74,10 @@ struct WorkoutLogListView: View {
}
}
- // Gate on the *resolved* split, not the stale `splitID` string — a split
+ // Gate on the *resolved* routine, not the stale `routineID` string — a routine
// deleted on the phone leaves the id dangling, and offering Add Exercise
// against it would only show a misleading "All exercises added" picker.
- if split != nil {
+ if routine != nil {
Section {
Button {
showingExercisePicker = true
@@ -96,13 +96,13 @@ struct WorkoutLogListView: View {
ContentUnavailableView(
"No Exercises",
systemImage: "figure.strengthtraining.traditional",
- description: Text(split == nil
+ description: Text(routine == nil
? "No exercises in this workout."
: "Tap + to add exercises.")
)
}
}
- .navigationTitle(doc.splitName ?? Split.unnamed)
+ .navigationTitle(doc.routineName ?? Routine.unnamed)
// Absorb phone edits while this screen is open (H1's watch-side gap):
// without this, the snapshot stays frozen at init, and the next watch
// tick — made against a state that's missing the phone's edit — would be
@@ -120,7 +120,7 @@ struct WorkoutLogListView: View {
}
}
.navigationDestination(item: $selectedLogID) { logID in
- // RunFlowView hosts the run and, in a flow-mode split, auto-advances across
+ // RunFlowView hosts the run and, in a flow-mode routine, auto-advances across
// exercises. It owns the live-run mirror wiring (incoming-frame filter,
// `navigatedRunID`, and the parameterized live-ended signal) since the on-screen
// log advances during a flowing run.
diff --git a/Workouts Watch App/WorkoutSessionCoordinator.swift b/Workouts Watch App/WorkoutSessionCoordinator.swift
index 2c1031d..709dd9a 100644
--- a/Workouts Watch App/WorkoutSessionCoordinator.swift
+++ b/Workouts Watch App/WorkoutSessionCoordinator.swift
@@ -61,7 +61,7 @@ enum SessionEndPlanner {
!isRunning && !activeIDs(in: workouts).isEmpty
}
- /// The run whose split configures a self-started session: the most recently started
+ /// The run whose routine configures a self-started session: the most recently started
/// active one (ties broken by id for determinism).
static func runToStart(in workouts: [WorkoutDocument]) -> WorkoutDocument? {
let ids = activeIDs(in: workouts)
@@ -148,17 +148,17 @@ final class WorkoutSessionCoordinator {
}
/// Self-start a session when an active run has none (see `SessionEndPlanner.shouldStart`).
- /// The activity type comes from the run's split so the saved Health workout is categorized
- /// the same way a phone-launched one would be; a run whose split is gone (deleted, or not
+ /// The activity type comes from the run's routine so the saved Health workout is categorized
+ /// the same way a phone-launched one would be; a run whose routine is gone (deleted, or not
/// yet synced) falls back to traditional strength training.
private func startIfNeeded(workouts: [WorkoutDocument]) {
guard SessionEndPlanner.shouldStart(isRunning: sessionManager.isRunning, workouts: workouts),
let run = SessionEndPlanner.runToStart(in: workouts) else { return }
- let split = run.splitID.flatMap { id in
- (try? context.fetch(FetchDescriptor())).flatMap { $0.first { $0.id == id } }
+ let routine = run.routineID.flatMap { id in
+ (try? context.fetch(FetchDescriptor())).flatMap { $0.first { $0.id == id } }
}
let configuration = HKWorkoutConfiguration()
- configuration.activityType = split?.activityTypeEnum.hkActivityType ?? .traditionalStrengthTraining
+ configuration.activityType = routine?.activityTypeEnum.hkActivityType ?? .traditionalStrengthTraining
configuration.locationType = .indoor
sessionManager.start(with: configuration)
}
diff --git a/Workouts Watch AppTests/SessionEndPlannerTests.swift b/Workouts Watch AppTests/SessionEndPlannerTests.swift
index efecdec..711ca1d 100644
--- a/Workouts Watch AppTests/SessionEndPlannerTests.swift
+++ b/Workouts Watch AppTests/SessionEndPlannerTests.swift
@@ -19,7 +19,7 @@ struct SessionEndPlannerTests {
private func workout(_ id: String, _ status: WorkoutStatus, start: Date = ts) -> WorkoutDocument {
WorkoutDocument(
schemaVersion: WorkoutDocument.currentSchemaVersion,
- id: id, splitID: nil, splitName: nil, start: start, end: nil,
+ id: id, routineID: nil, routineName: nil, start: start, end: nil,
status: status.rawValue, createdAt: start, updatedAt: start,
logs: [], metrics: nil)
}
diff --git a/Workouts Watch AppTests/WatchCacheApplierTests.swift b/Workouts Watch AppTests/WatchCacheApplierTests.swift
index a903ab5..4e6f683 100644
--- a/Workouts Watch AppTests/WatchCacheApplierTests.swift
+++ b/Workouts Watch AppTests/WatchCacheApplierTests.swift
@@ -6,9 +6,9 @@ import Testing
/// Locks the phone→watch state apply/prune contract — the pure `WatchCacheApplier` seam the
/// `WatchConnectivityBridge` delegates its cache mutation to. The bridge wraps `WCSession`, so
/// this session-free seam is the testable surface for the three behaviors that matter:
-/// 1. An authoritative push upserts the splits/workouts the phone sent.
+/// 1. An authoritative push upserts the routines/workouts the phone sent.
/// 2. An authoritative **empty** push prunes stale rows the phone no longer has (the
-/// recently-fixed case — otherwise a deleted split / aged-out run is orphaned forever).
+/// recently-fixed case — otherwise a deleted routine / aged-out run is orphaned forever).
/// 3. A `nil` set (the phone's payload failed to decode — a build/schema mismatch) is a
/// decode failure, not an empty set: it must skip entirely and NOT prune real rows.
///
@@ -20,15 +20,15 @@ struct WatchCacheApplierTests {
private static let ts = Date(timeIntervalSince1970: 1_700_000_000)
private func makeContext() throws -> ModelContext {
- let schema = Schema([Split.self, Exercise.self, Workout.self, WorkoutLog.self])
+ let schema = Schema([Routine.self, Exercise.self, Workout.self, WorkoutLog.self])
// cloudKitDatabase: .none matches WorkoutsModelContainer (the watch's read-through cache).
let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true, cloudKitDatabase: .none)
return ModelContext(try ModelContainer(for: schema, configurations: [config]))
}
- private func split(id: String, name: String) -> SplitDocument {
- SplitDocument(
- schemaVersion: SplitDocument.currentSchemaVersion, id: id, name: name, color: "blue",
+ private func routine(id: String, name: String) -> RoutineDocument {
+ RoutineDocument(
+ schemaVersion: RoutineDocument.currentSchemaVersion, id: id, name: name, color: "blue",
systemImage: "dumbbell.fill", order: 0, createdAt: Self.ts, updatedAt: Self.ts,
exercises: [ExerciseDocument(id: "EX-\(id)", name: "Bench Press", order: 0, sets: 4,
reps: 10, weight: 135, loadType: LoadType.weight.rawValue,
@@ -39,7 +39,7 @@ struct WatchCacheApplierTests {
private func workout(id: String, name: String = "Push") -> WorkoutDocument {
WorkoutDocument(
- schemaVersion: WorkoutDocument.currentSchemaVersion, id: id, splitID: "S", splitName: name,
+ schemaVersion: WorkoutDocument.currentSchemaVersion, id: id, routineID: "S", routineName: name,
start: Self.ts, end: nil, status: WorkoutStatus.inProgress.rawValue,
createdAt: Self.ts, updatedAt: Self.ts,
logs: [WorkoutLogDocument(id: "L-\(id)", exerciseName: "Bench Press", order: 0, sets: 4,
@@ -50,8 +50,8 @@ struct WatchCacheApplierTests {
)
}
- private func splitIDs(in ctx: ModelContext) -> Set {
- Set((try? ctx.fetch(FetchDescriptor()))?.map(\.id) ?? [])
+ private func routineIDs(in ctx: ModelContext) -> Set {
+ Set((try? ctx.fetch(FetchDescriptor()))?.map(\.id) ?? [])
}
private func workoutIDs(in ctx: ModelContext) -> Set {
@@ -60,25 +60,25 @@ struct WatchCacheApplierTests {
// MARK: - Authoritative upsert
- @Test func authoritativePushUpsertsSplitsAndWorkouts() throws {
+ @Test func authoritativePushUpsertsRoutinesAndWorkouts() throws {
let ctx = try makeContext()
WatchCacheApplier.apply(
- splits: [split(id: "SP1", name: "Upper"), split(id: "SP2", name: "Lower")],
+ routines: [routine(id: "SP1", name: "Upper"), routine(id: "SP2", name: "Lower")],
workouts: [workout(id: "01WKA"), workout(id: "01WKB")],
into: ctx
)
- #expect(splitIDs(in: ctx) == ["SP1", "SP2"])
+ #expect(routineIDs(in: ctx) == ["SP1", "SP2"])
#expect(workoutIDs(in: ctx) == ["01WKA", "01WKB"])
}
@Test func reUpsertSameIDUpdatesInPlaceWithoutDuplicating() throws {
let ctx = try makeContext()
- WatchCacheApplier.apply(splits: [split(id: "SP1", name: "Original")], workouts: [], into: ctx)
- WatchCacheApplier.apply(splits: [split(id: "SP1", name: "Renamed")], workouts: [], into: ctx)
+ WatchCacheApplier.apply(routines: [routine(id: "SP1", name: "Original")], workouts: [], into: ctx)
+ WatchCacheApplier.apply(routines: [routine(id: "SP1", name: "Renamed")], workouts: [], into: ctx)
- let splits = try ctx.fetch(FetchDescriptor())
- #expect(splits.count == 1)
- #expect(splits.first?.name == "Renamed")
+ let routines = try ctx.fetch(FetchDescriptor())
+ #expect(routines.count == 1)
+ #expect(routines.first?.name == "Renamed")
}
// MARK: - Authoritative empty prunes stale rows (the recently-fixed case)
@@ -87,67 +87,67 @@ struct WatchCacheApplierTests {
let ctx = try makeContext()
// Seed the cache as if the phone had previously pushed real state.
WatchCacheApplier.apply(
- splits: [split(id: "SP1", name: "Upper")],
+ routines: [routine(id: "SP1", name: "Upper")],
workouts: [workout(id: "01WKA")],
into: ctx
)
- #expect(splitIDs(in: ctx) == ["SP1"])
+ #expect(routineIDs(in: ctx) == ["SP1"])
#expect(workoutIDs(in: ctx) == ["01WKA"])
// The user deleted everything on the phone (or all runs aged out): an authoritative
// EMPTY push must clear the now-stale rows rather than orphan them forever.
- WatchCacheApplier.apply(splits: [], workouts: [], into: ctx)
- #expect(splitIDs(in: ctx).isEmpty)
+ WatchCacheApplier.apply(routines: [], workouts: [], into: ctx)
+ #expect(routineIDs(in: ctx).isEmpty)
#expect(workoutIDs(in: ctx).isEmpty)
}
@Test func partialPushPrunesOnlyRowsThePhoneNoLongerSends() throws {
let ctx = try makeContext()
WatchCacheApplier.apply(
- splits: [split(id: "SP1", name: "Keep"), split(id: "SP2", name: "Drop")],
+ routines: [routine(id: "SP1", name: "Keep"), routine(id: "SP2", name: "Drop")],
workouts: [workout(id: "01WKKEEP"), workout(id: "01WKDROP")],
into: ctx
)
// The phone now sends only the survivors — the omitted ones are pruned.
WatchCacheApplier.apply(
- splits: [split(id: "SP1", name: "Keep")],
+ routines: [routine(id: "SP1", name: "Keep")],
workouts: [workout(id: "01WKKEEP")],
into: ctx
)
- #expect(splitIDs(in: ctx) == ["SP1"])
+ #expect(routineIDs(in: ctx) == ["SP1"])
#expect(workoutIDs(in: ctx) == ["01WKKEEP"])
}
@Test func authoritativeEmptyPushOnEmptyCacheIsANoOp() throws {
let ctx = try makeContext()
- WatchCacheApplier.apply(splits: [], workouts: [], into: ctx)
- #expect(splitIDs(in: ctx).isEmpty)
+ WatchCacheApplier.apply(routines: [], workouts: [], into: ctx)
+ #expect(routineIDs(in: ctx).isEmpty)
#expect(workoutIDs(in: ctx).isEmpty)
}
// MARK: - nil-decode guard: a decode failure must NOT prune
- @Test func nilSplitsSkipsEntirelyAndDoesNotPrune() throws {
+ @Test func nilRoutinesSkipsEntirelyAndDoesNotPrune() throws {
let ctx = try makeContext()
- WatchCacheApplier.apply(splits: [split(id: "SP1", name: "Upper")],
+ WatchCacheApplier.apply(routines: [routine(id: "SP1", name: "Upper")],
workouts: [workout(id: "01WKA")], into: ctx)
- // A corrupt/absent splits payload decodes to nil (build mismatch). Skip — the existing
+ // A corrupt/absent routines payload decodes to nil (build mismatch). Skip — the existing
// rows must survive, never pruned against a bogus set.
- let applied = WatchCacheApplier.apply(splits: nil, workouts: [], into: ctx)
+ let applied = WatchCacheApplier.apply(routines: nil, workouts: [], into: ctx)
#expect(applied == false)
- #expect(splitIDs(in: ctx) == ["SP1"])
+ #expect(routineIDs(in: ctx) == ["SP1"])
#expect(workoutIDs(in: ctx) == ["01WKA"])
}
@Test func nilWorkoutsSkipsEntirelyAndDoesNotPrune() throws {
let ctx = try makeContext()
- WatchCacheApplier.apply(splits: [split(id: "SP1", name: "Upper")],
+ WatchCacheApplier.apply(routines: [routine(id: "SP1", name: "Upper")],
workouts: [workout(id: "01WKA")], into: ctx)
- let applied = WatchCacheApplier.apply(splits: [], workouts: nil, into: ctx)
+ let applied = WatchCacheApplier.apply(routines: [], workouts: nil, into: ctx)
#expect(applied == false)
- #expect(splitIDs(in: ctx) == ["SP1"])
+ #expect(routineIDs(in: ctx) == ["SP1"])
#expect(workoutIDs(in: ctx) == ["01WKA"])
}
@@ -167,7 +167,7 @@ struct WatchCacheApplierTests {
]
doc.logs[0].updatedAt = Self.ts
- WatchCacheApplier.apply(splits: [], workouts: [doc], into: ctx)
+ WatchCacheApplier.apply(routines: [], workouts: [doc], into: ctx)
let entity = try #require(CacheMapper.fetchWorkout(id: "01WKENTRIES", in: ctx))
let rebuilt = WorkoutDocument(from: entity)
#expect(rebuilt.logs[0].weight == 42.5)
@@ -179,9 +179,9 @@ struct WatchCacheApplierTests {
let ctx = try makeContext()
// Typed optionals to disambiguate onto the nil-aware overload (an authoritative,
// successfully-decoded — if empty — push), whose Bool return is the applied/skip signal.
- let splits: [SplitDocument]? = []
+ let routines: [RoutineDocument]? = []
let workouts: [WorkoutDocument]? = []
- let applied = WatchCacheApplier.apply(splits: splits, workouts: workouts, into: ctx)
+ let applied = WatchCacheApplier.apply(routines: routines, workouts: workouts, into: ctx)
#expect(applied == true)
}
}
diff --git a/Workouts Watch AppTests/WatchConnectivityBridgeTests.swift b/Workouts Watch AppTests/WatchConnectivityBridgeTests.swift
index 79af9c7..7bcf054 100644
--- a/Workouts Watch AppTests/WatchConnectivityBridgeTests.swift
+++ b/Workouts Watch AppTests/WatchConnectivityBridgeTests.swift
@@ -17,14 +17,14 @@ struct WatchConnectivityBridgeTests {
private static let ts = Date(timeIntervalSince1970: 1_700_000_000)
private func makeContainer() throws -> ModelContainer {
- let schema = Schema([Split.self, Exercise.self, Workout.self, WorkoutLog.self])
+ let schema = Schema([Routine.self, Exercise.self, Workout.self, WorkoutLog.self])
let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true, cloudKitDatabase: .none)
return try ModelContainer(for: schema, configurations: [config])
}
- private func workout(id: String, name: String, splitName: String) -> WorkoutDocument {
+ private func workout(id: String, name: String, routineName: String) -> WorkoutDocument {
WorkoutDocument(
- schemaVersion: WorkoutDocument.currentSchemaVersion, id: id, splitID: "S", splitName: splitName,
+ schemaVersion: WorkoutDocument.currentSchemaVersion, id: id, routineID: "S", routineName: routineName,
start: Self.ts, end: nil, status: WorkoutStatus.inProgress.rawValue,
createdAt: Self.ts, updatedAt: Self.ts,
logs: [WorkoutLogDocument(id: "L-\(id)", exerciseName: name, order: 0, sets: 4,
@@ -38,7 +38,7 @@ struct WatchConnectivityBridgeTests {
@Test func updateWritesWorkoutOptimisticallyIntoTheCache() throws {
let container = try makeContainer()
let bridge = WatchConnectivityBridge(container: container)
- let doc = workout(id: "01WKOPT", name: "Bench Press", splitName: "Push")
+ let doc = workout(id: "01WKOPT", name: "Bench Press", routineName: "Push")
#expect(CacheMapper.fetchWorkout(id: doc.id, in: container.mainContext) == nil)
@@ -46,7 +46,7 @@ struct WatchConnectivityBridgeTests {
let fetched = try #require(CacheMapper.fetchWorkout(id: doc.id, in: container.mainContext))
#expect(fetched.id == "01WKOPT")
- #expect(fetched.splitName == "Push")
+ #expect(fetched.routineName == "Push")
#expect(fetched.logsArray.first?.exerciseName == "Bench Press")
}
@@ -54,16 +54,16 @@ struct WatchConnectivityBridgeTests {
let container = try makeContainer()
let bridge = WatchConnectivityBridge(container: container)
- bridge.update(workout: workout(id: "01WKEDIT", name: "Bench Press", splitName: "Push"))
+ bridge.update(workout: workout(id: "01WKEDIT", name: "Bench Press", routineName: "Push"))
- var edited = workout(id: "01WKEDIT", name: "Incline Press", splitName: "Push Day")
+ var edited = workout(id: "01WKEDIT", name: "Incline Press", routineName: "Push Day")
edited.status = WorkoutStatus.completed.rawValue
bridge.update(workout: edited)
let all = try container.mainContext.fetch(FetchDescriptor())
#expect(all.count == 1) // updated in place, not duplicated
let fetched = try #require(CacheMapper.fetchWorkout(id: "01WKEDIT", in: container.mainContext))
- #expect(fetched.splitName == "Push Day")
+ #expect(fetched.routineName == "Push Day")
#expect(fetched.status == .completed)
#expect(fetched.logsArray.first?.exerciseName == "Incline Press")
}
@@ -72,7 +72,7 @@ struct WatchConnectivityBridgeTests {
let bridge = try WatchConnectivityBridge(container: makeContainer())
#expect(bridge.lastSyncDate == nil)
#expect(bridge.editingWorkoutID == nil)
- #expect(bridge.editingSplitID == nil)
+ #expect(bridge.editingRoutineID == nil)
#expect(bridge.liveIncoming == nil)
#expect(bridge.presentable == nil)
#expect(bridge.navigatedRunID == nil)
diff --git a/Workouts/AppServices.swift b/Workouts/AppServices.swift
index 16af465..11221d3 100644
--- a/Workouts/AppServices.swift
+++ b/Workouts/AppServices.swift
@@ -43,6 +43,18 @@ final class AppServices {
let liveRunState = LiveRunState()
self.liveRunState = liveRunState
self.watchBridge = PhoneConnectivityBridge(container: container, syncEngine: syncEngine, liveRunState: liveRunState)
+ // Launch the wrist session only when a run actually begins (the first
+ // exercise leaves `.notStarted` on the phone) — creating a workout alone is
+ // just a peek and must not start anything on the watch. Watch-originated
+ // starts don't fire this hook; the watch already owns its session.
+ syncEngine.onWorkoutBecameActive = { [weak self] doc in
+ guard let self else { return }
+ let routine = doc.routineID.flatMap { id in
+ CacheMapper.fetchRoutine(id: syncEngine.currentRoutineID(for: id), in: container.mainContext)
+ }
+ let activityType = routine?.activityTypeEnum ?? .traditionalStrength
+ self.workoutLauncher.launchWatchWorkout(activityType: activityType.hkActivityType)
+ }
#if DEBUG
if ScreenshotSeed.isActive { ScreenshotSeed.populate(container.mainContext) }
#endif
diff --git a/Workouts/Connectivity/PhoneConnectivityBridge.swift b/Workouts/Connectivity/PhoneConnectivityBridge.swift
index 1bd5c10..df45ee9 100644
--- a/Workouts/Connectivity/PhoneConnectivityBridge.swift
+++ b/Workouts/Connectivity/PhoneConnectivityBridge.swift
@@ -5,7 +5,7 @@ import WatchConnectivity
/// Phone side of the iPhone↔Watch bridge. The phone owns iCloud Drive; the watch
/// is a thin remote that round-trips through it:
-/// • Phone → Watch: pushes all splits + recent workouts as the latest
+/// • Phone → Watch: pushes all routines + recent workouts as the latest
/// application context whenever the cache changes (local or remote).
/// • Watch → Phone: receives an updated `WorkoutDocument` and applies it via the
/// SyncEngine write path (file → observer → cache → push back).
@@ -19,12 +19,12 @@ final class PhoneConnectivityBridge: NSObject {
private var session: WCSession?
/// Exclusive-edit lock published to the watch. While the phone has a workout's
- /// exercise (or a split) open in an editor, the watch parks any matching run and
+ /// exercise (or a routine) open in an editor, the watch parks any matching run and
/// blocks re-entry — so the two devices never drive the same run at once. Included in
/// every `pushAll` (the latest-wins context replaces wholesale, so a push that omitted
/// them would clear the lock prematurely).
private(set) var editingWorkoutID: String?
- private(set) var editingSplitID: String?
+ private(set) var editingRoutineID: String?
/// While true (scene backgrounded), the edit locks are *published* to the watch as
/// cleared without being forgotten locally. An editor left open in a pocketed — or
@@ -70,7 +70,7 @@ final class PhoneConnectivityBridge: NSObject {
syncEngine.onCacheChanged = { [weak self] in self?.pushAll() }
}
- /// Sends the current splits + most-recent workouts to the watch.
+ /// Sends the current routines + most-recent workouts to the watch.
func pushAll() {
guard let session, session.activationState == .activated, session.isPaired,
session.isWatchAppInstalled else {
@@ -78,7 +78,7 @@ final class PhoneConnectivityBridge: NSObject {
return
}
- let splits = (try? context.fetch(FetchDescriptor(sortBy: [SortDescriptor(\.order)]))) ?? []
+ let routines = (try? context.fetch(FetchDescriptor(sortBy: [SortDescriptor(\.order)]))) ?? []
// The watch only needs what it can act on: every active run (in-progress /
// not-started) plus recently-completed ones, kept ~24h so a run that just
@@ -107,24 +107,24 @@ final class PhoneConnectivityBridge: NSObject {
let restSeconds = UserDefaults.standard.object(forKey: WCPayload.restSecondsKey) as? Int ?? 45
let doneCountdownSeconds = UserDefaults.standard.object(forKey: WCPayload.doneCountdownSecondsKey) as? Int ?? 5
let weightUnit = UserDefaults.standard.string(forKey: WCPayload.weightUnitKey) ?? WeightUnit.lb.rawValue
- let splitDocs = splits.map(SplitDocument.init(from:))
+ let routineDocs = routines.map(RoutineDocument.init(from:))
func encode(_ included: [Workout]) -> [String: Any] {
WCPayload.encodeState(
- splits: splitDocs,
+ routines: routineDocs,
workouts: included.map(WorkoutDocument.init(from:)),
restSeconds: restSeconds,
doneCountdownSeconds: doneCountdownSeconds,
weightUnit: weightUnit,
editingWorkoutID: locksSuspended ? nil : editingWorkoutID,
- editingSplitID: locksSuspended ? nil : editingSplitID
+ editingRoutineID: locksSuspended ? nil : editingRoutineID
)
}
do {
let payload = encode(workouts)
try session.updateApplicationContext(payload)
- let bytes = ((payload[WCPayload.splitsKey] as? Data)?.count ?? 0)
+ let bytes = ((payload[WCPayload.routinesKey] as? Data)?.count ?? 0)
+ ((payload[WCPayload.workoutsKey] as? Data)?.count ?? 0)
- Self.log.info("pushAll: \(splits.count) splits, \(workouts.count) workouts (\(bytes) bytes)")
+ Self.log.info("pushAll: \(routines.count) routines, \(workouts.count) workouts (\(bytes) bytes)")
} catch {
// Realistically payload-too-large. The context is the watch's lifeline — a
// dropped push means it silently never hears about this state again — so
@@ -145,7 +145,7 @@ final class PhoneConnectivityBridge: NSObject {
func setLocksSuspended(_ suspended: Bool) {
guard locksSuspended != suspended else { return }
locksSuspended = suspended
- if editingWorkoutID != nil || editingSplitID != nil { pushAll() }
+ if editingWorkoutID != nil || editingRoutineID != nil { pushAll() }
}
/// Mark (or clear, with `nil`) the workout currently open in a phone exercise editor.
@@ -157,11 +157,11 @@ final class PhoneConnectivityBridge: NSObject {
pushAll()
}
- /// Mark (or clear, with `nil`) the split currently open in a phone editor. The watch
- /// parks any active run sourced from that split (matched by `splitID`).
- func setEditingSplit(_ id: String?) {
- guard editingSplitID != id else { return }
- editingSplitID = id
+ /// Mark (or clear, with `nil`) the routine currently open in a phone editor. The watch
+ /// parks any active run sourced from that routine (matched by `routineID`).
+ func setEditingRoutine(_ id: String?) {
+ guard editingRoutineID != id else { return }
+ editingRoutineID = id
pushAll()
}
diff --git a/Workouts/ContentView.swift b/Workouts/ContentView.swift
index 4f814da..91a012b 100644
--- a/Workouts/ContentView.swift
+++ b/Workouts/ContentView.swift
@@ -11,20 +11,30 @@ struct ContentView: View {
@Environment(LiveRunState.self) private var liveRun
var body: some View {
- WorkoutLogsView()
- // Prop the phone up and it runs the live Apple Watch workout — two-way: drive from
- // either device and the other follows.
- .fullScreenCover(isPresented: Binding(
- get: { liveRun.presentable != nil },
- set: { presenting in if !presenting { liveRun.mute() } }
- )) {
- if let frame = liveRun.presentable {
- // Re-key on the followed log so a flow-mode auto-advance (the driver
- // moving to the next exercise) rebuilds the mirror for the new exercise
- // instead of leaving a stale one pinned to the previous log.
- LiveRunCoverView(frame: frame) { liveRun.mute() }
- .id(frame.logID)
- }
+ TabView {
+ Tab("Today", systemImage: "sun.max.fill") {
+ TodayView()
}
+ Tab("Progress", systemImage: "chart.line.uptrend.xyaxis") {
+ ProgressTabView()
+ }
+ Tab("Settings", systemImage: "gearshape.2") {
+ SettingsView()
+ }
+ }
+ // Prop the phone up and it runs the live Apple Watch workout — two-way: drive from
+ // either device and the other follows.
+ .fullScreenCover(isPresented: Binding(
+ get: { liveRun.presentable != nil },
+ set: { presenting in if !presenting { liveRun.mute() } }
+ )) {
+ if let frame = liveRun.presentable {
+ // Re-key on the followed log so a flow-mode auto-advance (the driver
+ // moving to the next exercise) rebuilds the mirror for the new exercise
+ // instead of leaving a stale one pinned to the previous log.
+ LiveRunCoverView(frame: frame) { liveRun.mute() }
+ .id(frame.logID)
+ }
+ }
}
}
diff --git a/Workouts/Diagnostics/DiagnosticsView.swift b/Workouts/Diagnostics/DiagnosticsView.swift
index 983ef23..81c4870 100644
--- a/Workouts/Diagnostics/DiagnosticsView.swift
+++ b/Workouts/Diagnostics/DiagnosticsView.swift
@@ -19,7 +19,7 @@ final class DiagnosticsViewModel {
/// `generate()` runs) and each of its pieces tears itself down after each call.
/// The schema scanner resolves the forward-gate ceiling per file, because
/// Workouts has two document types with independent version counters — a
- /// `Splits/` file is gated by `SplitDocument.currentSchemaVersion`, a
+ /// `Splits/` file is gated by `RoutineDocument.currentSchemaVersion`, a
/// `Workouts/` file by `WorkoutDocument.currentSchemaVersion`, and a `Stubs/`
/// tombstone carries no versioned payload so it's never counted as skipped.
private let coordinator = DiagnosticsCoordinator(
@@ -27,12 +27,13 @@ final class DiagnosticsViewModel {
schemaScanner: SchemaSkipScanner(
ceilingForURL: { url in
let path = url.path
- if path.contains("/Splits/") { return SplitDocument.currentSchemaVersion }
+ // PINNED: routine documents live under the on-disk "Splits/" directory.
+ if path.contains("/Splits/") { return RoutineDocument.currentSchemaVersion }
if path.contains("/Workouts/") { return WorkoutDocument.currentSchemaVersion }
return .max
},
representativeCeiling: max(
- SplitDocument.currentSchemaVersion,
+ RoutineDocument.currentSchemaVersion,
WorkoutDocument.currentSchemaVersion
)
)
diff --git a/Workouts/Diagnostics/SchemaSkipScanner.swift b/Workouts/Diagnostics/SchemaSkipScanner.swift
index 1a7b259..4b7dfbd 100644
--- a/Workouts/Diagnostics/SchemaSkipScanner.swift
+++ b/Workouts/Diagnostics/SchemaSkipScanner.swift
@@ -12,9 +12,9 @@
//
// ADAPTATION FROM THE REFERENCE: the reference scanner takes a single
// `currentSchemaVersion: Int`. Workouts has TWO document types with
-// INDEPENDENT version counters (`SplitDocument.currentSchemaVersion` and
+// INDEPENDENT version counters (`RoutineDocument.currentSchemaVersion` and
// `WorkoutDocument.currentSchemaVersion`), so one global ceiling can't classify
-// both correctly — a Split written at a version the workout counter has already
+// both correctly — a Routine written at a version the workout counter has already
// reached would be misjudged. The forward-gate ceiling therefore has to be
// resolved per file (its location under `Documents/` tells us its type), so the
// scanner takes a `CeilingResolver` closure. A single-Int convenience init is
diff --git a/Workouts/ExerciseFigure/ExerciseInfo.swift b/Workouts/ExerciseFigure/ExerciseInfo.swift
index 2519b7f..9a5925a 100644
--- a/Workouts/ExerciseFigure/ExerciseInfo.swift
+++ b/Workouts/ExerciseFigure/ExerciseInfo.swift
@@ -22,7 +22,7 @@ struct ExerciseInfo {
var targets: [String]
var sections: [Section]
/// Parsed `**Defaults:**` bullet — the library's suggested starting plan for an
- /// exercise added outside a split (no prior log, no split entry to copy from).
+ /// exercise added outside a routine (no prior log, no routine entry to copy from).
/// `nil` when the bullet is absent (not yet authored) or unparseable.
var defaults: Defaults?
diff --git a/Workouts/HealthKit/WorkoutLauncher.swift b/Workouts/HealthKit/WorkoutLauncher.swift
index ec29221..f1bc3a3 100644
--- a/Workouts/HealthKit/WorkoutLauncher.swift
+++ b/Workouts/HealthKit/WorkoutLauncher.swift
@@ -19,7 +19,7 @@ final class WorkoutLauncher {
private let healthStore = HKHealthStore()
/// Configuration handed to watchOS; the watch starts a session with the same
- /// shape (see `WorkoutSessionManager`). The activity type comes from the split so
+ /// shape (see `WorkoutSessionManager`). The activity type comes from the routine so
/// the saved Health workout is categorized correctly.
static func makeConfiguration(activityType: HKWorkoutActivityType) -> HKWorkoutConfiguration {
let configuration = HKWorkoutConfiguration()
diff --git a/Workouts/Progress/ProgressPlanner.swift b/Workouts/Progress/ProgressPlanner.swift
new file mode 100644
index 0000000..b3008d4
--- /dev/null
+++ b/Workouts/Progress/ProgressPlanner.swift
@@ -0,0 +1,557 @@
+//
+// ProgressPlanner.swift
+// Workouts
+//
+// Copyright 2026 Rouslan Zenetl. All Rights Reserved.
+//
+
+import Foundation
+
+// The Progress tab's derivation engine — a pure, unit-testable planner (precedent:
+// `SeedReconcilePlanner`). Everything here is a deterministic function of plain value
+// snapshots: per-schedule weekly adherence, per-goal rollups with streaks, trend
+// buckets for the charts, and the achievements grid. Nothing is ever persisted —
+// streaks and badges are recomputed from workout documents on every read, so they
+// survive cache rebuilds by construction (see UX-REDESIGN.md).
+
+// MARK: - Inputs
+
+/// A plain snapshot of a `Schedule` cache entity. `routineID` must already be
+/// live-resolved through `SyncEngine.currentRoutineID(for:)` so seed clone-on-edit
+/// redirects don't orphan history.
+struct ScheduleFacts: Sendable, Equatable {
+ var id: String
+ var goal: GoalKind?
+ var recurrence: ScheduleRecurrence
+ var weekdays: [Int]? // Calendar weekday numbers 1 (Sun) – 7 (Sat); .fixedDays only
+ var timesPerWeek: Int? // .frequency only
+ var onceDate: Date? // .once only
+ var createdAt: Date
+ var routineID: String // live-resolved
+ var routineName: String
+}
+
+/// A plain snapshot of a `Workout` cache entity. `routineID` live-resolved like
+/// `ScheduleFacts.routineID`. `volume` is the caller's best value (recorded metric,
+/// else derived from set entries); `activeMinutes` is the completed duration.
+struct WorkoutFacts: Sendable, Equatable {
+ var routineID: String?
+ var start: Date
+ var end: Date?
+ var completed: Bool
+ var volume: Double
+ var activeMinutes: Double
+ var energyKcal: Double
+}
+
+// MARK: - Adherence outputs
+
+/// One schedule-week (or goal-week) verdict: how many required units were met.
+/// `expected == 0` marks a *vacuous* week — the schedule imposed nothing (created
+/// mid-week past its fixed days, a one-off in another week); vacuous weeks neither
+/// break nor extend streaks.
+struct WeekMark: Sendable, Equatable {
+ var weekStart: Date
+ var met: Int
+ var expected: Int
+
+ var hit: Bool { met >= expected }
+ var isVacuous: Bool { expected == 0 }
+
+ /// Fill fraction for the week-strip squares (0…1).
+ var fraction: Double {
+ expected > 0 ? min(1, Double(met) / Double(expected)) : 0
+ }
+}
+
+/// One schedule's adherence across the track's week grid. `weeks` aligns 1:1 with the
+/// grid (oldest → newest); `nil` = the schedule didn't exist / doesn't apply that week.
+struct ScheduleTrack: Sendable, Equatable {
+ var schedule: ScheduleFacts
+ var weeks: [WeekMark?]
+ /// The current week can no longer be met (a daily's missed day, too few days left
+ /// for the remaining frequency count). Distinct from "not yet hit".
+ var currentWeekBroken: Bool
+
+ var currentWeek: WeekMark? { weeks.last ?? nil }
+}
+
+/// A goal's rolled-up adherence: per-week sums across member schedules, plus the
+/// streak. The goal is the ledger category — its track continues unbroken across
+/// schedule rewrites (UX-REDESIGN.md).
+struct GoalTrack: Sendable, Equatable, Identifiable {
+ var goal: GoalKind? // nil = the "Unassigned" group
+ var scheduleTracks: [ScheduleTrack]
+ var weeks: [WeekMark] // rollup, one per grid week
+ /// Consecutive fully-hit weeks ending now. The current week counts once hit;
+ /// merely "not yet hit but still possible" neither counts nor breaks.
+ var streakWeeks: Int
+ /// False when any member schedule's current week is already unmeetable.
+ var streakAlive: Bool
+
+ var id: String { goal?.rawValue ?? "unassigned" }
+}
+
+// MARK: - Trend outputs
+
+/// Chart time windows, per the trend-visualization pattern (7D/30D/90D/1Y/All).
+enum TrendRange: String, CaseIterable, Identifiable, Sendable {
+ case week = "7D"
+ case month = "30D"
+ case quarter = "90D"
+ case year = "1Y"
+ case all = "All"
+
+ var id: String { rawValue }
+
+ func startDate(now: Date, calendar: Calendar) -> Date? {
+ switch self {
+ case .week: calendar.date(byAdding: .day, value: -7, to: now)
+ case .month: calendar.date(byAdding: .day, value: -30, to: now)
+ case .quarter: calendar.date(byAdding: .day, value: -90, to: now)
+ case .year: calendar.date(byAdding: .year, value: -1, to: now)
+ case .all: nil
+ }
+ }
+
+ /// Bucket granularity: short ranges read daily, a quarter weekly, longer monthly.
+ var bucket: Calendar.Component {
+ switch self {
+ case .week, .month: .day
+ case .quarter: .weekOfYear
+ case .year, .all: .month
+ }
+ }
+}
+
+/// One chart bucket (a day, week, or month depending on the range).
+struct TrendPoint: Sendable, Equatable, Identifiable {
+ var date: Date // bucket start
+ var workouts: Int
+ var volume: Double
+ var activeMinutes: Double
+ var energyKcal: Double
+
+ var id: Date { date }
+}
+
+// MARK: - Achievements
+
+/// A derived badge. `progress` is 0…1 toward earning (1 when earned) so locked badges
+/// can show how close they are.
+struct Achievement: Sendable, Equatable, Identifiable {
+ var id: String
+ var title: String
+ var detail: String
+ var systemImage: String
+ var colorName: String
+ var earned: Bool
+ var progress: Double
+}
+
+// MARK: - Planner
+
+enum ProgressPlanner {
+
+ // MARK: Goal tracks
+
+ /// Roll schedules + workouts up into per-goal adherence tracks over the last
+ /// `weeksBack` calendar weeks (oldest → newest, ending at the current week).
+ /// Groups appear in `goalOrder`; schedules keep their input order within a group;
+ /// goal-less schedules land in a trailing `goal == nil` group. Goals with no
+ /// schedules produce no track.
+ static func goalTracks(
+ schedules: [ScheduleFacts],
+ workouts: [WorkoutFacts],
+ weeksBack: Int = 12,
+ goalOrder: [GoalKind] = GoalKind.allCases,
+ calendar: Calendar = .current,
+ now: Date = Date()
+ ) -> [GoalTrack] {
+ let doneDays = completedDays(workouts: workouts, calendar: calendar)
+ let grid = weekGrid(weeksBack: weeksBack, calendar: calendar, now: now)
+
+ var groups: [(GoalKind?, [ScheduleFacts])] = goalOrder.map { kind in
+ (kind, schedules.filter { $0.goal == kind })
+ }
+ groups.append((nil, schedules.filter { $0.goal == nil }))
+
+ return groups.compactMap { goal, members in
+ guard !members.isEmpty else { return nil }
+ let tracks = members.map { schedule in
+ scheduleTrack(schedule: schedule, doneDays: doneDays[schedule.routineID] ?? [],
+ grid: grid, calendar: calendar, now: now)
+ }
+ let rollup = grid.enumerated().map { index, weekStart in
+ let marks = tracks.compactMap { $0.weeks[index] }
+ return WeekMark(weekStart: weekStart,
+ met: marks.reduce(0) { $0 + $1.met },
+ expected: marks.reduce(0) { $0 + $1.expected })
+ }
+ let alive = !tracks.contains { $0.currentWeekBroken }
+ return GoalTrack(goal: goal,
+ scheduleTracks: tracks,
+ weeks: rollup,
+ streakWeeks: streak(weeks: rollup, alive: alive),
+ streakAlive: alive)
+ }
+ }
+
+ /// One schedule's marks across the grid plus its current-week brokenness.
+ static func scheduleTrack(
+ schedule: ScheduleFacts,
+ doneDays: Set,
+ grid: [Date],
+ calendar: Calendar,
+ now: Date
+ ) -> ScheduleTrack {
+ let weeks = grid.map { weekStart in
+ weekMark(schedule: schedule, doneDays: doneDays, weekStart: weekStart,
+ calendar: calendar, now: now)
+ }
+ let broken = grid.last.map { currentWeekStart in
+ currentWeekBroken(schedule: schedule, doneDays: doneDays,
+ weekStart: currentWeekStart, calendar: calendar, now: now)
+ } ?? false
+ return ScheduleTrack(schedule: schedule, weeks: weeks, currentWeekBroken: broken)
+ }
+
+ /// The verdict for one schedule-week, or nil when the schedule doesn't apply
+ /// (created after the week, a one-off in a different week, a future week).
+ ///
+ /// Expectation semantics differ by recurrence so the *current* week reads as
+ /// "on track so far" rather than "not done yet":
+ /// - `.daily` / `.fixedDays`: expected counts only required days **up to today**;
+ /// met counts distinct completed days in that same window (so a Tuesday make-up
+ /// for a missed fixed Monday still counts — catch-ups are honored).
+ /// - `.frequency`: expected is the full weekly target (clamped to the days the
+ /// schedule existed that week); mid-week it reads "1 of 2" until actually met.
+ /// - `.once`: expected 1, only in the week containing its date.
+ static func weekMark(
+ schedule: ScheduleFacts,
+ doneDays: Set,
+ weekStart: Date,
+ calendar: Calendar,
+ now: Date
+ ) -> WeekMark? {
+ guard let window = window(for: schedule, weekStart: weekStart,
+ calendar: calendar, clampToDay: calendar.startOfDay(for: now))
+ else { return nil }
+
+ let done = window.elapsedDays.filter(doneDays.contains).count
+
+ switch schedule.recurrence {
+ case .daily:
+ return WeekMark(weekStart: weekStart, met: done, expected: window.elapsedDays.count)
+ case .fixedDays:
+ let wanted = Set(schedule.weekdays ?? [])
+ let required = window.elapsedDays
+ .filter { wanted.contains(calendar.component(.weekday, from: $0)) }.count
+ return WeekMark(weekStart: weekStart, met: min(done, required), expected: required)
+ case .frequency:
+ let target = min(max(schedule.timesPerWeek ?? 0, 0), window.fullDays.count)
+ return WeekMark(weekStart: weekStart, met: min(done, target), expected: target)
+ case .once:
+ guard let once = schedule.onceDate,
+ calendar.dateInterval(of: .weekOfYear, for: once)?.start == weekStart
+ else { return nil }
+ return WeekMark(weekStart: weekStart, met: min(done, 1), expected: 1)
+ }
+ }
+
+ /// True when the current week can no longer be met: the days remaining (today
+ /// included) are fewer than the requirement still outstanding. A daily's missed
+ /// yesterday breaks immediately; a 2×/week with one done and two days left doesn't.
+ static func currentWeekBroken(
+ schedule: ScheduleFacts,
+ doneDays: Set,
+ weekStart: Date,
+ calendar: Calendar,
+ now: Date
+ ) -> Bool {
+ let today = calendar.startOfDay(for: now)
+ guard let window = window(for: schedule, weekStart: weekStart,
+ calendar: calendar, clampToDay: today)
+ else { return false }
+
+ let done = window.fullDays.filter(doneDays.contains).count
+ let daysLeft = window.fullDays.filter { $0 >= today }.count
+
+ let fullRequirement: Int
+ switch schedule.recurrence {
+ case .daily:
+ fullRequirement = window.fullDays.count
+ case .fixedDays:
+ let wanted = Set(schedule.weekdays ?? [])
+ fullRequirement = window.fullDays
+ .filter { wanted.contains(calendar.component(.weekday, from: $0)) }.count
+ case .frequency:
+ fullRequirement = min(max(schedule.timesPerWeek ?? 0, 0), window.fullDays.count)
+ case .once:
+ guard let once = schedule.onceDate,
+ calendar.dateInterval(of: .weekOfYear, for: once)?.start == weekStart
+ else { return false }
+ fullRequirement = 1
+ }
+ return fullRequirement - done > daysLeft
+ }
+
+ // MARK: Trends
+
+ /// Completed workouts bucketed for the chart: daily for 7D/30D, weekly for 90D,
+ /// monthly for 1Y/All. Empty buckets are zero-filled from the range start (All:
+ /// from the first workout) through now, so gaps read as gaps rather than the line
+ /// skipping them. Empty input (or an empty window) returns [].
+ static func trendPoints(
+ workouts: [WorkoutFacts],
+ range: TrendRange,
+ calendar: Calendar = .current,
+ now: Date = Date()
+ ) -> [TrendPoint] {
+ let cutoff = range.startDate(now: now, calendar: calendar)
+ let completed = workouts.filter { workout in
+ workout.completed && (cutoff.map { workout.start >= $0 } ?? true)
+ }
+
+ func bucketStart(_ date: Date) -> Date {
+ calendar.dateInterval(of: range.bucket, for: date)?.start ?? date
+ }
+
+ var byBucket: [Date: TrendPoint] = [:]
+ for workout in completed {
+ let key = bucketStart(workout.start)
+ var point = byBucket[key] ?? TrendPoint(date: key, workouts: 0, volume: 0,
+ activeMinutes: 0, energyKcal: 0)
+ point.workouts += 1
+ point.volume += workout.volume
+ point.activeMinutes += workout.activeMinutes
+ point.energyKcal += workout.energyKcal
+ byBucket[key] = point
+ }
+
+ // Zero-fill from the window start so every bucket exists.
+ let firstDate = cutoff ?? completed.map(\.start).min()
+ guard let firstDate else { return [] }
+ var points: [TrendPoint] = []
+ var cursor = bucketStart(firstDate)
+ let last = bucketStart(now)
+ while cursor <= last {
+ points.append(byBucket[cursor]
+ ?? TrendPoint(date: cursor, workouts: 0, volume: 0, activeMinutes: 0, energyKcal: 0))
+ guard let next = calendar.date(byAdding: range.bucket, value: 1, to: cursor),
+ next > cursor else { break }
+ cursor = next
+ }
+ return points
+ }
+
+ // MARK: Achievements
+
+ /// The full badge set, earned and locked alike — every one a pure derivation of
+ /// history (recomputable after any cache rebuild). Streak badges read the goal
+ /// tracks' best run, so their reach is capped by the tracks' `weeksBack`.
+ static func achievements(
+ tracks: [GoalTrack],
+ workouts: [WorkoutFacts],
+ calendar: Calendar = .current,
+ now: Date = Date()
+ ) -> [Achievement] {
+ let completed = workouts.filter(\.completed)
+ let count = completed.count
+ let totalVolume = completed.reduce(0) { $0 + $1.volume }
+
+ var badges: [Achievement] = []
+
+ // Session-count milestones.
+ let countMilestones: [(Int, String, String)] = [
+ (1, "First Rep", "figure.walk"),
+ (10, "Ten Strong", "10.circle.fill"),
+ (50, "Fifty Club", "50.circle.fill"),
+ (100, "Century", "medal.fill"),
+ (250, "Relentless", "flame.circle.fill"),
+ ]
+ for (target, title, symbol) in countMilestones {
+ badges.append(Achievement(
+ id: "count-\(target)", title: title,
+ detail: "Complete \(target) workout\(target == 1 ? "" : "s")",
+ systemImage: symbol, colorName: "blue",
+ earned: count >= target,
+ progress: min(1, Double(count) / Double(target))))
+ }
+
+ // Weekly-streak milestones — best consecutive-hit run across any goal track.
+ let bestRun = tracks.map { bestHitRun(weeks: $0.weeks) }.max() ?? 0
+ let streakMilestones: [(Int, String)] = [
+ (4, "4-Week Rhythm"), (8, "8-Week Habit"), (12, "12-Week Lifestyle"),
+ ]
+ for (target, title) in streakMilestones {
+ badges.append(Achievement(
+ id: "streak-\(target)", title: title,
+ detail: "Hit a goal every week for \(target) weeks straight",
+ systemImage: "flame.fill", colorName: "orange",
+ earned: bestRun >= target,
+ progress: min(1, Double(bestRun) / Double(target))))
+ }
+
+ // Consecutive active days.
+ let dayRun = longestDayRun(workouts: completed, calendar: calendar)
+ badges.append(Achievement(
+ id: "days-7", title: "Seven Straight",
+ detail: "Work out 7 days in a row",
+ systemImage: "calendar", colorName: "green",
+ earned: dayRun >= 7,
+ progress: min(1, Double(dayRun) / 7)))
+
+ // Lifetime volume.
+ let volumeMilestones: [(Double, String, String)] = [
+ (100_000, "Heavy Lifter", "scalemass.fill"),
+ (1_000_000, "Million Mover", "trophy.fill"),
+ ]
+ for (target, title, symbol) in volumeMilestones {
+ badges.append(Achievement(
+ id: "volume-\(Int(target))", title: title,
+ detail: "Lift \(Int(target).formatted()) total volume",
+ systemImage: symbol, colorName: "purple",
+ earned: totalVolume >= target,
+ progress: min(1, totalVolume / target)))
+ }
+
+ // Time-of-day one-offs.
+ let hours = completed.map { calendar.component(.hour, from: $0.start) }
+ badges.append(Achievement(
+ id: "early-bird", title: "Early Bird",
+ detail: "Start a workout before 7 AM",
+ systemImage: "sunrise.fill", colorName: "yellow",
+ earned: hours.contains { $0 < 7 },
+ progress: hours.contains { $0 < 7 } ? 1 : 0))
+ badges.append(Achievement(
+ id: "night-owl", title: "Night Owl",
+ detail: "Start a workout after 9 PM",
+ systemImage: "moon.stars.fill", colorName: "indigo",
+ earned: hours.contains { $0 >= 21 },
+ progress: hours.contains { $0 >= 21 } ? 1 : 0))
+
+ // Clean sweep: some week in which every goal's requirement was fully hit.
+ let sweep = cleanSweepExists(tracks: tracks)
+ badges.append(Achievement(
+ id: "clean-sweep", title: "Clean Sweep",
+ detail: "Hit every goal in a single week",
+ systemImage: "checkmark.seal.fill", colorName: "teal",
+ earned: sweep, progress: sweep ? 1 : 0))
+
+ return badges
+ }
+
+ // MARK: - Shared helpers
+
+ /// Start-of-day set of every day with a completed workout, keyed by routine ID.
+ static func completedDays(workouts: [WorkoutFacts], calendar: Calendar) -> [String: Set] {
+ var result: [String: Set] = [:]
+ for workout in workouts where workout.completed {
+ guard let routineID = workout.routineID else { continue }
+ result[routineID, default: []].insert(calendar.startOfDay(for: workout.start))
+ }
+ return result
+ }
+
+ /// Week starts oldest → newest, ending at the week containing `now`.
+ static func weekGrid(weeksBack: Int, calendar: Calendar, now: Date) -> [Date] {
+ guard weeksBack > 0,
+ let currentStart = calendar.dateInterval(of: .weekOfYear, for: now)?.start
+ else { return [] }
+ return (0.. (fullDays: [Date], elapsedDays: [Date])? {
+ guard let weekEnd = calendar.date(byAdding: .weekOfYear, value: 1, to: weekStart)
+ else { return nil }
+ let createdDay = calendar.startOfDay(for: schedule.createdAt)
+ let start = max(weekStart, createdDay)
+ guard start < weekEnd, start <= clampToDay else { return nil }
+
+ var fullDays: [Date] = []
+ var cursor = start
+ while cursor < weekEnd {
+ fullDays.append(cursor)
+ guard let next = calendar.date(byAdding: .day, value: 1, to: cursor), next > cursor
+ else { break }
+ cursor = next
+ }
+ let elapsedDays = fullDays.filter { $0 <= clampToDay }
+ return (fullDays, elapsedDays)
+ }
+
+ /// Streak ending now: consecutive hit weeks scanned newest → oldest. A vacuous
+ /// week is transparent (skipped); the current week counts once hit, is skipped
+ /// while still meetable, and zeroes the streak when already broken.
+ static func streak(weeks: [WeekMark], alive: Bool) -> Int {
+ guard !weeks.isEmpty else { return 0 }
+ var remaining = weeks
+ let current = remaining.removeLast()
+
+ var count = 0
+ if !current.isVacuous {
+ if current.hit { count += 1 }
+ else if !alive { return 0 }
+ // else: still in play — neither counts nor breaks.
+ }
+ for week in remaining.reversed() {
+ if week.isVacuous { continue }
+ if week.hit { count += 1 } else { break }
+ }
+ return count
+ }
+
+ /// Longest run of consecutive hit weeks anywhere in the track (vacuous weeks are
+ /// transparent here too).
+ static func bestHitRun(weeks: [WeekMark]) -> Int {
+ var best = 0, run = 0
+ for week in weeks {
+ if week.isVacuous { continue }
+ if week.hit { run += 1; best = max(best, run) } else { run = 0 }
+ }
+ return best
+ }
+
+ /// Longest run of consecutive calendar days with at least one completed workout.
+ static func longestDayRun(workouts: [WorkoutFacts], calendar: Calendar) -> Int {
+ let days = Set(workouts.filter(\.completed).map { calendar.startOfDay(for: $0.start) })
+ .sorted()
+ var best = 0, run = 0
+ var previous: Date?
+ for day in days {
+ if let previous,
+ let next = calendar.date(byAdding: .day, value: 1, to: previous),
+ calendar.isDate(next, inSameDayAs: day) {
+ run += 1
+ } else {
+ run = 1
+ }
+ best = max(best, run)
+ previous = day
+ }
+ return best
+ }
+
+ /// True when some grid week saw every goal track's requirement fully hit —
+ /// non-vacuously for at least two goals (a single-goal "sweep" is just a hit week).
+ static func cleanSweepExists(tracks: [GoalTrack]) -> Bool {
+ guard let weekCount = tracks.first?.weeks.count, !tracks.isEmpty else { return false }
+ for index in 0..= 2 && marks.allSatisfy(\.hit) { return true }
+ }
+ return false
+ }
+}
diff --git a/Workouts/Resources/ExerciseMotions/Meditation.info.md b/Workouts/Resources/ExerciseMotions/Meditation.info.md
new file mode 100644
index 0000000..f50ebb0
--- /dev/null
+++ b/Workouts/Resources/ExerciseMotions/Meditation.info.md
@@ -0,0 +1,41 @@
+# Meditation
+
+Sit tall with the legs crossed, rest the hands on the knees, and follow the
+breath. A timed stillness practice that lowers stress, settles the nervous
+system, and closes a session — or a day — with focus.
+
+- **Category:** Mindfulness
+- **Type:** Seated meditation (timed hold)
+- **Targets:** Breath, focus, posture
+- **Prescription:** Sit 5–10 min, attention on the breath, returning to it whenever the mind wanders
+- **Defaults:** 1 × 300 s
+
+## Setup
+
+Sit on the floor (or a cushion) with the legs crossed — lotus, half-lotus, or a
+simple cross, whatever the hips allow. Stack the spine tall, rest the hands on
+the knees, and soften the gaze or close the eyes.
+
+## Execution
+
+1. Lengthen up through the crown of the head, letting the shoulders drop.
+2. Breathe slowly through the nose — a long inhale, a longer exhale.
+3. Rest the attention on the breath moving in and out.
+4. When the mind wanders, notice it without judgment and return to the breath.
+
+## Cues
+
+- Sit on the front edge of a cushion to tilt the pelvis and free the spine.
+- Let the breath deepen on its own — observe it rather than forcing it.
+- Keep the face, jaw, and hands soft; effort lives only in the tall spine.
+
+## Common Mistakes
+
+- Slumping through the lower back as the sit goes on.
+- Chasing every thought instead of letting it pass.
+- Straining into a full lotus the hips aren't ready for — a simple cross works.
+
+## Progression
+
+A few minutes seated comfortably → longer sits with a steady tall spine →
+half-lotus or full lotus as hip mobility allows.
diff --git a/Workouts/Resources/ExerciseMotions/Meditation.motion.json b/Workouts/Resources/ExerciseMotions/Meditation.motion.json
new file mode 100644
index 0000000..6501a12
--- /dev/null
+++ b/Workouts/Resources/ExerciseMotions/Meditation.motion.json
@@ -0,0 +1,30 @@
+{
+ "name": "Meditation",
+ "primary": 1,
+ "camera": {"yaw": 45},
+ "working": ["spine"],
+ "frames": [
+ {
+ "hold": 1.2, "tween": 2.6,
+ "root": {"pos": [160, 143], "pitch": 2},
+ "spine": [6, 2],
+ "neck": 4, "head": -8,
+ "shoulder_r": {"flexion": 30, "abduction": 8, "rotation": -25}, "elbow_r": 100,
+ "shoulder_l": {"flexion": 30, "abduction": 8, "rotation": -25}, "elbow_l": 100,
+ "hip_r": {"flexion": 88, "abduction": 45, "rotation": 50}, "knee_r": 146, "ankle_r": 0,
+ "hip_l": {"flexion": 88, "abduction": 45, "rotation": 50}, "knee_l": 146, "ankle_l": 0,
+ "pins": {"hand_r": [174, 95], "hand_l": [176, 96]}
+ },
+ {
+ "hold": 0.8, "tween": 2.8,
+ "root": {"pos": [160, 142], "pitch": 1},
+ "spine": [3, -2],
+ "neck": 2, "head": -6,
+ "shoulder_r": {"flexion": 33, "abduction": 8, "rotation": -25}, "elbow_r": 96,
+ "shoulder_l": {"flexion": 33, "abduction": 8, "rotation": -25}, "elbow_l": 96,
+ "hip_r": {"flexion": 88, "abduction": 45, "rotation": 50}, "knee_r": 146, "ankle_r": 0,
+ "hip_l": {"flexion": 88, "abduction": 45, "rotation": 50}, "knee_l": 146, "ankle_l": 0,
+ "pins": {"hand_r": [174, 93], "hand_l": [176, 94]}
+ }
+ ]
+}
diff --git a/Workouts/Resources/StarterSplits/Meditation.split.json b/Workouts/Resources/StarterSplits/Meditation.split.json
new file mode 100644
index 0000000..52d4e47
--- /dev/null
+++ b/Workouts/Resources/StarterSplits/Meditation.split.json
@@ -0,0 +1,23 @@
+{
+ "activityType" : 8,
+ "color" : "purple",
+ "createdAt" : "2020-01-01T00:00:00Z",
+ "exercises" : [
+ {
+ "durationSeconds" : 300,
+ "id" : "01DXF6DT00SNZFGBWZ6JACASCQ",
+ "loadType" : 2,
+ "name" : "Meditation",
+ "order" : 0,
+ "reps" : 0,
+ "sets" : 1,
+ "weight" : 0
+ }
+ ],
+ "id" : "01DXF6DT00ANFJR7GX9G808G4M",
+ "name" : "Meditation",
+ "order" : 13,
+ "schemaVersion" : 3,
+ "systemImage" : "figure.mind.and.body",
+ "updatedAt" : "2020-01-01T00:00:00Z"
+}
\ No newline at end of file
diff --git a/Workouts/Screenshots/ScreenshotRootView.swift b/Workouts/Screenshots/ScreenshotRootView.swift
index 580a054..6e0adbe 100644
--- a/Workouts/Screenshots/ScreenshotRootView.swift
+++ b/Workouts/Screenshots/ScreenshotRootView.swift
@@ -26,7 +26,17 @@ struct ScreenshotRootView: View {
@ViewBuilder
private var content: some View {
- if let workout = activeWorkout {
+ // The full tab shell and the Progress tab need no particular workout,
+ // so they're handled before the active-workout unwrap.
+ if ScreenshotSeed.screen(default: "workouts") == "today" {
+ ContentView()
+ } else if ScreenshotSeed.screen(default: "workouts") == "progress" {
+ ProgressTabView()
+ } else if ScreenshotSeed.screen(default: "workouts") == "progress-bottom" {
+ ProgressTabView(initialAnchor: .bottom)
+ } else if ScreenshotSeed.screen(default: "workouts") == "exercise-trends" {
+ NavigationStack { ExerciseTrendsListView() }
+ } else if let workout = activeWorkout {
switch ScreenshotSeed.screen(default: "workouts") {
case "exercise":
let logID = WorkoutDocument(from: workout).logs.first { $0.exerciseName == "Seated Row" }?.id
diff --git a/Workouts/Seed/SeedLibrary.swift b/Workouts/Seed/SeedLibrary.swift
index fb54481..194e13d 100644
--- a/Workouts/Seed/SeedLibrary.swift
+++ b/Workouts/Seed/SeedLibrary.swift
@@ -1,13 +1,15 @@
import Foundation
import IndieSync
-/// The bundled immutable starter-split library. Each seed ships as byte-canonical
-/// `SplitDocument` JSON in `Resources/StarterSplits/*.split.json`, keyed by a fixed
-/// ULID (the shared `01DXF6DT00` prefix, minted from a frozen 2020 timestamp) so
-/// every install and device agrees on the same identity.
+/// The bundled immutable starter-routine library. Each seed ships as byte-canonical
+/// `RoutineDocument` JSON in `Resources/StarterSplits/*.split.json` (the resource
+/// directory and file extension keep their legacy "split" spelling — bundled asset
+/// names are pinned, see below), keyed by a fixed ULID (the shared `01DXF6DT00`
+/// prefix, minted from a frozen 2020 timestamp) so every install and device agrees
+/// on the same identity.
///
/// Seeds are never mutated in place: editing one clones it to a fresh random ULID
-/// and soft-deletes the seed (see `SyncEngine.save(split:)`). The seed's tombstone
+/// and soft-deletes the seed (see `SyncEngine.save(routine:)`). The seed's tombstone
/// is exempt from pruning, so it vetoes resurrection forever; the app bundle — not
/// the stub — is the canonical restore source, since the seed content is immutable.
enum SeedLibrary {
@@ -17,7 +19,7 @@ enum SeedLibrary {
/// seeding at once is then semantically empty (both wrote the same bytes).
struct Seed: Sendable {
let id: String
- let doc: SplitDocument
+ let doc: RoutineDocument
let data: Data
}
@@ -31,11 +33,13 @@ enum SeedLibrary {
let bundle = Bundle(for: BundleToken.self)
// XcodeGen flattens resource groups, so the files land in the bundle root as
// ".split.json" — enumerate all json and filter on the compound suffix.
+ // PINNED: the bundled resource extension stays ".split.json" (asset names in
+ // StarterSplits/ are shipped files, not symbols — the rename must not touch them).
let urls = (bundle.urls(forResourcesWithExtension: "json", subdirectory: nil) ?? [])
.filter { $0.lastPathComponent.hasSuffix(".split.json") }
let loaded: [Seed] = urls.compactMap { url in
guard let data = try? Data(contentsOf: url),
- let doc = try? DocumentCoder.decode(SplitDocument.self, from: data)
+ let doc = try? DocumentCoder.decode(RoutineDocument.self, from: data)
else { return nil }
return Seed(id: doc.id, doc: doc, data: data)
}
@@ -53,7 +57,7 @@ enum SeedLibrary {
/// would otherwise read as an edit and fork the seed; normalizing both timestamps
/// to a common value before the `==` isolates real changes. A non-seed id has no
/// seed to compare against — treated as pristine (callers gate on `isSeed` first).
- static func isPristine(_ doc: SplitDocument) -> Bool {
+ static func isPristine(_ doc: RoutineDocument) -> Bool {
guard let seed = seed(id: doc.id) else { return true }
var edited = doc
var pristine = seed.doc
diff --git a/Workouts/Speech/SpeechSettings.swift b/Workouts/Speech/SpeechSettings.swift
index b6d6e2b..d537b68 100644
--- a/Workouts/Speech/SpeechSettings.swift
+++ b/Workouts/Speech/SpeechSettings.swift
@@ -112,7 +112,7 @@ enum SpeechSettings {
}
}
-/// What the automatic workout narration says while a flow-mode split runs, chosen in
+/// What the automatic workout narration says while a flow-mode routine runs, chosen in
/// Settings (only meaningful when "Speak Exercise Cues" is on). Stored as its raw string
/// under `spokenCueStyle`, read by both the Settings picker and the run flow.
enum SpokenCueStyle: String, CaseIterable, Identifiable {
diff --git a/Workouts/Sync/DuplicateCleanup.swift b/Workouts/Sync/DuplicateCleanup.swift
index f1e615b..6fbc1a8 100644
--- a/Workouts/Sync/DuplicateCleanup.swift
+++ b/Workouts/Sync/DuplicateCleanup.swift
@@ -3,22 +3,22 @@ import Foundation
// Pure planning logic for the developer-facing "duplicate cleanup" tool. No I/O
// and no `SyncEngine` dependency, so it's fully unit-testable in isolation:
// `SyncEngine.scanForDuplicates()` gathers the already-decoded documents and the
-// referenced-split-id set, hands them to `DuplicateCleanupPlanner.plan(...)`, and
+// referenced-routine-id set, hands them to `DuplicateCleanupPlanner.plan(...)`, and
// `SyncEngine.performCleanup(_:)` executes the resulting plan file by file.
//
// Duplicates are detected by a *content* fingerprint that deliberately ignores
-// identity (ids), timestamps, and cosmetic fields — two splits/workouts that were
+// identity (ids), timestamps, and cosmetic fields — two routines/workouts that were
// created independently (e.g. by a sync hiccup, a restore, or manual testing) but
// carry the same real content are duplicates even though every id differs.
// MARK: - Plan
struct DuplicateCleanupPlan: Sendable {
- struct SplitGroup: Sendable, Identifiable {
+ struct RoutineGroup: Sendable, Identifiable {
/// Members that survive — always non-empty when the group is emitted.
- var keep: [SplitDocument]
+ var keep: [RoutineDocument]
/// Members slated for deletion — always non-empty when the group is emitted.
- var delete: [SplitDocument]
+ var delete: [RoutineDocument]
var id: String {
keep.isEmpty ? (delete.first?.id ?? "") : keep.map(\.id).joined(separator: ",")
@@ -32,14 +32,14 @@ struct DuplicateCleanupPlan: Sendable {
var id: String { keep.id }
}
- var splitGroups: [SplitGroup]
+ var routineGroups: [RoutineGroup]
var workoutGroups: [WorkoutGroup]
- var isEmpty: Bool { splitGroups.isEmpty && workoutGroups.isEmpty }
+ var isEmpty: Bool { routineGroups.isEmpty && workoutGroups.isEmpty }
/// Total number of documents this plan would delete across every group.
var deleteCount: Int {
- splitGroups.reduce(0) { $0 + $1.delete.count } + workoutGroups.reduce(0) { $0 + $1.delete.count }
+ routineGroups.reduce(0) { $0 + $1.delete.count } + workoutGroups.reduce(0) { $0 + $1.delete.count }
}
}
@@ -47,22 +47,23 @@ struct DuplicateCleanupPlan: Sendable {
enum DuplicateCleanupPlanner {
/// Builds a cleanup plan from already-decoded, already-`isReadable`-filtered
- /// documents. `referencedSplitIDs` must include every split id any workout
- /// (readable or quarantined) points at, plus any clone-redirect targets — the
- /// caller (`SyncEngine.scanForDuplicates`) is responsible for assembling it.
+ /// documents. `referencedRoutineIDs` must include every routine id any workout
+ /// or schedule (readable or quarantined) points at, plus any clone-redirect
+ /// targets — the caller (`SyncEngine.scanForDuplicates`) is responsible for
+ /// assembling it.
static func plan(
- splits: [SplitDocument],
+ routines: [RoutineDocument],
workouts: [WorkoutDocument],
- referencedSplitIDs: Set,
+ referencedRoutineIDs: Set,
isSeed: (String) -> Bool
) -> DuplicateCleanupPlan {
DuplicateCleanupPlan(
- splitGroups: planSplits(splits, referencedSplitIDs: referencedSplitIDs, isSeed: isSeed),
+ routineGroups: planRoutines(routines, referencedRoutineIDs: referencedRoutineIDs, isSeed: isSeed),
workoutGroups: planWorkouts(workouts)
)
}
- // MARK: Splits
+ // MARK: Routines
/// Hashable projection of a `MachineSetting`. The fingerprints keep the
/// nil / empty distinction (non-machine vs. machine-with-nothing-recorded)
@@ -88,15 +89,15 @@ enum DuplicateCleanupPlanner {
var machineSettings: [MachineSettingFingerprint]?
}
- /// Content-only projection of a split: ignores id, dates, color, systemImage,
+ /// Content-only projection of a routine: ignores id, dates, color, systemImage,
/// order, and activityType.
- private struct SplitFingerprint: Hashable {
+ private struct RoutineFingerprint: Hashable {
var name: String
var exercises: [ExerciseFingerprint]
}
- private static func fingerprint(_ doc: SplitDocument) -> SplitFingerprint {
- SplitFingerprint(
+ private static func fingerprint(_ doc: RoutineDocument) -> RoutineFingerprint {
+ RoutineFingerprint(
name: doc.name.trimmingCharacters(in: .whitespacesAndNewlines),
exercises: doc.exercises.sorted { $0.order < $1.order }.map {
ExerciseFingerprint(
@@ -115,19 +116,19 @@ enum DuplicateCleanupPlanner {
/// (ULIDs sort chronologically) survives — deterministic across devices.
/// Everyone else in the group is deleted. A group with nothing to delete
/// (e.g. every member is protected) is dropped entirely.
- private static func planSplits(
- _ splits: [SplitDocument],
- referencedSplitIDs: Set,
+ private static func planRoutines(
+ _ routines: [RoutineDocument],
+ referencedRoutineIDs: Set,
isSeed: (String) -> Bool
- ) -> [DuplicateCleanupPlan.SplitGroup] {
- let grouped = Dictionary(grouping: splits, by: fingerprint)
- var groups: [DuplicateCleanupPlan.SplitGroup] = []
+ ) -> [DuplicateCleanupPlan.RoutineGroup] {
+ let grouped = Dictionary(grouping: routines, by: fingerprint)
+ var groups: [DuplicateCleanupPlan.RoutineGroup] = []
for (_, members) in grouped where members.count > 1 {
- var keep: [SplitDocument] = []
- var candidates: [SplitDocument] = []
+ var keep: [RoutineDocument] = []
+ var candidates: [RoutineDocument] = []
for member in members {
- if referencedSplitIDs.contains(member.id) || isSeed(member.id) {
+ if referencedRoutineIDs.contains(member.id) || isSeed(member.id) {
keep.append(member)
} else {
candidates.append(member)
@@ -138,7 +139,7 @@ enum DuplicateCleanupPlanner {
candidates.removeAll { $0.id == survivorID }
}
guard !candidates.isEmpty else { continue }
- groups.append(DuplicateCleanupPlan.SplitGroup(
+ groups.append(DuplicateCleanupPlan.RoutineGroup(
keep: keep.sorted { $0.id < $1.id },
delete: candidates.sorted { $0.id < $1.id }
))
@@ -169,8 +170,8 @@ enum DuplicateCleanupPlanner {
/// startedAt/completedAt, exact start/end time (only the calendar day of
/// `start` matters), and metrics.
private struct WorkoutFingerprint: Hashable {
- var splitID: String
- var splitName: String
+ var routineID: String
+ var routineName: String
var year: Int
var month: Int
var day: Int
@@ -181,8 +182,8 @@ enum DuplicateCleanupPlanner {
private static func fingerprint(_ doc: WorkoutDocument) -> WorkoutFingerprint {
let comps = Calendar.current.dateComponents([.year, .month, .day], from: doc.start)
return WorkoutFingerprint(
- splitID: doc.splitID ?? "",
- splitName: doc.splitName ?? "",
+ routineID: doc.routineID ?? "",
+ routineName: doc.routineName ?? "",
year: comps.year ?? 0,
month: comps.month ?? 0,
day: comps.day ?? 0,
diff --git a/Workouts/Sync/SeedReconcile.swift b/Workouts/Sync/SeedReconcile.swift
index 2a416fd..ec864eb 100644
--- a/Workouts/Sync/SeedReconcile.swift
+++ b/Workouts/Sync/SeedReconcile.swift
@@ -1,12 +1,12 @@
import Foundation
-// Pure decision logic for bringing an install's on-disk starter-split library in
+// Pure decision logic for bringing an install's on-disk starter-routine library in
// line with the current app bundle. No I/O and no `SyncEngine` dependency, so the
// safety-critical part is fully unit-testable in isolation: `SyncEngine.reconcileSeeds()`
-// gathers the live seed-path files, stub set, and live split names, hands each seed to
+// gathers the live seed-path files, stub set, and live routine names, hands each seed to
// `SeedReconcilePlanner.decision(...)`, and executes the returned skip / upgrade / write.
//
-// Why this exists: starter seeds ship as byte-canonical `SplitDocument` JSON under
+// Why this exists: starter seeds ship as byte-canonical `RoutineDocument` JSON under
// FIXED ULIDs (see `SeedLibrary`). `autoSeedIfEmpty` only ever writes them into a
// verifiably empty container, so an existing install can never pick up an upgraded
// seed revision or a newly-shipped seed. This planner decides, per seed, whether the
@@ -26,10 +26,10 @@ struct SeedReconcileInput: Equatable {
/// The seed's fixed ULID.
let seedID: String
/// The bundle seed document (canonical current content).
- let seedDoc: SplitDocument
+ let seedDoc: RoutineDocument
/// The decoded live file currently sitting at the seed's fixed path, or `nil`
/// when no live file exists there (deleted, or never written).
- let liveDoc: SplitDocument?
+ let liveDoc: RoutineDocument?
/// Whether a tombstone stub exists for this seed's id.
let hasStub: Bool
}
@@ -54,7 +54,7 @@ enum SeedSkipReason: Equatable {
case quarantined
/// No live file, but a stub exists: the user deleted this seed. Respect the veto.
case vetoed
- /// No live file and no stub, but a live split already uses this seed's name
+ /// No live file and no stub, but a live routine already uses this seed's name
/// (e.g. a legacy runtime-built or user-cloned "Upper Body"). Writing the seed
/// would duplicate it by name.
case nameCollision
@@ -64,11 +64,11 @@ enum SeedSkipReason: Equatable {
enum SeedReconcilePlanner {
/// The automatic on-connect decision for one seed (see the decision table in the
- /// file header). `liveSplitNames` must be the names of every live split currently
+ /// file header). `liveRoutineNames` must be the names of every live routine currently
/// in the container/cache after reconcile — the name-collision guard consults it.
static func decision(
for input: SeedReconcileInput,
- liveSplitNames: Set
+ liveRoutineNames: Set
) -> SeedReconcileDecision {
if let live = input.liveDoc {
// A live file sits at the seed path. Never touch one a newer app version
@@ -80,7 +80,7 @@ enum SeedReconcilePlanner {
}
// No live file at the seed path.
if input.hasStub { return .skip(.vetoed) }
- if liveSplitNames.contains(input.seedDoc.name) { return .skip(.nameCollision) }
+ if liveRoutineNames.contains(input.seedDoc.name) { return .skip(.nameCollision) }
return .write
}
@@ -88,27 +88,27 @@ enum SeedReconcilePlanner {
///
/// A byte compare is wrong here: a container file may have been written by an
/// older app version, carrying extra now-removed keys and a lower `schemaVersion`.
- /// Decoding both into the current `SplitDocument` already drops unknown keys, so
+ /// Decoding both into the current `RoutineDocument` already drops unknown keys, so
/// the only remaining artificial difference is the version stamp — normalize the
/// live doc's `schemaVersion` to the seed's before the `==` so an older wrapper of
/// identical content reads as up to date (no churn), while a genuine content
/// change reads as different (upgrade).
- static func semanticallyEqual(live: SplitDocument, seed: SplitDocument) -> Bool {
+ static func semanticallyEqual(live: RoutineDocument, seed: RoutineDocument) -> Bool {
var normalized = live
normalized.schemaVersion = seed.schemaVersion
return normalized == seed
}
- /// Whether the deliberate "Restore Starter Splits" action should (re)write a seed:
- /// only when no live file sits at its path and no live split already uses its name.
+ /// Whether the deliberate "Restore Starter Routines" action should (re)write a seed:
+ /// only when no live file sits at its path and no live routine already uses its name.
/// Unlike `decision(...)` a stub does NOT veto here — restore lifts the veto — but
/// the engine still removes the stub as a separate execution step. A seed whose
/// live file already exists is left for `decision(...)` to upgrade.
static func shouldRestore(
hasLiveFile: Bool,
seedName: String,
- liveSplitNames: Set
+ liveRoutineNames: Set
) -> Bool {
- !hasLiveFile && !liveSplitNames.contains(seedName)
+ !hasLiveFile && !liveRoutineNames.contains(seedName)
}
}
diff --git a/Workouts/Sync/SyncEngine.swift b/Workouts/Sync/SyncEngine.swift
index 9752126..ea75764 100644
--- a/Workouts/Sync/SyncEngine.swift
+++ b/Workouts/Sync/SyncEngine.swift
@@ -55,13 +55,13 @@ final class SyncEngine {
/// Maps a seed's id to its clone's id after a clone-on-edit fork, so a view still
/// holding the seed's id resolves to the live clone. In-memory only (@Observable
/// notifies on change); not persisted — durability comes from `repointWorkouts`,
- /// which rewrites `splitID` on the workout documents themselves at fork time.
+ /// which rewrites `routineID` on the workout documents themselves at fork time.
/// This map only bridges views that captured the seed's id before the swap.
private(set) var cloneRedirects: [String: String] = [:]
- /// Follow the redirect chain from `id` to the current live split id. A seed
+ /// Follow the redirect chain from `id` to the current live routine id. A seed
/// redirects at most once, but the loop tolerates (and breaks) any chain or cycle.
- func currentSplitID(for id: String) -> String {
+ func currentRoutineID(for id: String) -> String {
var current = id
var seen: Set = [current]
while let next = cloneRedirects[current], seen.insert(next).inserted {
@@ -74,6 +74,14 @@ final class SyncEngine {
/// this to push fresh state to the watch.
var onCacheChanged: (() -> Void)?
+ /// Fired when a phone-side save first moves a workout from `.notStarted` into
+ /// `.inProgress` — the moment a run actually begins. AppServices launches the
+ /// watch session here rather than at creation, so peeking into a routine never
+ /// spins one up. Watch-originated updates (`ingestFromWatch`) never fire it —
+ /// the watch already owns a session — and neither does editing an old finished
+ /// workout back to in-progress (the transition must be from `.notStarted`).
+ var onWorkoutBecameActive: ((WorkoutDocument) -> Void)?
+
private let log = Logger(subsystem: "dev.rzen.indie.Workouts", category: "sync")
private let modelContainer: ModelContainer
private var store: DocumentFileStore?
@@ -176,7 +184,9 @@ final class SyncEngine {
}
}
log.info("connect[\(attempt)]: preparing directories…")
- await store.prepareDirectories(["Splits", "Workouts", "Stubs"])
+ // PINNED: the iCloud directory is "Splits" (routine documents live under it).
+ // The directory name is on-disk data — the Split→Routine rename must not touch it.
+ await store.prepareDirectories(["Splits", "Workouts", "Schedules", "Stubs"])
safety.cancel()
guard attempt == connectAttempt else { return }
@@ -323,21 +333,21 @@ final class SyncEngine {
onCacheChanged?()
return
}
- await save(workout: merged)
+ await save(workout: merged, notifyingActive: false)
} else {
// First time we've seen this workout — nothing to merge against.
- await save(workout: doc)
+ await save(workout: doc, notifyingActive: false)
}
}
// MARK: - Public CRUD (mirror-first: cache now, file via the write queue)
- /// Returns the *effective* id of the split that was written — normally `doc.id`,
+ /// Returns the *effective* id of the routine that was written — normally `doc.id`,
/// but the clone's fresh id when an edited seed forks. Open views follow the swap
- /// via `currentSplitID(for:)`.
+ /// via `currentRoutineID(for:)`.
@discardableResult
- func save(split doc: SplitDocument) async -> String {
- // Seeds are immutable: a real edit forks the seed into a user-owned split and
+ func save(routine doc: RoutineDocument) async -> String {
+ // Seeds are immutable: a real edit forks the seed into a user-owned routine and
// soft-deletes the original, keeping the curated seed intact and restorable. A
// pristine (no-op) save must NOT fork — the edit sheets stamp `updatedAt`
// unconditionally, so merely opening and saving a seed's editor without a change
@@ -346,19 +356,19 @@ final class SyncEngine {
return await cloneSeedOnEdit(doc)
}
- CacheMapper.upsertSplit(doc, relativePath: doc.relativePath, into: context)
+ CacheMapper.upsertRoutine(doc, relativePath: doc.relativePath, into: context)
saveCacheAndNotify()
- enqueueWrite(.split(doc), timestamp: doc.updatedAt)
+ enqueueWrite(.routine(doc), timestamp: doc.updatedAt)
return doc.id
}
- /// Fork an edited seed into a user-owned split. Writes the clone under a fresh
+ /// Fork an edited seed into a user-owned routine. Writes the clone under a fresh
/// ULID and upserts it into the cache immediately (same rationale as
/// `ingestFromWatch`: a same-process write doesn't reliably wake the metadata
/// observer, and open screens must see the clone the moment `save` returns), then
/// soft-deletes the seed and drops its cache entity. Records the id redirect so a
/// view still holding the seed's id follows the identity swap.
- private func cloneSeedOnEdit(_ doc: SplitDocument) async -> String {
+ private func cloneSeedOnEdit(_ doc: RoutineDocument) async -> String {
guard let store else { return doc.id }
var clone = doc
clone.id = ULID.make()
@@ -366,9 +376,10 @@ final class SyncEngine {
// Exercise ids are kept as-is — they only need uniqueness within the document.
do {
try await store.write(clone, to: clone.relativePath)
- CacheMapper.upsertSplit(clone, relativePath: clone.relativePath, into: context)
+ CacheMapper.upsertRoutine(clone, relativePath: clone.relativePath, into: context)
// Soft-delete the seed (writes its veto stub, then removes the live file),
// then evict its now-orphaned cache entity.
+ // PINNED: tombstone kind stays "split" — existing stubs on disk carry it.
await softDelete(id: doc.id, kind: "split", livePath: doc.relativePath)
deleteCachedEntity(id: doc.id)
try context.save()
@@ -378,63 +389,73 @@ final class SyncEngine {
onCacheChanged?()
return clone.id
} catch {
- report("Failed to fork edited starter split", error)
+ report("Failed to fork edited starter routine", error)
return doc.id
}
}
- /// Rewrite `splitID` on every workout that references `oldID`, so split lookups
+ /// Rewrite `routineID` on every workout that references `oldID`, so routine lookups
/// (category grouping, add-exercise, plan mirroring, health estimates) keep
/// resolving after a seed fork — durably, across relaunches, and on the watch,
- /// which the in-memory `cloneRedirects` map can't reach. `splitName` stays frozen
+ /// which the in-memory `cloneRedirects` map can't reach. `routineName` stays frozen
/// at what the workout was started as, matching rename semantics for regular
- /// splits. Best effort per workout: a failed rewrite is reported and the redirect
+ /// routines. Best effort per workout: a failed rewrite is reported and the redirect
/// map still covers it for this session.
private func repointWorkouts(from oldID: String, to newID: String) async {
guard let store else { return }
let referencing = (try? context.fetch(
- FetchDescriptor(predicate: #Predicate { $0.splitID == oldID })
+ FetchDescriptor(predicate: #Predicate { $0.routineID == oldID })
)) ?? []
guard !referencing.isEmpty else { return }
for workout in referencing {
var wDoc = WorkoutDocument(from: workout)
- wDoc.splitID = newID
+ wDoc.routineID = newID
wDoc.updatedAt = Date()
do {
try await store.write(wDoc, to: wDoc.relativePath)
CacheMapper.upsertWorkout(wDoc, relativePath: wDoc.relativePath, into: context)
} catch {
- report("Failed to repoint workout \(wDoc.id) at edited split", error)
+ report("Failed to repoint workout \(wDoc.id) at edited routine", error)
}
}
do { try context.save() } catch { report("Cache save failed", error) }
}
- /// Push edited machine settings onto the originating split's exercise (matched
+ /// Push edited machine settings onto the originating routine's exercise (matched
/// by name — logs reference exercises by name only), following the seed
/// clone-on-edit redirect so the live clone is updated. Skips silently when the
- /// split is gone or no exercise matches, and when nothing changed (so a pristine
- /// seed isn't needlessly forked). Never caches the split's id across the save,
+ /// routine is gone or no exercise matches, and when nothing changed (so a pristine
+ /// seed isn't needlessly forked). Never caches the routine's id across the save,
/// since saving a seed mints a new one.
- func writeBackMachineSettings(_ settings: [MachineSetting], exerciseName: String, splitID: String?) async {
- guard let splitID else { return }
- let liveID = currentSplitID(for: splitID)
- guard let split = CacheMapper.fetchSplit(id: liveID, in: context) else { return }
- var splitDoc = SplitDocument(from: split)
- guard let idx = splitDoc.exercises.firstIndex(where: { $0.name == exerciseName }) else { return }
- guard splitDoc.exercises[idx].machineSettings != settings else { return }
- splitDoc.exercises[idx].machineSettings = settings
- splitDoc.updatedAt = Date()
- await save(split: splitDoc)
+ func writeBackMachineSettings(_ settings: [MachineSetting], exerciseName: String, routineID: String?) async {
+ guard let routineID else { return }
+ let liveID = currentRoutineID(for: routineID)
+ guard let routine = CacheMapper.fetchRoutine(id: liveID, in: context) else { return }
+ var routineDoc = RoutineDocument(from: routine)
+ guard let idx = routineDoc.exercises.firstIndex(where: { $0.name == exerciseName }) else { return }
+ guard routineDoc.exercises[idx].machineSettings != settings else { return }
+ routineDoc.exercises[idx].machineSettings = settings
+ routineDoc.updatedAt = Date()
+ await save(routine: routineDoc)
}
func save(workout doc: WorkoutDocument) async {
+ await save(workout: doc, notifyingActive: true)
+ }
+
+ /// `notifyingActive: false` is the watch-ingest path — the watch already runs
+ /// its own session, so a watch-originated update must not fire the
+ /// became-active hook (which would launch a second session at the wrist).
+ private func save(workout doc: WorkoutDocument, notifyingActive: Bool) async {
// The month bucket in a workout's path derives from `start`, so editing the
// start date (or a device in a different time zone) can move the file to a new
// path. Capture the previously-written path before the upsert overwrites it —
// the queued write removes it after landing, otherwise the same id would live
// at two paths and the old copy would re-import on the next reconcile.
- let previousPath = CacheMapper.fetchWorkout(id: doc.id, in: context)?.jsonRelativePath
+ // (Both reads happen before the upsert mutates the same entity in place.)
+ let previous = CacheMapper.fetchWorkout(id: doc.id, in: context)
+ let previousPath = previous?.jsonRelativePath
+ let previousStatus = previous?.status
CacheMapper.upsertWorkout(doc, relativePath: doc.relativePath, into: context)
saveCacheAndNotify()
enqueueWrite(
@@ -442,12 +463,26 @@ final class SyncEngine {
timestamp: doc.updatedAt,
stalePath: previousPath != doc.relativePath ? previousPath : nil
)
+ if notifyingActive, previousStatus == .notStarted,
+ doc.status == WorkoutStatus.inProgress.rawValue {
+ onWorkoutBecameActive?(doc)
+ }
}
- func delete(split: Split) async {
- let id = split.id, livePath = split.jsonRelativePath
+ /// Persist a schedule. Schedules live in a flat `Schedules/` directory (no month
+ /// bucketing), so — unlike `save(workout:)` — a save can never move the file to a
+ /// new path: mirror the cache, then queue the plain write.
+ func save(schedule doc: ScheduleDocument) async {
+ CacheMapper.upsertSchedule(doc, relativePath: doc.relativePath, into: context)
+ saveCacheAndNotify()
+ enqueueWrite(.schedule(doc), timestamp: doc.updatedAt)
+ }
+
+ func delete(routine: Routine) async {
+ let id = routine.id, livePath = routine.jsonRelativePath
deleteCachedEntity(id: id)
saveCacheAndNotify()
+ // PINNED: tombstone kind stays "split" — existing stubs on disk carry it.
enqueueWrite(.delete(id: id, kind: "split", livePath: livePath), timestamp: Date())
}
@@ -458,6 +493,13 @@ final class SyncEngine {
enqueueWrite(.delete(id: id, kind: "workout", livePath: livePath), timestamp: Date())
}
+ func delete(schedule: Schedule) async {
+ let id = schedule.id, livePath = schedule.jsonRelativePath
+ deleteCachedEntity(id: id)
+ saveCacheAndNotify()
+ enqueueWrite(.delete(id: id, kind: "schedule", livePath: livePath), timestamp: Date())
+ }
+
/// Persist pending cache mutations and fan out the change notification — the
/// tail of every local write's immediate cache mirror.
private func saveCacheAndNotify() {
@@ -552,10 +594,12 @@ final class SyncEngine {
guard let store, let tombstones else { return .retry }
do {
switch entry.payload {
- case .split(let doc):
+ case .routine(let doc):
try await store.write(doc, to: doc.relativePath)
case .workout(let doc):
try await store.write(doc, to: doc.relativePath)
+ case .schedule(let doc):
+ try await store.write(doc, to: doc.relativePath)
case .delete(let id, let kind, let livePath):
// The stub is the authoritative delete record (it vetoes
// resurrection everywhere); the live-file removal is best-effort —
@@ -635,16 +679,23 @@ final class SyncEngine {
return
}
+ // PINNED: routine documents live under the "Splits/" directory on disk — the
+ // path prefix is data, unchanged by the Split→Routine symbol rename.
if relativePath.hasPrefix("Splits/") {
- guard let doc = try? DocumentCoder.decode(SplitDocument.self, from: data), doc.isReadable else { return }
+ guard let doc = try? DocumentCoder.decode(RoutineDocument.self, from: data), doc.isReadable else { return }
if await tombstones.stubExists(id: doc.id) { try? await store.remove(at: relativePath); return }
if supersededByPendingWrite(id: doc.id, fileTimestamp: doc.updatedAt) { return }
- CacheMapper.upsertSplit(doc, relativePath: relativePath, into: context)
+ CacheMapper.upsertRoutine(doc, relativePath: relativePath, into: context)
} else if relativePath.hasPrefix("Workouts/") {
guard let doc = try? DocumentCoder.decode(WorkoutDocument.self, from: data), doc.isReadable else { return }
if await tombstones.stubExists(id: doc.id) { try? await store.remove(at: relativePath); return }
if supersededByPendingWrite(id: doc.id, fileTimestamp: doc.updatedAt) { return }
CacheMapper.upsertWorkout(doc, relativePath: relativePath, into: context)
+ } else if relativePath.hasPrefix("Schedules/") {
+ guard let doc = try? DocumentCoder.decode(ScheduleDocument.self, from: data), doc.isReadable else { return }
+ if await tombstones.stubExists(id: doc.id) { try? await store.remove(at: relativePath); return }
+ if supersededByPendingWrite(id: doc.id, fileTimestamp: doc.updatedAt) { return }
+ CacheMapper.upsertSchedule(doc, relativePath: relativePath, into: context)
}
}
@@ -661,8 +712,9 @@ final class SyncEngine {
let tombstoned = await tombstones.listStubIDs()
let dataFiles = await store.list().filter { !$0.hasPrefix("Stubs/") }
- var liveSplitIDs = Set()
+ var liveRoutineIDs = Set()
var liveWorkoutIDs = Set()
+ var liveScheduleIDs = Set()
var unreadablePaths = Set()
for path in dataFiles {
@@ -680,18 +732,25 @@ final class SyncEngine {
unreadablePaths.insert(path)
continue
}
+ // PINNED: routine documents live under "Splits/" on disk (see importFile).
if path.hasPrefix("Splits/") {
- guard let doc = try? DocumentCoder.decode(SplitDocument.self, from: data), doc.isReadable else { continue }
+ guard let doc = try? DocumentCoder.decode(RoutineDocument.self, from: data), doc.isReadable else { continue }
if tombstoned.contains(doc.id) { try? await store.remove(at: path); continue }
- liveSplitIDs.insert(doc.id)
+ liveRoutineIDs.insert(doc.id)
if supersededByPendingWrite(id: doc.id, fileTimestamp: doc.updatedAt) { continue }
- CacheMapper.upsertSplit(doc, relativePath: path, into: context)
+ CacheMapper.upsertRoutine(doc, relativePath: path, into: context)
} else if path.hasPrefix("Workouts/") {
guard let doc = try? DocumentCoder.decode(WorkoutDocument.self, from: data), doc.isReadable else { continue }
if tombstoned.contains(doc.id) { try? await store.remove(at: path); continue }
liveWorkoutIDs.insert(doc.id)
if supersededByPendingWrite(id: doc.id, fileTimestamp: doc.updatedAt) { continue }
CacheMapper.upsertWorkout(doc, relativePath: path, into: context)
+ } else if path.hasPrefix("Schedules/") {
+ guard let doc = try? DocumentCoder.decode(ScheduleDocument.self, from: data), doc.isReadable else { continue }
+ if tombstoned.contains(doc.id) { try? await store.remove(at: path); continue }
+ liveScheduleIDs.insert(doc.id)
+ if supersededByPendingWrite(id: doc.id, fileTimestamp: doc.updatedAt) { continue }
+ CacheMapper.upsertSchedule(doc, relativePath: path, into: context)
}
}
@@ -700,8 +759,8 @@ final class SyncEngine {
// right now; pruning would make an eviction look like a deletion), and
// never for an id with a queued-but-unwritten write (its file simply
// hasn't landed yet; pruning would evaporate the pending edit's mirror).
- if let splits = try? context.fetch(FetchDescriptor()) {
- for s in splits where !liveSplitIDs.contains(s.id)
+ if let routines = try? context.fetch(FetchDescriptor()) {
+ for s in routines where !liveRoutineIDs.contains(s.id)
&& !unreadablePaths.contains(s.jsonRelativePath)
&& backlog.pendingWrite(for: s.id) == nil {
context.delete(s)
@@ -714,6 +773,13 @@ final class SyncEngine {
context.delete(w)
}
}
+ if let schedules = try? context.fetch(FetchDescriptor()) {
+ for s in schedules where !liveScheduleIDs.contains(s.id)
+ && !unreadablePaths.contains(s.jsonRelativePath)
+ && backlog.pendingWrite(for: s.id) == nil {
+ context.delete(s)
+ }
+ }
do {
try context.save()
// A fully clean pass supersedes any earlier failure; a pass with
@@ -728,8 +794,9 @@ final class SyncEngine {
// MARK: - Cache deletes
private func deleteCachedEntity(id: String) {
- if let s = CacheMapper.fetchSplit(id: id, in: context) { context.delete(s) }
+ if let s = CacheMapper.fetchRoutine(id: id, in: context) { context.delete(s) }
if let w = CacheMapper.fetchWorkout(id: id, in: context) { context.delete(w) }
+ if let sc = CacheMapper.fetchSchedule(id: id, in: context) { context.delete(sc) }
}
/// Observer `.removed` handling. Skips entities with a queued-but-unwritten
@@ -737,12 +804,15 @@ final class SyncEngine {
/// raced the rewrite), and the pending edit's mirror must survive until the
/// drain delivers it.
private func deleteCachedEntity(jsonRelativePath path: String) {
- if let splits = try? context.fetch(FetchDescriptor(predicate: #Predicate { $0.jsonRelativePath == path })) {
- splits.filter { backlog.pendingWrite(for: $0.id) == nil }.forEach(context.delete)
+ if let routines = try? context.fetch(FetchDescriptor(predicate: #Predicate { $0.jsonRelativePath == path })) {
+ routines.filter { backlog.pendingWrite(for: $0.id) == nil }.forEach(context.delete)
}
if let workouts = try? context.fetch(FetchDescriptor(predicate: #Predicate { $0.jsonRelativePath == path })) {
workouts.filter { backlog.pendingWrite(for: $0.id) == nil }.forEach(context.delete)
}
+ if let schedules = try? context.fetch(FetchDescriptor(predicate: #Predicate { $0.jsonRelativePath == path })) {
+ schedules.filter { backlog.pendingWrite(for: $0.id) == nil }.forEach(context.delete)
+ }
}
private func idFromStubPath(_ path: String) -> String {
@@ -804,7 +874,7 @@ final class SyncEngine {
// Verbatim bundle bytes: byte-identical files across devices make a
// same-path conflict (two devices seeding at once) semantically empty.
try await store.writeData(seed.data, to: seed.doc.relativePath)
- CacheMapper.upsertSplit(seed.doc, relativePath: seed.doc.relativePath, into: context)
+ CacheMapper.upsertRoutine(seed.doc, relativePath: seed.doc.relativePath, into: context)
} catch {
report("Failed to seed \(seed.doc.name)", error)
}
@@ -837,7 +907,7 @@ final class SyncEngine {
/// • live file present, older revision → overwrite with bundle bytes (upgrade)
/// • live file present, newer app version → skip (quarantine — never downgrade)
/// • no live file, stub present → skip (user deleted it; veto stands)
- /// • no live file, no stub, name in use → skip (don't duplicate a same-name split)
+ /// • no live file, no stub, name in use → skip (don't duplicate a same-name routine)
/// • no live file, no stub, name free → write bundle bytes
///
/// Safe even on a stale metadata index: writing bundle bytes another device also
@@ -848,17 +918,17 @@ final class SyncEngine {
let livePaths = Set(await store.list().filter { !$0.hasPrefix("Stubs/") })
let stubIDs = await tombstones.listStubIDs()
- let liveSplitNames = Set(((try? context.fetch(FetchDescriptor())) ?? []).map(\.name))
+ let liveRoutineNames = Set(((try? context.fetch(FetchDescriptor())) ?? []).map(\.name))
var didChange = false
for seed in SeedLibrary.seeds {
// Decode the live file at the seed's fixed path, if one exists. A file that
// exists but can't be read right now (evicted + download timed out) is left
// untouched — a later connect retries it.
- var liveDoc: SplitDocument?
+ var liveDoc: RoutineDocument?
if livePaths.contains(seed.doc.relativePath) {
guard let data = try? await store.readData(from: seed.doc.relativePath),
- let decoded = try? DocumentCoder.decode(SplitDocument.self, from: data) else {
+ let decoded = try? DocumentCoder.decode(RoutineDocument.self, from: data) else {
continue
}
liveDoc = decoded
@@ -869,7 +939,7 @@ final class SyncEngine {
hasStub: stubIDs.contains(seed.id)
)
- switch SeedReconcilePlanner.decision(for: input, liveSplitNames: liveSplitNames) {
+ switch SeedReconcilePlanner.decision(for: input, liveRoutineNames: liveRoutineNames) {
case .skip:
continue
case .upgrade:
@@ -880,7 +950,7 @@ final class SyncEngine {
// rewriting the seed bytes here would transiently resurrect the deleted
// seed alongside the user's clone. The stub now present vetoes that.
// (No name-collision guard here: on a legitimate upgrade the seed itself
- // is a live split by that name, so a name check would block every upgrade.)
+ // is a live routine by that name, so a name check would block every upgrade.)
if await tombstones.stubExists(id: seed.id) { continue }
// Overwriting an existing seed-path file with canonical bundle bytes is
// otherwise always safe (the file can only ever be an older seed revision).
@@ -889,7 +959,7 @@ final class SyncEngine {
// Re-check the veto and name guards against fresh state — the metadata
// index may have moved since the batch was gathered.
if await tombstones.stubExists(id: seed.id) { continue }
- let names = Set(((try? context.fetch(FetchDescriptor())) ?? []).map(\.name))
+ let names = Set(((try? context.fetch(FetchDescriptor())) ?? []).map(\.name))
if names.contains(seed.doc.name) { continue }
if await writeSeedBytes(seed) { didChange = true }
}
@@ -902,7 +972,7 @@ final class SyncEngine {
}
/// Force-restore the starter library on explicit user request ("Restore Starter
- /// Splits"). For each seed with no live file and no live same-name split, lift its
+ /// Routines"). For each seed with no live file and no live same-name routine, lift its
/// veto stub if present — the ONE place the forever-veto is deliberately lifted —
/// and write the CURRENT bundle bytes (not the stub's old contents; the bundle is
/// the canonical restore source). Seeds whose live file already exists are left for
@@ -915,11 +985,11 @@ final class SyncEngine {
var restored = 0
for seed in SeedLibrary.seeds {
- let liveNames = Set(((try? context.fetch(FetchDescriptor())) ?? []).map(\.name))
+ let liveNames = Set(((try? context.fetch(FetchDescriptor())) ?? []).map(\.name))
guard SeedReconcilePlanner.shouldRestore(
hasLiveFile: livePaths.contains(seed.doc.relativePath),
seedName: seed.doc.name,
- liveSplitNames: liveNames
+ liveRoutineNames: liveNames
) else { continue }
do {
@@ -929,11 +999,11 @@ final class SyncEngine {
try await tombstones.removeStub(at: "\(seed.id).json")
}
try await store.writeData(seed.data, to: seed.doc.relativePath)
- CacheMapper.upsertSplit(seed.doc, relativePath: seed.doc.relativePath, into: context)
+ CacheMapper.upsertRoutine(seed.doc, relativePath: seed.doc.relativePath, into: context)
restored += 1
lastSyncError = nil
} catch {
- report("Failed to restore starter split \(seed.doc.name)", error)
+ report("Failed to restore starter routine \(seed.doc.name)", error)
}
}
@@ -951,11 +1021,11 @@ final class SyncEngine {
guard let store else { return false }
do {
try await store.writeData(seed.data, to: seed.doc.relativePath)
- CacheMapper.upsertSplit(seed.doc, relativePath: seed.doc.relativePath, into: context)
+ CacheMapper.upsertRoutine(seed.doc, relativePath: seed.doc.relativePath, into: context)
lastSyncError = nil
return true
} catch {
- report("Failed to reconcile starter split \(seed.doc.name)", error)
+ report("Failed to reconcile starter routine \(seed.doc.name)", error)
return false
}
}
@@ -968,18 +1038,18 @@ final class SyncEngine {
}
/// Scans every live (non-stub) document and builds a plan for exact-content
- /// duplicate splits/workouts (see `DuplicateCleanupPlanner`). Read-only — no
+ /// duplicate routines/workouts (see `DuplicateCleanupPlanner`). Read-only — no
/// files are touched. Fails closed: if ANY file can't be read or decoded, no
- /// plan is produced, because an unreadable workout could reference any split
- /// and silently proceeding could misjudge that split as unreferenced.
+ /// plan is produced, because an unreadable workout or schedule could reference
+ /// any routine and silently proceeding could misjudge it as unreferenced.
func scanForDuplicates() async throws -> DuplicateCleanupPlan {
guard let store else { throw DuplicateScanError.notConnected }
let paths = await store.list().filter { !$0.hasPrefix("Stubs/") }
- var splits: [SplitDocument] = []
+ var routines: [RoutineDocument] = []
var workouts: [WorkoutDocument] = []
- var referencedSplitIDs = Set()
+ var referencedRoutineIDs = Set()
var failedPaths: [String] = []
for path in paths {
@@ -992,12 +1062,13 @@ final class SyncEngine {
continue
}
+ // PINNED: routine documents live under "Splits/" on disk (see importFile).
if path.hasPrefix("Splits/") {
do {
- let doc = try DocumentCoder.decode(SplitDocument.self, from: data)
- // Quarantined (written by a newer app version) splits are never
+ let doc = try DocumentCoder.decode(RoutineDocument.self, from: data)
+ // Quarantined (written by a newer app version) routines are never
// judged as duplicates or deleted.
- if doc.isReadable { splits.append(doc) }
+ if doc.isReadable { routines.append(doc) }
} catch {
log.error("scanForDuplicates: decode failed for \(path, privacy: .public): \(error)")
failedPaths.append(path)
@@ -1005,17 +1076,29 @@ final class SyncEngine {
} else if path.hasPrefix("Workouts/") {
do {
let doc = try DocumentCoder.decode(WorkoutDocument.self, from: data)
- // A quarantined workout's splitID still protects that split from
+ // A quarantined workout's routineID still protects that routine from
// deletion even though the workout itself is excluded below.
- if let splitID = doc.splitID {
- referencedSplitIDs.insert(splitID)
- referencedSplitIDs.insert(currentSplitID(for: splitID))
+ if let routineID = doc.routineID {
+ referencedRoutineIDs.insert(routineID)
+ referencedRoutineIDs.insert(currentRoutineID(for: routineID))
}
if doc.isReadable { workouts.append(doc) }
} catch {
log.error("scanForDuplicates: decode failed for \(path, privacy: .public): \(error)")
failedPaths.append(path)
}
+ } else if path.hasPrefix("Schedules/") {
+ do {
+ let doc = try DocumentCoder.decode(ScheduleDocument.self, from: data)
+ // Schedules are never duplicate candidates themselves, but the
+ // routine a schedule (even a quarantined one) points at must
+ // survive cleanup, or the schedule would dangle.
+ referencedRoutineIDs.insert(doc.routineID)
+ referencedRoutineIDs.insert(currentRoutineID(for: doc.routineID))
+ } catch {
+ log.error("scanForDuplicates: decode failed for \(path, privacy: .public): \(error)")
+ failedPaths.append(path)
+ }
}
}
@@ -1023,12 +1106,12 @@ final class SyncEngine {
throw DuplicateScanError.unreadableFiles(failedPaths.sorted())
}
- referencedSplitIDs.formUnion(cloneRedirects.values)
+ referencedRoutineIDs.formUnion(cloneRedirects.values)
return DuplicateCleanupPlanner.plan(
- splits: splits,
+ routines: routines,
workouts: workouts,
- referencedSplitIDs: referencedSplitIDs,
+ referencedRoutineIDs: referencedRoutineIDs,
isSeed: SeedLibrary.isSeed(id:)
)
}
@@ -1037,32 +1120,36 @@ final class SyncEngine {
/// immediately beforehand against the *current* cache state — a watch push or
/// another device's write can land between scan and delete — so nothing
/// referenced or active is ever removed even if the plan is a few seconds stale.
- func performCleanup(_ plan: DuplicateCleanupPlan) async -> (splitsDeleted: Int, workoutsDeleted: Int, skipped: Int) {
- var splitsDeleted = 0
+ func performCleanup(_ plan: DuplicateCleanupPlan) async -> (routinesDeleted: Int, workoutsDeleted: Int, skipped: Int) {
+ var routinesDeleted = 0
var workoutsDeleted = 0
var skipped = 0
- for group in plan.splitGroups {
+ for group in plan.routineGroups {
for doc in group.delete {
- let splitID = doc.id
- if SeedLibrary.isSeed(id: splitID) {
+ let routineID = doc.id
+ if SeedLibrary.isSeed(id: routineID) {
skipped += 1
continue
}
let referencingWorkouts = (try? context.fetch(
- FetchDescriptor(predicate: #Predicate { $0.splitID == splitID })
+ FetchDescriptor(predicate: #Predicate { $0.routineID == routineID })
)) ?? []
- guard referencingWorkouts.isEmpty else {
+ let referencingSchedules = (try? context.fetch(
+ FetchDescriptor(predicate: #Predicate { $0.routineID == routineID })
+ )) ?? []
+ guard referencingWorkouts.isEmpty, referencingSchedules.isEmpty else {
skipped += 1
continue
}
- if let cached = CacheMapper.fetchSplit(id: splitID, in: context) {
- await softDelete(id: splitID, kind: "split", livePath: cached.jsonRelativePath)
+ // PINNED: tombstone kind stays "split" — existing stubs on disk carry it.
+ if let cached = CacheMapper.fetchRoutine(id: routineID, in: context) {
+ await softDelete(id: routineID, kind: "split", livePath: cached.jsonRelativePath)
} else {
- await softDelete(id: splitID, kind: "split", livePath: doc.relativePath)
+ await softDelete(id: routineID, kind: "split", livePath: doc.relativePath)
}
- deleteCachedEntity(id: splitID)
- splitsDeleted += 1
+ deleteCachedEntity(id: routineID)
+ routinesDeleted += 1
}
}
@@ -1081,7 +1168,7 @@ final class SyncEngine {
}
saveCacheAndNotify()
- return (splitsDeleted, workoutsDeleted, skipped)
+ return (routinesDeleted, workoutsDeleted, skipped)
}
// MARK: - Error Reporting
diff --git a/Workouts/Sync/WorkoutMergePlanner.swift b/Workouts/Sync/WorkoutMergePlanner.swift
index 4042c1b..b4702a9 100644
--- a/Workouts/Sync/WorkoutMergePlanner.swift
+++ b/Workouts/Sync/WorkoutMergePlanner.swift
@@ -64,7 +64,7 @@ enum WorkoutMergePlanner {
// 4. Prune tombstones past the grace period.
tombstones = tombstones.filter { now.timeIntervalSince($0.value) < tombstoneGrace }
- // 5. Base scalars (splitID/name, start, metrics, …) on the newer document overall — it
+ // 5. Base scalars (routineID/name, start, metrics, …) on the newer document overall — it
// carries a just-attached metrics summary or a rename — then override the reconciled
// parts. Ties prefer the cached copy (the phone is the sole writer). Sort is
// id-tie-broken so the result is deterministic even when two logs share an `order`.
diff --git a/Workouts/Sync/WriteBacklog.swift b/Workouts/Sync/WriteBacklog.swift
index 8602319..1bc6d75 100644
--- a/Workouts/Sync/WriteBacklog.swift
+++ b/Workouts/Sync/WriteBacklog.swift
@@ -13,9 +13,21 @@ import Foundation
/// supersede checks key on it, newer wins.
struct PendingWrite: Codable, Sendable {
enum Payload: Codable, Sendable {
- case split(SplitDocument)
+ case routine(RoutineDocument)
case workout(WorkoutDocument)
+ case schedule(ScheduleDocument)
case delete(id: String, kind: String, livePath: String)
+
+ // PINNED KEY: the case was renamed `.split` → `.routine`, but its persisted
+ // JSON key stays `"split"` so a `PendingWrites.json` sidecar written by a
+ // previous run still decodes. `workout` / `schedule` / `delete` keep their
+ // default keys.
+ enum CodingKeys: String, CodingKey {
+ case routine = "split"
+ case workout
+ case schedule
+ case delete
+ }
}
var payload: Payload
@@ -36,8 +48,9 @@ struct PendingWrite: Codable, Sendable {
var documentID: String {
switch payload {
- case .split(let doc): doc.id
+ case .routine(let doc): doc.id
case .workout(let doc): doc.id
+ case .schedule(let doc): doc.id
case .delete(let id, _, _): id
}
}
@@ -45,8 +58,9 @@ struct PendingWrite: Codable, Sendable {
/// Where a successful write lands; nil for deletes (they only remove).
var targetPath: String? {
switch payload {
- case .split(let doc): doc.relativePath
+ case .routine(let doc): doc.relativePath
case .workout(let doc): doc.relativePath
+ case .schedule(let doc): doc.relativePath
case .delete: nil
}
}
diff --git a/Workouts/Views/Common/RoutinePickerSheet.swift b/Workouts/Views/Common/RoutinePickerSheet.swift
new file mode 100644
index 0000000..92cb1db
--- /dev/null
+++ b/Workouts/Views/Common/RoutinePickerSheet.swift
@@ -0,0 +1,176 @@
+//
+// RoutinePickerSheet.swift
+// Workouts
+//
+// Copyright 2026 Rouslan Zenetl. All Rights Reserved.
+//
+
+import IndieSync
+import SwiftUI
+import SwiftData
+
+// MARK: - Routine Picker Sheet
+
+struct RoutinePickerSheet: View {
+ @Environment(SyncEngine.self) private var sync
+ @Environment(AppServices.self) private var services
+ @Environment(\.dismiss) private var dismiss
+
+ /// Called with the new workout's id right after it's saved, so the presenting
+ /// screen can navigate into it once the cache catches up.
+ var onStarted: (String) -> Void = { _ in }
+
+ @Query(sort: [SortDescriptor(\Routine.order), SortDescriptor(\Routine.name)])
+ private var routines: [Routine]
+
+ @Query(sort: \Workout.start, order: .reverse)
+ private var workouts: [Workout]
+
+ /// Set when the user picks a routine while other workouts are still going — drives the
+ /// "end the current one(s) or run in parallel?" prompt.
+ @State private var routineAwaitingConfirmation: Routine?
+
+ private var activeWorkouts: [Workout] {
+ workouts.filter { $0.status == .inProgress || $0.status == .notStarted }
+ }
+
+ /// The routines most recently trained, newest first, each with the start date
+ /// of its latest workout — derived from the workout log (`workouts` is
+ /// already sorted by start descending). Each workout's `routineID` follows the
+ /// seed clone-on-edit redirect so a workout started from a since-forked seed
+ /// still credits the live clone. Deduped, capped, and limited to routines that
+ /// still exist.
+ private var recentRoutines: [(routine: Routine, lastStart: Date)] {
+ var seen = Set()
+ var recents: [(routine: Routine, lastStart: Date)] = []
+ for workout in workouts {
+ guard let routineID = workout.routineID else { continue }
+ let liveID = sync.currentRoutineID(for: routineID)
+ guard !seen.contains(liveID) else { continue }
+ seen.insert(liveID)
+ if let routine = routines.first(where: { $0.id == liveID }) {
+ recents.append((routine, workout.start))
+ if recents.count == 3 { break }
+ }
+ }
+ return recents
+ }
+
+ /// One selectable routine row, shared by the Recent and All Routines sections.
+ /// Recent rows pass `lastTrained` to show how long ago the routine was run.
+ private func routineRow(_ routine: Routine, lastTrained: Date? = nil) -> some View {
+ Button {
+ confirmAndStart(with: routine)
+ } label: {
+ HStack {
+ Image(systemName: routine.systemImage)
+ .foregroundColor(Color.color(from: routine.color))
+ .frame(width: 28)
+ VStack(alignment: .leading, spacing: 2) {
+ Text(routine.name)
+ if let lastTrained {
+ Text(lastTrained.daysAgoLabel())
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+ }
+ Spacer()
+ Text("\(routine.exercisesArray.count) exercises")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+ }
+ }
+
+ var body: some View {
+ NavigationStack {
+ List {
+ if !recentRoutines.isEmpty {
+ Section("Recent") {
+ ForEach(recentRoutines, id: \.routine.id) { recent in
+ routineRow(recent.routine, lastTrained: recent.lastStart)
+ }
+ }
+ }
+
+ Section(recentRoutines.isEmpty ? "" : "All Routines") {
+ ForEach(routines) { routine in
+ routineRow(routine)
+ }
+ }
+ }
+ .navigationTitle("Select a Routine")
+ .toolbar {
+ ToolbarItem(placement: .navigationBarLeading) {
+ Button("Cancel") {
+ dismiss()
+ }
+ }
+ }
+ .confirmationDialog(
+ activePromptTitle,
+ isPresented: Binding(
+ get: { routineAwaitingConfirmation != nil },
+ set: { if !$0 { routineAwaitingConfirmation = nil } }
+ ),
+ titleVisibility: .visible,
+ presenting: routineAwaitingConfirmation
+ ) { routine in
+ Button("End Current & Start New") { endActiveThenStart(with: routine) }
+ Button("Start in Parallel") { start(with: routine) }
+ Button("Cancel", role: .cancel) { routineAwaitingConfirmation = nil }
+ } message: { _ in
+ Text(activePromptMessage)
+ }
+ }
+ }
+
+ private var activePromptTitle: String {
+ activeWorkouts.count == 1 ? "Workout in Progress" : "\(activeWorkouts.count) Workouts in Progress"
+ }
+
+ private var activePromptMessage: String {
+ let n = activeWorkouts.count
+ let those = n == 1 ? "it" : "them"
+ return "You already have \(n == 1 ? "a workout" : "\(n) workouts") going. End \(those) first, or run this one alongside."
+ }
+
+ /// Prompt before starting if other workouts are still going; otherwise start straight away.
+ private func confirmAndStart(with routine: Routine) {
+ if activeWorkouts.isEmpty {
+ start(with: routine)
+ } else {
+ routineAwaitingConfirmation = routine
+ }
+ }
+
+ /// End every in-flight workout (keeping its progress), then start the picked routine.
+ private func endActiveThenStart(with routine: Routine) {
+ // A pristine draft (no exercise ever started) has nothing worth keeping —
+ // "ending" it would mint a junk all-skipped completed workout, so it's
+ // discarded instead, same rule as backing out of one.
+ let toDiscard = activeWorkouts.filter(\.isPristineDraft)
+ let toEnd = activeWorkouts.filter { !$0.isPristineDraft }.map { WorkoutDocument(from: $0) }
+ routineAwaitingConfirmation = nil
+ Task {
+ for workout in toDiscard {
+ await sync.delete(workout: workout)
+ }
+ for var doc in toEnd {
+ doc.endKeepingProgress()
+ await sync.save(workout: doc)
+ }
+ }
+ start(with: routine)
+ }
+
+ private func start(with routine: Routine) {
+ // Hand the id back after the save so the presenter can poll the cache and
+ // navigate into the run — mirroring the exercise-list start path.
+ Task {
+ let id = await WorkoutStarter.start(routine: routine, sync: sync)
+ onStarted(id)
+ }
+ dismiss()
+ }
+}
diff --git a/Workouts/Views/Common/StartedWorkoutNavigator.swift b/Workouts/Views/Common/StartedWorkoutNavigator.swift
new file mode 100644
index 0000000..e07d5b6
--- /dev/null
+++ b/Workouts/Views/Common/StartedWorkoutNavigator.swift
@@ -0,0 +1,91 @@
+//
+// StartedWorkoutNavigator.swift
+// Workouts
+//
+// Copyright 2026 Rouslan Zenetl. All Rights Reserved.
+//
+
+import SwiftUI
+import SwiftData
+
+// MARK: - Started-Workout Navigation
+
+/// Drives a programmatic push into a freshly-started workout's log screen. Both start
+/// paths — the routine picker sheet and the Today board — mint a `WorkoutDocument`,
+/// write it, then hand its id here; the workout row only materializes once the
+/// file→observer→cache loop round-trips, so we poll the cache for the entity and push it
+/// the moment it lands.
+private struct StartedWorkoutNavigator: ViewModifier {
+ /// The pushed state is a plain value wrapping the run's id — never a retained
+ /// `@Model`. This screen stays pushed for the whole workout, and the run's entity
+ /// can be deleted and re-imported underneath it (observer remove/re-add churn, a
+ /// remote delete); a `@Model` held in `@State` would be *invalidated* by then —
+ /// deleted-and-saved models read `isDeleted == false` yet trap on any property
+ /// access (the TestFlight 2.3 (125) "crashed when watch ended an exercise" report).
+ private struct RunRoute: Identifiable, Hashable { let id: String }
+
+ @Environment(\.modelContext) private var modelContext
+ @Environment(SyncEngine.self) private var sync
+ @Binding var pendingWorkoutID: String?
+ @State private var resolvedRun: RunRoute?
+
+ func body(content: Content) -> some View {
+ content
+ .navigationDestination(item: $resolvedRun) { route in
+ // Re-fetch on every build so the destination always maps a live
+ // entity; a re-imported run resolves to its fresh instance.
+ if let workout = CacheMapper.fetchWorkout(id: route.id, in: modelContext) {
+ WorkoutLogListView(workout: workout)
+ } else {
+ ContentUnavailableView(
+ "Workout Unavailable",
+ systemImage: "xmark.circle",
+ description: Text("This workout is no longer on this device."))
+ }
+ }
+ .onChange(of: pendingWorkoutID) { _, id in
+ guard let id else { return }
+ pollForWorkout(id: id)
+ }
+ // Backing out of a run that never started an exercise discards it —
+ // tapping into a routine to look around must not persist a phantom
+ // workout. Anything with real progress is kept.
+ .onChange(of: resolvedRun) { old, new in
+ guard let old, new == nil else { return }
+ discardIfPristine(id: old.id)
+ }
+ }
+
+ /// Delete the just-popped workout if it's still an untouched draft. Fetches
+ /// fresh by id — never a retained entity — and re-checks liveness.
+ private func discardIfPristine(id: String) {
+ guard let workout = CacheMapper.fetchWorkout(id: id, in: modelContext),
+ !workout.isDeleted, workout.isPristineDraft else { return }
+ Task { await sync.delete(workout: workout) }
+ }
+
+ /// Poll for the entity after we write the document (the file→observer→cache loop
+ /// typically completes in well under a second). Clear the pending id once it
+ /// resolves, or silently after ~3 s if it never arrives.
+ private func pollForWorkout(id: String) {
+ Task {
+ for _ in 0..<20 {
+ try? await Task.sleep(for: .milliseconds(150))
+ if CacheMapper.fetchWorkout(id: id, in: modelContext) != nil {
+ resolvedRun = RunRoute(id: id)
+ pendingWorkoutID = nil
+ return
+ }
+ }
+ pendingWorkoutID = nil
+ }
+ }
+}
+
+extension View {
+ /// Navigate into a workout's log screen once it appears in the cache after being
+ /// started. Bind to the state you set to the new workout's id right after saving it.
+ func navigatesToStartedWorkout(pendingWorkoutID: Binding) -> some View {
+ modifier(StartedWorkoutNavigator(pendingWorkoutID: pendingWorkoutID))
+ }
+}
diff --git a/Workouts/Views/Common/WorkoutStarter.swift b/Workouts/Views/Common/WorkoutStarter.swift
new file mode 100644
index 0000000..68e8642
--- /dev/null
+++ b/Workouts/Views/Common/WorkoutStarter.swift
@@ -0,0 +1,45 @@
+//
+// WorkoutStarter.swift
+// Workouts
+//
+// Copyright 2026 Rouslan Zenetl. All Rights Reserved.
+//
+
+import IndieSync
+import Foundation
+
+/// The one place a workout is created from a routine. Both start paths — the routine
+/// picker sheet and the Today board — call this so the plan-time log snapshot and
+/// the returned id stay identical per site.
+@MainActor
+enum WorkoutStarter {
+ /// Builds and saves a fresh workout from a routine (plan-time log snapshot per
+ /// exercise) and returns the new workout's id so the caller can navigate once the
+ /// cache catches up. Deliberately does NOT touch the watch: the wrist session
+ /// launches only when the first exercise actually starts (see
+ /// `SyncEngine.onWorkoutBecameActive`), so a peek-then-back never spins one up.
+ static func start(routine: Routine, sync: SyncEngine) async -> String {
+ let startDate = Date()
+ let logs = routine.exercisesArray.enumerated().map { index, exercise in
+ WorkoutLogDocument(planFrom: ExerciseDocument(from: exercise), order: index, date: startDate)
+ }
+
+ // A freshly started workout has no `end` — only completion stamps it.
+ let doc = WorkoutDocument(
+ schemaVersion: WorkoutDocument.currentSchemaVersion,
+ id: ULID.make(),
+ routineID: routine.id,
+ routineName: routine.name,
+ start: startDate,
+ end: nil,
+ status: WorkoutStatus.notStarted.rawValue,
+ createdAt: startDate,
+ updatedAt: startDate,
+ logs: logs,
+ restSeconds: routine.restSeconds, autoAdvance: routine.autoAdvance
+ )
+
+ await sync.save(workout: doc)
+ return doc.id
+ }
+}
diff --git a/Workouts/Views/Exercises/ExerciseAddEditView.swift b/Workouts/Views/Exercises/ExerciseAddEditView.swift
index ffaa36b..8d100d9 100644
--- a/Workouts/Views/Exercises/ExerciseAddEditView.swift
+++ b/Workouts/Views/Exercises/ExerciseAddEditView.swift
@@ -18,8 +18,8 @@ struct ExerciseAddEditView: View {
// The exercise entity provides initial values (read-only).
let exercise: Exercise
- // The parent split is needed to rebuild and save the SplitDocument.
- let split: Split
+ // The parent routine is needed to rebuild and save the RoutineDocument.
+ let routine: Routine
// Local editable state
@State private var exerciseName: String
@@ -37,9 +37,9 @@ struct ExerciseAddEditView: View {
@State private var machineSettings: [MachineSetting]
@AppStorage("weightUnit") private var weightUnit: WeightUnit = .lb
- init(exercise: Exercise, split: Split) {
+ init(exercise: Exercise, routine: Routine) {
self.exercise = exercise
- self.split = split
+ self.routine = routine
// The tens/ones pickers step whole values; a fractional stored weight
// shows (and saves back) its whole part until UX #3's UI lands.
@@ -197,7 +197,7 @@ struct ExerciseAddEditView: View {
let newWeight = weightTens + weightOnes
let durationSecs = minutes * 60 + seconds
- var doc = SplitDocument(from: split)
+ var doc = RoutineDocument(from: routine)
if let idx = doc.exercises.firstIndex(where: { $0.id == exercise.id }) {
doc.exercises[idx].name = exerciseName
doc.exercises[idx].sets = sets
@@ -209,6 +209,6 @@ struct ExerciseAddEditView: View {
doc.exercises[idx].machineSettings = machineEnabled ? machineSettings : nil
}
doc.updatedAt = Date()
- Task { await sync.save(split: doc) }
+ Task { await sync.save(routine: doc) }
}
}
diff --git a/Workouts/Views/Exercises/ExerciseLibraryView.swift b/Workouts/Views/Exercises/ExerciseLibraryView.swift
index 9243e00..449e1b4 100644
--- a/Workouts/Views/Exercises/ExerciseLibraryView.swift
+++ b/Workouts/Views/Exercises/ExerciseLibraryView.swift
@@ -32,11 +32,11 @@ struct ExerciseLibraryDetailView: View {
let exerciseName: String
- /// This exercise wherever it appears in the user's splits — the source of the
+ /// This exercise wherever it appears in the user's routines — the source of the
/// recorded machine settings.
@Query private var exercises: [Exercise]
- /// Presents the settings editor; carries the splits the edit writes back to.
+ /// Presents the settings editor; carries the routines the edit writes back to.
@State private var settingsEdit: SettingsEditRoute?
/// True while content extends below the scroll area — drives the bottom fade
@@ -63,31 +63,31 @@ struct ExerciseLibraryDetailView: View {
}
/// Distinct recorded machine-settings lists for this exercise. Usually one; when
- /// splits disagree, each distinct list keeps its split's name as a label. Each
- /// group carries every split showing that list, so an edit updates them all and
+ /// routines disagree, each distinct list keeps its routine's name as a label. Each
+ /// group carries every routine showing that list, so an edit updates them all and
/// they stay deduped.
private var settingsGroups: [SettingsGroup] {
var groups: [SettingsGroup] = []
for exercise in exercises {
guard let settings = exercise.machineSettings, !settings.isEmpty else { continue }
if let i = groups.firstIndex(where: { $0.settings == settings }) {
- if let id = exercise.split?.id { groups[i].splitIDs.append(id) }
+ if let id = exercise.routine?.id { groups[i].routineIDs.append(id) }
continue
}
groups.append(SettingsGroup(
- label: exercise.split?.name,
+ label: exercise.routine?.name,
settings: settings,
- splitIDs: exercise.split.map { [$0.id] } ?? []
+ routineIDs: exercise.routine.map { [$0.id] } ?? []
))
}
if groups.count == 1 { groups[0].label = nil }
return groups
}
- /// Every split containing this exercise — the write targets when settings are
+ /// Every routine containing this exercise — the write targets when settings are
/// recorded here for the first time.
- private var containingSplitIDs: [String] {
- exercises.compactMap { $0.split?.id }
+ private var containingRoutineIDs: [String] {
+ exercises.compactMap { $0.routine?.id }
}
var body: some View {
@@ -103,10 +103,10 @@ struct ExerciseLibraryDetailView: View {
MachineSettingsCard(
groups: [],
onEdit: edit(group:),
- // Recording needs a split exercise to write to — without
+ // Recording needs a routine exercise to write to — without
// one the card explains instead of offering a dead button.
- onAdd: containingSplitIDs.isEmpty ? nil : {
- settingsEdit = SettingsEditRoute(initial: [], targetSplitIDs: containingSplitIDs)
+ onAdd: containingRoutineIDs.isEmpty ? nil : {
+ settingsEdit = SettingsEditRoute(initial: [], targetRoutineIDs: containingRoutineIDs)
}
)
}
@@ -165,10 +165,10 @@ struct ExerciseLibraryDetailView: View {
.onDisappear { services.speechAnnouncer.stop() }
.sheet(item: $settingsEdit) { route in
MachineSettingsSheet(exerciseName: exerciseName, initialSettings: route.initial) { updated in
- let targets = route.targetSplitIDs
+ let targets = route.targetRoutineIDs
Task {
- for splitID in targets {
- await sync.writeBackMachineSettings(updated, exerciseName: exerciseName, splitID: splitID)
+ for routineID in targets {
+ await sync.writeBackMachineSettings(updated, exerciseName: exerciseName, routineID: routineID)
}
}
}
@@ -176,29 +176,29 @@ struct ExerciseLibraryDetailView: View {
}
private func edit(group: SettingsGroup) {
- settingsEdit = SettingsEditRoute(initial: group.settings, targetSplitIDs: group.splitIDs)
+ settingsEdit = SettingsEditRoute(initial: group.settings, targetRoutineIDs: group.routineIDs)
}
}
-/// One distinct recorded settings list and the splits it came from (the edit targets).
+/// One distinct recorded settings list and the routines it came from (the edit targets).
fileprivate struct SettingsGroup {
var label: String?
var settings: [MachineSetting]
- var splitIDs: [String]
+ var routineIDs: [String]
}
-/// Sheet route: the settings to seed the editor with and the splits a save writes to.
+/// Sheet route: the settings to seed the editor with and the routines a save writes to.
private struct SettingsEditRoute: Identifiable {
let id = UUID()
var initial: [MachineSetting]
- var targetSplitIDs: [String]
+ var targetRoutineIDs: [String]
}
/// The user's recorded machine comfort settings, set apart from the reference text
-/// as a card: name–value rows, grouped per split when the recorded lists differ.
+/// as a card: name–value rows, grouped per routine when the recorded lists differ.
/// Empty `groups` renders the not-yet-recorded state (shown for machine-based
/// exercises so the feature is visible before first use), with an Add button when
-/// the exercise lives in at least one split (`onAdd` non-nil). A single group edits
+/// the exercise lives in at least one routine (`onAdd` non-nil). A single group edits
/// from the header; disagreeing groups edit per group.
private struct MachineSettingsCard: View {
let groups: [SettingsGroup]
@@ -222,7 +222,7 @@ private struct MachineSettingsCard: View {
if groups.isEmpty {
Text(onAdd != nil
? "None recorded yet — save the seat, pad, and pin positions once and they'll be here next time."
- : "None recorded yet. Add this exercise to a split, then record its settings here or from the workout screen.")
+ : "None recorded yet. Add this exercise to a routine, then record its settings here or from the workout screen.")
.font(.callout)
.foregroundStyle(.secondary)
}
diff --git a/Workouts/Views/Exercises/ExerciseListView.swift b/Workouts/Views/Exercises/ExerciseListView.swift
index 49b338f..d932d49 100644
--- a/Workouts/Views/Exercises/ExerciseListView.swift
+++ b/Workouts/Views/Exercises/ExerciseListView.swift
@@ -15,41 +15,41 @@ struct ExerciseListView: View {
@Environment(SyncEngine.self) private var sync
@Environment(\.dismiss) private var dismiss
- // Resolve the split by id, not a captured entity: editing a seed's exercise from
+ // Resolve the routine by id, not a captured entity: editing a seed's exercise from
// here clones the seed (new identity, old entity deleted), which would dangle a
- // stored `Split`. `currentSplitID` follows that swap.
- @State private var splitID: String
- @Query private var splits: [Split]
+ // stored `Routine`. `currentRoutineID` follows that swap.
+ @State private var routineID: String
+ @Query private var routines: [Routine]
@State private var showingAddSheet: Bool = false
@State private var itemToEdit: Exercise? = nil
@State private var itemToDelete: Exercise? = nil
@AppStorage("weightUnit") private var weightUnit: WeightUnit = .lb
- init(split: Split) {
- _splitID = State(initialValue: split.id)
+ init(routine: Routine) {
+ _routineID = State(initialValue: routine.id)
}
- private var split: Split? {
- let id = sync.currentSplitID(for: splitID)
- return splits.first { $0.id == id }
+ private var routine: Routine? {
+ let id = sync.currentRoutineID(for: routineID)
+ return routines.first { $0.id == id }
}
var body: some View {
Group {
- if let split {
- content(for: split)
+ if let routine {
+ content(for: routine)
} else {
- ContentUnavailableView("Split Unavailable", systemImage: "dumbbell")
+ ContentUnavailableView("Routine Unavailable", systemImage: "dumbbell")
.task { dismiss() }
}
}
}
@ViewBuilder
- private func content(for split: Split) -> some View {
+ private func content(for routine: Routine) -> some View {
Form {
- let sortedExercises = split.exercisesArray
+ let sortedExercises = routine.exercisesArray
if !sortedExercises.isEmpty {
ForEach(sortedExercises) { item in
@@ -86,14 +86,14 @@ struct ExerciseListView: View {
}
}
}
- .navigationTitle(split.name)
+ .navigationTitle(routine.name)
.sheet(isPresented: $showingAddSheet) {
ExercisePickerView(onExerciseSelected: { exerciseNames in
addExercises(names: exerciseNames)
}, allowMultiSelect: true)
}
.sheet(item: $itemToEdit) { item in
- ExerciseAddEditView(exercise: item, split: split)
+ ExerciseAddEditView(exercise: item, routine: routine)
}
.confirmationDialog(
"Delete Exercise?",
@@ -112,29 +112,29 @@ struct ExerciseListView: View {
itemToDelete = nil
}
} message: { item in
- Text("Remove \"\(item.name)\" from this split?")
+ Text("Remove \"\(item.name)\" from this routine?")
}
}
// MARK: - Helpers
private func moveExercises(from source: IndexSet, to destination: Int) {
- guard let split else { return }
- var exercises = split.exercisesArray
+ guard let routine else { return }
+ var exercises = routine.exercisesArray
exercises.move(fromOffsets: source, toOffset: destination)
- var doc = SplitDocument(from: split)
+ var doc = RoutineDocument(from: routine)
doc.exercises = exercises.enumerated().map { i, ex in
var ed = ExerciseDocument(from: ex)
ed.order = i
return ed
}
doc.updatedAt = Date()
- Task { await sync.save(split: doc) }
+ Task { await sync.save(routine: doc) }
}
private func addExercises(names: [String]) {
- guard let split else { return }
- var doc = SplitDocument(from: split)
+ guard let routine else { return }
+ var doc = RoutineDocument(from: routine)
let existingNames = Set(doc.exercises.map { $0.name })
let base = doc.exercises.count
let newDocs = names
@@ -154,17 +154,17 @@ struct ExerciseListView: View {
}
doc.exercises.append(contentsOf: newDocs)
doc.updatedAt = Date()
- Task { await sync.save(split: doc) }
+ Task { await sync.save(routine: doc) }
}
private func deleteExercise(_ exercise: Exercise) {
- guard let split else { return }
- var doc = SplitDocument(from: split)
+ guard let routine else { return }
+ var doc = RoutineDocument(from: routine)
doc.exercises.removeAll { $0.id == exercise.id }
for i in doc.exercises.indices {
doc.exercises[i].order = i
}
doc.updatedAt = Date()
- Task { await sync.save(split: doc) }
+ Task { await sync.save(routine: doc) }
}
}
diff --git a/Workouts/Views/Progress/AchievementsSection.swift b/Workouts/Views/Progress/AchievementsSection.swift
new file mode 100644
index 0000000..123bae7
--- /dev/null
+++ b/Workouts/Views/Progress/AchievementsSection.swift
@@ -0,0 +1,101 @@
+//
+// AchievementsSection.swift
+// Workouts
+//
+// Copyright 2026 Rouslan Zenetl. All Rights Reserved.
+//
+
+import SwiftUI
+
+/// The achievements badge grid. Every badge is a pure derivation of history
+/// (`ProgressPlanner.achievements`) — earned ones glow in their color, locked ones
+/// wear a progress ring showing how close they are. Tapping shows the how-to-earn
+/// detail.
+struct AchievementsSection: View {
+ let achievements: [Achievement]
+
+ @State private var selected: Achievement?
+
+ private var earnedCount: Int { achievements.filter(\.earned).count }
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 16) {
+ HStack {
+ Text("Achievements")
+ .font(.headline)
+ Spacer()
+ Text("\(earnedCount) of \(achievements.count)")
+ .font(.subheadline)
+ .foregroundStyle(.secondary)
+ .monospacedDigit()
+ }
+
+ LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 12), count: 4),
+ spacing: 16) {
+ ForEach(achievements) { achievement in
+ Button {
+ selected = achievement
+ } label: {
+ AchievementBadge(achievement: achievement)
+ }
+ .buttonStyle(.plain)
+ }
+ }
+ }
+ .progressCard()
+ .alert(
+ selected?.title ?? "",
+ isPresented: Binding(
+ get: { selected != nil },
+ set: { if !$0 { selected = nil } }
+ ),
+ presenting: selected
+ ) { _ in
+ Button("OK") { selected = nil }
+ } message: { achievement in
+ Text(achievement.earned
+ ? achievement.detail
+ : "\(achievement.detail) — \(Int((achievement.progress * 100).rounded()))% there.")
+ }
+ }
+}
+
+/// One badge: a tinted circle with the symbol, the title beneath. Locked badges are
+/// desaturated with a thin progress arc around the circle.
+struct AchievementBadge: View {
+ let achievement: Achievement
+
+ private var color: Color { Color.color(from: achievement.colorName) }
+
+ var body: some View {
+ VStack(spacing: 6) {
+ ZStack {
+ Circle()
+ .fill(achievement.earned ? color.opacity(0.18) : Color(.systemFill))
+ Image(systemName: achievement.systemImage)
+ .font(.title3)
+ .foregroundStyle(achievement.earned ? color : Color(.tertiaryLabel))
+ }
+ .frame(width: 52, height: 52)
+ .overlay {
+ if !achievement.earned, achievement.progress > 0 {
+ Circle()
+ .trim(from: 0, to: achievement.progress)
+ .stroke(color.opacity(0.6),
+ style: StrokeStyle(lineWidth: 3, lineCap: .round))
+ .rotationEffect(.degrees(-90))
+ }
+ }
+
+ Text(achievement.title)
+ .font(.caption2)
+ .multilineTextAlignment(.center)
+ .lineLimit(2, reservesSpace: true)
+ .foregroundStyle(achievement.earned ? .primary : .secondary)
+ }
+ .frame(maxWidth: .infinity)
+ .accessibilityElement(children: .ignore)
+ .accessibilityLabel(
+ "\(achievement.title), \(achievement.earned ? "earned" : "locked"): \(achievement.detail)")
+ }
+}
diff --git a/Workouts/Views/Progress/ExerciseTrendsView.swift b/Workouts/Views/Progress/ExerciseTrendsView.swift
new file mode 100644
index 0000000..d1218f3
--- /dev/null
+++ b/Workouts/Views/Progress/ExerciseTrendsView.swift
@@ -0,0 +1,144 @@
+//
+// ExerciseTrendsView.swift
+// Workouts
+//
+// Copyright 2026 Rouslan Zenetl. All Rights Reserved.
+//
+
+import SwiftUI
+import SwiftData
+
+/// Per-exercise drill-down: every weighted exercise with completed history, most
+/// recently trained first, each opening its weight-progression detail. This promotes
+/// the progression chart to a first-class Progress surface (it also remains on the
+/// library's reference pages).
+struct ExerciseTrendsListView: View {
+ @AppStorage("weightUnit") private var weightUnit: WeightUnit = .lb
+
+ /// Completed weighted logs, newest first.
+ @Query private var logs: [WorkoutLog]
+
+ init() {
+ let completedRaw = WorkoutStatus.completed.rawValue
+ let weightRaw = LoadType.weight.rawValue
+ _logs = Query(
+ filter: #Predicate {
+ $0.statusRaw == completedRaw && $0.loadType == weightRaw
+ },
+ sort: \WorkoutLog.date,
+ order: .reverse)
+ }
+
+ private struct ExerciseSummary: Identifiable {
+ var name: String
+ var sessions: Int
+ var best: Double
+ var last: Date
+
+ var id: String { name }
+ }
+
+ /// One row per exercise name: session count, best top-set weight, last trained.
+ /// `logs` is newest-first, so the first log seen per name fixes `last`.
+ private var summaries: [ExerciseSummary] {
+ var byName: [String: ExerciseSummary] = [:]
+ for log in logs {
+ let top = log.setEntries?.compactMap(\.weight).max() ?? log.weight
+ if var summary = byName[log.exerciseName] {
+ summary.sessions += 1
+ summary.best = max(summary.best, top)
+ byName[log.exerciseName] = summary
+ } else {
+ byName[log.exerciseName] = ExerciseSummary(
+ name: log.exerciseName, sessions: 1, best: top, last: log.date)
+ }
+ }
+ return byName.values.sorted { $0.last > $1.last }
+ }
+
+ var body: some View {
+ List(summaries) { summary in
+ NavigationLink {
+ ExerciseTrendDetailView(exerciseName: summary.name)
+ } label: {
+ HStack {
+ VStack(alignment: .leading, spacing: 2) {
+ Text(summary.name)
+ Text("\(summary.sessions) session\(summary.sessions == 1 ? "" : "s") · last \(summary.last.daysAgoLabel())")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+ Spacer()
+ Text(weightUnit.format(summary.best))
+ .font(.subheadline.weight(.semibold))
+ .monospacedDigit()
+ .foregroundStyle(.secondary)
+ }
+ }
+ }
+ .overlay {
+ if summaries.isEmpty {
+ ContentUnavailableView(
+ "No Weighted Exercises Yet",
+ systemImage: "dumbbell",
+ description: Text("Complete weighted sets and each exercise's progression shows up here."))
+ }
+ }
+ .navigationTitle("Exercise Trends")
+ }
+}
+
+/// One exercise's progression: headline stats over its completed history plus the
+/// weight-progression chart.
+struct ExerciseTrendDetailView: View {
+ let exerciseName: String
+
+ @AppStorage("weightUnit") private var weightUnit: WeightUnit = .lb
+
+ /// Completed logs for this exercise, oldest first (matching the chart's order).
+ @Query private var logs: [WorkoutLog]
+
+ init(exerciseName: String) {
+ self.exerciseName = exerciseName
+ let name = exerciseName
+ let completedRaw = WorkoutStatus.completed.rawValue
+ _logs = Query(
+ filter: #Predicate {
+ $0.exerciseName == name && $0.statusRaw == completedRaw
+ },
+ sort: \WorkoutLog.date,
+ order: .forward)
+ }
+
+ private var bestWeight: Double {
+ logs.map { $0.setEntries?.compactMap(\.weight).max() ?? $0.weight }.max() ?? 0
+ }
+
+ var body: some View {
+ ScrollView {
+ VStack(alignment: .leading, spacing: 16) {
+ HStack(spacing: 12) {
+ ProgressStatTile(
+ title: "Best",
+ value: weightUnit.format(bestWeight),
+ unit: nil)
+ ProgressStatTile(
+ title: "Sessions",
+ value: "\(logs.count)",
+ unit: nil)
+ ProgressStatTile(
+ title: "Last",
+ value: logs.last?.date.daysAgoLabel() ?? "—",
+ unit: nil)
+ }
+
+ WeightProgressionChartView(exerciseName: exerciseName)
+ .progressCard()
+ }
+ .padding(.horizontal)
+ }
+ .background(Color(.systemGroupedBackground))
+ .navigationTitle(exerciseName)
+ .navigationBarTitleDisplayMode(.inline)
+ }
+}
diff --git a/Workouts/Views/Progress/GoalTrackCard.swift b/Workouts/Views/Progress/GoalTrackCard.swift
new file mode 100644
index 0000000..7c56e14
--- /dev/null
+++ b/Workouts/Views/Progress/GoalTrackCard.swift
@@ -0,0 +1,138 @@
+//
+// GoalTrackCard.swift
+// Workouts
+//
+// Copyright 2026 Rouslan Zenetl. All Rights Reserved.
+//
+
+import SwiftUI
+
+/// One goal's adherence card: a 12-week strip (each square filled by that week's
+/// met/expected fraction), the weekly streak flame, and a this-week dot row per
+/// member schedule. The goal is the ledger category — this track continues unbroken
+/// across schedule rewrites.
+struct GoalTrackCard: View {
+ let track: GoalTrack
+
+ private var color: Color {
+ track.goal.map { Color.color(from: $0.colorName) } ?? Color(.systemGray)
+ }
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 12) {
+ HStack {
+ Label(track.goal?.displayName ?? "Unassigned",
+ systemImage: track.goal?.systemImage ?? "tag")
+ .font(.headline)
+ .foregroundStyle(color)
+ Spacer()
+ if track.streakWeeks > 0 {
+ streakBadge
+ }
+ }
+
+ weekStrip
+
+ VStack(alignment: .leading, spacing: 10) {
+ ForEach(track.scheduleTracks, id: \.schedule.id) { scheduleTrack in
+ scheduleRow(scheduleTrack)
+ }
+ }
+ }
+ .progressCard()
+ }
+
+ /// The streak flame: orange while the current week is still meetable, dimmed the
+ /// moment it can no longer be (the streak is at stake, not yet erased).
+ private var streakBadge: some View {
+ HStack(spacing: 3) {
+ Image(systemName: "flame.fill")
+ Text("\(track.streakWeeks) wk")
+ .monospacedDigit()
+ }
+ .font(.subheadline.weight(.semibold))
+ .foregroundStyle(track.streakAlive ? Color.orange : Color.secondary)
+ .accessibilityLabel("\(track.streakWeeks)-week streak\(track.streakAlive ? "" : ", at risk")")
+ }
+
+ private var weekStrip: some View {
+ HStack(spacing: 4) {
+ ForEach(Array(track.weeks.enumerated()), id: \.offset) { index, week in
+ RoundedRectangle(cornerRadius: 3, style: .continuous)
+ .fill(fill(for: week))
+ .frame(height: 16)
+ .overlay {
+ // Outline the in-flight week so "unfilled" reads as "not over
+ // yet" rather than "missed".
+ if index == track.weeks.count - 1 {
+ RoundedRectangle(cornerRadius: 3, style: .continuous)
+ .strokeBorder(color.opacity(0.8), lineWidth: 1.5)
+ }
+ }
+ }
+ }
+ .accessibilityElement(children: .ignore)
+ .accessibilityLabel(stripSummary)
+ }
+
+ private func fill(for week: WeekMark) -> Color {
+ if week.isVacuous { return Color(.systemFill).opacity(0.4) }
+ if week.fraction == 0 { return Color(.systemFill) }
+ return color.opacity(0.25 + 0.75 * week.fraction)
+ }
+
+ private var stripSummary: String {
+ let hit = track.weeks.filter { !$0.isVacuous && $0.hit }.count
+ let active = track.weeks.filter { !$0.isVacuous }.count
+ return "Hit \(hit) of the last \(active) weeks"
+ }
+
+ private func scheduleRow(_ scheduleTrack: ScheduleTrack) -> some View {
+ let schedule = scheduleTrack.schedule
+ return HStack(alignment: .firstTextBaseline) {
+ VStack(alignment: .leading, spacing: 2) {
+ Text(schedule.routineName)
+ .font(.subheadline)
+ Text(schedule.recurrence.summary(
+ weekdays: schedule.weekdays,
+ timesPerWeek: schedule.timesPerWeek,
+ date: schedule.onceDate))
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+ Spacer()
+ thisWeekDots(scheduleTrack)
+ }
+ }
+
+ /// This week for one schedule: `expected` dots, `met` of them filled. A broken
+ /// week adds a quiet "missed" tag — informative, not scolding.
+ @ViewBuilder
+ private func thisWeekDots(_ scheduleTrack: ScheduleTrack) -> some View {
+ if let week = scheduleTrack.currentWeek, !week.isVacuous {
+ HStack(spacing: 5) {
+ if scheduleTrack.currentWeekBroken {
+ Text("missed")
+ .font(.caption2)
+ .foregroundStyle(.secondary)
+ }
+ ForEach(0..= week.met {
+ Circle().strokeBorder(color.opacity(0.4), lineWidth: 1.5)
+ }
+ }
+ .frame(width: 9, height: 9)
+ }
+ }
+ .accessibilityElement(children: .ignore)
+ .accessibilityLabel("\(week.met) of \(week.expected) this week")
+ } else {
+ Text("—")
+ .font(.caption)
+ .foregroundStyle(.tertiary)
+ }
+ }
+}
diff --git a/Workouts/Views/Progress/ProgressTabView.swift b/Workouts/Views/Progress/ProgressTabView.swift
new file mode 100644
index 0000000..2e921c0
--- /dev/null
+++ b/Workouts/Views/Progress/ProgressTabView.swift
@@ -0,0 +1,314 @@
+//
+// ProgressTabView.swift
+// Workouts
+//
+// Copyright 2026 Rouslan Zenetl. All Rights Reserved.
+//
+
+import SwiftUI
+import SwiftData
+
+/// The Progress tab — observe by goals (UX-REDESIGN.md). Per-goal adherence tracks
+/// first, then this-week totals, aggregate trends, per-exercise drill-down, the
+/// achievements grid, and full history (absorbed from the old Log tab). Everything on
+/// this screen is derived by `ProgressPlanner` from the workout documents; nothing is
+/// persisted, so it all survives cache rebuilds.
+struct ProgressTabView: View {
+ /// Screenshot-capture affordance: `.bottom` opens the tab pre-scrolled to the
+ /// end (nil = the normal top). Only the DEBUG screenshot root passes it.
+ var initialAnchor: UnitPoint?
+
+ @Environment(SyncEngine.self) private var sync
+
+ @Query(sort: [SortDescriptor(\Schedule.order), SortDescriptor(\Schedule.createdAt)])
+ private var schedules: [Schedule]
+
+ @Query(sort: \Workout.start, order: .reverse)
+ private var workouts: [Workout]
+
+ var body: some View {
+ NavigationStack {
+ ScrollView {
+ let facts = workoutFacts
+ let tracks = ProgressPlanner.goalTracks(
+ schedules: scheduleFacts, workouts: facts,
+ goalOrder: GoalKind.orderedCases())
+
+ VStack(alignment: .leading, spacing: 16) {
+ WeekSummaryTiles(workouts: facts)
+
+ if tracks.isEmpty {
+ goalTracksHint
+ } else {
+ ForEach(tracks) { GoalTrackCard(track: $0) }
+ }
+
+ TrendsSection(workouts: facts)
+
+ exerciseTrendsLink
+
+ AchievementsSection(
+ achievements: ProgressPlanner.achievements(tracks: tracks, workouts: facts))
+
+ historySection
+ }
+ .padding(.horizontal)
+ .padding(.bottom, 24)
+ }
+ .defaultScrollAnchor(initialAnchor)
+ .background(Color(.systemGroupedBackground))
+ .navigationTitle("Progress")
+ }
+ }
+
+ // MARK: - Fact building (live-resolve IDs through the seed clone-on-edit redirect)
+
+ private var scheduleFacts: [ScheduleFacts] {
+ schedules.map { schedule in
+ ScheduleFacts(
+ id: schedule.id,
+ goal: schedule.goalKind,
+ recurrence: schedule.recurrenceEnum,
+ weekdays: schedule.weekdays,
+ timesPerWeek: schedule.timesPerWeek,
+ onceDate: schedule.date,
+ createdAt: schedule.createdAt,
+ routineID: sync.currentRoutineID(for: schedule.routineID),
+ routineName: schedule.routineName)
+ }
+ }
+
+ private var workoutFacts: [WorkoutFacts] {
+ workouts.map { workout in
+ WorkoutFacts(
+ routineID: workout.routineID.map { sync.currentRoutineID(for: $0) },
+ start: workout.start,
+ end: workout.end,
+ completed: workout.status == .completed,
+ volume: Self.volume(of: workout),
+ activeMinutes: workout.end.map { max(0, $0.timeIntervalSince(workout.start) / 60) } ?? 0,
+ energyKcal: workout.metricActiveEnergyKcal ?? 0)
+ }
+ }
+
+ /// Total lifted volume for one workout: the recorded watch metric when present,
+ /// else derived from per-set actuals, else the plan (the same fallback ladder the
+ /// documents' `effectiveSetEntries` uses).
+ static func volume(of workout: Workout) -> Double {
+ if let recorded = workout.metricTotalVolume { return recorded }
+ return workout.logs.reduce(0) { sum, log in
+ guard log.status == .completed, log.loadTypeEnum == .weight else { return sum }
+ if let entries = log.setEntries, !entries.isEmpty {
+ return sum + entries.reduce(0) { $0 + Double($1.reps ?? 0) * ($1.weight ?? 0) }
+ }
+ return sum + Double(log.sets * log.reps) * log.weight
+ }
+ }
+
+ // MARK: - Sections
+
+ private var goalTracksHint: some View {
+ VStack(alignment: .leading, spacing: 6) {
+ Label("No Goal Tracks Yet", systemImage: "chart.bar.fill")
+ .font(.headline)
+ Text("Add schedules on the Today tab and each goal builds a weekly adherence track here.")
+ .font(.subheadline)
+ .foregroundStyle(.secondary)
+ }
+ .progressCard()
+ }
+
+ private var exerciseTrendsLink: some View {
+ NavigationLink {
+ ExerciseTrendsListView()
+ } label: {
+ HStack(spacing: 12) {
+ Image(systemName: "chart.line.uptrend.xyaxis")
+ .font(.title3)
+ .foregroundStyle(.blue)
+ .frame(width: 32)
+ VStack(alignment: .leading, spacing: 2) {
+ Text("Exercise Trends")
+ .font(.headline)
+ .foregroundStyle(.primary)
+ Text("Weight progression for every exercise you've logged")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+ Spacer()
+ Image(systemName: "chevron.right")
+ .font(.footnote.weight(.semibold))
+ .foregroundStyle(.tertiary)
+ }
+ .progressCard()
+ }
+ .buttonStyle(.plain)
+ }
+
+ private var historySection: some View {
+ VStack(alignment: .leading, spacing: 12) {
+ HStack {
+ Text("History")
+ .font(.headline)
+ Spacer()
+ NavigationLink("All Workouts") {
+ WorkoutHistoryView()
+ }
+ .font(.subheadline)
+ }
+ if workouts.isEmpty {
+ Text("No workouts yet. Start one from the Today tab.")
+ .font(.subheadline)
+ .foregroundStyle(.secondary)
+ } else {
+ ForEach(workouts.prefix(3)) { workout in
+ NavigationLink {
+ WorkoutLogListView(workout: workout)
+ } label: {
+ HStack {
+ CalendarListItem(
+ date: workout.start,
+ title: workout.routineName ?? Routine.unnamed,
+ subtitle: subtitle(for: workout),
+ subtitle2: workout.statusName)
+ Spacer()
+ Image(systemName: "chevron.right")
+ .font(.footnote.weight(.semibold))
+ .foregroundStyle(.tertiary)
+ }
+ .contentShape(Rectangle())
+ }
+ .buttonStyle(.plain)
+ }
+ }
+ }
+ .progressCard()
+ }
+
+ private func subtitle(for workout: Workout) -> String {
+ if workout.status == .completed, let endDate = workout.end {
+ return workout.start.humanTimeInterval(to: endDate)
+ }
+ return workout.start.formattedDate()
+ }
+}
+
+// MARK: - Shared card chrome
+
+extension View {
+ /// The Progress tab's card treatment — one shared background so every section
+ /// reads as part of the same system.
+ func progressCard() -> some View {
+ padding(16)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .background(
+ Color(.secondarySystemGroupedBackground),
+ in: RoundedRectangle(cornerRadius: 16, style: .continuous))
+ }
+}
+
+// MARK: - This-week stat tiles
+
+/// Three at-a-glance totals for the current calendar week, each with a delta arrow
+/// against last week.
+struct WeekSummaryTiles: View {
+ let workouts: [WorkoutFacts]
+
+ @AppStorage("weightUnit") private var weightUnit: WeightUnit = .lb
+
+ var body: some View {
+ let calendar = Calendar.current
+ let now = Date()
+ let this = totals(in: calendar.dateInterval(of: .weekOfYear, for: now))
+ let last = totals(in: calendar.date(byAdding: .weekOfYear, value: -1, to: now)
+ .flatMap { calendar.dateInterval(of: .weekOfYear, for: $0) })
+
+ HStack(spacing: 12) {
+ ProgressStatTile(
+ title: "This Week",
+ value: "\(this.workouts)",
+ unit: this.workouts == 1 ? "workout" : "workouts",
+ delta: Double(this.workouts - last.workouts),
+ deltaText: "\(abs(this.workouts - last.workouts))")
+ ProgressStatTile(
+ title: "Volume",
+ value: this.volume.formatted(.number.notation(.compactName).precision(.significantDigits(3))),
+ unit: weightUnit.abbreviation,
+ delta: this.volume - last.volume,
+ deltaText: abs(this.volume - last.volume)
+ .formatted(.number.notation(.compactName).precision(.significantDigits(2))))
+ ProgressStatTile(
+ title: "Active Time",
+ value: Self.hoursMinutes(this.minutes),
+ unit: nil,
+ delta: this.minutes - last.minutes,
+ deltaText: Self.hoursMinutes(abs(this.minutes - last.minutes)))
+ }
+ }
+
+ private func totals(in interval: DateInterval?) -> (workouts: Int, volume: Double, minutes: Double) {
+ guard let interval else { return (0, 0, 0) }
+ let matching = workouts.filter { $0.completed && interval.contains($0.start) }
+ return (matching.count,
+ matching.reduce(0) { $0 + $1.volume },
+ matching.reduce(0) { $0 + $1.activeMinutes })
+ }
+
+ static func hoursMinutes(_ minutes: Double) -> String {
+ let total = Int(minutes.rounded())
+ let (h, m) = (total / 60, total % 60)
+ return h > 0 ? "\(h)h \(m)m" : "\(m)m"
+ }
+}
+
+/// One compact stat tile: caption, big value, and a vs-last-week arrow. Shared by the
+/// week summary row and the exercise drill-down.
+struct ProgressStatTile: View {
+ let title: String
+ let value: String
+ var unit: String?
+ /// Signed change vs the comparison period; only its sign is displayed.
+ var delta: Double?
+ /// Preformatted magnitude for the delta line (callers know their unit best).
+ var deltaText: String?
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 4) {
+ Text(title)
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ HStack(alignment: .firstTextBaseline, spacing: 3) {
+ Text(value)
+ .font(.title3.weight(.semibold))
+ .monospacedDigit()
+ .lineLimit(1)
+ .minimumScaleFactor(0.7)
+ if let unit {
+ Text(unit)
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+ }
+ if let delta, delta != 0 {
+ Label("\(deltaText ?? fallbackDeltaText(delta)) vs last wk",
+ systemImage: delta > 0 ? "arrow.up" : "arrow.down")
+ .font(.caption2.weight(.medium))
+ .foregroundStyle(delta > 0 ? .green : .secondary)
+ } else {
+ // Keep tile heights equal whether or not a delta renders.
+ Text("—")
+ .font(.caption2)
+ .foregroundStyle(.tertiary)
+ }
+ }
+ .padding(12)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .background(
+ Color(.secondarySystemGroupedBackground),
+ in: RoundedRectangle(cornerRadius: 16, style: .continuous))
+ }
+
+ private func fallbackDeltaText(_ delta: Double) -> String {
+ abs(delta).formatted(.number.notation(.compactName).precision(.significantDigits(2)))
+ }
+}
diff --git a/Workouts/Views/Progress/TrendsSection.swift b/Workouts/Views/Progress/TrendsSection.swift
new file mode 100644
index 0000000..c1f7ba9
--- /dev/null
+++ b/Workouts/Views/Progress/TrendsSection.swift
@@ -0,0 +1,134 @@
+//
+// TrendsSection.swift
+// Workouts
+//
+// Copyright 2026 Rouslan Zenetl. All Rights Reserved.
+//
+
+import SwiftUI
+import Charts
+
+/// Aggregate trends over time: one chart, a metric picker (workouts, volume, active
+/// time, energy), and the standard 7D/30D/90D/1Y/All range picker. Counts render as
+/// bars (discrete events); the other metrics as the smooth line + points pattern.
+struct TrendsSection: View {
+ let workouts: [WorkoutFacts]
+
+ @AppStorage("weightUnit") private var weightUnit: WeightUnit = .lb
+ @State private var range: TrendRange = .month
+ @State private var metric: Metric = .workouts
+
+ enum Metric: String, CaseIterable, Identifiable {
+ case workouts = "Workouts"
+ case volume = "Volume"
+ case time = "Time"
+ case energy = "Energy"
+
+ var id: String { rawValue }
+
+ var color: Color {
+ switch self {
+ case .workouts: .blue
+ case .volume: .purple
+ case .time: .green
+ case .energy: .orange
+ }
+ }
+ }
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 12) {
+ Text("Trends")
+ .font(.headline)
+
+ Picker("Metric", selection: $metric) {
+ ForEach(Metric.allCases) { metric in
+ Text(metric.rawValue).tag(metric)
+ }
+ }
+ .pickerStyle(.segmented)
+
+ Picker("Range", selection: $range) {
+ ForEach(TrendRange.allCases) { range in
+ Text(range.rawValue).tag(range)
+ }
+ }
+ .pickerStyle(.segmented)
+
+ chart
+ }
+ .progressCard()
+ }
+
+ @ViewBuilder
+ private var chart: some View {
+ let points = ProgressPlanner.trendPoints(workouts: workouts, range: range)
+ if points.isEmpty || points.allSatisfy({ $0.workouts == 0 }) {
+ ContentUnavailableView(
+ "No Data",
+ systemImage: "chart.xyaxis.line",
+ description: Text(workouts.contains(where: \.completed)
+ ? "No workouts in this time range. Try a wider range."
+ : "Complete workouts and your trends build up here."))
+ .frame(height: 220)
+ } else {
+ Chart(points) { point in
+ if metric == .workouts {
+ BarMark(
+ x: .value("Date", point.date, unit: range.bucket),
+ y: .value(metric.rawValue, value(of: point))
+ )
+ .foregroundStyle(metric.color.gradient)
+ .cornerRadius(3)
+ } else {
+ LineMark(
+ x: .value("Date", point.date, unit: range.bucket),
+ y: .value(metric.rawValue, value(of: point))
+ )
+ .interpolationMethod(.catmullRom)
+ .foregroundStyle(metric.color.gradient)
+
+ PointMark(
+ x: .value("Date", point.date, unit: range.bucket),
+ y: .value(metric.rawValue, value(of: point))
+ )
+ .symbolSize(30)
+ .foregroundStyle(metric.color)
+ }
+ }
+ .chartYAxisLabel(unitLabel)
+ .chartXAxis {
+ AxisMarks(values: .automatic) { _ in
+ AxisGridLine()
+ AxisValueLabel(format: axisDateFormat)
+ }
+ }
+ .frame(height: 220)
+ }
+ }
+
+ private func value(of point: TrendPoint) -> Double {
+ switch metric {
+ case .workouts: Double(point.workouts)
+ case .volume: point.volume
+ case .time: point.activeMinutes
+ case .energy: point.energyKcal
+ }
+ }
+
+ private var unitLabel: String {
+ switch metric {
+ case .workouts: ""
+ case .volume: weightUnit.abbreviation
+ case .time: "min"
+ case .energy: "kcal"
+ }
+ }
+
+ private var axisDateFormat: Date.FormatStyle {
+ switch range.bucket {
+ case .month: .dateTime.month(.abbreviated)
+ default: .dateTime.month().day()
+ }
+ }
+}
diff --git a/Workouts/Views/Splits/OrderableItem.swift b/Workouts/Views/Routines/OrderableItem.swift
similarity index 100%
rename from Workouts/Views/Splits/OrderableItem.swift
rename to Workouts/Views/Routines/OrderableItem.swift
diff --git a/Workouts/Views/Splits/SplitAddEditView.swift b/Workouts/Views/Routines/RoutineAddEditView.swift
similarity index 68%
rename from Workouts/Views/Splits/SplitAddEditView.swift
rename to Workouts/Views/Routines/RoutineAddEditView.swift
index 897fed7..e00f47f 100644
--- a/Workouts/Views/Splits/SplitAddEditView.swift
+++ b/Workouts/Views/Routines/RoutineAddEditView.swift
@@ -1,5 +1,5 @@
//
-// SplitAddEditView.swift
+// RoutineAddEditView.swift
// Workouts
//
// Created by rzen on 7/18/25 at 9:42 AM.
@@ -11,17 +11,17 @@ import IndieSync
import SwiftUI
import SwiftData
-struct SplitAddEditView: View {
+struct RoutineAddEditView: View {
@Environment(SyncEngine.self) private var sync
@Environment(\.modelContext) private var modelContext
@Environment(\.dismiss) private var dismiss
- // Resolve the split by id, not a captured entity: a clone-on-edit swaps a seed's
+ // Resolve the routine by id, not a captured entity: a clone-on-edit swaps a seed's
// identity mid-screen (the seed entity is deleted and a clone inserted), which
- // would dangle a stored `Split`. `currentSplitID` follows that swap. `splitID` is
+ // would dangle a stored `Routine`. `currentRoutineID` follows that swap. `routineID` is
// nil in create mode.
- @State private var splitID: String?
- @Query private var splits: [Split]
+ @State private var routineID: String?
+ @Query private var routines: [Routine]
var onDelete: (() -> Void)?
@@ -35,43 +35,43 @@ struct SplitAddEditView: View {
@State private var showingIconPicker: Bool = false
@State private var showingDeleteConfirmation: Bool = false
- var isEditing: Bool { splitID != nil }
+ var isEditing: Bool { routineID != nil }
- init(split: Split?, onDelete: (() -> Void)? = nil) {
- _splitID = State(initialValue: split?.id)
+ init(routine: Routine?, onDelete: (() -> Void)? = nil) {
+ _routineID = State(initialValue: routine?.id)
self.onDelete = onDelete
- if let split = split {
- _name = State(initialValue: split.name)
- _color = State(initialValue: split.color)
- _systemImage = State(initialValue: split.systemImage)
- _activityType = State(initialValue: split.activityTypeEnum)
- _autoAdvance = State(initialValue: split.autoAdvance ?? false)
- _restOverrideEnabled = State(initialValue: split.restSeconds != nil)
- _restSecondsValue = State(initialValue: split.restSeconds ?? 45)
+ if let routine = routine {
+ _name = State(initialValue: routine.name)
+ _color = State(initialValue: routine.color)
+ _systemImage = State(initialValue: routine.systemImage)
+ _activityType = State(initialValue: routine.activityTypeEnum)
+ _autoAdvance = State(initialValue: routine.autoAdvance ?? false)
+ _restOverrideEnabled = State(initialValue: routine.restSeconds != nil)
+ _restSecondsValue = State(initialValue: routine.restSeconds ?? 45)
}
}
- private var split: Split? {
- guard let splitID else { return nil }
- let id = sync.currentSplitID(for: splitID)
- return splits.first { $0.id == id }
+ private var routine: Routine? {
+ guard let routineID else { return nil }
+ let id = sync.currentRoutineID(for: routineID)
+ return routines.first { $0.id == id }
}
var body: some View {
NavigationStack {
- if isEditing && split == nil {
- // The id we held no longer maps to a live split (deleted on another
+ if isEditing && routine == nil {
+ // The id we held no longer maps to a live routine (deleted on another
// device, or a transient mid-clone frame). Show nothing and leave.
- ContentUnavailableView("Split Unavailable", systemImage: "dumbbell")
+ ContentUnavailableView("Routine Unavailable", systemImage: "dumbbell")
.task { dismiss() }
} else {
- form(for: split)
+ form(for: routine)
}
}
}
@ViewBuilder
- private func form(for split: Split?) -> some View {
+ private func form(for routine: Routine?) -> some View {
Form {
Section(header: Text("Name")) {
TextField("Name", text: $name)
@@ -133,29 +133,29 @@ struct SplitAddEditView: View {
} header: {
Text("Rest & Pacing")
} footer: {
- Text("Auto-Advance flows from one exercise to the next with a rest between, so you can run the whole split hands-free. Custom Rest Time sets this split's rest — used between sets, and between exercises when auto-advancing; otherwise the Settings default applies.")
+ Text("Auto-Advance flows from one exercise to the next with a rest between, so you can run the whole routine hands-free. Custom Rest Time sets this routine's rest — used between sets, and between exercises when auto-advancing; otherwise the Settings default applies.")
}
- if let split = split {
+ if let routine = routine {
Section(header: Text("Exercises")) {
NavigationLink {
- ExerciseListView(split: split)
+ ExerciseListView(routine: routine)
} label: {
ListItem(
text: "Exercises",
- count: split.exercisesArray.count
+ count: routine.exercisesArray.count
)
}
}
Section {
- Button("Delete Split", role: .destructive) {
+ Button("Delete Routine", role: .destructive) {
showingDeleteConfirmation = true
}
}
}
}
- .navigationTitle(isEditing ? "Edit Split" : "New Split")
+ .navigationTitle(isEditing ? "Edit Routine" : "New Routine")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
@@ -175,30 +175,30 @@ struct SplitAddEditView: View {
SFSymbolPicker(selection: $systemImage)
}
.confirmationDialog(
- "Delete This Split?",
+ "Delete This Routine?",
isPresented: $showingDeleteConfirmation,
titleVisibility: .visible
) {
Button("Delete", role: .destructive) {
- if let split = split {
+ if let routine = routine {
Task {
- await sync.delete(split: split)
+ await sync.delete(routine: routine)
}
dismiss()
onDelete?()
}
}
} message: {
- Text("This will permanently delete the split and all its exercises.")
+ Text("This will permanently delete the routine and all its exercises.")
}
}
private func save() {
if isEditing {
- // Update existing split. If the id no longer resolves (deleted remotely
+ // Update existing routine. If the id no longer resolves (deleted remotely
// mid-edit), there's nothing to save.
- guard let split = split else { return }
- var doc = SplitDocument(from: split)
+ guard let routine = routine else { return }
+ var doc = RoutineDocument(from: routine)
doc.name = name
doc.color = color
doc.systemImage = systemImage
@@ -206,12 +206,12 @@ struct SplitAddEditView: View {
doc.restSeconds = restOverrideEnabled ? restSecondsValue : nil
doc.autoAdvance = autoAdvance ? true : nil
doc.updatedAt = Date()
- Task { await sync.save(split: doc) }
+ Task { await sync.save(routine: doc) }
} else {
- // Create new split
- let existing = (try? modelContext.fetch(FetchDescriptor())) ?? []
- let doc = SplitDocument(
- schemaVersion: SplitDocument.currentSchemaVersion,
+ // Create new routine
+ let existing = (try? modelContext.fetch(FetchDescriptor())) ?? []
+ let doc = RoutineDocument(
+ schemaVersion: RoutineDocument.currentSchemaVersion,
id: ULID.make(),
name: name,
color: color,
@@ -224,7 +224,7 @@ struct SplitAddEditView: View {
restSeconds: restOverrideEnabled ? restSecondsValue : nil,
autoAdvance: autoAdvance ? true : nil
)
- Task { await sync.save(split: doc) }
+ Task { await sync.save(routine: doc) }
}
}
}
diff --git a/Workouts/Views/Splits/SplitDetailView.swift b/Workouts/Views/Routines/RoutineDetailView.swift
similarity index 69%
rename from Workouts/Views/Splits/SplitDetailView.swift
rename to Workouts/Views/Routines/RoutineDetailView.swift
index d095525..1c8a6ae 100644
--- a/Workouts/Views/Splits/SplitDetailView.swift
+++ b/Workouts/Views/Routines/RoutineDetailView.swift
@@ -1,5 +1,5 @@
//
-// SplitDetailView.swift
+// RoutineDetailView.swift
// Workouts
//
// Created by rzen on 7/25/25 at 3:27 PM.
@@ -11,70 +11,70 @@ import IndieSync
import SwiftUI
import SwiftData
-struct SplitDetailView: View {
+struct RoutineDetailView: View {
@Environment(SyncEngine.self) private var sync
@Environment(AppServices.self) private var services
@Environment(\.dismiss) private var dismiss
- // Resolve the split by id, not a captured entity: a clone-on-edit swaps a seed's
+ // Resolve the routine by id, not a captured entity: a clone-on-edit swaps a seed's
// identity mid-screen (the seed entity is deleted and a clone inserted), which
- // would dangle a stored `Split`. `currentSplitID` follows that swap.
- @State private var splitID: String
- @Query private var splits: [Split]
+ // would dangle a stored `Routine`. `currentRoutineID` follows that swap.
+ @State private var routineID: String
+ @Query private var routines: [Routine]
@State private var showingExerciseAddSheet: Bool = false
- @State private var showingSplitEditSheet: Bool = false
+ @State private var showingRoutineEditSheet: Bool = false
@State private var itemToEdit: Exercise? = nil
@State private var itemToDelete: Exercise? = nil
@AppStorage("weightUnit") private var weightUnit: WeightUnit = .lb
- init(split: Split) {
+ init(routine: Routine) {
// A closure-based `NavigationLink` builds this destination eagerly for every
- // row in the parent list, including during the update that fires when a split
+ // row in the parent list, including during the update that fires when a routine
// is deleted — and reading any persisted property (even `id`) on a deleted
// `@Model` traps. `isDeleted` alone misses a deletion that has already been
// saved (the model unregisters: `isDeleted` false again, `modelContext` nil,
- // reads still trap), so check both. An empty id maps to no live split, so
- // `body` shows the "Split Unavailable" state and dismisses; the row is on its
+ // reads still trap), so check both. An empty id maps to no live routine, so
+ // `body` shows the "Routine Unavailable" state and dismisses; the row is on its
// way out anyway.
- let live = !split.isDeleted && split.modelContext != nil
- _splitID = State(initialValue: live ? split.id : "")
+ let live = !routine.isDeleted && routine.modelContext != nil
+ _routineID = State(initialValue: live ? routine.id : "")
}
- private var split: Split? {
- let id = sync.currentSplitID(for: splitID)
- return splits.first { $0.id == id }
+ private var routine: Routine? {
+ let id = sync.currentRoutineID(for: routineID)
+ return routines.first { $0.id == id }
}
var body: some View {
Group {
- if let split {
- content(for: split)
+ if let routine {
+ content(for: routine)
} else {
- // The id we held no longer maps to a live split (deleted on another
+ // The id we held no longer maps to a live routine (deleted on another
// device, or a transient mid-clone frame). Show nothing and leave.
- ContentUnavailableView("Split Unavailable", systemImage: "dumbbell")
+ ContentUnavailableView("Routine Unavailable", systemImage: "dumbbell")
.task { dismiss() }
}
}
- // Editing this split (or any of its exercises, all reached from here) parks any
- // active watch run sourced from it — matched by splitID — so the watch can't keep
+ // Editing this routine (or any of its exercises, all reached from here) parks any
+ // active watch run sourced from it — matched by routineID — so the watch can't keep
// performing an exercise whose plan we're reconfiguring.
- .onAppear { services.watchBridge.setEditingSplit(sync.currentSplitID(for: splitID)) }
- .onDisappear { services.watchBridge.setEditingSplit(nil) }
+ .onAppear { services.watchBridge.setEditingRoutine(sync.currentRoutineID(for: routineID)) }
+ .onDisappear { services.watchBridge.setEditingRoutine(nil) }
}
@ViewBuilder
- private func content(for split: Split) -> some View {
+ private func content(for routine: Routine) -> some View {
Form {
- Section(header: Text("What is a Split?")) {
- Text("A \"split\" is simply how you divide (or \"split up\") your weekly training across different days. Instead of working every muscle group every session, you assign certain muscle groups, movement patterns, or training emphases to specific days.")
+ Section(header: Text("What is a Routine?")) {
+ Text("A \"routine\" is simply how you divide up your weekly training across different days. Instead of working every muscle group every session, you assign certain muscle groups, movement patterns, or training emphases to specific days.")
.font(.caption)
}
// Headerless — what the exercise list is needs no label; the section
// itself keeps the visual separation.
- if split.exercisesArray.isEmpty {
+ if routine.exercisesArray.isEmpty {
Section {
Text("No exercises added yet.")
Button(action: { showingExerciseAddSheet.toggle() }) {
@@ -83,7 +83,7 @@ struct SplitDetailView: View {
}
} else {
Section {
- ForEach(split.exercisesArray) { item in
+ ForEach(routine.exercisesArray) { item in
ListItem(
title: item.name,
subtitle: item.planSummary(weightUnit: weightUnit)
@@ -115,11 +115,11 @@ struct SplitDetailView: View {
}
}
}
- .navigationTitle(split.name)
+ .navigationTitle(routine.name)
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button {
- showingSplitEditSheet = true
+ showingRoutineEditSheet = true
} label: {
Image(systemName: "pencil")
}
@@ -130,13 +130,13 @@ struct SplitDetailView: View {
addExercises(names: exerciseNames)
}, allowMultiSelect: true)
}
- .sheet(isPresented: $showingSplitEditSheet) {
- SplitAddEditView(split: split) {
+ .sheet(isPresented: $showingRoutineEditSheet) {
+ RoutineAddEditView(routine: routine) {
dismiss()
}
}
.sheet(item: $itemToEdit) { item in
- ExerciseAddEditView(exercise: item, split: split)
+ ExerciseAddEditView(exercise: item, routine: routine)
}
.confirmationDialog(
"Delete Exercise?",
@@ -155,30 +155,30 @@ struct SplitDetailView: View {
itemToDelete = nil
}
} message: { item in
- Text("Remove \"\(item.name)\" from this split?")
+ Text("Remove \"\(item.name)\" from this routine?")
}
}
- /// Reorder and renumber. Resolves the current split at call time so it
+ /// Reorder and renumber. Resolves the current routine at call time so it
/// follows a clone-on-edit.
private func moveExercises(from source: IndexSet, to destination: Int) {
- guard let split else { return }
- var ordered = split.exercisesArray
+ guard let routine else { return }
+ var ordered = routine.exercisesArray
ordered.move(fromOffsets: source, toOffset: destination)
- var doc = SplitDocument(from: split)
+ var doc = RoutineDocument(from: routine)
doc.exercises = ordered.enumerated().map { i, ex in
var ed = ExerciseDocument(from: ex)
ed.order = i
return ed
}
doc.updatedAt = Date()
- Task { await sync.save(split: doc) }
+ Task { await sync.save(routine: doc) }
}
private func addExercises(names: [String]) {
- guard let split else { return }
- var doc = SplitDocument(from: split)
+ guard let routine else { return }
+ var doc = RoutineDocument(from: routine)
let existingNames = Set(doc.exercises.map { $0.name })
let base = doc.exercises.count
let newDocs = names
@@ -198,21 +198,21 @@ struct SplitDetailView: View {
}
doc.exercises.append(contentsOf: newDocs)
doc.updatedAt = Date()
- Task { await sync.save(split: doc) }
+ Task { await sync.save(routine: doc) }
// If a single exercise was added, open the edit sheet once the cache refreshes.
// We rely on the observer to populate it — no direct entity reference needed.
}
private func deleteExercise(_ exercise: Exercise) {
- guard let split else { return }
- var doc = SplitDocument(from: split)
+ guard let routine else { return }
+ var doc = RoutineDocument(from: routine)
doc.exercises.removeAll { $0.id == exercise.id }
// Re-number orders after removal
for i in doc.exercises.indices {
doc.exercises[i].order = i
}
doc.updatedAt = Date()
- Task { await sync.save(split: doc) }
+ Task { await sync.save(routine: doc) }
}
}
diff --git a/Workouts/Views/Routines/RoutineListView.swift b/Workouts/Views/Routines/RoutineListView.swift
new file mode 100644
index 0000000..c4b2db9
--- /dev/null
+++ b/Workouts/Views/Routines/RoutineListView.swift
@@ -0,0 +1,118 @@
+//
+// RoutineListView.swift
+// Workouts
+//
+// Created by rzen on 7/25/25 at 6:24 PM.
+//
+// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
+//
+
+import SwiftUI
+import SwiftData
+
+/// The Routines library screen, pushed from Settings → Library: a plain list of
+/// routines, one row each. Tapping a row opens `RoutineDetailView`; long-pressing
+/// offers Delete (the sole routine-delete affordance); the toolbar's + adds a new
+/// routine. Starter routines are seeded automatically on the true first run
+/// (`SyncEngine.autoSeedIfEmpty`), so an empty library just invites creating one.
+struct RoutineListView: View {
+ @Environment(SyncEngine.self) private var sync
+
+ @Query(sort: \Routine.name) private var routines: [Routine]
+
+ @State private var showingAddSheet = false
+ @State private var routineToDelete: Routine?
+
+ var body: some View {
+ List {
+ ForEach(routines) { routine in
+ NavigationLink {
+ RoutineDetailView(routine: routine)
+ } label: {
+ RoutineRow(routine: routine)
+ }
+ .contextMenu {
+ Button(role: .destructive) {
+ routineToDelete = routine
+ } label: {
+ Label("Delete", systemImage: "trash")
+ }
+ }
+ }
+ }
+ .listStyle(.insetGrouped)
+ .overlay {
+ if routines.isEmpty {
+ ContentUnavailableView(
+ "No Routines Yet",
+ systemImage: "dumbbell.fill",
+ description: Text("Create a routine to organize your workout routine.")
+ )
+ }
+ }
+ .navigationTitle("Routines")
+ .toolbar {
+ ToolbarItem(placement: .navigationBarTrailing) {
+ Button {
+ showingAddSheet = true
+ } label: {
+ Image(systemName: "plus")
+ }
+ .accessibilityLabel("Add Routine")
+ }
+ }
+ .sheet(isPresented: $showingAddSheet) {
+ RoutineAddEditView(routine: nil)
+ }
+ .confirmationDialog(
+ "Delete Routine?",
+ isPresented: Binding(
+ get: { routineToDelete != nil },
+ set: { if !$0 { routineToDelete = nil } }
+ ),
+ titleVisibility: .visible,
+ presenting: routineToDelete
+ ) { routine in
+ Button("Delete", role: .destructive) {
+ Task { await sync.delete(routine: routine) }
+ routineToDelete = nil
+ }
+ Button("Cancel", role: .cancel) {
+ routineToDelete = nil
+ }
+ } message: { routine in
+ Text("This will permanently delete \"\(routine.name)\" and all its exercises. Past workouts are kept.")
+ }
+ }
+}
+
+/// A single routine row: a small rounded-square badge (the routine's color at low
+/// opacity, its symbol tinted full-strength) beside the routine name and its
+/// exercise count.
+private struct RoutineRow: View {
+ var routine: Routine
+
+ var body: some View {
+ HStack(spacing: 12) {
+ Image(systemName: routine.systemImage)
+ .font(.title3)
+ .foregroundStyle(routineColor)
+ .frame(width: 36, height: 36)
+ .background(routineColor.opacity(0.12), in: RoundedRectangle(cornerRadius: 8))
+
+ VStack(alignment: .leading, spacing: 2) {
+ Text(routine.name)
+ .foregroundStyle(.primary)
+
+ Text("\(routine.exercisesArray.count) exercises")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+ }
+ .padding(.vertical, 2)
+ }
+
+ private var routineColor: Color {
+ Color.color(from: routine.color)
+ }
+}
diff --git a/Workouts/Views/Splits/SortableForEach.swift b/Workouts/Views/Routines/SortableForEach.swift
similarity index 100%
rename from Workouts/Views/Splits/SortableForEach.swift
rename to Workouts/Views/Routines/SortableForEach.swift
diff --git a/Workouts/Views/Settings/DuplicateCleanupView.swift b/Workouts/Views/Settings/DuplicateCleanupView.swift
index 1f13c43..ef62edc 100644
--- a/Workouts/Views/Settings/DuplicateCleanupView.swift
+++ b/Workouts/Views/Settings/DuplicateCleanupView.swift
@@ -7,10 +7,10 @@
import SwiftUI
-/// Developer-facing tool: scans iCloud Drive for splits/workouts that are exact
+/// Developer-facing tool: scans iCloud Drive for routines/workouts that are exact
/// content duplicates (see `DuplicateCleanupPlanner`) — typically the residue of
/// testing, a restore, or a sync hiccup — and offers to delete the extras. Never
-/// touches a split referenced by a workout, a bundled starter split, or an
+/// touches a routine referenced by a workout, a bundled starter routine, or an
/// in-progress workout.
struct DuplicateCleanupView: View {
@Environment(SyncEngine.self) private var sync
@@ -21,7 +21,7 @@ struct DuplicateCleanupView: View {
}
private struct CleanupSummary {
- var splitsDeleted: Int
+ var routinesDeleted: Int
var workoutsDeleted: Int
var skipped: Int
}
@@ -58,7 +58,7 @@ struct DuplicateCleanupView: View {
pendingPlan = nil
}
} message: { _ in
- Text("This permanently deletes the duplicate copies shown below. Referenced splits, starter splits, and in-progress workouts are always kept.")
+ Text("This permanently deletes the duplicate copies shown below. Referenced routines, starter routines, and in-progress workouts are always kept.")
}
}
@@ -93,7 +93,7 @@ struct DuplicateCleanupView: View {
Label("Scan for Duplicates", systemImage: "magnifyingglass")
}
} footer: {
- Text("Looks for splits and workouts that are exact content duplicates — for example, left over from testing or a sync hiccup — and offers to delete the extra copies. Splits referenced by a workout, starter splits, and in-progress workouts are never deleted.")
+ Text("Looks for routines and workouts that are exact content duplicates — for example, left over from testing or a sync hiccup — and offers to delete the extra copies. Routines referenced by a workout, starter routines, and in-progress workouts are never deleted.")
}
}
}
@@ -110,10 +110,10 @@ struct DuplicateCleanupView: View {
)
} else {
List {
- if !plan.splitGroups.isEmpty {
- Section("Duplicate Splits") {
- ForEach(plan.splitGroups) { group in
- splitGroupRow(group)
+ if !plan.routineGroups.isEmpty {
+ Section("Duplicate Routines") {
+ ForEach(plan.routineGroups) { group in
+ routineGroupRow(group)
}
}
}
@@ -139,7 +139,7 @@ struct DuplicateCleanupView: View {
}
}
- private func splitGroupRow(_ group: DuplicateCleanupPlan.SplitGroup) -> some View {
+ private func routineGroupRow(_ group: DuplicateCleanupPlan.RoutineGroup) -> some View {
VStack(alignment: .leading, spacing: 6) {
ForEach(group.keep) { doc in
memberRow(name: doc.name, detail: "\(doc.exercises.count) exercises", id: doc.id, isKeep: true)
@@ -154,14 +154,14 @@ struct DuplicateCleanupView: View {
private func workoutGroupRow(_ group: DuplicateCleanupPlan.WorkoutGroup) -> some View {
VStack(alignment: .leading, spacing: 6) {
memberRow(
- name: group.keep.splitName ?? "(no split)",
+ name: group.keep.routineName ?? "(no routine)",
detail: group.keep.start.formattedDate(),
id: group.keep.id,
isKeep: true
)
ForEach(group.delete) { doc in
memberRow(
- name: doc.splitName ?? "(no split)",
+ name: doc.routineName ?? "(no routine)",
detail: doc.start.formattedDate(),
id: doc.id,
isKeep: false
@@ -237,7 +237,7 @@ struct DuplicateCleanupView: View {
List {
Section {
Label(
- "Deleted \(summary.splitsDeleted) split\(summary.splitsDeleted == 1 ? "" : "s"), \(summary.workoutsDeleted) workout\(summary.workoutsDeleted == 1 ? "" : "s"). \(summary.skipped) skipped (referenced or active).",
+ "Deleted \(summary.routinesDeleted) routine\(summary.routinesDeleted == 1 ? "" : "s"), \(summary.workoutsDeleted) workout\(summary.workoutsDeleted == 1 ? "" : "s"). \(summary.skipped) skipped (referenced or active).",
systemImage: "checkmark.circle.fill"
)
.foregroundStyle(.green)
@@ -273,7 +273,7 @@ struct DuplicateCleanupView: View {
phase = .deleting
let result = await sync.performCleanup(plan)
phase = .done(CleanupSummary(
- splitsDeleted: result.splitsDeleted,
+ routinesDeleted: result.routinesDeleted,
workoutsDeleted: result.workoutsDeleted,
skipped: result.skipped
))
diff --git a/Workouts/Views/Settings/GoalsListView.swift b/Workouts/Views/Settings/GoalsListView.swift
new file mode 100644
index 0000000..591f0d2
--- /dev/null
+++ b/Workouts/Views/Settings/GoalsListView.swift
@@ -0,0 +1,41 @@
+//
+// GoalsListView.swift
+// Workouts
+//
+// Copyright 2026 Rouslan Zenetl. All Rights Reserved.
+//
+
+import SwiftUI
+
+/// A reorderable list of the practice-goal kinds. The chosen order is persisted via
+/// `GoalKind.saveOrder` and drives how schedules group on the Today screen. Pushed
+/// from Settings → Library, so it relies on Settings' own NavigationStack.
+struct GoalsListView: View {
+ @State private var order: [GoalKind] = []
+
+ var body: some View {
+ List {
+ Section {
+ ForEach(order) { kind in
+ Label(kind.displayName, systemImage: kind.systemImage)
+ .foregroundStyle(Color.color(from: kind.colorName))
+ }
+ .onMove(perform: move)
+ } footer: {
+ Text("Goals group your schedules on the Today screen. Drag to reorder.")
+ }
+ }
+ .navigationTitle("Goals")
+ .toolbar {
+ EditButton()
+ }
+ .onAppear {
+ if order.isEmpty { order = GoalKind.orderedCases() }
+ }
+ }
+
+ private func move(from source: IndexSet, to destination: Int) {
+ order.move(fromOffsets: source, toOffset: destination)
+ GoalKind.saveOrder(order)
+ }
+}
diff --git a/Workouts/Views/Settings/SettingsView.swift b/Workouts/Views/Settings/SettingsView.swift
index 94cd947..2a6df5f 100644
--- a/Workouts/Views/Settings/SettingsView.swift
+++ b/Workouts/Views/Settings/SettingsView.swift
@@ -15,7 +15,7 @@ struct SettingsView: View {
@Environment(SyncEngine.self) private var sync
@Environment(AppServices.self) private var services
- @Query private var splits: [Split]
+ @Query private var routines: [Routine]
@AppStorage("restSeconds") private var restSeconds: Int = 45
@AppStorage("doneCountdownSeconds") private var doneCountdownSeconds: Int = 5
@@ -78,12 +78,23 @@ struct SettingsView: View {
// MARK: - Library Section
Section(header: Text("Library")) {
NavigationLink {
- SplitListView()
+ GoalsListView()
} label: {
HStack {
- Label("Splits", systemImage: "dumbbell.fill")
+ Label("Goals", systemImage: "target")
Spacer()
- Text("\(splits.count)")
+ Text("\(GoalKind.allCases.count)")
+ .foregroundStyle(.secondary)
+ }
+ }
+
+ NavigationLink {
+ RoutineListView()
+ } label: {
+ HStack {
+ Label("Routines", systemImage: "dumbbell.fill")
+ Spacer()
+ Text("\(routines.count)")
.foregroundStyle(.secondary)
}
}
@@ -100,7 +111,7 @@ struct SettingsView: View {
}
}
- // MARK: - Starter Splits Section
+ // MARK: - Starter Routines Section
Section {
Button {
Task {
@@ -109,12 +120,12 @@ struct SettingsView: View {
let n = await sync.restoreSeeds()
isRestoringSeeds = false
restoreSeedsMessage = n == 0
- ? "All starter splits are already present."
- : "Restored \(n) starter split\(n == 1 ? "" : "s")."
+ ? "All starter routines are already present."
+ : "Restored \(n) starter routine\(n == 1 ? "" : "s")."
}
} label: {
HStack {
- Label("Restore Starter Splits", systemImage: "arrow.counterclockwise")
+ Label("Restore Starter Routines", systemImage: "arrow.counterclockwise")
if isRestoringSeeds {
Spacer()
ProgressView()
@@ -123,12 +134,12 @@ struct SettingsView: View {
}
.disabled(isRestoringSeeds || sync.iCloudStatus != .available)
} header: {
- Text("Starter Splits")
+ Text("Starter Routines")
} footer: {
if let restoreSeedsMessage {
Text(restoreSeedsMessage)
} else {
- Text("Brings back the bundled starter splits you've deleted. Splits you've edited or created are never touched.")
+ Text("Brings back the bundled starter routines you've deleted. Routines you've edited or created are never touched.")
}
}
diff --git a/Workouts/Views/Splits/SplitListView.swift b/Workouts/Views/Splits/SplitListView.swift
deleted file mode 100644
index 1b2f1a4..0000000
--- a/Workouts/Views/Splits/SplitListView.swift
+++ /dev/null
@@ -1,118 +0,0 @@
-//
-// SplitListView.swift
-// Workouts
-//
-// Created by rzen on 7/25/25 at 6:24 PM.
-//
-// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
-//
-
-import SwiftUI
-import SwiftData
-
-/// The Splits library screen, pushed from Settings → Library: a plain list of
-/// splits, one row each. Tapping a row opens `SplitDetailView`; long-pressing
-/// offers Delete (the sole split-delete affordance); the toolbar's + adds a new
-/// split. Starter splits are seeded automatically on the true first run
-/// (`SyncEngine.autoSeedIfEmpty`), so an empty library just invites creating one.
-struct SplitListView: View {
- @Environment(SyncEngine.self) private var sync
-
- @Query(sort: \Split.name) private var splits: [Split]
-
- @State private var showingAddSheet = false
- @State private var splitToDelete: Split?
-
- var body: some View {
- List {
- ForEach(splits) { split in
- NavigationLink {
- SplitDetailView(split: split)
- } label: {
- SplitRow(split: split)
- }
- .contextMenu {
- Button(role: .destructive) {
- splitToDelete = split
- } label: {
- Label("Delete", systemImage: "trash")
- }
- }
- }
- }
- .listStyle(.insetGrouped)
- .overlay {
- if splits.isEmpty {
- ContentUnavailableView(
- "No Splits Yet",
- systemImage: "dumbbell.fill",
- description: Text("Create a split to organize your workout routine.")
- )
- }
- }
- .navigationTitle("Splits")
- .toolbar {
- ToolbarItem(placement: .navigationBarTrailing) {
- Button {
- showingAddSheet = true
- } label: {
- Image(systemName: "plus")
- }
- .accessibilityLabel("Add Split")
- }
- }
- .sheet(isPresented: $showingAddSheet) {
- SplitAddEditView(split: nil)
- }
- .confirmationDialog(
- "Delete Split?",
- isPresented: Binding(
- get: { splitToDelete != nil },
- set: { if !$0 { splitToDelete = nil } }
- ),
- titleVisibility: .visible,
- presenting: splitToDelete
- ) { split in
- Button("Delete", role: .destructive) {
- Task { await sync.delete(split: split) }
- splitToDelete = nil
- }
- Button("Cancel", role: .cancel) {
- splitToDelete = nil
- }
- } message: { split in
- Text("This will permanently delete \"\(split.name)\" and all its exercises. Past workouts are kept.")
- }
- }
-}
-
-/// A single split row: a small rounded-square badge (the split's color at low
-/// opacity, its symbol tinted full-strength) beside the split name and its
-/// exercise count.
-private struct SplitRow: View {
- var split: Split
-
- var body: some View {
- HStack(spacing: 12) {
- Image(systemName: split.systemImage)
- .font(.title3)
- .foregroundStyle(splitColor)
- .frame(width: 36, height: 36)
- .background(splitColor.opacity(0.12), in: RoundedRectangle(cornerRadius: 8))
-
- VStack(alignment: .leading, spacing: 2) {
- Text(split.name)
- .foregroundStyle(.primary)
-
- Text("\(split.exercisesArray.count) exercises")
- .font(.caption)
- .foregroundStyle(.secondary)
- }
- }
- .padding(.vertical, 2)
- }
-
- private var splitColor: Color {
- Color.color(from: split.color)
- }
-}
diff --git a/Workouts/Views/Today/DayPickerSheet.swift b/Workouts/Views/Today/DayPickerSheet.swift
new file mode 100644
index 0000000..379db8a
--- /dev/null
+++ b/Workouts/Views/Today/DayPickerSheet.swift
@@ -0,0 +1,98 @@
+//
+// DayPickerSheet.swift
+// Workouts
+//
+// Copyright 2026 Rouslan Zenetl. All Rights Reserved.
+//
+
+import SwiftUI
+import UIKit
+
+/// The calendar toolbar button's sheet: jumps the Today board to any day. Days with
+/// at least one logged workout carry a small dot. Built on `UICalendarView` rather
+/// than the SwiftUI graphical `DatePicker` solely because only the UIKit calendar
+/// supports per-day decorations. Picking a day (or "Today") dismisses immediately —
+/// this is navigation, not a form, so there's nothing to confirm.
+struct DayPickerSheet: View {
+ @Binding var selectedDate: Date
+ /// Day-precision (year/month/day) components of the days to decorate.
+ let workoutDays: Set
+
+ @Environment(\.dismiss) private var dismiss
+
+ var body: some View {
+ NavigationStack {
+ DayCalendarView(selectedDate: $selectedDate, workoutDays: workoutDays) {
+ dismiss()
+ }
+ .padding(.horizontal)
+ .navigationTitle("Go to Day")
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ ToolbarItem(placement: .topBarLeading) {
+ Button("Today") {
+ selectedDate = Date()
+ dismiss()
+ }
+ }
+ ToolbarItem(placement: .topBarTrailing) {
+ Button("Done") { dismiss() }
+ }
+ }
+ }
+ .presentationDetents([.medium, .large])
+ }
+}
+
+/// `UICalendarView` wrapper: single-date selection plus a dot decoration on each
+/// day in `workoutDays`.
+private struct DayCalendarView: UIViewRepresentable {
+ @Binding var selectedDate: Date
+ let workoutDays: Set
+ /// Called after a day is picked — the sheet dismisses itself here.
+ var onPick: () -> Void
+
+ func makeCoordinator() -> Coordinator { Coordinator(self) }
+
+ func makeUIView(context: Context) -> UICalendarView {
+ let view = UICalendarView()
+ view.calendar = Calendar.current
+ view.delegate = context.coordinator
+ let selection = UICalendarSelectionSingleDate(delegate: context.coordinator)
+ selection.setSelected(
+ Calendar.current.dateComponents([.year, .month, .day], from: selectedDate),
+ animated: false
+ )
+ view.selectionBehavior = selection
+ return view
+ }
+
+ func updateUIView(_ uiView: UICalendarView, context: Context) {
+ context.coordinator.parent = self
+ }
+
+ @MainActor
+ final class Coordinator: NSObject, UICalendarViewDelegate, UICalendarSelectionSingleDateDelegate {
+ var parent: DayCalendarView
+
+ init(_ parent: DayCalendarView) { self.parent = parent }
+
+ func calendarView(_ calendarView: UICalendarView,
+ decorationFor dateComponents: DateComponents) -> UICalendarView.Decoration? {
+ // The view hands back components richer than year/month/day (era, calendar,
+ // …) — reduce to day precision so set membership compares equal.
+ var day = DateComponents()
+ day.year = dateComponents.year
+ day.month = dateComponents.month
+ day.day = dateComponents.day
+ return parent.workoutDays.contains(day) ? .default(color: .systemGreen, size: .small) : nil
+ }
+
+ func dateSelection(_ selection: UICalendarSelectionSingleDate,
+ didSelectDate dateComponents: DateComponents?) {
+ guard let dateComponents, let date = Calendar.current.date(from: dateComponents) else { return }
+ parent.selectedDate = date
+ parent.onPick()
+ }
+ }
+}
diff --git a/Workouts/Views/Today/ScheduleAddEditView.swift b/Workouts/Views/Today/ScheduleAddEditView.swift
new file mode 100644
index 0000000..83a34df
--- /dev/null
+++ b/Workouts/Views/Today/ScheduleAddEditView.swift
@@ -0,0 +1,200 @@
+//
+// ScheduleAddEditView.swift
+// Workouts
+//
+// Copyright 2026 Rouslan Zenetl. All Rights Reserved.
+//
+
+import IndieSync
+import SwiftUI
+import SwiftData
+
+/// Add / edit a schedule: pick a routine, tag it with a goal, and set how often it
+/// recurs. Fields are snapshotted in `init` so the sheet never holds the live entity.
+struct ScheduleAddEditView: View {
+ @Environment(SyncEngine.self) private var sync
+ @Environment(\.modelContext) private var modelContext
+ @Environment(\.dismiss) private var dismiss
+
+ @Query(sort: [SortDescriptor(\Routine.order), SortDescriptor(\Routine.name)])
+ private var routines: [Routine]
+
+ /// nil id → add mode; a value → edit mode (id / order / createdAt preserved).
+ @State private var scheduleID: String?
+ @State private var order: Int
+ @State private var createdAt: Date
+ @State private var routineID: String
+ /// The schedule's routine name when it was created — surfaced when that routine
+ /// has since been deleted so the user knows what they're reassigning.
+ @State private var originalRoutineName: String
+ @State private var goal: GoalKind?
+ @State private var recurrence: ScheduleRecurrence
+ @State private var weekdays: Set
+ @State private var timesPerWeek: Int
+ @State private var date: Date
+
+ /// Calendar weekday numbers in Monday-first display order (Mon…Sat = 2…7, Sun = 1).
+ private let weekdayOrder = [2, 3, 4, 5, 6, 7, 1]
+ /// Gregorian short names, 0-indexed from Sunday — weekday `n` is `symbols[n - 1]`.
+ private let weekdaySymbols = Calendar(identifier: .gregorian).shortWeekdaySymbols
+
+ /// `defaultDate` seeds the `.once` day when adding — the Today board passes the day
+ /// currently on screen, so "navigate to a day, tap +" plans a one-off for that day.
+ init(schedule: Schedule? = nil, defaultDate: Date = Date()) {
+ _scheduleID = State(initialValue: schedule?.id)
+ _order = State(initialValue: schedule?.order ?? 0)
+ _createdAt = State(initialValue: schedule?.createdAt ?? Date())
+ _routineID = State(initialValue: schedule?.routineID ?? "")
+ _originalRoutineName = State(initialValue: schedule?.routineName ?? "")
+ _goal = State(initialValue: schedule?.goalKind)
+ _recurrence = State(initialValue: schedule?.recurrenceEnum ?? .once)
+ _weekdays = State(initialValue: Set(schedule?.weekdays ?? []))
+ _timesPerWeek = State(initialValue: schedule?.timesPerWeek ?? 3)
+ _date = State(initialValue: schedule?.date ?? defaultDate)
+ }
+
+ private var isEditing: Bool { scheduleID != nil }
+
+ /// The live routine currently selected; nil when the stored id no longer resolves.
+ private var selectedRoutine: Routine? { routines.first { $0.id == routineID } }
+
+ private var canSave: Bool {
+ guard selectedRoutine != nil else { return false }
+ if recurrence == .fixedDays { return !weekdays.isEmpty }
+ return true
+ }
+
+ var body: some View {
+ NavigationStack {
+ Form {
+ routineSection
+ goalSection
+ recurrenceSection
+ }
+ .navigationTitle(isEditing ? "Edit Schedule" : "New Schedule")
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbar {
+ ToolbarItem(placement: .navigationBarLeading) {
+ Button("Cancel") { dismiss() }
+ }
+ ToolbarItem(placement: .navigationBarTrailing) {
+ Button("Save", action: save).disabled(!canSave)
+ }
+ }
+ .onAppear {
+ // Default to the first routine when adding (the @Query isn't populated at init).
+ if routineID.isEmpty { routineID = routines.first?.id ?? "" }
+ }
+ }
+ }
+
+ // MARK: - Sections
+
+ private var routineSection: some View {
+ Section {
+ Picker("Routine", selection: $routineID) {
+ ForEach(routines) { routine in
+ Text(routine.name).tag(routine.id)
+ }
+ }
+ } header: {
+ Text("Routine")
+ } footer: {
+ if selectedRoutine == nil, !originalRoutineName.isEmpty {
+ Text("\"\(originalRoutineName)\" is no longer available. Choose a routine.")
+ }
+ }
+ }
+
+ private var goalSection: some View {
+ Section {
+ Picker("Goal", selection: $goal) {
+ Text("None").tag(GoalKind?.none)
+ ForEach(GoalKind.orderedCases()) { kind in
+ Label(kind.displayName, systemImage: kind.systemImage)
+ .foregroundStyle(Color.color(from: kind.colorName))
+ .tag(GoalKind?.some(kind))
+ }
+ }
+ .pickerStyle(.inline)
+ .labelsHidden()
+ } header: {
+ Text("Goal")
+ }
+ }
+
+ @ViewBuilder
+ private var recurrenceSection: some View {
+ Section {
+ Picker("Repeat", selection: $recurrence) {
+ ForEach(ScheduleRecurrence.allCases, id: \.self) { r in
+ Text(r.displayName).tag(r)
+ }
+ }
+
+ switch recurrence {
+ case .once:
+ DatePicker("Date", selection: $date, displayedComponents: .date)
+ case .daily:
+ EmptyView()
+ case .fixedDays:
+ weekdayPicker
+ case .frequency:
+ Stepper(value: $timesPerWeek, in: 1...7) {
+ Text("\(timesPerWeek)× per week")
+ }
+ }
+ } header: {
+ Text("Repeat")
+ }
+ }
+
+ /// A row of toggleable weekday chips, Monday-first.
+ private var weekdayPicker: some View {
+ HStack(spacing: 6) {
+ ForEach(weekdayOrder, id: \.self) { n in
+ let on = weekdays.contains(n)
+ Button {
+ if on { weekdays.remove(n) } else { weekdays.insert(n) }
+ } label: {
+ Text(weekdaySymbols[n - 1])
+ .font(.caption.weight(.semibold))
+ .frame(maxWidth: .infinity)
+ .padding(.vertical, 8)
+ .background(on ? Color.accentColor : Color.secondary.opacity(0.15), in: Capsule())
+ .foregroundStyle(on ? Color.white : Color.primary)
+ }
+ .buttonStyle(.plain)
+ }
+ }
+ .padding(.vertical, 4)
+ }
+
+ // MARK: - Save
+
+ private func save() {
+ guard let routine = selectedRoutine else { return }
+ let doc = ScheduleDocument(
+ schemaVersion: ScheduleDocument.currentSchemaVersion,
+ id: scheduleID ?? ULID.make(),
+ routineID: routine.id,
+ routineName: routine.name,
+ goal: goal?.rawValue,
+ recurrence: recurrence.rawValue,
+ weekdays: recurrence == .fixedDays ? weekdays.sorted() : nil,
+ timesPerWeek: recurrence == .frequency ? timesPerWeek : nil,
+ date: recurrence == .once ? date : nil,
+ order: isEditing ? order : nextOrder(),
+ createdAt: createdAt,
+ updatedAt: Date()
+ )
+ Task { await sync.save(schedule: doc) }
+ dismiss()
+ }
+
+ /// One past the highest existing schedule order — appends a new schedule to the end.
+ private func nextOrder() -> Int {
+ let existing = (try? modelContext.fetch(FetchDescriptor())) ?? []
+ return (existing.map(\.order).max() ?? -1) + 1
+ }
+}
diff --git a/Workouts/Views/Today/TodayView.swift b/Workouts/Views/Today/TodayView.swift
new file mode 100644
index 0000000..e655ffc
--- /dev/null
+++ b/Workouts/Views/Today/TodayView.swift
@@ -0,0 +1,293 @@
+//
+// TodayView.swift
+// Workouts
+//
+// Copyright 2026 Rouslan Zenetl. All Rights Reserved.
+//
+
+import SwiftUI
+import SwiftData
+
+/// The Today board — the today-projection of the user's schedules, grouped by goal.
+/// A schedule row shows its routine, its recurrence, and today's status for that
+/// routine; tapping starts (or opens) that routine's workout for today.
+struct TodayView: View {
+ @Environment(SyncEngine.self) private var sync
+ @Environment(AppServices.self) private var services
+
+ @Query(sort: [SortDescriptor(\Schedule.order), SortDescriptor(\Schedule.createdAt)])
+ private var schedules: [Schedule]
+
+ @Query(sort: [SortDescriptor(\Routine.order), SortDescriptor(\Routine.name)])
+ private var routines: [Routine]
+
+ @Query(sort: \Workout.start, order: .reverse)
+ private var workouts: [Workout]
+
+ /// ID of the workout to push into — a just-started one (the navigator polls for
+ /// its cache entity) or an existing today-workout (resolves immediately). One
+ /// id-based path for both, so no destination ever retains a `@Model`.
+ @State private var pendingWorkoutID: String?
+ @State private var scheduleToEdit: Schedule?
+ @State private var showingAddSchedule = false
+ @State private var showingDayPicker = false
+
+ /// The day the board is projecting — today by default; the calendar toolbar
+ /// button navigates to any other day.
+ @State private var selectedDate = Date()
+
+ private var isViewingToday: Bool { selectedDate.isSameDay(as: Date()) }
+
+ /// The viewed day is strictly before today (day-precision).
+ private var isViewingPastDay: Bool { selectedDate < Calendar.current.startOfDay(for: Date()) }
+
+ /// Schedules that belong on the viewed day's board: recurring ones always show
+ /// (the board displays their recurrence, not a due-filter — yet); a one-off shows
+ /// only on its own day. A dateless `.once` (shouldn't happen) shows everywhere
+ /// rather than nowhere.
+ private var visibleSchedules: [Schedule] {
+ schedules.filter { schedule in
+ guard schedule.recurrenceEnum == .once, let date = schedule.date else { return true }
+ return date.isSameDay(as: selectedDate)
+ }
+ }
+
+ /// Non-empty goal groups in the user's chosen goal order.
+ private var goalGroups: [(kind: GoalKind, schedules: [Schedule])] {
+ GoalKind.orderedCases().compactMap { kind in
+ let matches = visibleSchedules.filter { $0.goalKind == kind }
+ return matches.isEmpty ? nil : (kind, matches)
+ }
+ }
+
+ /// Schedules with no goal, or a goal raw value this version doesn't know.
+ private var unassignedSchedules: [Schedule] {
+ visibleSchedules.filter { $0.goalKind == nil }
+ }
+
+ /// Motivational openers for the board title — each reads as a phrase completed
+ /// by "Today" ("Seize Today"). One per day, rotating by day-of-era: stable all
+ /// day, fresh the next.
+ private static let greetings: [String] = [
+ "Seize",
+ "Own",
+ "Win",
+ "Conquer",
+ "Show Up",
+ "Stay the Course",
+ "Make It Count",
+ "Do the Work",
+ "Start Strong",
+ "No Excuses",
+ "Begin Again",
+ "Keep the Streak",
+ ]
+
+ private var greetingLine: String {
+ let day = Calendar.current.ordinality(of: .day, in: .era, for: Date()) ?? 0
+ return Self.greetings[day % Self.greetings.count]
+ }
+
+ var body: some View {
+ NavigationStack {
+ List {
+ ForEach(goalGroups, id: \.kind) { group in
+ Section {
+ ForEach(group.schedules) { scheduleRow($0) }
+ } header: {
+ Label(group.kind.displayName, systemImage: group.kind.systemImage)
+ .foregroundStyle(Color.color(from: group.kind.colorName))
+ }
+ }
+
+ if !unassignedSchedules.isEmpty {
+ Section("Unassigned") {
+ ForEach(unassignedSchedules) { scheduleRow($0) }
+ }
+ }
+ }
+ .overlay {
+ if schedules.isEmpty {
+ ContentUnavailableView(
+ "No Schedules Yet",
+ systemImage: "calendar.badge.plus",
+ description: Text("Plan your week by adding a schedule for a routine.")
+ )
+ } else if visibleSchedules.isEmpty {
+ ContentUnavailableView(
+ "Nothing Planned",
+ systemImage: "calendar",
+ description: Text("Nothing is scheduled for this day. Tap + to plan something.")
+ )
+ }
+ }
+ .navigationTitle(isViewingToday ? "\(greetingLine) Today" : selectedDate.formatDate())
+ .toolbar {
+ ToolbarItem(placement: .topBarLeading) {
+ Button {
+ showingDayPicker = true
+ } label: {
+ Label("Go to Day", systemImage: "calendar")
+ }
+ }
+ ToolbarItem(placement: .topBarTrailing) {
+ Button {
+ showingAddSchedule = true
+ } label: {
+ Label("Add Schedule", systemImage: "plus")
+ }
+ }
+ }
+ // Row taps land here for both cases: a fresh start (entity arrives a beat
+ // after the file write) and an existing today-workout (resolves at once).
+ .navigatesToStartedWorkout(pendingWorkoutID: $pendingWorkoutID)
+ .safeAreaInset(edge: .bottom) {
+ SyncStatusBanner()
+ .padding(.horizontal)
+ .padding(.bottom, 8)
+ }
+ .sheet(isPresented: $showingDayPicker) {
+ DayPickerSheet(selectedDate: $selectedDate, workoutDays: workoutDays)
+ }
+ .sheet(isPresented: $showingAddSchedule) {
+ // Seed a would-be one-off with the day on screen, so navigating to a
+ // day and tapping + plans that day.
+ ScheduleAddEditView(defaultDate: selectedDate)
+ }
+ .sheet(item: $scheduleToEdit) { schedule in
+ ScheduleAddEditView(schedule: schedule)
+ }
+ }
+ }
+
+ // MARK: - Row
+
+ private func scheduleRow(_ schedule: Schedule) -> some View {
+ let routine = resolvedRoutine(for: schedule)
+ let today = todaysWorkout(for: schedule)
+ return Button {
+ openOrStart(schedule: schedule, routine: routine, today: today)
+ } label: {
+ HStack(spacing: 12) {
+ Image(systemName: routine?.systemImage ?? "figure.strengthtraining.traditional")
+ .font(.title3)
+ .foregroundStyle(iconColor(routine: routine, schedule: schedule))
+ .frame(width: 32)
+
+ VStack(alignment: .leading, spacing: 2) {
+ Text(routine?.name ?? schedule.routineName)
+ .foregroundStyle(.primary)
+ Text(schedule.recurrenceSummary)
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+
+ Spacer()
+
+ if let today {
+ todayStatus(for: today)
+ } else if isViewingPastDay {
+ skippedTag
+ }
+ }
+ .contentShape(Rectangle())
+ }
+ .tint(.primary)
+ .swipeActions(edge: .trailing, allowsFullSwipe: false) {
+ Button(role: .destructive) {
+ Task { await sync.delete(schedule: schedule) }
+ } label: {
+ Label("Delete", systemImage: "trash")
+ }
+ .tint(.red)
+
+ Button {
+ scheduleToEdit = schedule
+ } label: {
+ Label("Edit", systemImage: "pencil")
+ }
+ }
+ }
+
+ /// Today's status decoration: a completed run shows a green check + its finish
+ /// time; an unfinished one shows an orange "In Progress" tag.
+ @ViewBuilder
+ private func todayStatus(for workout: Workout) -> some View {
+ if workout.status == .completed {
+ HStack(spacing: 4) {
+ Image(systemName: "checkmark.circle.fill")
+ .foregroundStyle(.green)
+ Text((workout.end ?? workout.start).formattedTime())
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
+ } else {
+ Text("In Progress")
+ .font(.caption2.weight(.semibold))
+ .foregroundStyle(.orange)
+ .padding(.horizontal, 8)
+ .padding(.vertical, 3)
+ .background(.orange.opacity(0.15), in: Capsule())
+ }
+ }
+
+ /// Signpost on a past-day schedule row with no logged workout. Deliberately
+ /// undifferentiated for now — a later pass will distinguish *abandoned* from an
+ /// explicit skip carrying an excuse ("sick day"); see UX-REDESIGN.md.
+ private var skippedTag: some View {
+ Text("Skipped")
+ .font(.caption2.weight(.semibold))
+ .foregroundStyle(.secondary)
+ .padding(.horizontal, 8)
+ .padding(.vertical, 3)
+ .background(.secondary.opacity(0.15), in: Capsule())
+ }
+
+ // MARK: - Resolution helpers
+
+ /// The live routine this schedule points at, following the seed clone-on-edit
+ /// redirect; nil when the routine has been deleted.
+ private func resolvedRoutine(for schedule: Schedule) -> Routine? {
+ let liveID = sync.currentRoutineID(for: schedule.routineID)
+ return routines.first { $0.id == liveID }
+ }
+
+ /// The most recent workout started on the viewed day for this schedule's routine
+ /// (comparing through the clone redirect on both sides). `workouts` is
+ /// start-descending, so the first match is the newest.
+ private func todaysWorkout(for schedule: Schedule) -> Workout? {
+ let liveRoutineID = sync.currentRoutineID(for: schedule.routineID)
+ return workouts.first { workout in
+ guard let rid = workout.routineID, workout.start.isSameDay(as: selectedDate) else { return false }
+ return sync.currentRoutineID(for: rid) == liveRoutineID
+ }
+ }
+
+ /// Day-precision (year/month/day) components of every day with at least one
+ /// logged workout — the day picker decorates these with a dot.
+ private var workoutDays: Set {
+ Set(workouts.map { Calendar.current.dateComponents([.year, .month, .day], from: $0.start) })
+ }
+
+ /// Row icon color: the routine's own color, or the goal's color as a generic
+ /// fallback when the routine is gone.
+ private func iconColor(routine: Routine?, schedule: Schedule) -> Color {
+ if let routine { return Color.color(from: routine.color) }
+ return Color.color(from: schedule.goalKind?.colorName ?? "")
+ }
+
+ /// Tap: open the viewed day's workout if one exists, otherwise start a fresh one
+ /// from the resolved routine — but only when the board is on today, since a start
+ /// always happens *now*. Does nothing on other days without a workout, or for an
+ /// orphaned schedule (no live routine).
+ private func openOrStart(schedule: Schedule, routine: Routine?, today: Workout?) {
+ if let today {
+ guard !today.isDeleted else { return }
+ pendingWorkoutID = today.id
+ } else if isViewingToday, let routine {
+ Task {
+ pendingWorkoutID = await WorkoutStarter.start(routine: routine, sync: sync)
+ }
+ }
+ }
+}
diff --git a/Workouts/Views/WorkoutLogs/ExerciseProgressView.swift b/Workouts/Views/WorkoutLogs/ExerciseProgressView.swift
index 0a8c9f9..0094622 100644
--- a/Workouts/Views/WorkoutLogs/ExerciseProgressView.swift
+++ b/Workouts/Views/WorkoutLogs/ExerciseProgressView.swift
@@ -40,7 +40,7 @@ struct ExerciseProgressView: View {
/// True when this run was auto-advanced into from the previous exercise (flow mode).
/// It suppresses the Ready page and begins the exercise immediately on appear — the
- /// whole point of a hands-free flowing split.
+ /// whole point of a hands-free flowing routine.
let enteredViaFlow: Bool
/// Invoked at the terminal rest's end in flow mode to hand off to the next exercise;
@@ -196,11 +196,11 @@ struct ExerciseProgressView: View {
/// appear, hands-free.
private var showsReady: Bool { !enteredViaFlow }
- /// This split runs as a continuous flow — finishing one exercise rests, then
+ /// This routine runs as a continuous flow — finishing one exercise rests, then
/// auto-advances into the next — rather than returning to the list per exercise.
private var autoAdvance: Bool { doc.autoAdvance == true }
- /// Rest length for this run: the split's per-split override, else the global default.
+ /// Rest length for this run: the routine's per-routine override, else the global default.
/// Governs both between-set rests and (in flow mode) the between-exercise rest.
private var effectiveRest: Int { max(1, doc.restSeconds ?? restSeconds) }
diff --git a/Workouts/Views/WorkoutLogs/ExerciseView.swift b/Workouts/Views/WorkoutLogs/ExerciseView.swift
index e1e8417..c9f5012 100644
--- a/Workouts/Views/WorkoutLogs/ExerciseView.swift
+++ b/Workouts/Views/WorkoutLogs/ExerciseView.swift
@@ -186,7 +186,7 @@ struct ExerciseView: View {
/// Apply edited machine settings: update the log's plan-time snapshot (persisted
/// with the workout), then write the same settings back to the originating
- /// split's exercise as the durable default — same sequencing as the workout
+ /// routine's exercise as the durable default — same sequencing as the workout
/// list's machine sheet.
private func applyMachineSettings(_ settings: [MachineSetting]) {
guard let i = doc.logs.firstIndex(where: { $0.id == logID }) else { return }
@@ -195,10 +195,10 @@ struct ExerciseView: View {
doc.updatedAt = Date()
let snapshot = doc
let exerciseName = doc.logs[i].exerciseName
- let splitID = doc.splitID
+ let routineID = doc.routineID
Task {
await sync.save(workout: snapshot)
- await sync.writeBackMachineSettings(settings, exerciseName: exerciseName, splitID: splitID)
+ await sync.writeBackMachineSettings(settings, exerciseName: exerciseName, routineID: routineID)
}
}
diff --git a/Workouts/Views/WorkoutLogs/PlanEditView.swift b/Workouts/Views/WorkoutLogs/PlanEditView.swift
index a63995b..e86b396 100644
--- a/Workouts/Views/WorkoutLogs/PlanEditView.swift
+++ b/Workouts/Views/WorkoutLogs/PlanEditView.swift
@@ -163,13 +163,13 @@ struct PlanEditView: View {
let exerciseName = doc.logs[i].exerciseName
let workoutDoc = doc
- // 2) Mirror the plan onto the matching exercise in the split template,
+ // 2) Mirror the plan onto the matching exercise in the routine template,
// following the seed clone-on-edit redirect so a prior fork doesn't
// silently break the mirror.
- var splitDoc: SplitDocument?
- if let splitID = doc.splitID,
- let split = CacheMapper.fetchSplit(id: sync.currentSplitID(for: splitID), in: modelContext) {
- var sDoc = SplitDocument(from: split)
+ var routineDoc: RoutineDocument?
+ if let routineID = doc.routineID,
+ let routine = CacheMapper.fetchRoutine(id: sync.currentRoutineID(for: routineID), in: modelContext) {
+ var sDoc = RoutineDocument(from: routine)
if let ei = sDoc.exercises.firstIndex(where: { $0.name == exerciseName }) {
sDoc.exercises[ei].sets = sets
sDoc.exercises[ei].reps = reps
@@ -177,14 +177,14 @@ struct PlanEditView: View {
sDoc.exercises[ei].durationSeconds = totalSeconds
sDoc.exercises[ei].loadType = selectedLoadType.rawValue
sDoc.updatedAt = Date()
- splitDoc = sDoc
+ routineDoc = sDoc
}
}
Task {
await sync.save(workout: workoutDoc)
- if let splitDoc {
- await sync.save(split: splitDoc)
+ if let routineDoc {
+ await sync.save(routine: routineDoc)
}
}
}
diff --git a/Workouts/Views/WorkoutLogs/RunFlowView.swift b/Workouts/Views/WorkoutLogs/RunFlowView.swift
index 201bb43..57db4c9 100644
--- a/Workouts/Views/WorkoutLogs/RunFlowView.swift
+++ b/Workouts/Views/WorkoutLogs/RunFlowView.swift
@@ -8,12 +8,12 @@
import SwiftUI
/// Thin coordinator that lets a single pushed run walk across exercises in a flow-mode
-/// split (`autoAdvance`). It owns which log is on screen and swaps it when the running
+/// routine (`autoAdvance`). It owns which log is on screen and swaps it when the running
/// `ExerciseProgressView` finishes an exercise; `.id(currentLogID)` rebuilds the child
/// fresh for each exercise, so the delicate per-exercise run/mirror machinery stays
/// exactly as it is — this only automates the "open the next exercise" step.
///
-/// For a non-flow split nothing ever calls `advance`, so this is transparent: it presents
+/// For a non-flow routine nothing ever calls `advance`, so this is transparent: it presents
/// one exercise and behaves identically to hosting `ExerciseProgressView` directly.
struct RunFlowView: View {
@Environment(LiveRunState.self) private var liveRun
diff --git a/Workouts/Views/WorkoutLogs/WorkoutHistoryView.swift b/Workouts/Views/WorkoutLogs/WorkoutHistoryView.swift
new file mode 100644
index 0000000..9f00db0
--- /dev/null
+++ b/Workouts/Views/WorkoutLogs/WorkoutHistoryView.swift
@@ -0,0 +1,101 @@
+//
+// WorkoutHistoryView.swift
+// Workouts
+//
+// Created by rzen on 7/13/25 at 6:52 PM.
+//
+// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
+//
+
+import SwiftUI
+import SwiftData
+
+/// The full workout history list. Formerly the "Log" tab's root (`WorkoutLogsView`);
+/// now pushed from the Progress tab, which absorbed history in the four-tab redesign.
+struct WorkoutHistoryView: View {
+ @Environment(SyncEngine.self) private var sync
+ @Environment(AppServices.self) private var services
+
+ @Query(sort: \Workout.start, order: .reverse)
+ private var workouts: [Workout]
+
+ @State private var itemToDelete: Workout?
+
+ var body: some View {
+ List {
+ ForEach(workouts) { workout in
+ NavigationLink {
+ WorkoutLogListView(workout: workout)
+ } label: {
+ CalendarListItem(
+ date: workout.start,
+ title: workout.routineName ?? Routine.unnamed,
+ subtitle: getSubtitle(for: workout),
+ subtitle2: workout.statusName
+ )
+ }
+ .swipeActions(edge: .trailing, allowsFullSwipe: false) {
+ Button {
+ itemToDelete = workout
+ } label: {
+ Label("Delete", systemImage: "trash")
+ }
+ .tint(.red)
+ }
+ }
+ }
+ .overlay {
+ if workouts.isEmpty {
+ ContentUnavailableView(
+ "No Workouts Yet",
+ systemImage: "list.bullet.clipboard",
+ description: Text("Start a new workout from one of your routines.")
+ )
+ }
+ }
+ .navigationTitle("History")
+ // Write-queue banner tiers (calm "pending" capsule / prominent fault).
+ // On the log screen so a stalled save is visible where editing happens.
+ .safeAreaInset(edge: .bottom) {
+ SyncStatusBanner()
+ }
+ .confirmationDialog(
+ "Delete Workout?",
+ isPresented: Binding(
+ get: { itemToDelete != nil },
+ set: { if !$0 { itemToDelete = nil } }
+ ),
+ titleVisibility: .visible,
+ presenting: itemToDelete
+ ) { workout in
+ Button("Delete", role: .destructive) {
+ Task { await sync.delete(workout: workout) }
+ itemToDelete = nil
+ }
+ // Only offered when the workout actually has a Health record. Capture
+ // the UUID before the delete prunes the cache entity.
+ if let healthUUID = workout.metricHealthKitWorkoutUUID {
+ Button("Delete + Remove from Apple Health", role: .destructive) {
+ services.workoutHealthDeleter.deleteFromHealth(uuidString: healthUUID)
+ Task { await sync.delete(workout: workout) }
+ itemToDelete = nil
+ }
+ }
+ Button("Cancel", role: .cancel) {
+ itemToDelete = nil
+ }
+ } message: { workout in
+ if workout.metricSourceRaw == MetricSource.watch.rawValue {
+ Text("This workout was recorded by Apple Watch; if it can't be removed from Apple Health here, delete it in the Health app.")
+ }
+ }
+ }
+
+ private func getSubtitle(for workout: Workout) -> String {
+ if workout.status == .completed, let endDate = workout.end {
+ return workout.start.humanTimeInterval(to: endDate)
+ } else {
+ return workout.start.formattedDate()
+ }
+ }
+}
diff --git a/Workouts/Views/WorkoutLogs/WorkoutLogListView.swift b/Workouts/Views/WorkoutLogs/WorkoutLogListView.swift
index cf45774..9374596 100644
--- a/Workouts/Views/WorkoutLogs/WorkoutLogListView.swift
+++ b/Workouts/Views/WorkoutLogs/WorkoutLogListView.swift
@@ -55,12 +55,12 @@ struct WorkoutLogListView: View {
doc.logs.sorted { $0.order < $1.order }
}
- /// The split this workout was started from (for adding more exercises),
+ /// The routine this workout was started from (for adding more exercises),
/// following the seed clone-on-edit redirect so a mid-workout fork still
/// resolves to the live clone.
- private var split: Split? {
- guard let splitID = doc.splitID else { return nil }
- return CacheMapper.fetchSplit(id: sync.currentSplitID(for: splitID), in: modelContext)
+ private var routine: Routine? {
+ guard let routineID = doc.routineID else { return nil }
+ return CacheMapper.fetchRoutine(id: sync.currentRoutineID(for: routineID), in: modelContext)
}
var body: some View {
@@ -166,7 +166,7 @@ struct WorkoutLogListView: View {
// doc so it shows the latest local state immediately.
ExerciseView(workout: workout, logID: route.id, seedDoc: doc)
}
- .navigationTitle(doc.splitName ?? Split.unnamed)
+ .navigationTitle(doc.routineName ?? Routine.unnamed)
// Absorb edits made in pushed children (ExerciseView/Plan/Notes) once the
// cache reflects them, so the list shows live status on return.
.onChange(of: workout.updatedAt) { _, _ in
@@ -185,12 +185,12 @@ struct WorkoutLogListView: View {
}
}
.sheet(isPresented: $showingAddSheet) {
- SplitExercisePickerSheet(
- split: split,
+ RoutineExercisePickerSheet(
+ routine: routine,
existingExerciseNames: Set(sortedLogs.map { $0.exerciseName })
) { selection in
switch selection {
- case .fromSplit(let exercise): addExerciseFromSplit(exercise)
+ case .fromRoutine(let exercise): addExerciseFromRoutine(exercise)
case .fromLibrary(let name): addExerciseFromLibrary(name)
}
}
@@ -246,7 +246,7 @@ struct WorkoutLogListView: View {
/// (so the propped-phone mirror cover doesn't stack on top of it).
@ViewBuilder
private func progressView(logID: String) -> some View {
- // RunFlowView hosts the run and, in a flow-mode split, auto-advances across
+ // RunFlowView hosts the run and, in a flow-mode routine, auto-advances across
// exercises. It owns the live-run mirror wiring (incoming frame filter,
// `navigatedRunID`, and the parameterized live-ended signal) since the on-screen
// log advances during a flowing run.
@@ -321,11 +321,11 @@ struct WorkoutLogListView: View {
save()
}
- private func addExerciseFromSplit(_ exercise: Exercise) {
+ private func addExerciseFromRoutine(_ exercise: Exercise) {
appendExercise(ExerciseDocument(from: exercise))
}
- /// Add an exercise that isn't part of this workout's split, picked from the full
+ /// Add an exercise that isn't part of this workout's routine, picked from the full
/// exercise library. Seeds a plan from, in priority order: the most recent logged
/// set of this exercise (any workout), the library's authored `**Defaults:**`, or
/// a plain 3×10 fallback when neither exists.
@@ -334,7 +334,7 @@ struct WorkoutLogListView: View {
}
/// Append a freshly-planned exercise to the working doc, persist, and push
- /// straight into its progress flow. Shared tail of `addExerciseFromSplit` and
+ /// straight into its progress flow. Shared tail of `addExerciseFromRoutine` and
/// `addExerciseFromLibrary`.
private func appendExercise(_ plan: ExerciseDocument) {
let now = Date()
@@ -354,7 +354,7 @@ struct WorkoutLogListView: View {
addedLog = LogRoute(id: newLog.id)
}
- /// Plan defaults for a library exercise with no split entry: copy the most recent
+ /// Plan defaults for a library exercise with no routine entry: copy the most recent
/// logged set of this exercise (sets/reps/weight/loadType/duration and machine
/// settings) if one exists anywhere; otherwise the library's authored defaults
/// (weight always starts at 0 — there's no prior lift to copy); otherwise a plain
@@ -396,9 +396,9 @@ struct WorkoutLogListView: View {
/// Apply edited machine settings from the sheet: update the log's plan-time
/// snapshot (persisted with the workout), then write the same settings back to
- /// the originating split's exercise as the durable default. Sequenced in one
- /// task so the split's clone-on-edit repoint (which rewrites this workout's
- /// `splitID`) sees the log edit already persisted rather than clobbering it.
+ /// the originating routine's exercise as the durable default. Sequenced in one
+ /// task so the routine's clone-on-edit repoint (which rewrites this workout's
+ /// `routineID`) sees the log edit already persisted rather than clobbering it.
private func applyMachineSettings(_ settings: [MachineSetting], toLogID id: String) {
guard let i = doc.logs.firstIndex(where: { $0.id == id }) else { return }
doc.logs[i].machineSettings = settings
@@ -408,10 +408,10 @@ struct WorkoutLogListView: View {
let workoutSnapshot = doc
let exerciseName = doc.logs[i].exerciseName
- let splitID = doc.splitID
+ let routineID = doc.routineID
Task {
await sync.save(workout: workoutSnapshot)
- await sync.writeBackMachineSettings(settings, exerciseName: exerciseName, splitID: splitID)
+ await sync.writeBackMachineSettings(settings, exerciseName: exerciseName, routineID: routineID)
}
}
@@ -579,12 +579,12 @@ private struct WorkoutLogRow: View {
}
}
-// MARK: - Split Exercise Picker Sheet
+// MARK: - Routine Exercise Picker Sheet
-struct SplitExercisePickerSheet: View {
+struct RoutineExercisePickerSheet: View {
@Environment(\.dismiss) private var dismiss
- let split: Split?
+ let routine: Routine?
let existingExerciseNames: Set
let onSelected: (Selection) -> Void
@AppStorage("weightUnit") private var weightUnit: WeightUnit = .lb
@@ -593,31 +593,31 @@ struct SplitExercisePickerSheet: View {
/// library section's name list is stable for the sheet's lifetime.
@State private var libraryInfoByName: [String: ExerciseInfo] = [:]
- /// A pick from either source: the split path hands back the live `Exercise`
+ /// A pick from either source: the routine path hands back the live `Exercise`
/// entity (as today), the library path just the exercise name.
enum Selection {
- case fromSplit(Exercise)
+ case fromRoutine(Exercise)
case fromLibrary(String)
}
- /// Section 1 — this workout's split, minus exercises already added.
- private var splitExercises: [Exercise] {
- guard let split else { return [] }
- return split.exercisesArray.filter { !existingExerciseNames.contains($0.name) }
+ /// Section 1 — this workout's routine, minus exercises already added.
+ private var routineExercises: [Exercise] {
+ guard let routine else { return [] }
+ return routine.exercisesArray.filter { !existingExerciseNames.contains($0.name) }
}
/// Section 2 — every library exercise not already in the workout and not
/// already offered in section 1.
private var libraryExerciseNames: [String] {
- let shown = Set(splitExercises.map(\.name))
+ let shown = Set(routineExercises.map(\.name))
return ExerciseMotionLibrary.exerciseNames.filter {
!existingExerciseNames.contains($0) && !shown.contains($0)
}
}
- private var filteredSplitExercises: [Exercise] {
- guard !searchText.isEmpty else { return splitExercises }
- return splitExercises.filter { $0.name.localizedCaseInsensitiveContains(searchText) }
+ private var filteredRoutineExercises: [Exercise] {
+ guard !searchText.isEmpty else { return routineExercises }
+ return routineExercises.filter { $0.name.localizedCaseInsensitiveContains(searchText) }
}
private var filteredLibraryNames: [String] {
@@ -628,7 +628,7 @@ struct SplitExercisePickerSheet: View {
/// Whether there's anything left to add at all — independent of the current
/// search, so a no-results search doesn't get confused with a truly empty sheet.
private var hasAnyExercises: Bool {
- !splitExercises.isEmpty || !libraryExerciseNames.isEmpty
+ !routineExercises.isEmpty || !libraryExerciseNames.isEmpty
}
/// "3 × 12" / "3 × 30 s" — the library's suggested plan for a row's caption.
@@ -648,15 +648,15 @@ struct SplitExercisePickerSheet: View {
systemImage: "checkmark.circle",
description: Text("Every exercise is already part of this workout.")
)
- } else if filteredSplitExercises.isEmpty && filteredLibraryNames.isEmpty {
+ } else if filteredRoutineExercises.isEmpty && filteredLibraryNames.isEmpty {
ContentUnavailableView.search(text: searchText)
} else {
List {
- if !filteredSplitExercises.isEmpty, let split {
- Section("From \(split.name)") {
- ForEach(filteredSplitExercises) { exercise in
+ if !filteredRoutineExercises.isEmpty, let routine {
+ Section("From \(routine.name)") {
+ ForEach(filteredRoutineExercises) { exercise in
Button {
- onSelected(.fromSplit(exercise))
+ onSelected(.fromRoutine(exercise))
dismiss()
} label: {
HStack {
@@ -730,7 +730,7 @@ struct SplitExercisePickerSheet: View {
/// machine-based log (non-nil `machineSettings`), so there's no machine toggle —
/// just the ordered rows (reusing `MachineSettingsEditor`) plus Save/Cancel. Save
/// hands the edited array back to the caller, which persists it to the log and
-/// writes it back to the split's exercise as the new default. Shared with
+/// writes it back to the routine's exercise as the new default. Shared with
/// `ExerciseView`'s Machine Settings section.
struct MachineSettingsSheet: View {
@Environment(\.dismiss) private var dismiss
@@ -750,7 +750,7 @@ struct MachineSettingsSheet: View {
Form {
Section(
header: Text("Settings"),
- footer: Text("Saved to this workout and set as the default for \(exerciseName) in its split.")
+ footer: Text("Saved to this workout and set as the default for \(exerciseName) in its routine.")
) {
MachineSettingsEditor(settings: $settings)
}
diff --git a/Workouts/Views/WorkoutLogs/WorkoutLogsView.swift b/Workouts/Views/WorkoutLogs/WorkoutLogsView.swift
deleted file mode 100644
index 855f9bd..0000000
--- a/Workouts/Views/WorkoutLogs/WorkoutLogsView.swift
+++ /dev/null
@@ -1,381 +0,0 @@
-//
-// WorkoutLogsView.swift
-// Workouts
-//
-// Created by rzen on 7/13/25 at 6:52 PM.
-//
-// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
-//
-
-import IndieSync
-import SwiftUI
-import SwiftData
-
-struct WorkoutLogsView: View {
- @Environment(SyncEngine.self) private var sync
- @Environment(AppServices.self) private var services
-
- @Query(sort: \Workout.start, order: .reverse)
- private var workouts: [Workout]
-
- @State private var showingSplitPicker = false
- @State private var showingSettings = false
- @State private var itemToDelete: Workout?
- /// ID of the just-started workout; drives the push into its log screen once the
- /// cache observer delivers the entity a beat after the file write (see
- /// `navigatesToStartedWorkout`).
- @State private var pendingWorkoutID: String?
-
- // WorkoutLogsView is the app's root screen, so it owns its NavigationStack.
- var body: some View {
- NavigationStack {
- List {
- ForEach(workouts) { workout in
- NavigationLink {
- WorkoutLogListView(workout: workout)
- } label: {
- CalendarListItem(
- date: workout.start,
- title: workout.splitName ?? Split.unnamed,
- subtitle: getSubtitle(for: workout),
- subtitle2: workout.statusName
- )
- }
- .swipeActions(edge: .trailing, allowsFullSwipe: false) {
- Button {
- itemToDelete = workout
- } label: {
- Label("Delete", systemImage: "trash")
- }
- .tint(.red)
- }
- }
- }
- .overlay {
- if workouts.isEmpty {
- ContentUnavailableView(
- "No Workouts Yet",
- systemImage: "list.bullet.clipboard",
- description: Text("Start a new workout from one of your splits.")
- )
- }
- }
- .navigationTitle("Workout Logs")
- // Write-queue banner tiers (calm "pending" capsule / prominent fault).
- // On the root screen so a stalled save is visible where editing happens.
- .safeAreaInset(edge: .bottom) {
- SyncStatusBanner()
- }
- .toolbar {
- ToolbarItem(placement: .navigationBarLeading) {
- Button {
- showingSettings = true
- } label: {
- Image(systemName: "gearshape.2")
- }
- .accessibilityLabel("Settings")
- }
- ToolbarItem(placement: .navigationBarTrailing) {
- Button("Start New") {
- showingSplitPicker.toggle()
- }
- }
- }
- .sheet(isPresented: $showingSettings) {
- SettingsView()
- }
- .sheet(isPresented: $showingSplitPicker) {
- SplitPickerSheet { pendingWorkoutID = $0 }
- }
- // Once "Start New" saves a workout, drop straight into its log screen —
- // the same landing the split's exercise-list start path uses.
- .navigatesToStartedWorkout(pendingWorkoutID: $pendingWorkoutID)
- .confirmationDialog(
- "Delete Workout?",
- isPresented: Binding(
- get: { itemToDelete != nil },
- set: { if !$0 { itemToDelete = nil } }
- ),
- titleVisibility: .visible,
- presenting: itemToDelete
- ) { workout in
- Button("Delete", role: .destructive) {
- Task { await sync.delete(workout: workout) }
- itemToDelete = nil
- }
- // Only offered when the workout actually has a Health record. Capture
- // the UUID before the delete prunes the cache entity.
- if let healthUUID = workout.metricHealthKitWorkoutUUID {
- Button("Delete + Remove from Apple Health", role: .destructive) {
- services.workoutHealthDeleter.deleteFromHealth(uuidString: healthUUID)
- Task { await sync.delete(workout: workout) }
- itemToDelete = nil
- }
- }
- Button("Cancel", role: .cancel) {
- itemToDelete = nil
- }
- } message: { workout in
- if workout.metricSourceRaw == MetricSource.watch.rawValue {
- Text("This workout was recorded by Apple Watch; if it can't be removed from Apple Health here, delete it in the Health app.")
- }
- }
- }
- }
-
- private func getSubtitle(for workout: Workout) -> String {
- if workout.status == .completed, let endDate = workout.end {
- return workout.start.humanTimeInterval(to: endDate)
- } else {
- return workout.start.formattedDate()
- }
- }
-}
-
-// MARK: - Split Picker Sheet
-
-struct SplitPickerSheet: View {
- @Environment(SyncEngine.self) private var sync
- @Environment(AppServices.self) private var services
- @Environment(\.dismiss) private var dismiss
-
- /// Called with the new workout's id right after it's saved, so the presenting
- /// screen can navigate into it once the cache catches up.
- var onStarted: (String) -> Void = { _ in }
-
- @Query(sort: [SortDescriptor(\Split.order), SortDescriptor(\Split.name)])
- private var splits: [Split]
-
- @Query(sort: \Workout.start, order: .reverse)
- private var workouts: [Workout]
-
- /// Set when the user picks a split while other workouts are still going — drives the
- /// "end the current one(s) or run in parallel?" prompt.
- @State private var splitAwaitingConfirmation: Split?
-
- private var activeWorkouts: [Workout] {
- workouts.filter { $0.status == .inProgress || $0.status == .notStarted }
- }
-
- /// The splits most recently trained, newest first, each with the start date
- /// of its latest workout — derived from the workout log (`workouts` is
- /// already sorted by start descending). Each workout's `splitID` follows the
- /// seed clone-on-edit redirect so a workout started from a since-forked seed
- /// still credits the live clone. Deduped, capped, and limited to splits that
- /// still exist.
- private var recentSplits: [(split: Split, lastStart: Date)] {
- var seen = Set()
- var recents: [(split: Split, lastStart: Date)] = []
- for workout in workouts {
- guard let splitID = workout.splitID else { continue }
- let liveID = sync.currentSplitID(for: splitID)
- guard !seen.contains(liveID) else { continue }
- seen.insert(liveID)
- if let split = splits.first(where: { $0.id == liveID }) {
- recents.append((split, workout.start))
- if recents.count == 3 { break }
- }
- }
- return recents
- }
-
- /// One selectable split row, shared by the Recent and All Splits sections.
- /// Recent rows pass `lastTrained` to show how long ago the split was run.
- private func splitRow(_ split: Split, lastTrained: Date? = nil) -> some View {
- Button {
- confirmAndStart(with: split)
- } label: {
- HStack {
- Image(systemName: split.systemImage)
- .foregroundColor(Color.color(from: split.color))
- .frame(width: 28)
- VStack(alignment: .leading, spacing: 2) {
- Text(split.name)
- if let lastTrained {
- Text(lastTrained.daysAgoLabel())
- .font(.caption)
- .foregroundColor(.secondary)
- }
- }
- Spacer()
- Text("\(split.exercisesArray.count) exercises")
- .font(.caption)
- .foregroundColor(.secondary)
- }
- }
- }
-
- var body: some View {
- NavigationStack {
- List {
- if !recentSplits.isEmpty {
- Section("Recent") {
- ForEach(recentSplits, id: \.split.id) { recent in
- splitRow(recent.split, lastTrained: recent.lastStart)
- }
- }
- }
-
- Section(recentSplits.isEmpty ? "" : "All Splits") {
- ForEach(splits) { split in
- splitRow(split)
- }
- }
- }
- .navigationTitle("Select a Split")
- .toolbar {
- ToolbarItem(placement: .navigationBarLeading) {
- Button("Cancel") {
- dismiss()
- }
- }
- }
- .confirmationDialog(
- activePromptTitle,
- isPresented: Binding(
- get: { splitAwaitingConfirmation != nil },
- set: { if !$0 { splitAwaitingConfirmation = nil } }
- ),
- titleVisibility: .visible,
- presenting: splitAwaitingConfirmation
- ) { split in
- Button("End Current & Start New") { endActiveThenStart(with: split) }
- Button("Start in Parallel") { start(with: split) }
- Button("Cancel", role: .cancel) { splitAwaitingConfirmation = nil }
- } message: { _ in
- Text(activePromptMessage)
- }
- }
- }
-
- private var activePromptTitle: String {
- activeWorkouts.count == 1 ? "Workout in Progress" : "\(activeWorkouts.count) Workouts in Progress"
- }
-
- private var activePromptMessage: String {
- let n = activeWorkouts.count
- let those = n == 1 ? "it" : "them"
- return "You already have \(n == 1 ? "a workout" : "\(n) workouts") going. End \(those) first, or run this one alongside."
- }
-
- /// Prompt before starting if other workouts are still going; otherwise start straight away.
- private func confirmAndStart(with split: Split) {
- if activeWorkouts.isEmpty {
- start(with: split)
- } else {
- splitAwaitingConfirmation = split
- }
- }
-
- /// End every in-flight workout (keeping its progress), then start the picked split.
- private func endActiveThenStart(with split: Split) {
- let toEnd = activeWorkouts.map { WorkoutDocument(from: $0) }
- splitAwaitingConfirmation = nil
- Task {
- for var doc in toEnd {
- doc.endKeepingProgress()
- await sync.save(workout: doc)
- }
- }
- start(with: split)
- }
-
- private func start(with split: Split) {
- let startDate = Date()
- let logs = split.exercisesArray.enumerated().map { index, exercise in
- WorkoutLogDocument(planFrom: ExerciseDocument(from: exercise), order: index, date: startDate)
- }
-
- // A freshly started workout has no `end` — only completion stamps it.
- let doc = WorkoutDocument(
- schemaVersion: WorkoutDocument.currentSchemaVersion,
- id: ULID.make(),
- splitID: split.id,
- splitName: split.name,
- start: startDate,
- end: nil,
- status: WorkoutStatus.notStarted.rawValue,
- createdAt: startDate,
- updatedAt: startDate,
- logs: logs,
- restSeconds: split.restSeconds, autoAdvance: split.autoAdvance
- )
-
- // Hand the id back after the save so the presenter can poll the cache and
- // navigate into the run — mirroring the exercise-list start path.
- Task {
- await sync.save(workout: doc)
- onStarted(doc.id)
- }
- // Bring the Apple Watch up into the session so the user can run it from the wrist,
- // tagged with the split's activity type so the saved Health workout is categorized.
- services.workoutLauncher.launchWatchWorkout(activityType: split.activityTypeEnum.hkActivityType)
- dismiss()
- }
-}
-
-// MARK: - Started-Workout Navigation
-
-/// Drives a programmatic push into a freshly-started workout's log screen. Both start
-/// paths — the split picker sheet and a split's exercise list — mint a `WorkoutDocument`,
-/// write it, then hand its id here; the workout row only materializes once the
-/// file→observer→cache loop round-trips, so we poll the cache for the entity and push it
-/// the moment it lands.
-private struct StartedWorkoutNavigator: ViewModifier {
- /// The pushed state is a plain value wrapping the run's id — never a retained
- /// `@Model`. This screen stays pushed for the whole workout, and the run's entity
- /// can be deleted and re-imported underneath it (observer remove/re-add churn, a
- /// remote delete); a `@Model` held in `@State` would be *invalidated* by then —
- /// deleted-and-saved models read `isDeleted == false` yet trap on any property
- /// access (the TestFlight 2.3 (125) "crashed when watch ended an exercise" report).
- private struct RunRoute: Identifiable, Hashable { let id: String }
-
- @Environment(\.modelContext) private var modelContext
- @Binding var pendingWorkoutID: String?
- @State private var resolvedRun: RunRoute?
-
- func body(content: Content) -> some View {
- content
- .navigationDestination(item: $resolvedRun) { route in
- // Re-fetch on every build so the destination always maps a live
- // entity; a re-imported run resolves to its fresh instance.
- if let workout = CacheMapper.fetchWorkout(id: route.id, in: modelContext) {
- WorkoutLogListView(workout: workout)
- } else {
- ContentUnavailableView(
- "Workout Unavailable",
- systemImage: "xmark.circle",
- description: Text("This workout is no longer on this device."))
- }
- }
- .onChange(of: pendingWorkoutID) { _, id in
- guard let id else { return }
- pollForWorkout(id: id)
- }
- }
-
- /// Poll for the entity after we write the document (the file→observer→cache loop
- /// typically completes in well under a second). Clear the pending id once it
- /// resolves, or silently after ~3 s if it never arrives.
- private func pollForWorkout(id: String) {
- Task {
- for _ in 0..<20 {
- try? await Task.sleep(for: .milliseconds(150))
- if CacheMapper.fetchWorkout(id: id, in: modelContext) != nil {
- resolvedRun = RunRoute(id: id)
- pendingWorkoutID = nil
- return
- }
- }
- pendingWorkoutID = nil
- }
- }
-}
-
-extension View {
- /// Navigate into a workout's log screen once it appears in the cache after being
- /// started. Bind to the state you set to the new workout's id right after saving it.
- func navigatesToStartedWorkout(pendingWorkoutID: Binding) -> some View {
- modifier(StartedWorkoutNavigator(pendingWorkoutID: pendingWorkoutID))
- }
-}
diff --git a/Workouts/Views/WorkoutLogs/WorkoutSummaryView.swift b/Workouts/Views/WorkoutLogs/WorkoutSummaryView.swift
index 49d8602..8a9403e 100644
--- a/Workouts/Views/WorkoutLogs/WorkoutSummaryView.swift
+++ b/Workouts/Views/WorkoutLogs/WorkoutSummaryView.swift
@@ -33,7 +33,7 @@ struct WorkoutSummaryView: View {
.font(.system(size: 48))
.foregroundStyle(.green)
.padding(.top, 8)
- Text(workout.splitName ?? Split.unnamed)
+ Text(workout.routineName ?? Routine.unnamed)
.font(.title2.bold())
WorkoutMetricsView(workout: workout)
diff --git a/WorkoutsTests/DuplicateCleanupPlannerTests.swift b/WorkoutsTests/DuplicateCleanupPlannerTests.swift
index 1fdedf7..6908daf 100644
--- a/WorkoutsTests/DuplicateCleanupPlannerTests.swift
+++ b/WorkoutsTests/DuplicateCleanupPlannerTests.swift
@@ -3,7 +3,7 @@ import Testing
@testable import Workouts
/// Locks the survivor rules of `DuplicateCleanupPlanner` — the safety-critical
-/// part of the developer duplicate-cleanup tool. Referenced-split and seed
+/// part of the developer duplicate-cleanup tool. Referenced-routine and seed
/// protection must always beat "earliest ULID wins," an in-progress workout must
/// never be touched, and the HealthKit-link holder must always survive (deleting
/// it would cascade-delete the real Health sample). Fixed 26-char ULID-like ids
@@ -26,12 +26,12 @@ struct DuplicateCleanupPlannerTests {
)
}
- private func split(
- id splitID: String, name: String = "Push Day", exercises: [ExerciseDocument]? = nil,
+ private func routine(
+ id routineID: String, name: String = "Push Day", exercises: [ExerciseDocument]? = nil,
createdAt: Date = Date(timeIntervalSince1970: 0)
- ) -> SplitDocument {
- SplitDocument(
- schemaVersion: SplitDocument.currentSchemaVersion, id: splitID, name: name, color: "indigo",
+ ) -> RoutineDocument {
+ RoutineDocument(
+ schemaVersion: RoutineDocument.currentSchemaVersion, id: routineID, name: name, color: "indigo",
systemImage: "dumbbell.fill", order: 0, createdAt: createdAt, updatedAt: createdAt,
exercises: exercises ?? [exercise()], activityType: nil
)
@@ -46,13 +46,13 @@ struct DuplicateCleanupPlannerTests {
}
private func workout(
- id workoutID: String, splitID: String? = "SPLIT-1", splitName: String? = "Push Day",
+ id workoutID: String, routineID: String? = "RT-1", routineName: String? = "Push Day",
start: Date, status: String = WorkoutStatus.completed.rawValue,
logs: [WorkoutLogDocument]? = nil, metrics: WorkoutMetrics? = nil
) -> WorkoutDocument {
WorkoutDocument(
- schemaVersion: WorkoutDocument.currentSchemaVersion, id: workoutID, splitID: splitID,
- splitName: splitName, start: start, end: nil, status: status, createdAt: start, updatedAt: start,
+ schemaVersion: WorkoutDocument.currentSchemaVersion, id: workoutID, routineID: routineID,
+ routineName: routineName, start: start, end: nil, status: status, createdAt: start, updatedAt: start,
logs: logs ?? [log(date: start)], metrics: metrics
)
}
@@ -77,56 +77,56 @@ struct DuplicateCleanupPlannerTests {
)
}
- // MARK: - Split survivor rules
+ // MARK: - Routine survivor rules
- @Test func identicalUnreferencedSplitsKeepEarliestULID() {
- let earlier = split(id: id(1))
- let later = split(id: id(2))
+ @Test func identicalUnreferencedRoutinesKeepEarliestULID() {
+ let earlier = routine(id: id(1))
+ let later = routine(id: id(2))
let plan = DuplicateCleanupPlanner.plan(
- splits: [later, earlier], workouts: [], referencedSplitIDs: [], isSeed: { _ in false }
+ routines: [later, earlier], workouts: [], referencedRoutineIDs: [], isSeed: { _ in false }
)
- #expect(plan.splitGroups.count == 1)
- #expect(plan.splitGroups[0].keep.map(\.id) == [id(1)])
- #expect(plan.splitGroups[0].delete.map(\.id) == [id(2)])
+ #expect(plan.routineGroups.count == 1)
+ #expect(plan.routineGroups[0].keep.map(\.id) == [id(1)])
+ #expect(plan.routineGroups[0].delete.map(\.id) == [id(2)])
}
/// Referenced-protection must override "earliest ULID wins": a later,
- /// referenced split survives and the earlier, unreferenced duplicate is the
+ /// referenced routine survives and the earlier, unreferenced duplicate is the
/// one deleted — the opposite of the no-protection tiebreak.
@Test func referencedProtectionOverridesEarliestWins() {
- let earlier = split(id: id(1)) // unreferenced, would win the earliest-id tiebreak alone
- let later = split(id: id(2)) // referenced by a workout
+ let earlier = routine(id: id(1)) // unreferenced, would win the earliest-id tiebreak alone
+ let later = routine(id: id(2)) // referenced by a workout
let plan = DuplicateCleanupPlanner.plan(
- splits: [earlier, later], workouts: [], referencedSplitIDs: [id(2)], isSeed: { _ in false }
+ routines: [earlier, later], workouts: [], referencedRoutineIDs: [id(2)], isSeed: { _ in false }
)
- #expect(plan.splitGroups.count == 1)
- #expect(plan.splitGroups[0].keep.map(\.id) == [id(2)])
- #expect(plan.splitGroups[0].delete.map(\.id) == [id(1)])
+ #expect(plan.routineGroups.count == 1)
+ #expect(plan.routineGroups[0].keep.map(\.id) == [id(2)])
+ #expect(plan.routineGroups[0].delete.map(\.id) == [id(1)])
}
@Test func seedMemberNeverDeleted() throws {
- let seedSplit = split(id: id(1))
- let duplicate = split(id: id(2))
+ let seedRoutine = routine(id: id(1))
+ let duplicate = routine(id: id(2))
let plan = DuplicateCleanupPlanner.plan(
- splits: [seedSplit, duplicate], workouts: [], referencedSplitIDs: [],
+ routines: [seedRoutine, duplicate], workouts: [], referencedRoutineIDs: [],
isSeed: { $0 == id(1) }
)
- let group = try #require(plan.splitGroups.first)
+ let group = try #require(plan.routineGroups.first)
#expect(group.keep.map(\.id) == [id(1)])
#expect(group.delete.map(\.id) == [id(2)])
#expect(!group.delete.contains { $0.id == id(1) })
}
@Test func differingExerciseContentNotGrouped() {
- let a = split(id: id(1), exercises: [exercise(weight: 100)])
- let b = split(id: id(2), exercises: [exercise(weight: 105)])
+ let a = routine(id: id(1), exercises: [exercise(weight: 100)])
+ let b = routine(id: id(2), exercises: [exercise(weight: 105)])
let plan = DuplicateCleanupPlanner.plan(
- splits: [a, b], workouts: [], referencedSplitIDs: [], isSeed: { _ in false }
+ routines: [a, b], workouts: [], referencedRoutineIDs: [], isSeed: { _ in false }
)
- #expect(plan.splitGroups.isEmpty)
+ #expect(plan.routineGroups.isEmpty)
}
- /// Machine comfort settings are real content: splits identical except for a
+ /// Machine comfort settings are real content: routines identical except for a
/// setting value — or for the nil (non-machine) vs. empty (machine, nothing
/// recorded) distinction — must not be judged duplicates.
@Test func differingMachineSettingsNotGrouped() {
@@ -137,14 +137,14 @@ struct DuplicateCleanupPlannerTests {
var machineNothingRecorded = exercise()
machineNothingRecorded.machineSettings = []
- let a = split(id: id(1), exercises: [lowSeat])
- let b = split(id: id(2), exercises: [highSeat])
- let c = split(id: id(3), exercises: [machineNothingRecorded])
- let d = split(id: id(4)) // machineSettings nil — not a machine exercise
+ let a = routine(id: id(1), exercises: [lowSeat])
+ let b = routine(id: id(2), exercises: [highSeat])
+ let c = routine(id: id(3), exercises: [machineNothingRecorded])
+ let d = routine(id: id(4)) // machineSettings nil — not a machine exercise
let plan = DuplicateCleanupPlanner.plan(
- splits: [a, b, c, d], workouts: [], referencedSplitIDs: [], isSeed: { _ in false }
+ routines: [a, b, c, d], workouts: [], referencedRoutineIDs: [], isSeed: { _ in false }
)
- #expect(plan.splitGroups.isEmpty)
+ #expect(plan.routineGroups.isEmpty)
}
// MARK: - Workout grouping / survivor rules
@@ -155,7 +155,7 @@ struct DuplicateCleanupPlannerTests {
let w1 = workout(id: id(1), start: day)
let w2 = workout(id: id(2), start: sameDayLater)
let plan = DuplicateCleanupPlanner.plan(
- splits: [], workouts: [w1, w2], referencedSplitIDs: [], isSeed: { _ in false }
+ routines: [], workouts: [w1, w2], referencedRoutineIDs: [], isSeed: { _ in false }
)
#expect(plan.workoutGroups.count == 1)
#expect(plan.workoutGroups[0].keep.id == id(1))
@@ -166,7 +166,7 @@ struct DuplicateCleanupPlannerTests {
let w1 = workout(id: id(1), start: fixedDate(day: 15))
let w2 = workout(id: id(2), start: fixedDate(day: 16))
let plan = DuplicateCleanupPlanner.plan(
- splits: [], workouts: [w1, w2], referencedSplitIDs: [], isSeed: { _ in false }
+ routines: [], workouts: [w1, w2], referencedRoutineIDs: [], isSeed: { _ in false }
)
#expect(plan.workoutGroups.isEmpty)
}
@@ -176,7 +176,7 @@ struct DuplicateCleanupPlannerTests {
let w1 = workout(id: id(1), start: start, status: WorkoutStatus.inProgress.rawValue)
let w2 = workout(id: id(2), start: start, status: WorkoutStatus.inProgress.rawValue)
let plan = DuplicateCleanupPlanner.plan(
- splits: [], workouts: [w1, w2], referencedSplitIDs: [], isSeed: { _ in false }
+ routines: [], workouts: [w1, w2], referencedRoutineIDs: [], isSeed: { _ in false }
)
#expect(plan.workoutGroups.isEmpty)
}
@@ -189,7 +189,7 @@ struct DuplicateCleanupPlannerTests {
let earlierNoLink = workout(id: id(1), start: start, metrics: nil)
let laterWithLink = workout(id: id(2), start: start, metrics: metrics(healthKitWorkoutUUID: "HK-UUID"))
let plan = DuplicateCleanupPlanner.plan(
- splits: [], workouts: [earlierNoLink, laterWithLink], referencedSplitIDs: [], isSeed: { _ in false }
+ routines: [], workouts: [earlierNoLink, laterWithLink], referencedRoutineIDs: [], isSeed: { _ in false }
)
let group = try #require(plan.workoutGroups.first)
#expect(group.keep.id == id(2))
diff --git a/WorkoutsTests/ExerciseLibraryQueryTests.swift b/WorkoutsTests/ExerciseLibraryQueryTests.swift
index 0be1cf5..b09a28a 100644
--- a/WorkoutsTests/ExerciseLibraryQueryTests.swift
+++ b/WorkoutsTests/ExerciseLibraryQueryTests.swift
@@ -10,7 +10,7 @@ import Testing
struct ExerciseLibraryQueryTests {
private func makeContext() throws -> ModelContext {
- let schema = Schema([Split.self, Exercise.self, Workout.self, WorkoutLog.self])
+ let schema = Schema([Routine.self, Exercise.self, Workout.self, WorkoutLog.self])
// cloudKitDatabase: .none matches WorkoutsModelContainer — see SeedLibraryTests.
let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true, cloudKitDatabase: .none)
let container = try ModelContainer(for: schema, configurations: [config])
@@ -19,15 +19,16 @@ struct ExerciseLibraryQueryTests {
@Test func machineSettingsFetchFindsRecordedSettings() throws {
let context = try makeContext()
- let split = Split(id: "S1", name: "Upper Body", color: "indigo",
+ // PINNED: routine documents live under the on-disk "Splits/" directory.
+ let routine = Routine(id: "S1", name: "Upper Body", color: "indigo",
systemImage: "dumbbell.fill", order: 0,
createdAt: .now, updatedAt: .now, jsonRelativePath: "Splits/S1.json")
let exercise = Exercise(id: "E1", name: "Chest Press", order: 0, sets: 3, reps: 10,
weight: 100, loadType: LoadType.weight.rawValue,
durationTotalSeconds: 0,
machineSettings: [MachineSetting(name: "Seat Height", value: "4")])
- exercise.split = split
- context.insert(split)
+ exercise.routine = routine
+ context.insert(routine)
context.insert(exercise)
try context.save()
@@ -37,12 +38,12 @@ struct ExerciseLibraryQueryTests {
))
#expect(fetched.count == 1)
#expect(fetched.first?.machineSettings == [MachineSetting(name: "Seat Height", value: "4")])
- #expect(fetched.first?.split?.name == "Upper Body")
+ #expect(fetched.first?.routine?.name == "Upper Body")
}
@Test func weightedLogFetchGatesChart() throws {
let context = try makeContext()
- let workout = Workout(id: "W1", splitID: nil, splitName: "Upper Body",
+ let workout = Workout(id: "W1", routineID: nil, routineName: "Upper Body",
start: .now, end: .now, statusRaw: WorkoutStatus.completed.rawValue,
createdAt: .now, updatedAt: .now,
jsonRelativePath: "Workouts/2026/07/W1.json")
diff --git a/WorkoutsTests/ExerciseMotionTests.swift b/WorkoutsTests/ExerciseMotionTests.swift
index 5f5f2ef..9b9fb26 100644
--- a/WorkoutsTests/ExerciseMotionTests.swift
+++ b/WorkoutsTests/ExerciseMotionTests.swift
@@ -131,7 +131,7 @@ struct ExerciseMotionTests {
bundle.url(forResource: "figure-fixtures", withExtension: "json"),
"figure-fixtures.json must be bundled as a WorkoutsTests resource (see project.yml)")
let fixtures = try JSONDecoder().decode(FigureFixtures.self, from: Data(contentsOf: url))
- #expect(fixtures.exercises.count == 64)
+ #expect(fixtures.exercises.count == 66)
for exercise in fixtures.exercises {
let resources = try #require(ExerciseMotionLibrary.resources(for: exercise.name),
diff --git a/WorkoutsTests/Fixtures/figure-fixtures.json b/WorkoutsTests/Fixtures/figure-fixtures.json
index 4139aad..2d77976 100644
--- a/WorkoutsTests/Fixtures/figure-fixtures.json
+++ b/WorkoutsTests/Fixtures/figure-fixtures.json
@@ -1 +1 @@
-{"profile": "neutral", "pitch": 10.0, "exercises": [{"name": "Abdominal", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[128.0, 126.0], [123.50070744350393, 83.31559451522868], [125.00047162900262, 42.39449550813546]], "head": [127.78393364820391, 22.89002207499297], "nose": [[137.6895074402238, 24.26100998553362], [144.6213839214148, 25.22042122738718]], "arm_r": [[125.00047162900262, 44.30462546247169], [153.5959458166933, 35.37057588667207], [137.46787065175624, 10.458926161567888]], "arm_l": [[130.90931814707588, 42.453981059823654], [159.51563713035878, 33.55366614061362], [143.51615744049056, 8.561724009237087]], "leg_r": [[128.0, 127.2155372436685], [171.17430886688956, 111.58284855334222], [168.0, 150.8509233159615], [178.9653082306577, 151.71059641116776]], "leg_l": [[133.90884651807326, 126.7540782623559], [176.35384130990658, 109.29150625391821], [172.0, 148.449771713863], [182.93464482641144, 149.6288883001427]]}, {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[128.0, 126.0], [123.11337415401846, 76.7383796324037], [167.64073825067507, 57.70967534760443]], "head": [186.18441534201082, 50.33136579709479], "nose": [[193.66654290527876, 56.965954490223126], [198.8685566836205, 61.57870955266287]], "arm_r": [[167.64073825067507, 59.61980530194066], [195.90006093783734, 49.70250854036303], [172.72817683159388, 30.93775236749825]], "arm_l": [[173.54958476874833, 57.76916089929262], [201.81359837899106, 47.864837419955926], [178.71564429723324, 29.01188919273017]], "leg_r": [[128.0, 127.2155372436685], [171.1743088668896, 111.58284855334223], [168.0, 150.8509233159615], [178.9653082306577, 151.71059641116778]], "leg_l": [[133.90884651807326, 126.7540782623559], [176.35384130990658, 109.29150625391821], [172.0, 148.449771713863], [182.93464482641144, 149.62888830014273]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[128.0, 126.0], [122.50068568111516, 81.31026966131319], [148.00041311155596, 46.22466606468976]], "head": [160.31364261806914, 30.70388407251191], "nose": [[169.4012247293501, 34.87711440053437], [175.74537923860663, 37.79049858562185]], "arm_r": [[148.00041311155596, 48.134796019025984], [177.4772385118675, 42.64130176976346], [162.0341655680046, 17.312178498974532]], "arm_l": [[153.90925962962922, 46.28415161637795], [183.38904849856343, 40.8061016600034], [168.05011894867513, 15.415678998609911]], "leg_r": [[128.0, 127.2155372436685], [171.17430886688956, 111.58284855334223], [168.0, 150.8509233159615], [178.96530823065774, 151.71059641116776]], "leg_l": [[133.90884651807326, 126.7540782623559], [176.35384130990658, 109.29150625391821], [172.0, 148.449771713863], [182.93464482641144, 149.62888830014273]]}}}, {"name": "Abductor", "camera": 90.0, "frames": [{"order": ["spine", "arm_l", "arm_r", "leg_l", "leg_r", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 116.0], [160.0, 73.16086274396895], [160.0, 32.29134099396232]], "head": [160.0, 12.59518593371817], "arm_r": [[149.0, 32.29134099396232], [139.72949016875157, 60.82303648281693], [131.3275549188913, 86.68153428172431]], "arm_l": [[171.0, 32.29134099396232], [180.27050983124843, 60.82303648281693], [188.6724450811087, 86.68153428172431]], "leg_r": [[153.0, 116.0], [147.39401020336322, 133.10345930302373], [145.10544255919902, 172.83189963719428], [143.92179588176742, 171.6549669223465]], "leg_l": [[167.0, 116.0], [172.60598979663678, 133.10345930302373], [174.89455744080098, 172.83189963719428], [176.07820411823258, 171.6549669223465]]}, {"order": ["spine", "arm_l", "arm_r", "leg_l", "leg_r", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 116.0], [160.0, 73.16086274396895], [160.0, 32.29134099396232]], "head": [160.0, 12.59518593371817], "arm_r": [[149.0, 32.29134099396232], [139.72949016875157, 60.82303648281693], [131.3275549188913, 86.68153428172431]], "arm_l": [[171.0, 32.29134099396232], [180.27050983124843, 60.82303648281693], [188.6724450811087, 86.68153428172431]], "leg_r": [[153.0, 116.0], [130.0, 130.9232660108731], [120.61056874428219, 169.76167264761645], [115.7543569835581, 168.1244147020733]], "leg_l": [[167.0, 116.0], [190.0, 130.9232660108731], [199.38943125571782, 169.76167264761645], [204.24564301644193, 168.1244147020733]]}], "tween": {"t": 0.5, "sample": {"order": ["spine", "arm_l", "arm_r", "leg_l", "leg_r", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 116.0], [160.0, 73.16086274396895], [160.0, 32.29134099396232]], "head": [160.0, 12.59518593371817], "arm_r": [[149.0, 32.29134099396232], [139.72949016875157, 60.82303648281693], [131.3275549188913, 86.68153428172431]], "arm_l": [[171.0, 32.29134099396232], [180.27050983124843, 60.82303648281693], [188.6724450811087, 86.68153428172431]], "leg_r": [[153.0, 116.0], [138.40398580536578, 132.34142152089595], [132.44536528849622, 171.75877049117304], [129.36356808016237, 170.42094139199114]], "leg_l": [[167.0, 116.0], [181.59601419463425, 132.34142152089595], [187.5546347115038, 171.75877049117304], [190.63643191983766, 170.42094139199114]]}}, "orbit": [{"yaw": 127.0, "sample": {"order": ["spine", "arm_r", "leg_r", "arm_l", "leg_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 116.0], [160.0, 73.16086274396895], [160.0, 32.29134099396232]], "head": [160.0, 12.59518593371817], "nose": [[150.25538327938614, 14.840724837485546], [146.0426781173218, 15.811496044064228]], "arm_r": [[154.77104199355594, 32.327140292628755], [144.38560499931648, 59.71678856368911], [127.45898099051918, 84.10364976890271]], "arm_l": [[168.78499061052023, 33.440885896654606], [173.2070703112358, 62.76814640805501], [169.70061398975147, 88.91108067494635]], "leg_r": [[157.9655840337451, 116.45381562691817], [126.61174048168456, 131.4098395903588], [118.23109913779213, 170.5183789577097], [110.96489412668988, 168.85049407976638]], "leg_l": [[165.59044857033106, 116.73152857444055], [143.19089005938437, 132.85925017338457], [138.46571149104156, 172.44611886252008], [133.09011101583206, 171.0256258036004]], "props": {"bg": [{"kind": "poly", "pts": [[170.17558907270748, 64.59122072790889], [164.32336047718314, 120.25119531754564], [171.43542568533545, 120.66441958783159], [177.28765428085978, 65.00444499819484]], "w": 16, "color": "equipment"}, {"kind": "poly", "pts": [[131.35448580972135, 127.88309544322196], [171.286261312086, 133.1082995463687], [185.5103917283906, 133.9347480869406], [145.57861622602596, 128.70954398379385]], "w": 10, "color": "equipment"}, {"kind": "line", "pts": [[157.9099183587413, 135.8785623534417], [156.44686120986023, 149.79355600085088]], "w": 6, "color": "equipment"}], "fg": [{"kind": "line", "pts": [[125.25318741486642, 144.3309051200959], [127.97029354850271, 118.48877406062171]], "w": 6, "color": "prop"}, {"kind": "line", "pts": [[141.8323369925662, 145.78031570312166], [144.54944312620253, 119.93818464364746]], "w": 6, "color": "prop"}]}}}, {"yaw": 270.0, "sample": {"order": ["leg_l", "leg_r", "arm_l", "arm_r", "spine", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 116.0], [160.0, 73.16086274396895], [160.0, 32.29134099396232]], "head": [160.0, 12.59518593371817], "arm_r": [[171.0, 32.29134099396232], [180.27050983124843, 59.10236470334958], [188.67244508110866, 79.06508551601353]], "arm_l": [[149.0, 32.29134099396232], [139.72949016875157, 59.102364703349586], [131.3275549188913, 79.06508551601354]], "leg_r": [[167.0, 116.0], [172.60598979663678, 117.59341061272917], [174.89455744080098, 153.54028741485138], [176.07820411823258, 148.71567825347657]], "leg_l": [[153.0, 116.0], [147.39401020336322, 117.59341061272917], [145.10544255919902, 153.54028741485138], [143.9217958817674, 148.71567825347657]], "props": {"bg": [{"kind": "poly", "pts": [[160.0, 69.87214486573811], [160.0, 122.49493162974898], [160.0, 126.59917334965701], [160.0, 73.97638658564614]], "w": 16, "color": "equipment"}, {"kind": "poly", "pts": [[185.0, 125.9911475918806], [135.0, 125.9911475918806], [135.0, 134.19963103169664], [185.0, 134.19963103169664]], "w": 10, "color": "equipment"}, {"kind": "line", "pts": [[160.0, 134.79385241571816], [160.0, 147.9495491067209]], "w": 6, "color": "equipment"}], "fg": [{"kind": "line", "pts": [[172.60598979663678, 129.80941468294597], [172.60598979663678, 105.37740654251236]], "w": 6, "color": "prop"}, {"kind": "line", "pts": [[147.39401020336322, 129.80941468294597], [147.39401020336322, 105.37740654251236]], "w": 6, "color": "prop"}]}}}]}, {"name": "Adductor", "camera": 90.0, "frames": [{"order": ["spine", "arm_l", "arm_r", "leg_l", "leg_r", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 116.0], [160.0, 73.16086274396895], [160.0, 32.29134099396232]], "head": [160.0, 12.59518593371817], "arm_r": [[149.0, 32.29134099396232], [139.72949016875157, 60.82303648281693], [131.3275549188913, 86.68153428172431]], "arm_l": [[171.0, 32.29134099396232], [180.27050983124843, 60.82303648281693], [188.6724450811087, 86.68153428172431]], "leg_r": [[153.0, 116.0], [130.0, 130.9232660108731], [120.61056874428219, 169.76167264761645], [115.7543569835581, 168.1244147020733]], "leg_l": [[167.0, 116.0], [190.0, 130.9232660108731], [199.38943125571782, 169.76167264761645], [204.24564301644193, 168.1244147020733]]}, {"order": ["spine", "arm_l", "arm_r", "leg_l", "leg_r", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 116.0], [160.0, 73.16086274396895], [160.0, 32.29134099396232]], "head": [160.0, 12.59518593371817], "arm_r": [[149.0, 32.29134099396232], [139.72949016875157, 60.82303648281693], [131.3275549188913, 86.68153428172431]], "arm_l": [[171.0, 32.29134099396232], [180.27050983124843, 60.82303648281693], [188.6724450811087, 86.68153428172431]], "leg_r": [[153.0, 116.0], [147.39401020336322, 133.10345930302373], [145.10544255919902, 172.83189963719428], [143.92179588176742, 171.6549669223465]], "leg_l": [[167.0, 116.0], [172.60598979663678, 133.10345930302373], [174.89455744080098, 172.83189963719428], [176.07820411823258, 171.6549669223465]]}], "tween": {"t": 0.5, "sample": {"order": ["spine", "arm_l", "arm_r", "leg_l", "leg_r", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 116.0], [160.0, 73.16086274396895], [160.0, 32.29134099396232]], "head": [160.0, 12.59518593371817], "arm_r": [[149.0, 32.29134099396232], [139.72949016875157, 60.82303648281693], [131.3275549188913, 86.68153428172431]], "arm_l": [[171.0, 32.29134099396232], [180.27050983124843, 60.82303648281693], [188.6724450811087, 86.68153428172431]], "leg_r": [[153.0, 116.0], [138.40398580536578, 132.34142152089595], [132.44536528849622, 171.75877049117304], [129.36356808016237, 170.42094139199114]], "leg_l": [[167.0, 116.0], [181.59601419463425, 132.34142152089595], [187.5546347115038, 171.75877049117304], [190.63643191983766, 170.42094139199114]]}}}, {"name": "Arm Circles", "camera": 40.0, "frames": [{"order": ["leg_l", "leg_r", "spine", "arm_l", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 66.0], [160.0, 23.160862743968956], [160.0, -17.708659006037678]], "head": [160.534691310332, -37.31490683420151], "nose": [[170.48425726540742, -36.311842875321474], [175.84330179220183, -35.771571626994444]], "arm_r": [[152.92933629344807, -16.245414568883305], [171.3991805532178, -12.140315628510749], [189.7987414793807, -10.625787364292506]], "arm_l": [[171.59710274696485, -17.663090429721073], [198.08553053273266, -15.217403989394604], [224.47316185370062, -15.355973659669438]], "leg_r": [[155.50048673219422, 66.93115555091643], [164.01536833498193, 112.0157431100973], [156.0, 149.83419719709872], [165.63509355919305, 153.02761752718845]], "leg_l": [[169.0259523082187, 66.57765746255455], [178.07794715293392, 111.28365043871992], [164.0, 147.99480835522493], [174.22144670557157, 152.02318799487986]]}, {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 66.0], [160.0, 23.160862743968956], [160.0, -17.708659006037678]], "head": [160.534691310332, -37.31490683420151], "nose": [[170.48425726540742, -36.311842875321474], [175.84330179220183, -35.771571626994444]], "arm_r": [[152.92933629344807, -16.245414568883305], [146.05740343975492, -39.183541265595636], [137.2281578147836, -62.68175177962408]], "arm_l": [[171.59710274696485, -17.663090429721073], [184.0087981838679, -44.59188377321808], [194.38980082196048, -72.06557524262496]], "leg_r": [[155.50048673219422, 66.93115555091643], [164.01536833498193, 112.0157431100973], [156.0, 149.83419719709872], [165.63509355919305, 153.02761752718845]], "leg_l": [[169.0259523082187, 66.57765746255455], [178.07794715293392, 111.28365043871992], [164.0, 147.99480835522493], [174.22144670557157, 152.02318799487986]]}, {"order": ["arm_l", "leg_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 66.0], [160.0, 23.160862743968956], [160.0, -17.708659006037678]], "head": [160.534691310332, -37.31490683420151], "nose": [[170.48425726540742, -36.311842875321474], [175.84330179220183, -35.771571626994444]], "arm_r": [[152.92933629344807, -16.245414568883305], [132.85541810268234, -12.374042228280558], [112.85788723111405, -11.092351163556003]], "arm_l": [[171.59710274696485, -17.663090429721073], [190.0669470067346, -21.76818937009363], [208.46650793289749, -28.432616704957745]], "leg_r": [[155.50048673219422, 66.93115555091643], [164.01536833498193, 112.0157431100973], [156.0, 149.83419719709872], [165.63509355919305, 153.02761752718845]], "leg_l": [[169.0259523082187, 66.57765746255455], [178.07794715293392, 111.28365043871992], [164.0, 147.99480835522493], [174.22144670557157, 152.02318799487986]]}, {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 66.0], [160.0, 23.160862743968956], [160.0, -17.708659006037678]], "head": [160.534691310332, -37.31490683420151], "nose": [[170.48425726540742, -36.311842875321474], [175.84330179220183, -35.771571626994444]], "arm_r": [[152.92933629344807, -16.245414568883305], [148.67105145730744, 12.807960658166763], [146.4124332310594, 41.68142191233785]], "arm_l": [[171.59710274696485, -17.663090429721073], [181.7863665161021, 8.400424721307992], [193.92031961085468, 34.295403219710764]], "leg_r": [[155.50048673219422, 66.93115555091643], [164.01536833498193, 112.0157431100973], [156.0, 149.83419719709872], [165.63509355919305, 153.02761752718845]], "leg_l": [[169.0259523082187, 66.57765746255455], [178.07794715293392, 111.28365043871992], [164.0, 147.99480835522493], [174.22144670557157, 152.02318799487986]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "leg_r", "spine", "arm_l", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 66.0], [160.0, 23.160862743968956], [160.0, -17.708659006037678]], "head": [160.534691310332, -37.31490683420151], "nose": [[170.48425726540742, -36.311842875321474], [175.84330179220183, -35.771571626994444]], "arm_r": [[152.92933629344807, -16.245414568883305], [162.2109378471654, -30.551313123154785], [170.1431632791755, -46.9375812492998]], "arm_l": [[171.59710274696485, -17.663090429721073], [194.69997296127158, -34.82924303104664], [216.40087295386246, -54.06488107406239]], "leg_r": [[155.50048673219422, 66.93115555091643], [164.01536833498193, 112.0157431100973], [156.00000000000003, 149.83419719709875], [165.63509355919308, 153.02761752718848]], "leg_l": [[169.0259523082187, 66.57765746255455], [178.07794715293392, 111.28365043871992], [164.0, 147.99480835522493], [174.22144670557157, 152.02318799487986]]}}}, {"name": "Arm Curl", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[130.0, 124.0], [130.7861315886847, 81.14917968668395], [134.4304502554608, 40.41789305174976]], "head": [137.2139122746621, 20.913419618607264], "nose": [[146.73819953969002, 23.961033782707986], [153.3955951537561, 26.091290105818835]], "arm_r": [[134.4304502554608, 42.328023006085985], [159.00501158413056, 59.27389864999469], [180.9456226327057, 79.4230168259295]], "arm_l": [[140.33929677353407, 40.47737860343795], [164.91385810220382, 57.423254247346655], [186.85446915077895, 77.57237242328145]], "leg_r": [[130.0, 125.2155372436685], [173.8364696761423, 111.48589278988837], [176.0, 150.82053882198593], [186.98389774627694, 150.2346069268626]], "leg_l": [[135.90884651807326, 124.7540782623559], [179.40646070778647, 110.0162218015468], [180.0, 149.4041949728996], [190.99878894026918, 149.24345139631006]]}, {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[130.0, 124.0], [130.7861315886847, 81.14917968668395], [134.4304502554608, 40.41789305174976]], "head": [137.2139122746621, 20.913419618607264], "nose": [[146.73819953969002, 23.961033782707986], [153.3955951537561, 26.091290105818835]], "arm_r": [[134.4304502554608, 42.328023006085985], [159.00501158413056, 59.27389864999469], [178.28863987472675, 36.64170344793001]], "arm_l": [[140.33929677353407, 40.47737860343795], [164.91385810220382, 57.423254247346655], [184.19748639280002, 34.791059045281976]], "leg_r": [[130.0, 125.2155372436685], [173.8364696761423, 111.48589278988837], [176.0, 150.82053882198593], [186.98389774627694, 150.2346069268626]], "leg_l": [[135.90884651807326, 124.7540782623559], [179.40646070778647, 110.0162218015468], [180.0, 149.4041949728996], [190.99878894026918, 149.24345139631006]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[130.0, 124.0], [130.7861315886847, 81.14917968668395], [134.4304502554608, 40.41789305174976]], "head": [137.2139122746621, 20.913419618607264], "nose": [[146.73819953969002, 23.961033782707986], [153.3955951537561, 26.091290105818835]], "arm_r": [[134.4304502554608, 42.328023006085985], [159.00501158413056, 59.27389864999469], [188.94905553678657, 57.47026639867471]], "arm_l": [[140.33929677353407, 40.47737860343795], [164.91385810220382, 57.423254247346655], [194.85790205485984, 55.61962199602667]], "leg_r": [[130.0, 125.2155372436685], [173.8364696761423, 111.48589278988837], [176.0, 150.82053882198593], [186.98389774627694, 150.2346069268626]], "leg_l": [[135.90884651807326, 124.7540782623559], [179.40646070778647, 110.0162218015468], [180.0, 149.4041949728996], [190.99878894026918, 149.24345139631006]]}}}, {"name": "Back Extension", "camera": 0.0, "frames": [{"order": ["arm_l", "leg_l", "spine", "arm_r", "leg_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 102.0], [192.3935695959019, 114.23040065827821], [224.68848379368362, 140.99266081782648]], "head": [239.07527980045666, 154.6747597962834], "nose": [[236.62097246800994, 164.3689010949183], [234.92751919881226, 171.05778437257678]], "arm_r": [[224.68848379368362, 142.90279077216272], [194.76156228588889, 140.84188928718248], [216.3417562960484, 161.3650377548679]], "arm_l": [[230.59733031175688, 141.05214636951465], [200.67040880396215, 138.99124488453444], [222.25060281412166, 159.51439335221986]], "leg_r": [[150.0, 103.2155372436685], [110.70964035040065, 126.77422264541534], [72.00000000000003, 136.6990008460836], [76.92410730224489, 146.3858838484451]], "leg_l": [[155.90884651807326, 102.7540782623559], [113.84739023733808, 121.09462279932025], [76.79945624721068, 135.94696647498034], [82.97447053750626, 144.9119197054579]]}, {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 102.0], [190.45633894911956, 85.80672899774845], [224.97226147206464, 62.800530599496355]], "head": [242.948142398048, 54.16630451840163], "nose": [[252.82867196388915, 55.70745198004789], [259.7424903480551, 56.78585709811745]], "arm_r": [[224.97226147206464, 64.71066055383258], [210.42797286467453, 90.55062860967689], [238.4353856595906, 79.96292255537792]], "arm_l": [[230.8811079901379, 62.86001615118454], [216.3368193827478, 88.69998420702885], [244.34423217766386, 78.11227815272986]], "leg_r": [[150.0, 103.2155372436685], [110.70964035040065, 126.77422264541532], [72.00000000000003, 136.69900084608358], [76.92410730224489, 146.38588384844508]], "leg_l": [[155.90884651807326, 102.7540782623559], [113.84739023733808, 121.09462279932026], [76.79945624721073, 135.94696647498048], [82.97447053750635, 144.91191970545802]]}], "tween": {"t": 0.5, "sample": {"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 102.0], [193.48509363643677, 99.79587312945566], [234.97087262292237, 101.97422073835621]], "head": [254.72463943482512, 105.05537821855494], "nose": [[260.3028000822107, 113.35502422256974], [264.1663589793951, 119.10354478146067]], "arm_r": [[234.97087262292237, 103.88435069269244], [208.60635924306342, 117.98164008833311], [238.0041003816883, 123.87181271381567]], "arm_l": [[240.87971914099563, 102.03370629004439], [214.51520576113668, 116.13099568568506], [243.91294689976155, 122.02116831116763]], "leg_r": [[150.0, 103.2155372436685], [110.70964035040063, 126.77422264541534], [72.0, 136.6990008460836], [76.92410730224486, 146.3858838484451]], "leg_l": [[155.90884651807326, 102.7540782623559], [113.84739023733808, 121.09462279932026], [76.79945624721068, 135.94696647498034], [82.97447053750628, 144.9119197054579]]}}}, {"name": "Barbell Squat", "camera": 0.0, "frames": [{"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [164.54698815214294, 21.395540020190396], [168.88491937775055, -19.25009421342108]], "head": [171.66838139695184, -38.75456764656357], "nose": [[181.66247319251514, -38.41086838579267], [188.6582089816488, -38.17028332129074]], "arm_r": [[168.88491937775055, -17.33996425908485], [192.65203640748376, 0.688316816865914], [172.53258338641072, -21.22686681380081]], "arm_l": [[174.7937658958238, -19.19060866173289], [192.95847995970774, 4.322244384596905], [180.4718928817179, -22.541266754049982]], "leg_r": [[160.0, 65.2155372436685], [169.19599513541698, 109.6022295401473], [160.0, 147.93938849669397], [170.70535714285714, 150.42986750584978]], "leg_l": [[165.90884651807326, 64.7540782623559], [173.95930503181157, 109.35608682232831], [164.0, 147.50785240061987], [174.65358746612156, 150.2050526233538]]}, {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[117.0, 108.0], [143.17988057153133, 73.6944252453616], [163.29538590931725, 37.85749664208181]], "head": [175.05109095516673, 21.92297247450017], "nose": [[184.9566647471866, 23.29396038504082], [191.88854122837762, 24.25337162689438]], "arm_r": [[163.29538590931725, 39.76762659641804], [192.92714924506498, 35.152816875860026], [163.0, 33.095125219384]], "arm_l": [[169.2042324273905, 37.916982193770004], [196.41794186727452, 25.4828719446184], [167.0, 31.274403701974496]], "leg_r": [[117.0, 109.2155372436685], [162.9998620689883, 109.32647413907678], [160.0, 148.60784736415684], [170.47933607337416, 145.31447656703358]], "leg_l": [[122.90884651807325, 108.7540782623559], [168.90764416620357, 109.08161372334803], [164.00000000000003, 148.17631126808274], [174.6275376973515, 145.38121892992268]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[138.5, 86.0], [154.42497830672875, 46.114735055074036], [166.88571296317096, 7.1105000413806465]], "head": [174.3778448314892, -11.151456927521664], "nose": [[184.3409329061434, -10.293042099665406], [191.3142957927856, -9.692220541423389]], "arm_r": [[166.88571296317096, 9.020629995716874], [178.30322698758025, 36.3415537826384], [169.3591627143218, 8.140887535208648]], "arm_l": [[172.79455948124422, 7.169985593068835], [178.3035324959653, 36.21181789545084], [177.20992304137073, 6.687222030755185]], "leg_r": [[138.5, 87.2155372436685], [176.68686825538467, 112.47279577877578], [159.99999999999997, 148.27361793042542], [170.68903086130587, 150.83120352836477]], "leg_l": [[144.40884651807326, 86.7540782623559], [182.10526165656407, 112.71605172140461], [164.0, 147.8420818343513], [174.57848897743455, 150.81215017210735]]}}}, {"name": "Bench Press", "camera": 0.0, "frames": [{"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 116.0], [106.5, 116.0], [65.0, 116.0]], "head": [45.194638625168594, 113.25882502327553], "nose": [[46.60741399901631, 103.35912473775934], [47.58162570573677, 96.53255903615947]], "arm_r": [[65.0, 117.91012995433623], [91.55143264884377, 131.6629772522244], [93.0, 102.15320590614091]], "arm_l": [[70.90884651807325, 116.05948555168817], [97.42740111345603, 129.87371855164292], [96.99999999999999, 100.33248438873136]], "leg_r": [[150.0, 117.2155372436685], [195.30008769797305, 109.34310611741422], [196.0, 148.72938534005917], [206.93968862700535, 147.59655128433957]], "leg_l": [[155.90884651807326, 116.7540782623559], [201.21645940182967, 108.92375949218865], [200.0, 148.29784924398507], [210.98222903324333, 147.68232827813742]]}, {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 116.0], [106.5, 116.0], [65.0, 116.0]], "head": [45.194638625168594, 113.25882502327553], "nose": [[46.60741399901631, 103.35912473775934], [47.58162570573677, 96.53255903615947]], "arm_r": [[65.0, 117.91012995433623], [60.64117536816883, 88.67940591693508], [46.121662397853754, 62.825928192786506]], "arm_l": [[70.90884651807325, 116.05948555168817], [65.41872444969624, 87.01419136030722], [49.604935830325346, 61.907887764618856]], "leg_r": [[150.0, 117.2155372436685], [195.30008769797305, 109.34310611741422], [196.0, 148.72938534005917], [206.93968862700535, 147.59655128433957]], "leg_l": [[155.90884651807326, 116.7540782623559], [201.21645940182967, 108.92375949218865], [200.0, 148.29784924398507], [210.98222903324333, 147.68232827813742]]}], "tween": {"t": 0.5, "sample": {"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 116.0], [106.5, 116.0], [65.0, 116.0]], "head": [45.194638625168594, 113.25882502327553], "nose": [[46.60741399901631, 103.35912473775934], [47.58162570573677, 96.53255903615947]], "arm_r": [[65.0, 117.91012995433623], [93.09853317509568, 107.55922265396269], [80.0, 80.97983921637844]], "arm_l": [[70.90884651807325, 116.05948555168817], [98.69349816980488, 104.91708615451506], [84.0, 79.1591176989689]], "leg_r": [[150.0, 117.2155372436685], [195.30008769797305, 109.34310611741422], [196.0, 148.72938534005917], [206.93968862700535, 147.59655128433957]], "leg_l": [[155.90884651807326, 116.7540782623559], [201.21645940182967, 108.92375949218865], [200.0, 148.29784924398507], [210.98222903324333, 147.68232827813742]]}}}, {"name": "Bird Dog", "camera": 0.0, "frames": [{"order": ["arm_r", "leg_r", "leg_l", "spine", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[190.0, 106.0], [147.03555718411147, 99.29848248056776], [106.04649104941329, 92.90508070915538]], "head": [86.19556801658685, 95.30543819395331], "nose": [[77.1079859053059, 99.47866852197576], [70.76383139604934, 102.39205270706324]], "arm_r": [[111.95533756748654, 92.96456626084355], [115.93122267253811, 122.24819097380495], [111.0, 151.39056507548827]], "arm_l": [[106.04649104941329, 94.81521066349163], [110.07168565309334, 124.09230142764625], [105.0, 153.2112865928978]], "leg_r": [[195.90884651807326, 106.7540782623559], [201.51483631471004, 151.7175669488609], [241.51483631471004, 151.7175669488609], [250.52550880188895, 157.93105468496077]], "leg_l": [[190.0, 107.2155372436685], [195.60598979663678, 152.17902593017354], [235.60598979663678, 152.17902593017354], [244.61666228381569, 158.3925136662734]]}, {"order": ["arm_r", "leg_r", "leg_l", "spine", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[190.0, 106.0], [147.03555718411147, 99.29848248056776], [106.04649104941329, 92.90508070915538]], "head": [86.19556801658685, 95.30543819395331], "nose": [[76.52653951347901, 97.85688637216273], [69.76505872945553, 99.64109538787199]], "arm_r": [[111.95533756748654, 92.96456626084355], [115.93122267253811, 122.24819097380495], [111.0, 151.39056507548827]], "arm_l": [[106.04649104941329, 94.81521066349163], [77.6809337814338, 85.1965493841931], [49.31537651345428, 75.57788810489458]], "leg_r": [[195.90884651807326, 106.7540782623559], [240.3414345273704, 95.02927615912368], [278.9784675789331, 84.83379606935654], [286.04913128548503, 93.13226764344691]], "leg_l": [[190.0, 107.2155372436685], [195.60598979663678, 152.17902593017354], [235.60598979663678, 152.17902593017354], [244.61666228381569, 158.3925136662734]]}, {"order": ["arm_r", "leg_r", "leg_l", "spine", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[190.0, 106.0], [147.03555718411147, 99.29848248056776], [106.04649104941329, 92.90508070915538]], "head": [86.19556801658685, 95.30543819395331], "nose": [[77.1079859053059, 99.47866852197576], [70.76383139604934, 102.39205270706324]], "arm_r": [[111.95533756748654, 92.96456626084355], [115.93122267253811, 122.24819097380495], [111.0, 151.39056507548827]], "arm_l": [[106.04649104941329, 94.81521066349163], [110.07168565309334, 124.09230142764625], [105.0, 153.2112865928978]], "leg_r": [[195.90884651807326, 106.7540782623559], [201.51483631471004, 151.7175669488609], [241.51483631471004, 151.7175669488609], [250.52550880188895, 157.93105468496077]], "leg_l": [[190.0, 107.2155372436685], [195.60598979663678, 152.17902593017354], [235.60598979663678, 152.17902593017354], [244.61666228381569, 158.3925136662734]]}, {"order": ["leg_r", "arm_r", "spine", "arm_l", "leg_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[190.0, 106.0], [147.03555718411147, 99.29848248056776], [106.04649104941329, 92.90508070915538]], "head": [86.19556801658685, 95.30543819395331], "nose": [[76.52653951347901, 97.85688637216273], [69.76505872945553, 99.64109538787199]], "arm_r": [[111.95533756748654, 92.96456626084355], [83.58978029950704, 83.34590498154503], [55.22422303152753, 73.72724370224651]], "arm_l": [[106.04649104941329, 94.81521066349163], [110.07168565309334, 124.09230142764625], [105.0, 153.2112865928978]], "leg_r": [[195.90884651807326, 106.7540782623559], [201.51483631471004, 151.7175669488609], [241.51483631471004, 151.7175669488609], [250.52550880188895, 157.93105468496077]], "leg_l": [[190.0, 107.2155372436685], [234.43258800929715, 95.49073514043629], [273.06962106085984, 85.29525505066914], [280.1402847674118, 93.59372662475951]]}], "tween": {"t": 0.5, "sample": {"order": ["arm_r", "leg_r", "leg_l", "spine", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[190.0, 106.0], [147.03555718411147, 99.29848248056776], [106.04649104941329, 92.90508070915538]], "head": [86.19556801658685, 95.30543819395331], "nose": [[76.78202489248983, 98.67963526151854], [70.20417654698848, 101.0374038833528]], "arm_r": [[111.95533756748654, 92.96456626084355], [115.93122267253811, 122.24819097380495], [111.0, 151.39056507548827]], "arm_l": [[106.04649104941329, 94.81521066349163], [82.84966999647663, 113.55006179251055], [57.03634935368697, 128.60417717799652]], "leg_r": [[195.90884651807326, 106.7540782623559], [234.0445748556052, 132.08616357365625], [273.7023693105576, 126.9444353308775], [281.8124200154689, 134.26302654838682]], "leg_l": [[190.0, 107.2155372436685], [195.60598979663678, 152.17902593017354], [235.60598979663678, 152.17902593017354], [244.61666228381569, 158.3925136662734]]}}, "orbit": [{"yaw": 37.0, "sample": {"order": ["arm_r", "arm_l", "spine", "leg_r", "leg_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[190.0, 106.0], [155.68707029783513, 94.80852282274789], [122.95174655898823, 84.13159632031196]], "head": [107.09809451775733, 84.4574513154624], "nose": [[97.95900332573831, 88.5166399269906], [92.89233625341937, 90.7670340686228]], "arm_r": [[134.2907264664134, 84.17910359422488], [137.46600949517568, 113.87822453047738], [133.5277599609364, 142.5052657333192]], "arm_l": [[116.3317813043157, 85.65709393064989], [119.54644464966532, 115.35483396198678], [115.49601639130758, 143.94380727355005]], "leg_r": [[198.93171981481694, 106.60223367767217], [203.40886233537387, 152.15157118192892], [235.35428273726558, 156.3317344644463], [242.55052575493283, 163.48687425758854]], "leg_l": [[185.78729483793566, 106.97077120657869], [190.2644373584926, 152.52010871083542], [222.2098577603843, 156.7002719933528], [229.40610077805155, 163.85541178649504]], "props": {"bg": [], "fg": []}}}, {"yaw": 180.0, "sample": {"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[190.0, 106.0], [232.96444281588853, 99.29848248056776], [273.9535089505867, 92.90508070915538]], "head": [293.80443198341317, 95.30543819395332], "nose": [[302.8920140946941, 99.47866852197578], [309.23616860395066, 102.39205270706326]], "arm_r": [[273.9535089505867, 94.81521066349163], [269.97762384553516, 124.09883537645301], [274.90884651807323, 153.24120947813634]], "arm_l": [[279.8623554686599, 92.96456626084357], [275.83716086497986, 122.2416570249982], [280.90884651807323, 151.36064219024973]], "leg_r": [[190.0, 107.2155372436685], [184.39401020336322, 152.17902593017354], [144.39401020336322, 152.17902593017354], [135.38333771618431, 158.39251366627337]], "leg_l": [[195.90884651807326, 106.7540782623559], [190.30285672143648, 151.7175669488609], [150.30285672143648, 151.7175669488609], [141.29218423425758, 157.93105468496077]], "props": {"bg": [], "fg": []}}}]}, {"name": "Bodyweight Squat", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [163.03440660786944, 21.26521672923713], [165.92930026825064, -19.504748919881465]], "head": [168.01986953360372, -39.09300638186289], "nose": [[178.01986953360372, -39.09300638186289], [185.01986953360372, -39.09300638186289]], "arm_r": [[165.92930026825064, -17.594618965545237], [166.97628516932568, 11.9316160765854], [170.6323654714801, 41.25563043734954]], "arm_l": [[171.8381467863239, -19.445263368193277], [172.88513168739894, 10.080971673937361], [176.54121198955337, 39.4049860347015]], "leg_r": [[160.0, 65.2155372436685], [160.0, 111.19909733724643], [160.0, 147.939388496694], [160.0, 152.2888332649806]], "leg_l": [[165.90884651807326, 64.7540782623559], [173.95930503181157, 109.35608682232831], [164.0, 147.50785240061987], [174.65358746612154, 150.2050526233538]]}, {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[146.0, 106.0], [169.0442059174294, 69.57790593735669], [185.91158816514124, 32.150800629466346]], "head": [196.21234966334234, 15.267900562266604], "nose": [[206.14003446926995, 16.468348143027285], [213.0878575307592, 17.30847326270656]], "arm_r": [[185.91158816514124, 34.06093058380257], [215.8933129757141, 33.02985173593716], [245.43754556608036, 27.899549586052117]], "arm_l": [[191.8204346832145, 32.21028618115454], [221.80215949378737, 31.179207333289117], [251.34639208415362, 26.048905183404077]], "leg_r": [[146.0, 107.2155372436685], [189.49303604512522, 121.96649178745926], [160.0, 148.57746287018125], [169.92809767204204, 153.24184306651708]], "leg_l": [[151.90884651807326, 106.7540782623559], [194.83033076218078, 123.04799530614652], [164.0, 148.14592677410715], [173.6741219853583, 153.3019727452271]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[153.0, 85.0], [166.42277500746357, 44.23170229834066], [176.44238319738355, 4.551065281965364]], "head": [182.7884763254854, -14.12726447827346], "nose": [[192.77038521149322, -13.526019942799008], [199.7573288004463, -13.105172417491014]], "arm_r": [[176.44238319738355, 6.461195236301592], [198.38299424595868, 26.610313412236394], [222.4987000644752, 44.18389617406784]], "arm_l": [[182.3512297154568, 4.610550833653553], [204.29184076403195, 24.759669009588354], [228.40754658254846, 42.3332517714198]], "leg_r": [[153.0, 86.2155372436685], [178.8862823773017, 124.14007916459117], [160.0, 144.6419652845282], [168.23298591666023, 151.5226164668142]], "leg_l": [[158.90884651807326, 85.7540782623559], [190.7002154063726, 118.4951797068221], [164.0, 147.82688958736352], [173.4411956216626, 153.38593746786563]]}}}, {"name": "Butterfly Stretch", "camera": 52.0, "frames": [{"order": ["leg_l", "spine", "leg_r", "arm_l", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 118.0], [167.50046261033052, 77.00750092244417], [188.06995283948805, 54.81524765398234]], "head": [198.03156476589623, 45.452201386939706], "nose": [[203.41726561244826, 53.87800855251433], [206.71863397655784, 59.04292469083349]], "arm_r": [[179.4018345498141, 55.99124107973272], [179.3792722923378, 85.04096750172937], [179.7792766637896, 113.93409674302181]], "arm_l": [[200.37592029395185, 54.85187061649525], [200.26403873897775, 84.5670097002087], [200.5936451686343, 114.1932218141525]], "leg_r": [[154.48392472475294, 118.74835945275024], [153.99624906356505, 134.04977472288715], [176.83193200832542, 158.80286328447318], [178.74974735954865, 164.86196005792743]], "leg_l": [[169.15392444003692, 118.46425693551305], [213.29985218754484, 127.71028366316729], [191.32252689605835, 158.54310040943946], [200.45869159355496, 163.62289220662225]]}, {"order": ["leg_l", "spine", "leg_r", "arm_l", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 118.0], [169.39539005652387, 77.23415963036267], [194.15610676495888, 64.60624353918908]], "head": [205.57273436097324, 59.76538829462121], "nose": [[209.6047010216403, 68.91652182262033], [212.25797438704146, 74.938510816213]], "arm_r": [[185.48798847528494, 65.78223696493946], [183.36407363225595, 94.71822023026505], [181.67844369976953, 123.52169374631076]], "arm_l": [[206.46207421942268, 64.64286650170199], [205.1884194198, 94.35768290107595], [204.37141029440315, 124.00281258392798]], "leg_r": [[154.48392472475294, 118.74835945275024], [149.23043809783283, 133.36867530000643], [174.0881495814435, 157.71001146004073], [175.23930996841958, 163.86691836751203]], "leg_l": [[169.15392444003692, 118.46425693551305], [214.26107828554112, 126.25220384951403], [193.48786929959434, 156.78423338611003], [202.8873875451986, 161.82209527241005]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "spine", "leg_r", "arm_l", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 118.0], [168.41999929273476, 77.11286671031178], [191.23476868132948, 59.53447897382967]], "head": [202.0041618662562, 52.379193807430994], "nose": [[206.70285021478765, 61.20655625628143], [209.69657099521638, 66.8308191226959]], "arm_r": [[182.56665039165554, 60.71047239958005], [181.08833447545757, 90.58306392738217], [180.06729563247913, 120.42215403788741]], "arm_l": [[203.54073613579325, 59.57110193634258], [202.3592043555531, 89.54289707323235], [201.64032860894966, 119.51249717266958]], "leg_r": [[154.48392472475294, 118.74835945275024], [151.6094041501542, 133.7297564954217], [175.4705656164614, 158.27213348395813], [177.00298826323856, 164.3849879986275]], "leg_l": [[169.15392444003692, 118.46425693551305], [213.84170829120612, 126.99293200911858], [192.42833487469392, 157.67765196031715], [201.7026563745822, 162.74017215214639]]}}}, {"name": "Calfs", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[140.0, 118.0], [130.9947902691037, 76.01258331473494], [128.14293290308055, 35.16126798294198]], "head": [131.61589645641916, 15.764341775082897], "nose": [[141.46845419959308, 17.475220472543253], [148.36210847067855, 18.672290974183092]], "arm_r": [[128.14293290308055, 37.071397937278206], [155.6576522621167, 48.84552396045507], [183.3010064025833, 37.36733835695226]], "arm_l": [[134.0517794211538, 35.22075353463017], [161.85257237368958, 46.324036719323196], [187.80927886043239, 31.51096362388954]], "leg_r": [[140.0, 119.2155372436685], [185.99829621354849, 118.82564110752023], [212.0, 148.75976983403476], [218.68954801152353, 140.16030879539852]], "leg_l": [[145.90884651807326, 118.7540782623559], [191.8707292881921, 116.9102687478058], [216.0, 148.32823373796066], [223.2019010505247, 140.1399483462988]]}, {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[140.0, 118.0], [130.9947902691037, 76.01258331473494], [128.14293290308055, 35.16126798294198]], "head": [131.61589645641916, 15.764341775082897], "nose": [[141.46845419959308, 17.475220472543253], [148.36210847067855, 18.672290974183092]], "arm_r": [[128.14293290308055, 37.071397937278206], [155.6576522621167, 48.84552396045507], [183.3010064025833, 37.36733835695226]], "arm_l": [[134.0517794211538, 35.22075353463017], [161.85257237368958, 46.324036719323196], [187.80927886043239, 31.51096362388954]], "leg_r": [[140.0, 119.2155372436685], [185.1751715569397, 110.6752678976193], [212.0, 139.89650005692488], [222.040000508351, 135.47051611931823]], "leg_l": [[145.90884651807326, 118.7540782623559], [190.80108243714875, 108.87222292080564], [216.0, 139.46496396085078], [226.26606991421917, 135.57431163727887]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[140.0, 118.0], [130.9947902691037, 76.01258331473494], [128.14293290308055, 35.16126798294198]], "head": [131.61589645641916, 15.764341775082897], "nose": [[141.46845419959308, 17.475220472543253], [148.36210847067855, 18.672290974183092]], "arm_r": [[128.14293290308055, 37.071397937278206], [155.6576522621167, 48.84552396045507], [183.3010064025833, 37.36733835695226]], "arm_l": [[134.0517794211538, 35.22075353463017], [161.85257237368958, 46.324036719323196], [187.80927886043239, 31.51096362388954]], "leg_r": [[140.0, 119.2155372436685], [185.76022286869107, 114.59616916585809], [212.0, 144.3281349454798], [220.6686979213759, 137.6594754981849]], "leg_l": [[145.90884651807326, 118.7540782623559], [191.50375298446497, 112.75527283004915], [216.0, 143.8965988494057], [225.03661802939996, 137.7197628895158]]}}}, {"name": "Cat-Cow", "camera": 0.0, "frames": [{"order": ["arm_r", "leg_r", "leg_l", "spine", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[190.0, 106.0], [145.1601730939384, 123.58044185519884], [109.35354598857317, 94.41322108943268]], "head": [89.42965202673827, 96.1298541129813], "nose": [[79.80600507406713, 93.4122413130513], [73.0771732024989, 91.51209267785273]], "arm_r": [[115.26239250664642, 94.47270664112087], [120.87972526065431, 123.49440084545728], [111.0, 151.39056507548827]], "arm_l": [[109.35354598857317, 96.32335104376892], [114.9679818582322, 125.3455889056239], [105.0, 153.2112865928978]], "leg_r": [[195.90884651807326, 106.7540782623559], [201.51483631471004, 151.7175669488609], [241.51483631471004, 151.7175669488609], [250.52550880188895, 157.93105468496077]], "leg_l": [[190.0, 107.2155372436685], [195.60598979663678, 152.17902593017354], [235.60598979663678, 152.17902593017354], [244.61666228381569, 158.3925136662734]]}, {"order": ["arm_r", "leg_r", "leg_l", "spine", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[190.0, 106.0], [152.08424735154603, 74.85543177687914], [109.94656592660422, 96.70312504123498]], "head": [93.56352504082439, 108.00037547050745], "nose": [[86.43854210873769, 115.01711390210609], [81.48879464043186, 119.89166358434622]], "arm_r": [[115.85541244467747, 96.76261059292317], [124.5590964138107, 125.03612878182669], [111.0, 151.39056507548827]], "arm_l": [[109.94656592660422, 98.61325499557122], [118.63058159288913, 126.89263803630388], [105.0, 153.2112865928978]], "leg_r": [[195.90884651807326, 106.7540782623559], [201.51483631471004, 151.7175669488609], [241.51483631471004, 151.7175669488609], [250.52550880188895, 157.93105468496077]], "leg_l": [[190.0, 107.2155372436685], [195.60598979663678, 152.17902593017354], [235.60598979663678, 152.17902593017354], [244.61666228381569, 158.39251366627337]]}], "tween": {"t": 0.5, "sample": {"order": ["arm_r", "leg_r", "leg_l", "spine", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[190.0, 106.0], [147.07348578236468, 98.95228286813989], [105.7422999370209, 95.05845136735941]], "head": [86.94844752130274, 101.79493314402872], "nose": [[77.25780748643307, 104.26302870834161], [70.48077400378631, 105.98906189268591]], "arm_r": [[111.65114645509415, 95.1179369190476], [120.46983670889635, 123.3568712620384], [111.0, 151.39056507548824]], "arm_l": [[105.7422999370209, 96.96858132169565], [114.56083205483813, 125.20756355913188], [105.0, 153.21128659289778]], "leg_r": [[195.90884651807326, 106.7540782623559], [201.51483631471007, 151.7175669488609], [241.51483631471004, 151.7175669488609], [250.52550880188895, 157.93105468496077]], "leg_l": [[190.0, 107.2155372436685], [195.60598979663678, 152.17902593017354], [235.60598979663678, 152.17902593017354], [244.61666228381569, 158.39251366627337]]}}}, {"name": "Chest Opener", "camera": 0.0, "frames": [{"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 66.0], [161.1256802152048, 23.170537162876016], [160.7504534768032, -17.70220939343298]], "head": [160.05246354275317, -37.38636608818675], "nose": [[169.95803733477305, -38.757353998727396], [176.88991381596406, -39.71676524058096]], "arm_r": [[160.7504534768032, -15.792079439096753], [147.59931907313086, 10.762100915602247], [151.77451210193283, 40.018811065315994]], "arm_l": [[166.65929999487645, -17.642723841744793], [153.50816559120412, 8.911456512954208], [157.6833586200061, 38.16816666266796]], "leg_r": [[160.0, 67.2155372436685], [168.06607636623892, 111.81480911859667], [158.0, 149.939388496694], [168.64599592855438, 152.6655047598624]], "leg_l": [[165.90884651807326, 66.7540782623559], [176.5209189274121, 110.83326271847169], [162.0, 147.53823689459546], [172.24958208092917, 151.47082311825417]]}, {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 66.0], [154.58807701263999, 23.32473036716258], [140.95635941600804, -15.463026121913273]], "head": [131.8765494212171, -33.012428782016556], "nose": [[139.35867698448504, -39.64701747514489], [144.5606907628268, -44.25977253758464]], "arm_r": [[140.95635941600804, -13.552896167577046], [138.72959228006906, 15.909837994427669], [150.0, 43.289936129031]], "arm_l": [[146.8652059340813, -15.403540570225084], [141.44082956091827, 13.653731466709052], [154.0, 40.48440685860927]], "leg_r": [[160.0, 67.2155372436685], [168.06607636623892, 111.81480911859667], [158.0, 149.939388496694], [168.64599592855438, 152.6655047598624]], "leg_l": [[165.90884651807326, 66.7540782623559], [176.5209189274121, 110.83326271847169], [162.0, 147.53823689459546], [172.24958208092917, 151.47082311825417]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 66.0], [157.86202344571535, 23.15540830526413], [150.7809886433799, -17.175586742622897]], "head": [145.7733885622911, -36.2443727887196], "nose": [[154.86097067357207, -40.41760311674206], [161.2051251828286, -43.330987301829545]], "arm_r": [[150.7809886433799, -15.26545678828667], [142.95151607473076, 13.254877696829503], [150.73286795258437, 41.78798182584565]], "arm_l": [[156.68983516145317, -17.11610119093471], [147.31553692727067, 10.948717387584443], [155.7728940366232, 39.29464460290591]], "leg_r": [[160.0, 67.2155372436685], [168.06607636623892, 111.81480911859668], [158.0, 149.939388496694], [168.64599592855438, 152.6655047598624]], "leg_l": [[165.90884651807326, 66.7540782623559], [176.5209189274121, 110.8332627184717], [162.0, 147.53823689459546], [172.24958208092917, 151.47082311825417]]}}}, {"name": "Chest Press", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[116.0, 122.0], [104.04264946482697, 80.73349088839885], [98.30413502720322, 40.17760054359378]], "head": [100.39470429255628, 20.589343081612355], "nose": [[110.35779236721048, 19.730928253756097], [117.3311552538527, 19.130106695514076]], "arm_r": [[98.30413502720322, 42.08773049793001], [93.0946896971953, 71.18311980971865], [116.07602299076464, 52.19245316293397]], "arm_l": [[104.21298154527646, 40.237086095281974], [99.00353621526855, 69.3324754070706], [121.98486950883789, 50.341808760285936]], "leg_r": [[116.0, 123.2155372436685], [160.6875870401012, 112.47166989865372], [158.0, 151.77496208102255], [168.97514242459204, 152.5028201333689]], "leg_l": [[121.90884651807325, 122.7540782623559], [166.38862646620098, 111.20411892847166], [162.0, 150.35861823193625], [172.93359315614495, 151.5471554084083]]}, {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[116.0, 122.0], [104.04264946482697, 80.73349088839885], [98.30413502720322, 40.17760054359378]], "head": [100.39470429255628, 20.589343081612355], "nose": [[110.35779236721048, 19.730928253756097], [117.3311552538527, 19.130106695514076]], "arm_r": [[98.30413502720322, 42.08773049793001], [128.1397918882514, 45.17594372902312], [157.2486636765313, 52.323340481322006]], "arm_l": [[104.21298154527646, 40.237086095281974], [134.04863840632467, 43.32529932637508], [163.15751019460455, 50.47269607867397]], "leg_r": [[116.0, 123.2155372436685], [160.6875870401012, 112.47166989865372], [158.0, 151.77496208102255], [168.97514242459204, 152.5028201333689]], "leg_l": [[121.90884651807325, 122.7540782623559], [166.38862646620098, 111.20411892847166], [162.0, 150.35861823193625], [172.93359315614495, 151.5471554084083]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[116.0, 122.0], [104.04264946482697, 80.73349088839885], [98.30413502720322, 40.17760054359378]], "head": [100.39470429255628, 20.589343081612355], "nose": [[110.35779236721048, 19.730928253756097], [117.3311552538527, 19.130106695514076]], "arm_r": [[98.30413502720322, 42.08773049793001], [116.35858572176467, 65.68280376169301], [145.58968766532172, 59.036797490709745]], "arm_l": [[104.21298154527646, 40.237086095281974], [122.26743223983792, 63.83215935904497], [151.49853418339498, 57.18615308806171]], "leg_r": [[116.0, 123.2155372436685], [160.6875870401012, 112.47166989865372], [158.0, 151.77496208102255], [168.97514242459204, 152.5028201333689]], "leg_l": [[121.90884651807325, 122.7540782623559], [166.38862646620098, 111.20411892847166], [162.0, 150.35861823193625], [172.93359315614495, 151.5471554084083]]}}}, {"name": "Child's Pose", "camera": 0.0, "frames": [{"order": ["arm_r", "leg_r", "spine", "arm_l", "leg_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[201.0, 108.0], [155.82571467885413, 111.8022553753272], [124.45770572282038, 141.37389729585914]], "head": [111.33652514301022, 156.23877421337863], "nose": [[117.27325277234276, 164.28583983105068], [121.38774953839007, 169.86292328970427]], "arm_r": [[130.36655224089364, 141.4333828475473], [107.12136215799222, 160.10999998477317], [79.99999999999999, 147.481718557415]], "arm_l": [[124.45770572282038, 143.28402725019538], [108.99208153799717, 168.5998024546127], [83.99999999999999, 152.2568633338612]], "leg_r": [[206.90884651807326, 108.7540782623559], [209.31630050524868, 153.9931512401667], [247.56849074377007, 142.47595437727284], [257.8378754352393, 146.35811326384913]], "leg_l": [[201.0, 109.2155372436685], [203.40745398717542, 154.45461022147933], [241.6596442256968, 142.93741335858545], [251.92902891716605, 146.81957224516174]]}, {"order": ["arm_r", "leg_r", "spine", "arm_l", "leg_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[202.0, 110.0], [156.74378800446112, 116.33532375305779], [128.93070318831133, 149.67279357603658]], "head": [118.03792248801076, 166.19137912675268], "nose": [[124.78564870801182, 173.57163595561101], [129.46956295252383, 178.69461945579832]], "arm_r": [[134.8395497063846, 149.73227912772478], [106.04161630328194, 158.0116388932225], [78.00000000000001, 147.5121030513906]], "arm_l": [[128.93070318831133, 151.58292353037282], [105.75011328400946, 170.33724855225375], [82.0, 152.28724782783678]], "leg_r": [[207.90884651807326, 110.7540782623559], [209.5142233663883, 156.02763866028954], [247.10192819782466, 142.55467510695092], [257.5635498770713, 145.9022207575537]], "leg_l": [[202.0, 111.2155372436685], [203.60537684831505, 156.48909764160214], [241.1930816797514, 143.01613408826356], [251.65470335899806, 146.36367973886632]]}], "tween": {"t": 0.5, "sample": {"order": ["arm_r", "leg_r", "spine", "leg_l", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[201.5, 109.0], [156.26996632297158, 114.06239831006303], [126.62305565266601, 145.56456984475278]], "head": [114.58675518962505, 161.29461868726145], "nose": [[120.93805820162584, 169.01866879389266], [125.34330093897469, 174.37604437034733]], "arm_r": [[132.53190217073927, 145.62405539644095], [105.28609458574061, 133.25830599136376], [99.20171180988373, 104.32807532258633]], "arm_l": [[126.62305565266601, 147.47469979908902], [102.5416627187038, 129.85554190324453], [118.69316943490644, 104.95861963534743]], "leg_r": [[207.40884651807326, 109.7540782623559], [209.41533833687873, 155.012118237712], [247.3482845451267, 142.51275480992763], [257.71734094714066, 146.12884622273043]], "leg_l": [[201.5, 110.2155372436685], [203.50649181880547, 155.4735772190246], [241.43943802705343, 142.97421379124023], [251.8084944290674, 146.59030520404303]]}}}, {"name": "Cobra Stretch", "camera": 0.0, "frames": [{"order": ["arm_r", "leg_r", "spine", "leg_l", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[188.0, 147.0], [146.2115550987712, 134.51133070811812], [104.6860248829862, 130.99893053725023]], "head": [84.69820834260429, 130.3115446386733], "nose": [[77.21608077933637, 136.94613333180163], [72.0140670009946, 141.5588883942414]], "arm_r": [[110.59487140105945, 131.0584160889384], [133.96056156318676, 149.58862856981378], [104.00000000000001, 148.07421618993888]], "arm_l": [[104.6860248829862, 132.90906049158647], [78.52170000775969, 147.36354601159968], [89.13411237249052, 119.7296105142682]], "leg_r": [[193.90884651807326, 147.7540782623559], [235.07408286009883, 167.9708002769172], [270.0, 148.76880890060664], [280.80420398805575, 150.8036171891036]], "leg_l": [[188.0, 148.2155372436685], [233.91528622289013, 150.9635728393661], [273.8970875772516, 152.15171001244428], [282.10647978204673, 159.3620820259009]]}, {"order": ["leg_r", "arm_r", "leg_l", "spine", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[188.0, 147.0], [148.3804213554066, 126.63849409496028], [126.34500567148017, 90.56665811833622]], "head": [116.64881326655342, 73.34001274777336], "nose": [[106.82714865461051, 75.22014685216816], [99.95575837047687, 76.5355180981072]], "arm_r": [[132.2538521895534, 90.6261436700244], [118.68593676222457, 116.976177686032], [104.21670561230384, 142.85698070747202]], "arm_l": [[126.34500567148017, 92.47678807267246], [117.22115287129492, 120.6215411178757], [107.1206945871057, 148.44093505140947]], "leg_r": [[193.90884651807326, 147.7540782623559], [235.07408286009883, 167.9708002769172], [270.0, 148.76880890060664], [280.80420398805575, 150.8036171891036]], "leg_l": [[188.0, 148.2155372436685], [233.91528622289013, 150.9635728393661], [273.8970875772516, 152.15171001244428], [282.10647978204673, 159.3620820259009]]}], "tween": {"t": 0.5, "sample": {"order": ["arm_r", "leg_r", "spine", "leg_l", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[188.0, 147.0], [147.58496764320086, 130.84243706328948], [112.31672612283286, 109.07150914920337]], "head": [95.26392283575102, 98.78029639980302], "nose": [[86.28759453505705, 103.18773672704067], [80.02305400384289, 106.2636701871341]], "arm_r": [[118.22557264090611, 109.13099470089155], [131.2607963421022, 135.74054653115266], [104.0, 148.0742161899389]], "arm_l": [[112.31672612283286, 110.9816391035396], [89.20688866169617, 129.8204583254148], [69.74512702248809, 107.33663014299796]], "leg_r": [[193.90884651807326, 147.7540782623559], [235.07408286009883, 167.97080027691717], [270.0, 148.76880890060661], [280.80420398805575, 150.8036171891036]], "leg_l": [[188.0, 148.2155372436685], [233.91528622289013, 150.9635728393661], [273.8970875772516, 152.15171001244425], [282.10647978204673, 159.3620820259009]]}}}, {"name": "Crunch", "camera": 0.0, "frames": [{"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[166.0, 146.0], [122.5, 146.0], [81.0, 146.0]], "head": [61.437047985323886, 141.90493909881587], "nose": [[63.546821344204844, 132.13002956602958], [65.00220317992915, 125.387018180479]], "arm_r": [[81.0, 147.91012995433624], [53.810766388900504, 135.42419773253272], [66.48931424112148, 162.20036579120364]], "arm_l": [[86.90884651807325, 146.05948555168817], [59.71961290697375, 133.57355332988467], [72.39816075919472, 160.34972138855557]], "leg_r": [[166.0, 147.2155372436685], [194.0368351722767, 111.30128659027031], [205.0, 149.18515274969292], [215.27575990220512, 145.31938923995287]], "leg_l": [[171.90884651807326, 146.7540782623559], [199.48140422111373, 110.49289964731176], [208.99999999999997, 148.75361665361882], [219.41521534843125, 145.26855340182198]]}, {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[166.0, 144.0], [117.46766838873427, 144.00204748311933], [96.1737983030601, 103.01749698724782]], "head": [92.0155644867049, 83.75175017138899], "nose": [[101.80342162264763, 81.70288397605682], [108.65045482778427, 80.26961266064238]], "arm_r": [[96.1737983030601, 104.92762694158407], [89.93644757852732, 76.02900671779581], [75.8523006949506, 102.11501576632395]], "arm_l": [[102.08264482113334, 103.076982538936], [95.84529409660057, 74.17836231514775], [81.76114721302385, 100.26437136367588]], "leg_r": [[166.0, 145.2155372436685], [195.90537209116957, 110.79416451246408], [205.0, 149.15476825571733], [215.45316340216056, 145.78169389384772]], "leg_l": [[171.90884651807326, 144.7540782623559], [201.45982836379503, 110.03713295240527], [208.99999999999997, 148.72323215964326], [219.5809665807253, 145.76173557962537]]}], "tween": {"t": 0.5, "sample": {"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[166.0, 145.0], [121.16522782962937, 145.67055250176602], [84.95160625843393, 123.01582062720574]], "head": [70.80947063470298, 109.08853582080536], "nose": [[77.93445356678967, 102.07179738920672], [82.8842010350955, 97.19724770696659]], "arm_r": [[84.95160625843393, 124.92595058154197], [66.27616715930534, 101.80439316837644], [65.49085871006913, 131.33850168811765]], "arm_l": [[90.86045277650717, 123.07530617889392], [72.18501367737859, 99.95374876572838], [71.39970522814238, 129.48785728546957]], "leg_r": [[166.0, 146.2155372436685], [194.97879634694823, 111.03391792015745], [205.0, 149.16996050270512], [215.36848703280944, 145.55228605768735]], "leg_l": [[171.90884651807326, 145.7540782623559], [200.47950405948097, 110.25018764814351], [209.0, 148.73842440663103], [219.50242901519758, 145.51718692476985]]}}}, {"name": "Dead Bug", "camera": 0.0, "frames": [{"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[185.0, 148.0], [141.5, 148.0], [100.0, 148.0]], "head": [80.43704798532389, 143.90493909881587], "nose": [[80.43704798532389, 133.90493909881587], [80.43704798532389, 127.01128482773042]], "arm_r": [[100.0, 149.91012995433624], [100.0, 120.36589736396999], [100.0, 90.82166477360374]], "arm_l": [[105.90884651807325, 148.05948555168817], [105.90884651807325, 118.51525296132193], [105.90884651807325, 88.97102037095569]], "leg_r": [[185.0, 149.2155372436685], [205.16507275229756, 108.4991273664634], [244.55738287278587, 115.33953023297677], [246.4675128271221, 104.67122081865428]], "leg_l": [[190.90884651807326, 148.7540782623559], [211.07391927037082, 108.03766838515077], [250.46622939085913, 114.87807125166415], [252.37635934519537, 104.20976183734166]]}, {"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[185.0, 148.0], [141.5, 148.0], [100.0, 148.0]], "head": [80.43704798532389, 143.90493909881587], "nose": [[80.43704798532389, 133.90493909881587], [80.43704798532389, 127.01128482773042]], "arm_r": [[100.0, 149.91012995433624], [78.78679656440357, 129.01920274473565], [57.57359312880715, 108.12827553513509]], "arm_l": [[105.90884651807325, 148.05948555168817], [105.90884651807325, 118.51525296132193], [105.90884651807325, 88.97102037095569]], "leg_r": [[185.0, 149.2155372436685], [205.16507275229756, 108.4991273664634], [244.55738287278587, 115.33953023297677], [246.4675128271221, 104.67122081865428]], "leg_l": [[190.90884651807326, 148.7540782623559], [235.12688453123593, 136.2673872310511], [273.57735236876863, 125.40939502991648], [270.5453414547817, 114.99615735074556]]}, {"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[185.0, 148.0], [141.5, 148.0], [100.0, 148.0]], "head": [80.43704798532389, 143.90493909881587], "nose": [[80.43704798532389, 133.90493909881587], [80.43704798532389, 127.01128482773042]], "arm_r": [[100.0, 149.91012995433624], [100.0, 120.36589736396999], [100.0, 90.82166477360374]], "arm_l": [[105.90884651807325, 148.05948555168817], [105.90884651807325, 118.51525296132193], [105.90884651807325, 88.97102037095569]], "leg_r": [[185.0, 149.2155372436685], [205.16507275229756, 108.4991273664634], [244.55738287278587, 115.33953023297677], [246.4675128271221, 104.67122081865428]], "leg_l": [[190.90884651807326, 148.7540782623559], [211.07391927037082, 108.03766838515077], [250.46622939085913, 114.87807125166415], [252.37635934519537, 104.20976183734166]]}, {"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[185.0, 148.0], [141.5, 148.0], [100.0, 148.0]], "head": [80.43704798532389, 143.90493909881587], "nose": [[80.43704798532389, 133.90493909881587], [80.43704798532389, 127.01128482773042]], "arm_r": [[100.0, 149.91012995433624], [100.0, 120.36589736396999], [100.0, 90.82166477360374]], "arm_l": [[105.90884651807325, 148.05948555168817], [84.69564308247682, 127.16855834208761], [63.482439646880394, 106.27763113248703]], "leg_r": [[185.0, 149.2155372436685], [229.21803801316267, 136.7288462123637], [267.6685058506954, 125.87085401122908], [264.63649493670846, 115.45761633205817]], "leg_l": [[190.90884651807326, 148.7540782623559], [211.07391927037082, 108.03766838515077], [250.46622939085913, 114.87807125166415], [252.37635934519537, 104.20976183734166]]}], "tween": {"t": 0.5, "sample": {"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[185.0, 148.0], [141.5, 148.0], [100.0, 148.0]], "head": [80.43704798532389, 143.90493909881587], "nose": [[80.43704798532389, 133.90493909881587], [80.43704798532389, 127.01128482773042]], "arm_r": [[100.0, 149.91012995433624], [88.51949702904732, 122.61481816034394], [77.03899405809463, 95.31950636635166]], "arm_l": [[105.90884651807325, 148.05948555168817], [105.90884651807325, 118.51525296132193], [105.90884651807325, 88.97102037095569]], "leg_r": [[185.0, 149.2155372436685], [205.16507275229756, 108.4991273664634], [244.55738287278587, 115.33953023297677], [246.4675128271221, 104.67122081865428]], "leg_l": [[190.90884651807326, 148.7540782623559], [226.14689090154624, 119.63505607061941], [266.0920722917292, 117.57342185184505], [265.5163767730568, 106.755382661499]]}}}, {"name": "Deadlift", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[92.0, 90.0], [135.1183334051322, 83.18419756301371], [174.00472051595008, 68.41408112378903]], "head": [193.49212181165478, 63.983410276466856], "nose": [[203.4198066175824, 65.18385785722754], [210.36762967907163, 66.02398297690681]], "arm_r": [[174.00472051595008, 70.32421107812527], [166.15560263227576, 98.83930806126465], [159.29930173624587, 127.60161513162308]], "arm_l": [[179.91356703402334, 68.4735666754772], [171.25570462090087, 96.76072587520919], [163.5755155861773, 125.32040024801982]], "leg_r": [[92.0, 91.2155372436685], [113.48276422122781, 131.27301891844934], [106.37742255819141, 170.03886151266857], [117.33935559069171, 169.138408861487]], "leg_l": [[97.90884651807325, 90.7540782623559], [116.99012526171511, 131.97395705768585], [104.17165311229812, 169.28877339739455], [115.1488380723927, 169.98611906473377]]}, {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 64.0], [157.55369572851146, 21.811685497906495], [164.76009510168907, -18.436936383401104]], "head": [168.91832891804427, -37.70268319925994], "nose": [[178.8947013481789, -37.01566563380052], [185.87764969999765, -36.53478862063847]], "arm_r": [[164.76009510168907, -16.526806429064877], [170.85644942928292, 12.400979889721263], [191.1744301067988, 34.137801113206514]], "arm_l": [[170.66894161976234, -18.377450831712917], [175.4474723060758, 10.789583023349335], [194.20923781110346, 33.84326058608493]], "leg_r": [[150.0, 65.2155372436685], [161.22887526493585, 109.1462686355685], [150.0, 146.95458074368176], [160.55768072289158, 149.9956086837736]], "leg_l": [[155.90884651807326, 64.7540782623559], [166.01236624892732, 108.94900788074216], [154.0, 146.52304464760766], [164.49226113348837, 149.77625928644824]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[121.0, 77.0], [152.0282252858047, 46.91558466206313], [178.42478914733817, 15.322067738987485]], "head": [192.44297443333522, 1.2737762956579388], "nose": [[202.3983153476498, 2.217803456164566], [209.36608873622006, 2.878530937391008]], "arm_r": [[178.42478914733817, 17.23219769332372], [170.69935078808214, 45.78003344335117], [163.8809515483398, 74.551077451591]], "arm_l": [[184.33363566541144, 15.381553290675667], [175.8851979024652, 43.73005997979728], [168.3328137749549, 72.32277049751575]], "leg_r": [[121.0, 78.2155372436685], [155.53625674578984, 108.1389004808649], [150.0, 147.1520799545231], [160.9996521719293, 147.2382272564324]], "leg_l": [[126.90884651807325, 77.7540782623559], [161.1827791149295, 107.96854417466466], [154.0, 146.72054385844896], [164.98644510033364, 147.25816596777275]]}}}, {"name": "Dip", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 86.0], [166.05402989176287, 43.57777028291508], [171.82971358160557, 3.105987909144403]], "head": [176.32873466848287, -16.08535597292574], "nose": [[186.3154424382918, -15.569926132464488], [193.30584918157382, -15.209140144178978]], "arm_r": [[171.82971358160557, 5.0161178634806305], [165.65325797831116, 33.92742009448894], [174.0, 62.30512837601881]], "arm_l": [[177.73856009967884, 3.165473460832592], [170.58370649634907, 31.857168685039003], [178.0, 60.4844068586093]], "leg_r": [[160.0, 87.2155372436685], [187.68349106499423, 123.39464958143844], [147.73830967481126, 121.3330153626641], [143.44026726142926, 131.30473884268272]], "leg_l": [[165.90884651807326, 86.7540782623559], [193.5923375830675, 122.93319060012583], [153.64715619288452, 120.87155638135147], [149.34911377950252, 130.8432798613701]]}, {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 116.0], [172.06485767544996, 74.76384973073141], [189.02120768159105, 37.37575364089446]], "head": [199.02120768159102, 20.318383001845604], "nose": [[208.69023618469885, 22.86983118005503], [215.45171696872234, 24.65404019576428]], "arm_r": [[189.02120768159105, 39.285883595230686], [159.1021538806009, 37.11702038250827], [174.0, 62.76089578565258]], "arm_l": [[194.9300541996643, 37.43523919258265], [165.103239794315, 34.26526825327232], [178.0, 60.94017426824305]], "leg_r": [[160.0, 117.2155372436685], [199.83716857408416, 139.8661155629493], [159.86153549332033, 141.24088736010316], [156.46234855519592, 151.54357349890591]], "leg_l": [[165.90884651807326, 116.7540782623559], [205.74601509215742, 139.4046565816367], [165.7703820113936, 140.77942837879056], [162.37119507326918, 151.0821145175933]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 101.0], [169.07414305802632, 59.084192074171945], [180.54329523109206, 19.78605846585289]], "head": [187.873319765578, 1.460409776103674], "nose": [[197.75384933141916, 3.0015572377499415], [204.66766771558514, 4.079962355819498]], "arm_r": [[180.54329523109206, 21.696188420189117], [156.09947277307057, 38.82447106310195], [174.0, 62.533012080835704]], "arm_l": [[186.45214174916532, 19.845544017541076], [161.40189587227587, 36.1018888687581], [178.0, 60.71229056342617]], "leg_r": [[160.0, 102.2155372436685], [194.45196315629408, 132.23299183974086], [154.45348623372723, 131.88923344711145], [150.6012050398731, 142.0360958211416]], "leg_l": [[165.90884651807326, 101.7540782623559], [200.36080967436735, 131.77153285842823], [160.3623327518005, 131.42777446579885], [156.51005155794635, 141.574636839829]]}}}, {"name": "Dumbbell Bench Press", "camera": 0.0, "frames": [{"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 116.0], [106.5, 116.0], [65.0, 116.0]], "head": [45.194638625168594, 113.25882502327553], "nose": [[46.60741399901631, 103.35912473775934], [47.58162570573677, 96.53255903615947]], "arm_r": [[65.0, 117.91012995433623], [89.01219176211681, 135.62064948104614], [90.0, 106.09243691818973]], "arm_l": [[70.90884651807325, 116.05948555168817], [94.99848677843022, 133.66770585501038], [97.99999999999999, 104.2717154007802]], "leg_r": [[150.0, 117.2155372436685], [195.30008769797305, 109.34310611741422], [196.0, 148.72938534005917], [206.93968862700535, 147.59655128433957]], "leg_l": [[155.90884651807326, 116.7540782623559], [201.21645940182967, 108.92375949218865], [200.0, 148.29784924398507], [210.98222903324333, 147.68232827813742]]}, {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 116.0], [106.5, 116.0], [65.0, 116.0]], "head": [45.194638625168594, 113.25882502327553], "nose": [[46.60741399901631, 103.35912473775934], [47.58162570573677, 96.53255903615947]], "arm_r": [[65.0, 117.91012995433623], [60.069389734334294, 88.76765535754025], [44.92401672772445, 63.264791219785906]], "arm_l": [[70.90884651807325, 116.05948555168817], [65.94642267092193, 86.9222484977981], [50.637339472650595, 61.51436169540154]], "leg_r": [[150.0, 117.2155372436685], [195.30008769797305, 109.34310611741422], [196.0, 148.72938534005917], [206.93968862700535, 147.59655128433957]], "leg_l": [[155.90884651807326, 116.7540782623559], [201.21645940182967, 108.92375949218865], [200.0, 148.29784924398507], [210.98222903324333, 147.68232827813742]]}], "tween": {"t": 0.5, "sample": {"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 116.0], [106.5, 116.0], [65.0, 116.0]], "head": [45.194638625168594, 113.25882502327553], "nose": [[46.60741399901631, 103.35912473775934], [47.58162570573677, 96.53255903615947]], "arm_r": [[65.0, 117.91012995433623], [93.37505541110951, 108.318677244387], [78.0, 82.94945472240286]], "arm_l": [[70.90884651807325, 116.05948555168817], [99.46709581175503, 107.01069392508388], [85.0, 81.12873320499331]], "leg_r": [[150.0, 117.2155372436685], [195.30008769797305, 109.34310611741422], [196.0, 148.72938534005917], [206.93968862700535, 147.59655128433957]], "leg_l": [[155.90884651807326, 116.7540782623559], [201.21645940182967, 108.92375949218865], [200.0, 148.29784924398507], [210.98222903324333, 147.68232827813742]]}}}, {"name": "Dumbbell Curl", "camera": 0.0, "frames": [{"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [162.27661409656804, 21.21957229272239], [164.44855628065022, -19.59393919812868]], "head": [166.1916711356034, -39.215144441936815], "nose": [[176.1901940025219, -39.04327100097566], [183.18912786861665, -38.92296014479856]], "arm_r": [[164.44855628065022, -17.683809243792453], [164.44855628065022, 11.860423346573782], [169.65800161065815, 40.955812658362404]], "arm_l": [[170.35740279872348, -19.534453646440493], [170.35740279872348, 10.009778943925744], [175.5668481287314, 39.10516825571437]], "leg_r": [[160.0, 65.2155372436685], [168.06607636623892, 109.81480911859667], [158.0, 147.939388496694], [168.64599592855438, 150.6655047598624]], "leg_l": [[165.90884651807326, 64.7540782623559], [172.71840731660157, 109.55611956268012], [162.0, 147.50785240061987], [172.59772985997608, 150.4106343225862]]}, {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [162.27661409656804, 21.21957229272239], [164.44855628065022, -19.59393919812868]], "head": [166.1916711356034, -39.215144441936815], "nose": [[176.1901940025219, -39.04327100097566], [183.18912786861665, -38.92296014479856]], "arm_r": [[164.44855628065022, -17.683809243792453], [166.01863496793854, 11.819934002605898], [188.65992237462171, -7.562826542899472]], "arm_l": [[170.35740279872348, -19.534453646440493], [171.9274814860118, 9.96928959995786], [194.56876889269498, -9.41347094554751]], "leg_r": [[160.0, 65.2155372436685], [168.06607636623892, 109.81480911859667], [158.0, 147.939388496694], [168.64599592855438, 150.6655047598624]], "leg_l": [[165.90884651807326, 64.7540782623559], [172.71840731660157, 109.55611956268012], [162.0, 147.50785240061987], [172.59772985997608, 150.4106343225862]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [162.27661409656804, 21.21957229272239], [164.44855628065022, -19.59393919812868]], "head": [166.1916711356034, -39.215144441936815], "nose": [[176.1901940025219, -39.04327100097566], [183.18912786861665, -38.92296014479856]], "arm_r": [[164.44855628065022, -17.683809243792453], [165.23386472988642, 11.850299275948743], [193.51310946265176, 21.71236676541092]], "arm_l": [[170.35740279872348, -19.534453646440493], [171.14271124795968, 9.999654873300704], [199.42195598072502, 19.86172236276288]], "leg_r": [[160.0, 65.2155372436685], [168.06607636623892, 109.81480911859667], [158.0, 147.939388496694], [168.64599592855438, 150.6655047598624]], "leg_l": [[165.90884651807326, 64.7540782623559], [172.71840731660157, 109.55611956268012], [162.0, 147.50785240061987], [172.59772985997608, 150.4106343225862]]}}}, {"name": "Dumbbell Fly", "camera": 90.0, "frames": [{"order": ["arm_l", "arm_r", "spine", "leg_l", "leg_r", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 116.0], [160.0, 108.44630427148853], [160.0, 101.23990489831093]], "head": [160.0, 95.05956501081197], "nose": [[160.0, 85.05956501081197], [160.0, 78.40216939674589]], "arm_r": [[149.0, 101.23990489831093], [149.0, 71.89547687629675], [149.0, 41.895476876296755]], "arm_l": [[171.0, 101.23990489831093], [171.0, 71.89547687629675], [171.0, 41.895476876296755]], "leg_r": [[153.0, 116.0], [139.2057898989263, 116.18285705273352], [139.66261958055435, 156.14743421830946], [136.36799117671453, 155.65043725393338]], "leg_l": [[167.0, 116.0], [180.82470091432793, 118.16195384834933], [180.79806668035621, 158.1090965367697], [184.09202372933197, 157.69091950032282]]}, {"order": ["arm_l", "arm_r", "spine", "leg_l", "leg_r", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 116.0], [160.0, 108.44630427148853], [160.0, 101.23990489831093]], "head": [160.0, 95.05956501081197], "nose": [[160.0, 85.05956501081197], [160.0, 78.40216939674589]], "arm_r": [[149.0, 101.23990489831093], [119.16434313895179, 100.912118909318], [91.5012038069931, 89.43156725352722]], "arm_l": [[171.0, 101.23990489831093], [200.8356568610482, 100.912118909318], [228.49879619300688, 89.43156725352722]], "leg_r": [[153.0, 116.0], [139.2057898989263, 116.18285705273352], [139.66261958055435, 156.14743421830946], [136.36799117671453, 155.65043725393338]], "leg_l": [[167.0, 116.0], [180.82470091432793, 118.16195384834933], [180.79806668035621, 158.1090965367697], [184.09202372933197, 157.69091950032282]]}], "tween": {"t": 0.5, "sample": {"order": ["arm_l", "arm_r", "spine", "leg_l", "leg_r", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 116.0], [160.0, 108.44630427148853], [160.0, 101.23990489831093]], "head": [160.0, 95.05956501081197], "nose": [[160.0, 85.05956501081197], [160.0, 78.40216939674589]], "arm_r": [[149.0, 101.23990489831093], [128.92608180923423, 86.32207646778681], [109.72929837259207, 65.53785060661484]], "arm_l": [[171.0, 101.23990489831093], [191.07391819076574, 86.32207646778681], [210.2707016274079, 65.53785060661484]], "leg_r": [[153.0, 116.0], [139.20037079553018, 116.08135134071809], [139.88445590137957, 156.01219734499898], [136.58605196402783, 155.6991971878592]], "leg_l": [[167.0, 116.0], [180.83203697997072, 118.04697952897166], [180.53021656989924, 157.94616690783334], [183.8314873318548, 157.75202692624796]]}}}, {"name": "Dumbbell Row", "camera": 0.0, "frames": [{"order": ["arm_r", "leg_r", "leg_l", "spine", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[168.0, 82.0], [125.16086274396895, 74.5610618826667], [84.29134099396232, 67.46414390865907]], "head": [64.29134099396232, 67.46414390865907], "nose": [[56.582726094680645, 73.83417187834434], [51.2204149928478, 78.26532742926075]], "arm_r": [[90.20018751203557, 67.52362946034725], [102.09645322030373, 94.64572241081939], [92.0, 122.46652631042721]], "arm_l": [[84.29134099396232, 69.37427386299531], [85.51514091393564, 98.89391400097236], [66.0, 121.33282456880013]], "leg_r": [[173.90884651807326, 82.7540782623559], [211.1451698158023, 109.35220659139566], [241.63376726871167, 83.85261792608881], [247.1902245701585, 93.20185091031925]], "leg_l": [[168.0, 83.2155372436685], [162.4022022511762, 128.18001577127842], [202.0, 122.60784736415684], [211.34192807714578, 128.32723060939875]]}, {"order": ["arm_r", "leg_r", "leg_l", "spine", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[168.0, 82.0], [125.16086274396895, 74.5610618826667], [84.29134099396232, 67.46414390865907]], "head": [64.29134099396232, 67.46414390865907], "nose": [[56.582726094680645, 73.83417187834434], [51.2204149928478, 78.26532742926075]], "arm_r": [[90.20018751203557, 67.52362946034725], [119.61204459175761, 61.70220240985699], [99.99999999999999, 84.0590239429511]], "arm_l": [[84.29134099396232, 69.37427386299531], [85.51514091393564, 98.89391400097236], [66.0, 121.33282456880013]], "leg_r": [[173.90884651807326, 82.7540782623559], [211.1451698158023, 109.35220659139566], [241.63376726871167, 83.85261792608881], [247.1902245701585, 93.20185091031925]], "leg_l": [[168.0, 83.2155372436685], [162.4022022511762, 128.18001577127842], [202.0, 122.60784736415684], [211.34192807714578, 128.32723060939875]]}], "tween": {"t": 0.5, "sample": {"order": ["arm_r", "leg_r", "leg_l", "spine", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[168.0, 82.0], [125.16086274396895, 74.5610618826667], [84.29134099396232, 67.46414390865907]], "head": [64.29134099396232, 67.46414390865907], "nose": [[56.582726094680645, 73.83417187834434], [51.2204149928478, 78.26532742926075]], "arm_r": [[90.20018751203557, 67.52362946034725], [116.5166611402472, 81.70770635791291], [96.0, 103.26277512668918]], "arm_l": [[84.29134099396232, 69.37427386299531], [85.51514091393564, 98.89391400097236], [66.0, 121.33282456880013]], "leg_r": [[173.90884651807326, 82.7540782623559], [211.1451698158023, 109.35220659139566], [241.63376726871167, 83.85261792608881], [247.1902245701585, 93.20185091031925]], "leg_l": [[168.0, 83.2155372436685], [162.40220225117616, 128.18001577127842], [202.0, 122.60784736415684], [211.34192807714578, 128.32723060939878]]}}}, {"name": "Dumbbell Shoulder Press", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[130.0, 124.0], [130.7861315886847, 81.14917968668395], [134.4304502554608, 40.41789305174976]], "head": [137.2139122746621, 20.913419618607264], "nose": [[147.2080040702254, 21.25711887937817], [154.20373985935908, 21.497703943880097]], "arm_r": [[134.4304502554608, 42.328023006085985], [158.22858921859793, 60.31657064382028], [146.0, 33.338201171188686]], "arm_l": [[140.33929677353407, 40.47737860343795], [165.22803832271347, 56.97255699205585], [150.0, 31.51747965377915]], "leg_r": [[130.0, 125.2155372436685], [173.8364696761423, 111.48589278988837], [176.0, 150.82053882198593], [186.98389774627694, 150.2346069268626]], "leg_l": [[135.90884651807326, 124.7540782623559], [179.40646070778647, 110.0162218015468], [180.0, 149.4041949728996], [190.99878894026918, 149.24345139631006]]}, {"order": ["leg_l", "spine", "leg_r", "arm_l", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[130.0, 124.0], [130.7861315886847, 81.14917968668395], [134.4304502554608, 40.41789305174976]], "head": [137.2139122746621, 20.913419618607264], "nose": [[147.2080040702254, 21.25711887937817], [154.20373985935908, 21.497703943880097]], "arm_r": [[134.4304502554608, 42.328023006085985], [143.41856866823082, 14.140944212067012], [138.0, -14.91737872640951]], "arm_l": [[140.33929677353407, 40.47737860343795], [148.61393171247587, 12.079197208400549], [142.0, -16.738100243819016]], "leg_r": [[130.0, 125.2155372436685], [173.8364696761423, 111.48589278988837], [176.0, 150.82053882198593], [186.98389774627694, 150.2346069268626]], "leg_l": [[135.90884651807326, 124.7540782623559], [179.40646070778647, 110.0162218015468], [180.0, 149.4041949728996], [190.99878894026918, 149.24345139631006]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[130.0, 124.0], [130.7861315886847, 81.14917968668395], [134.4304502554608, 40.41789305174976]], "head": [137.2139122746621, 20.913419618607264], "nose": [[147.2080040702254, 21.25711887937817], [154.20373985935908, 21.497703943880097]], "arm_r": [[134.4304502554608, 42.328023006085985], [162.17101057015535, 31.079582070738752], [142.0, 9.210411222389595]], "arm_l": [[140.33929677353407, 40.47737860343795], [167.52019230026508, 27.97385205227781], [146.0, 7.3896897049800625]], "leg_r": [[130.0, 125.2155372436685], [173.8364696761423, 111.48589278988837], [176.0, 150.82053882198593], [186.98389774627694, 150.2346069268626]], "leg_l": [[135.90884651807326, 124.7540782623559], [179.40646070778647, 110.0162218015468], [180.0, 149.4041949728996], [190.99878894026918, 149.24345139631006]]}}}, {"name": "Face Pull", "camera": 0.0, "frames": [{"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 66.0], [153.06228919571583, 23.248423141253653], [158.8662093611655, -17.239211379257284]], "head": [162.6823892686964, -36.57349259579321], "nose": [[172.6454773433506, -35.715077767936954], [179.6188402299928, -35.114256209694936]], "arm_r": [[158.8662093611655, -15.329081424921057], [187.57122205368216, -6.742470038371721], [214.0, -20.722567816762492]], "arm_l": [[164.77505587923875, -17.179725827569097], [192.76969061470504, -6.559295559438974], [218.0, -22.543289334172012]], "leg_r": [[150.0, 67.2155372436685], [160.60371763953367, 111.29667164972889], [146.0, 147.96977299066958], [156.24068183654285, 151.9247829380784]], "leg_l": [[155.90884651807326, 66.7540782623559], [165.23395514054224, 111.11463772661938], [150.0, 147.5382368945955], [160.17101027135118, 151.6639291057432]]}, {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 66.0], [153.06228919571583, 23.248423141253653], [158.8662093611655, -17.239211379257284]], "head": [162.6823892686964, -36.57349259579321], "nose": [[172.6454773433506, -35.715077767936954], [179.6188402299928, -35.114256209694936]], "arm_r": [[158.8662093611655, -15.329081424921057], [179.33203423607728, 6.086650492133344], [170.2261581012055, -22.376263640994495]], "arm_l": [[164.77505587923875, -17.179725827569097], [186.60646021578427, 0.4368913694498724], [174.27938782058348, -24.463157719618334]], "leg_r": [[150.0, 67.2155372436685], [160.60371763953367, 111.29667164972889], [146.0, 147.96977299066958], [156.24068183654285, 151.9247829380784]], "leg_l": [[155.90884651807326, 66.7540782623559], [165.23395514054224, 111.11463772661938], [150.0, 147.5382368945955], [160.17101027135118, 151.6639291057432]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 66.0], [153.06228919571583, 23.248423141253653], [158.8662093611655, -17.239211379257284]], "head": [162.6823892686964, -36.57349259579321], "nose": [[172.6454773433506, -35.715077767936954], [179.6188402299928, -35.114256209694936]], "arm_r": [[158.8662093611655, -15.329081424921057], [179.91401379749405, 6.0440244594031185], [192.09814735171608, -21.172187554907964]], "arm_l": [[164.77505587923875, -17.179725827569097], [185.30308924151055, 3.421335695414859], [196.06151469937672, -23.838774964749273]], "leg_r": [[150.0, 67.2155372436685], [160.60371763953367, 111.29667164972889], [146.0, 147.96977299066958], [156.24068183654285, 151.9247829380784]], "leg_l": [[155.90884651807326, 66.7540782623559], [165.23395514054224, 111.11463772661938], [150.0, 147.5382368945955], [160.17101027135118, 151.6639291057432]]}}}, {"name": "Glute Bridge", "camera": 0.0, "frames": [{"order": ["arm_l", "leg_l", "spine", "arm_r", "leg_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[168.0, 144.0], [124.5, 144.0], [83.0, 144.0]], "head": [64.20614758428184, 137.2635182233307], "nose": [[66.66045491672855, 127.56937692469582], [68.35390818592623, 120.88049364703733]], "arm_r": [[83.0, 145.91012995433624], [112.92692150779473, 147.97103143931645], [142.85384301558946, 150.03193292429665]], "arm_l": [[88.90884651807325, 144.05948555168817], [118.83576802586798, 146.12038703666838], [148.76268953366272, 148.1812885216486]], "leg_r": [[168.0, 145.2155372436685], [197.5044798913556, 110.46025450639627], [205.0, 149.15476825571733], [215.58437753584877, 146.20511660320886]], "leg_l": [[173.90884651807326, 144.7540782623559], [203.11952081133066, 109.7589308468124], [208.99999999999997, 148.72323215964326], [219.69813598891116, 146.20283871368702]]}, {"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[172.0, 116.0], [130.5742805698035, 129.31344833951914], [93.2243080852167, 147.3068466003177]], "head": [74.95339893236468, 139.29569860961357], "nose": [[78.08659203536632, 129.79922026344212], [80.24971099599095, 123.24296544784039]], "arm_r": [[93.2243080852167, 149.21697655465394], [122.76854067558295, 144.0866744047689], [152.3127732659492, 138.9563722548839]], "arm_l": [[99.13315460328995, 147.3663321520059], [128.67738719365622, 142.23603000212086], [158.22161978402247, 137.10572785223582]], "leg_r": [[172.0, 117.2155372436685], [217.61231681635738, 111.34649988822532], [205.0, 148.72938534005917], [215.7014503494387, 151.23609550620586]], "leg_l": [[177.90884651807326, 116.7540782623559], [223.61346075114872, 111.62851219693835], [209.00000000000003, 148.29784924398507], [219.55091873231555, 151.36155338463374]]}], "tween": {"t": 0.5, "sample": {"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[170.0, 130.0], [127.02099954612248, 136.73033567600945], [86.57106738965523, 145.95306097398856]], "head": [68.02739029831947, 138.57475142347892], "nose": [[70.82298545434472, 128.97346777369498], [72.75244694506371, 122.34686197785895]], "arm_r": [[86.57106738965523, 147.86319092832477], [116.52995343229244, 146.316965264244], [146.48883947492965, 144.77073960016327]], "arm_l": [[92.47991390772847, 146.01254652567673], [122.43879995036569, 144.46632086159596], [152.3976859930029, 142.9200951975152]], "leg_r": [[170.0, 131.2155372436685], [210.61089504167376, 109.93924028266386], [205.0, 148.94207679788823], [215.98427899607043, 149.52103566600155]], "leg_l": [[175.90884651807326, 130.7540782623559], [216.72361168994397, 109.85955478107854], [209.0, 148.51054070181414], [219.9370385291809, 149.6679258108433]]}}}, {"name": "Goblet Squat", "camera": 0.0, "frames": [{"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 66.0], [163.7912748095231, 23.3238785947173], [167.40823813355095, -17.390122286184578]], "head": [169.8456250016539, -36.939465193360675], "nose": [[179.8441478685724, -36.76759175239952], [186.84308173466712, -36.64728089622242]], "arm_r": [[167.40823813355095, -15.47999233184835], [161.58327027278116, 13.501977080510542], [185.0, -4.965643768567148]], "arm_l": [[173.3170846516242, -17.33063673449639], [165.11280321558735, 11.087334645808118], [189.0, -6.7863652859766805]], "leg_r": [[160.0, 67.2155372436685], [173.9610906185993, 110.37987505887354], [162.0, 147.96977299066955], [172.496690241954, 151.20910105398104]], "leg_l": [[165.90884651807326, 66.7540782623559], [178.83656733035028, 110.22945395799843], [166.0, 147.53823689459546], [176.4181910389946, 151.0146634275683]]}, {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 104.0], [167.67276839345243, 64.77395322209524], [179.086797280632, 25.399263623249595]], "head": [186.57892914895024, 7.1373066543472845], "nose": [[196.55530157908487, 7.824324219806698], [203.53824993090362, 8.305201232968749]], "arm_r": [[179.086797280632, 27.309393577585823], [171.41227156937236, 55.87054440998645], [196.0, 38.9432027495061]], "arm_l": [[184.99564379870526, 25.458749174937783], [174.87887031847484, 53.27239289284542], [200.0, 37.12248123209657]], "leg_r": [[150.0, 105.2155372436685], [192.35367092323057, 122.89185045923996], [162.0, 148.54707837620566], [171.58691261951185, 153.8587544047633]], "leg_l": [[155.90884651807326, 104.7540782623559], [197.5802282343205, 123.93877901322136], [166.0, 148.11554228013156], [175.3143251279279, 153.87842212811603]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[155.0, 85.0], [165.87049934594478, 43.50075172802916], [173.41160389132622, 3.2917374825404693]], "head": [178.41920397241503, -15.777048563556235], "nose": [[188.40997278727033, -15.347469131885184], [195.40331033834335, -15.046772155872004]], "arm_r": [[173.41160389132622, 5.201867436876697], [165.81924442435937, 33.78430844561579], [190.5, 16.98877949046944]], "arm_l": [[179.32045040939948, 3.351223034228658], [169.30026135410338, 31.198755194030664], [194.5, 15.168057973059907]], "leg_r": [[155.0, 86.2155372436685], [187.70233881532357, 118.07459372668339], [162.0, 148.25842568343762], [171.52792396979862, 153.672062223347]], "leg_l": [[160.90884651807326, 85.7540782623559], [192.7002154063726, 118.49517970682211], [166.0, 147.82688958736352], [175.34124224802244, 153.54735913060154]]}}}, {"name": "Hip Circles", "camera": 45.0, "frames": [{"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 72.0], [159.4441210227361, 29.052652315278735], [156.867198580654, -12.126112205712047]], "head": [155.88069306833134, -31.945593306627956], "nose": [[165.85174201672737, -31.185209937076426], [170.78943214897384, -30.80866605688989]], "arm_r": [[149.08902398760196, -10.775446362053344], [131.8875970946982, 10.825618436816505], [159.08844008591993, 3.134054379460707]], "arm_l": [[168.82355861562613, -12.084049568730713], [184.12264494790446, 3.873360335346298], [192.6819593838462, -0.5811362594590932]], "leg_r": [[155.05025253169416, 72.85951462778281], [170.27809415713818, 116.23629730305997], [154.0, 149.79871604858775], [162.52693632902785, 155.58828277543466]], "leg_l": [[169.12793291022595, 72.53321385285722], [185.70098094242752, 115.44046187013774], [166.0, 148.10084546808693], [174.8346073365813, 154.25803608271772]]}, {"order": ["leg_l", "leg_r", "spine", "arm_l", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[145.0, 67.0], [142.7455452100055, 24.369372531538616], [132.582347706998, -12.53794996469125]], "head": [128.68424683806742, -30.794211269113987], "nose": [[138.53680458124134, -29.08333257165363], [143.48655204954719, -28.223817943870824]], "arm_r": [[125.10548640637649, -8.253658769857907], [111.30892979662221, 17.798644034020015], [137.0966365575342, 8.96567608177466]], "arm_l": [[144.23739444953964, -15.429512678884556], [161.6823363014897, -7.278796034128369], [169.55065570847063, -5.843835992193469]], "leg_r": [[140.05025253169416, 67.85951462778281], [156.0161760072335, 110.05383320510516], [154.0, 149.7227548136488], [164.73580332864196, 149.98477051650372]], "leg_l": [[154.12793291022595, 67.53321385285722], [172.15197971679743, 108.65115236715383], [166.0, 148.024884233148], [176.5089784339167, 149.4096928711608]]}, {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 63.0], [160.5558789772639, 20.245707058089195], [163.132801419346, -20.038101690788437]], "head": [164.11930693166866, -39.51497302275793], "nose": [[173.77107388531098, -36.8989847443863], [178.70876401755748, -35.560686837875664]], "arm_r": [[155.35462682629398, -18.687435847129734], [136.26937282033094, 2.2177744861050854], [164.21333813717047, -1.7897275383506326]], "arm_l": [[175.08916145431814, -19.996039053807102], [188.5044206735372, -4.734483615365129], [197.80685743509673, -5.504918177270433]], "leg_r": [[155.05025253169416, 63.85951462778281], [155.05915304318088, 109.27844419331313], [154.21378610932078, 148.62017241502275], [161.9880118482433, 150.1782104752242]], "leg_l": [[169.12793291022595, 63.53321385285723], [170.96275690184086, 109.06990579364435], [167.57786367283106, 147.57341467764627], [175.3006823934713, 150.2235671631545]]}, {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[175.0, 67.0], [177.2544547899945, 23.586408599708555], [187.417652293002, -16.850555347853984]], "head": [191.31575316193258, -36.46061287678006], "nose": [[201.1683109051065, -34.74973417931971], [206.11805837341234, -33.890219551536894]], "arm_r": [[179.94079099238047, -18.53815986364468], [158.70043603116835, -3.669811924778571], [187.42453366575347, -5.856419765892781]], "arm_l": [[199.07269903554362, -13.77022235142325], [209.07384253603584, 8.149653195646259], [219.8785528166899, 1.9184552781877287]], "leg_r": [[170.05025253169416, 67.85951462778281], [169.1103843231303, 113.37006357914862], [154.0, 149.7227548136488], [164.0955116276239, 154.04375169498837]], "leg_l": [[184.12793291022595, 67.53321385285722], [184.15710984832504, 113.073700371891], [166.0, 148.02488423314801], [175.72854456534554, 153.14721573509743]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[152.5, 69.5], [151.1060183087905, 26.75662358296359], [144.68658356094633, -12.85629329445861]], "head": [142.22797809252467, -32.09226080032802], "nose": [[152.15022272696726, -30.847647842708234], [157.090932702051, -30.227901819365403]], "arm_r": [[137.01245872650412, -10.006774915193887], [121.4328142515516, 14.075330717271115], [147.9656772757711, 5.706404729478997]], "arm_l": [[156.53889383730868, -14.313083193083294], [173.02499855449702, -2.137474215169938], [181.16570342816195, -3.677114282418678]], "leg_r": [[147.55025253169416, 70.35951462778281], [165.4435261192274, 112.61080343795655], [154.0, 150.2506348449002], [164.40256499711734, 153.63504937924387]], "leg_l": [[161.62793291022595, 70.03321385285722], [180.5764911170924, 111.62790675116207], [166.0, 148.40794644497956], [176.233437431852, 152.43961324032162]]}}}, {"name": "Hip Thrust", "camera": 0.0, "frames": [{"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 136.0], [107.30297316079633, 126.97118259325977], [69.2548115566876, 110.21707042748173]], "head": [60.175001561896664, 92.66766776737846], "nose": [[68.781162701048, 87.5751251365347], [74.78133380596279, 84.02463071165936]], "arm_r": [[69.2548115566876, 112.12720038181797], [99.25399859551796, 112.34470027256535], [124.24702183306655, 96.00315788768978]], "arm_l": [[75.16365807476085, 110.27655597916991], [105.08630887497687, 108.15636549973091], [124.9919151500325, 86.05260088982135]], "leg_r": [[150.0, 137.2155372436685], [187.9153388743531, 111.56461136348543], [203.0, 148.0484225268028], [212.7875096801391, 143.104270330527]], "leg_l": [[155.90884651807326, 136.7540782623559], [193.43783057440342, 110.55790777790622], [207.0, 147.6168864307287], [216.98400043067375, 143.06970414754042]]}, {"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[152.0, 114.0], [108.48104389225882, 113.9737793615104], [67.06410864858307, 111.09623166805618]], "head": [54.47770082758632, 95.78944430675715], "nose": [[61.84307038470169, 89.02547126666317], [66.96254629603588, 84.32401035894505]], "arm_r": [[67.06410864858307, 113.0063616223924], [96.52714518752813, 107.4415897188251], [121.83726912974487, 91.58044756166865]], "arm_l": [[72.97295516665632, 111.15571721974436], [101.48303474405499, 101.96078907057003], [118.96500119444003, 77.9512477713769]], "leg_r": [[152.0, 115.2155372436685], [197.52072750459536, 108.69321211492046], [203.0, 147.71419309307137], [213.72351917568668, 145.30068334308834]], "leg_l": [[157.90884651807326, 114.7540782623559], [203.4023403087058, 108.05000190891487], [207.0, 147.28265699699728], [217.82750079408547, 145.37171715459846]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[151.0, 125.0], [107.66880918157864, 120.45952464630693], [67.33627381329264, 110.50684590755573]], "head": [56.44349311299209, 93.98826035683962], "nose": [[64.47381860088683, 88.02890866739654], [70.06426717121788, 83.88020396264102]], "arm_r": [[67.33627381329264, 112.41697586189196], [97.22215189743444, 109.8424444392641], [122.61084176884933, 94.1035518711111]], "arm_l": [[73.24512033136588, 110.56633145924391], [102.70102224857601, 104.96504811148836], [121.73528941335792, 82.12904288061985]], "leg_r": [[151.0, 126.2155372436685], [193.77452083569057, 109.55101958727552], [203.0, 147.88130780993708], [213.4415940186742, 144.47365747936155]], "leg_l": [[156.90884651807326, 125.7540782623559], [199.54443640867484, 108.7477721024602], [207.0, 147.449771713863], [217.5874176218285, 144.51072057311362]]}}}, {"name": "Hollow Body Hold", "camera": 0.0, "frames": [{"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[185.0, 148.0], [141.5, 148.0], [100.0, 148.0]], "head": [80.43704798532389, 143.90493909881587], "nose": [[80.43704798532389, 133.90493909881587], [80.43704798532389, 127.01128482773042]], "arm_r": [[100.0, 149.91012995433624], [70.0, 149.9101299543362], [40.0, 149.9101299543362]], "arm_l": [[105.90884651807325, 148.05948555168817], [75.90884651807325, 148.05948555168817], [45.90884651807325, 148.05948555168817]], "leg_r": [[185.0, 149.2155372436685], [231.0, 149.2155372436685], [271.0, 149.2155372436685], [271.0, 138.38265196053422]], "leg_l": [[190.90884651807326, 148.7540782623559], [236.90884651807326, 148.7540782623559], [276.90884651807323, 148.7540782623559], [276.90884651807323, 137.92119297922162]]}, {"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[185.0, 148.0], [140.58688278525045, 148.43364741865258], [102.76846842615609, 129.42694930442767]], "head": [87.44757956377653, 116.76650487323789], "nose": [[93.93309205085959, 109.15479496153674], [98.43260531866537, 103.8739494143883]], "arm_r": [[102.76846842615609, 131.3370792587639], [75.5792348150566, 118.85114703696041], [48.3900012039571, 106.3652148151569]], "arm_l": [[108.67731494422934, 129.48643485611586], [81.48808133312984, 117.00050263431234], [54.298847722030345, 104.51457041250885]], "leg_r": [[185.0, 149.2155372436685], [229.99478963375506, 139.79689717094502], [269.12069366310726, 131.60677536857676], [266.8336650641119, 121.01061461985441]], "leg_l": [[190.90884651807326, 148.7540782623559], [235.90363615182832, 139.3354381896324], [275.0295401811805, 131.14531638726413], [272.74251158218516, 120.54915563854179]]}], "tween": {"t": 0.5, "sample": {"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[185.0, 148.0], [141.26935782158634, 148.2668263271459], [100.6970043790406, 138.60132020497852]], "head": [82.72112345305727, 129.96709412388378], "nose": [[86.18778863202375, 120.58720966100572], [88.58192963530344, 114.10929361221746]], "arm_r": [[100.6970043790406, 140.51145015931476], [71.40812416544262, 134.11690786335836], [42.11924395184462, 127.72236556740197]], "arm_l": [[106.60585089711385, 138.6608057566667], [77.31697068351586, 132.26626346071032], [48.02809046991787, 125.87172116475392]], "leg_r": [[185.0, 149.2155372436685], [230.74800718694058, 144.48027695599242], [270.5288830016715, 140.36265931453494], [269.3790699057273, 129.58911771044515]], "leg_l": [[190.90884651807326, 148.7540782623559], [236.65685370501384, 144.01881797467982], [276.4377295197447, 139.90120033322233], [275.28791642380054, 129.12765872913255]]}}}, {"name": "Incline Bench Press", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[148.0, 122.0], [121.21872582333387, 88.24229916701316], [95.66877459731904, 56.03667653324413]], "head": [88.82837173080566, 37.52834496527767], "nose": [[97.98669786795362, 33.51274259880355], [104.38151607145183, 30.7088408020571]], "arm_r": [[95.66877459731904, 57.94680648758036], [125.026216615892, 64.02878958265757], [106.0, 41.18627870131074]], "arm_l": [[101.57762111539229, 56.096162084932324], [131.28947904201019, 60.18109651350132], [110.0, 39.365557183901196]], "leg_r": [[148.0, 123.2155372436685], [191.96163186669585, 109.8796648279379], [198.0, 148.8205388219859], [208.68783472873713, 146.25810977141558]], "leg_l": [[153.90884651807326, 122.7540782623559], [197.8060125288814, 109.21381982938716], [201.99999999999997, 148.3890027259118], [212.79722051369865, 146.31855615798008]]}, {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[148.0, 122.0], [121.21872582333387, 88.24229916701316], [95.66877459731904, 56.03667653324413]], "head": [88.82837173080566, 37.52834496527767], "nose": [[97.98669786795362, 33.51274259880355], [104.38151607145183, 30.7088408020571]], "arm_r": [[95.66877459731904, 57.94680648758036], [104.38394002450866, 29.676718628853195], [97.99999999999999, 0.8091608278101887]], "arm_l": [[101.57762111539229, 56.096162084932324], [109.48815338938583, 27.5975292847013], [102.0, -1.01156068959933]], "leg_r": [[148.0, 123.2155372436685], [191.96163186669585, 109.8796648279379], [198.0, 148.8205388219859], [208.68783472873713, 146.25810977141558]], "leg_l": [[153.90884651807326, 122.7540782623559], [197.8060125288814, 109.21381982938716], [201.99999999999997, 148.3890027259118], [212.79722051369865, 146.31855615798008]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[148.0, 122.0], [121.21872582333387, 88.24229916701316], [95.66877459731904, 56.03667653324413]], "head": [88.82837173080566, 37.52834496527767], "nose": [[97.98669786795362, 33.51274259880355], [104.38151607145183, 30.7088408020571]], "arm_r": [[95.66877459731904, 57.94680648758036], [121.70710419240449, 43.273323253551595], [102.0, 20.997719764560486]], "arm_l": [[101.57762111539229, 56.096162084932324], [126.94689567822562, 40.32693556903181], [105.99999999999999, 19.17699824715095]], "leg_r": [[148.0, 123.2155372436685], [191.96163186669585, 109.8796648279379], [198.0, 148.8205388219859], [208.68783472873713, 146.25810977141558]], "leg_l": [[153.90884651807326, 122.7540782623559], [197.8060125288814, 109.21381982938716], [201.99999999999997, 148.3890027259118], [212.79722051369865, 146.31855615798008]]}}}, {"name": "Kneeling Hip Flexor Stretch", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 108.0], [167.48803432654196, 65.23222110289855], [189.54080710454963, 29.92034407316092]], "head": [200.72466517396458, 13.5914914933484], "nose": [[210.01618295035192, 17.288476401496972], [216.50646993231942, 19.87088474417535]], "arm_r": [[189.54080710454963, 31.830474027497146], [194.52030489904934, 60.96488587692039], [200.33667189428212, 89.9485305307548]], "arm_l": [[195.4496536226229, 29.979829624849106], [199.76126870996237, 59.21734341322912], [204.91426784576808, 88.32247976825207]], "leg_r": [[160.0, 109.2155372436685], [205.95650014293446, 111.18517604332762], [206.0, 150.57746287018125], [216.93848396248202, 149.43340250036692]], "leg_l": [[165.90884651807326, 108.7540782623559], [150.1759199250925, 151.32324086867877], [110.27335791469952, 148.57537222203848], [100.84451760697628, 154.15472060398542]]}, {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[162.0, 110.0], [172.77124244752997, 67.43012826213044], [202.09680242673178, 37.003417838858354]], "head": [216.23893805046274, 23.076133032457975], "nose": [[224.75555327463707, 28.31705519590993], [230.69188994773205, 31.970135395121623]], "arm_r": [[202.09680242673178, 38.91354779319458], [204.2210542863234, 68.38362245013457], [207.26105622745138, 97.77577678161127]], "arm_l": [[208.00564894480505, 37.062903390546545], [209.3595764755619, 66.57703280205601], [211.63136747047088, 96.03643315671671]], "leg_r": [[162.0, 111.2155372436685], [207.99997279057675, 111.26480978658026], [210.0, 150.60784736415684], [220.86856580612752, 148.93823445938295]], "leg_l": [[167.90884651807326, 110.7540782623559], [142.18597295841892, 148.3104391959247], [102.1859729584189, 148.3104391959247], [93.17530047123999, 154.52392693202455]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[161.0, 109.0], [170.11579656407298, 66.31583998394368], [195.919329904345, 33.27177313446985]], "head": [208.6408943099003, 18.073735691769016], "nose": [[217.57877685273942, 22.558627624738115], [223.815822522058, 25.688281172299863]], "arm_r": [[195.919329904345, 35.18190308880608], [199.52031557113526, 64.51252850700274], [203.99416062595552, 93.72639378717997]], "arm_l": [[201.82817642241827, 33.331258686158044], [204.71258284238976, 62.73861748692094], [208.4733956607904, 92.04978347246244]], "leg_r": [[161.0, 110.2155372436685], [206.98884942993953, 111.21293316469936], [208.0, 150.59265511716904], [218.9071791020003, 149.18833029841562]], "leg_l": [[166.90884651807326, 109.7540782623559], [146.0252835300541, 150.11770438059347], [106.04965044929027, 148.74293258343957], [96.8242742018906, 154.64294477047432]]}}}, {"name": "Lat Pull Down", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 122.0], [146.62433082233014, 79.24791483349992], [147.74955388155342, 38.34937572031629]], "head": [149.49266873650657, 18.728170476508154], "nose": [[155.28735587215587, 10.578225630072646], [159.30239092661319, 4.931274641291457]], "arm_r": [[147.74955388155342, 40.259505674652516], [174.77136716083461, 27.425922423252715], [194.0, 4.748391839859067]], "arm_l": [[153.6584003996267, 38.40886127200448], [181.60676431735172, 27.670890621268786], [198.0, 2.9276703224495346]], "leg_r": [[150.0, 123.2155372436685], [194.41395436835177, 111.42246307048075], [193.0, 150.79015432801035], [203.99312537163428, 151.17308446470886]], "leg_l": [[155.90884651807326, 122.7540782623559], [200.07727426166045, 110.09824525529687], [197.0, 149.373810478924], [207.96739988435445, 150.20720445545675]]}, {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 122.0], [146.62433082233014, 79.24791483349992], [147.74955388155342, 38.34937572031629]], "head": [149.49266873650657, 18.728170476508154], "nose": [[158.18575082167658, 13.785466372427464], [164.24792864816766, 10.338639236884735]], "arm_r": [[147.74955388155342, 40.259505674652516], [171.72345780323056, 58.020259740573785], [194.0, 38.231855442274124]], "arm_l": [[153.6584003996267, 38.40886127200448], [176.75165320875354, 57.2673942535663], [198.0, 36.41113392486461]], "leg_r": [[150.0, 123.2155372436685], [194.41395436835177, 111.42246307048075], [193.0, 150.79015432801035], [203.99312537163428, 151.17308446470886]], "leg_l": [[155.90884651807326, 122.7540782623559], [200.07727426166045, 110.09824525529687], [197.0, 149.373810478924], [207.96739988435445, 150.20720445545675]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 122.0], [146.62433082233014, 79.24791483349992], [147.74955388155342, 38.34937572031629]], "head": [149.49266873650657, 18.728170476508154], "nose": [[156.91670692063303, 12.028643848407304], [162.0776482783039, 7.371358528174099]], "arm_r": [[147.74955388155342, 40.259505674652516], [177.18620166864008, 45.95811146381125], [194.0, 21.49012364106659]], "arm_l": [[153.6584003996267, 38.40886127200448], [182.86086618586543, 45.17585880681934], [198.0, 19.66940212365707]], "leg_r": [[150.0, 123.2155372436685], [194.41395436835177, 111.42246307048075], [193.0, 150.79015432801035], [203.99312537163428, 151.17308446470886]], "leg_l": [[155.90884651807326, 122.7540782623559], [200.07727426166045, 110.09824525529687], [197.0, 149.373810478924], [207.96739988435445, 150.20720445545675]]}}, "orbit": [{"yaw": 37.0, "sample": {"order": ["spine", "leg_l", "arm_l", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 122.0], [147.3040707245407, 78.89514362473935], [148.20271381636047, 38.114194914475945]], "head": [149.59482723761698, 18.67515228851495], "nose": [[154.82362622309998, 10.15108863498608], [158.03017579167425, 4.923725199021456]], "arm_r": [[141.58274856168794, 39.63969252481388], [163.16332819238937, 29.629999065841375], [178.51999718740836, 8.961939106629913]], "arm_l": [[159.54169372378558, 38.16170218838887], [181.8622495962053, 30.344449654035888], [194.95446973694257, 7.314389402273139]], "leg_r": [[145.78729483793566, 122.97077120657869], [181.25785593812145, 115.81913656544059], [180.12862176996924, 155.0390638196267], [188.90812205815823, 156.57082043229062]], "leg_l": [[158.93171981481694, 122.60223367767217], [194.20619463380368, 114.56218166811979], [191.74857413428708, 153.51615916977624], [200.50752913482114, 155.49569120384058]], "props": {"bg": [{"kind": "poly", "pts": [[125.25533829692662, 127.16784412500894], [154.00621665862917, 130.9299910792746], [163.4889702694989, 131.4809567729892], [134.73809190779636, 127.71880981872354]], "w": 8, "color": "equipment"}, {"kind": "line", "pts": [[144.1631461190869, 131.31225668434325], [142.28207264195407, 149.20296280244077]], "w": 5, "color": "equipment"}, {"kind": "circle", "c": [258.197452678252, 32.37490189145329], "r": 4, "fill": true, "w": 3, "color": "prop"}, {"kind": "line", "pts": [[258.197452678252, 32.37490189145329], [186.73723346217548, 8.138164254451526]], "w": 4, "color": "equipment"}], "fg": [{"kind": "line", "pts": [[201.18079401782464, 4.809805831896043], [172.29367290652633, 11.46652267700701]], "w": 4, "color": "prop"}]}}}, {"yaw": 180.0, "sample": {"order": ["leg_r", "leg_l", "spine", "arm_r", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[150.0, 122.0], [153.37566917766986, 79.24791483349992], [152.25044611844658, 38.34937572031629]], "head": [150.50733126349343, 18.728170476508154], "nose": [[144.71264412784413, 10.578225630072646], [140.69760907338681, 4.931274641291457]], "arm_r": [[158.15929263651984, 38.40886127200448], [131.13747935723865, 25.575278020604674], [111.90884651807325, 2.897747437211029]], "arm_l": [[152.25044611844658, 40.259505674652516], [124.30208220072154, 29.521535023916826], [107.90884651807326, 4.778314725097573]], "leg_r": [[155.90884651807326, 122.7540782623559], [111.49489214972148, 110.96100408916814], [112.90884651807325, 150.3286953466977], [101.91572114643897, 150.71162548339626]], "leg_l": [[150.0, 123.2155372436685], [105.83157225641283, 110.55970423660949], [108.90884651807326, 149.83526946023665], [97.94144663371881, 150.66866343676938]], "props": {"bg": [{"kind": "poly", "pts": [[174.0, 126.78137981968192], [138.0, 126.78137981968192], [138.0, 132.25370211289263], [174.0, 132.25370211289263]], "w": 8, "color": "equipment"}, {"kind": "line", "pts": [[156.0, 131.39692620785908], [156.0, 148.31139338200543]], "w": 5, "color": "equipment"}, {"kind": "circle", "c": [28.0, 25.21166005905144], "r": 4, "fill": true, "w": 3, "color": "prop"}, {"kind": "line", "pts": [[28.0, 25.21166005905144], [109.90884651807326, 3.838031081154301]], "w": 4, "color": "equipment"}], "fg": [{"kind": "line", "pts": [[109.90884651807326, 8.005587345160627], [109.90884651807326, -0.32952518285202625]], "w": 4, "color": "prop"}]}}}]}, {"name": "Lateral Raise", "camera": 90.0, "frames": [{"order": ["leg_l", "leg_r", "spine", "arm_l", "arm_r", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 64.0], [160.0, 21.160862743968956], [160.0, -19.708659006037678]], "head": [160.0, -39.40481406628183], "arm_r": [[149.0, -19.708659006037678], [144.82480697119803, 9.8366392933887], [140.71304450611134, 39.47761343854478]], "arm_l": [[171.0, -19.708659006037678], [175.17519302880197, 9.8366392933887], [179.28695549388866, 39.47761343854478]], "leg_r": [[153.0, 64.0], [148.19169068968793, 109.05299216255729], [144.0105521589818, 148.22950708652013], [144.0105521589818, 150.13963704085637]], "leg_l": [[167.0, 64.0], [171.80830931031207, 109.05299216255729], [175.9894478410182, 148.22950708652013], [175.9894478410182, 150.13963704085637]]}, {"order": ["leg_l", "leg_r", "spine", "arm_l", "arm_r", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 64.0], [160.0, 21.160862743968956], [160.0, -19.708659006037678]], "head": [160.0, -39.40481406628183], "arm_r": [[149.0, -19.708659006037678], [119.01827518942713, -18.667409597798567], [89.69172300010406, -16.99693330126719]], "arm_l": [[171.0, -19.708659006037678], [200.98172481057287, -18.667409597798567], [230.30827699989595, -16.99693330126719]], "leg_r": [[153.0, 64.0], [148.19169068968793, 109.05299216255729], [144.0105521589818, 148.22950708652013], [144.0105521589818, 150.13963704085637]], "leg_l": [[167.0, 64.0], [171.80830931031207, 109.05299216255729], [175.9894478410182, 148.22950708652013], [175.9894478410182, 150.13963704085637]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "leg_r", "spine", "arm_l", "arm_r", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 64.0], [160.0, 21.160862743968956], [160.0, -19.708659006037678]], "head": [160.0, -39.40481406628183], "arm_r": [[149.0, -19.708659006037678], [126.70565523567817, 0.25529216051033654], [104.82092037786576, 20.450798446546216]], "arm_l": [[171.0, -19.708659006037678], [193.29434476432183, 0.25529216051033643], [215.17907962213422, 20.450798446546216]], "leg_r": [[153.0, 64.0], [148.19169068968793, 109.05299216255729], [144.0105521589818, 148.22950708652013], [144.0105521589818, 150.13963704085637]], "leg_l": [[167.0, 64.0], [171.80830931031207, 109.05299216255729], [175.9894478410182, 148.22950708652013], [175.9894478410182, 150.13963704085637]]}}}, {"name": "Leg Curl", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[118.0, 116.0], [101.72808522765135, 76.19044808081628], [91.71636489268228, 36.44745235385585]], "head": [97.22911200902226, 17.51429293718145], "nose": [[106.64265513311928, 20.888490004746693], [113.22050347862063, 23.246258626580946]], "arm_r": [[91.71636489268228, 38.35758230819208], [99.4809362457579, 66.89511958511801], [114.4809362457579, 92.48117554369131]], "arm_l": [[97.62521141075553, 36.50693790554404], [105.38978276383115, 65.04447518246997], [120.38978276383115, 90.63053114104326]], "leg_r": [[118.0, 117.2155372436685], [163.30115663856157, 109.34907394717814], [203.20371864895455, 106.60120530053786], [202.4363974377692, 95.79470838149437]], "leg_l": [[123.90884651807325, 116.7540782623559], [169.21000315663483, 108.88761496586552], [209.1125651670278, 106.13974631922524], [208.34524395584245, 95.33324940018176]]}, {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[118.0, 116.0], [101.72808522765135, 76.19044808081628], [91.71636489268228, 36.44745235385585]], "head": [97.22911200902226, 17.51429293718145], "nose": [[106.64265513311928, 20.888490004746693], [113.22050347862063, 23.246258626580946]], "arm_r": [[91.71636489268228, 38.35758230819208], [99.4809362457579, 66.89511958511801], [114.4809362457579, 92.48117554369131]], "arm_l": [[97.62521141075553, 36.50693790554404], [105.38978276383115, 65.04447518246997], [120.38978276383115, 90.63053114104326]], "leg_r": [[118.0, 117.2155372436685], [163.30115663856157, 109.34907394717814], [159.81492692865524, 148.59148443479444], [170.77306860766444, 149.53563259774617]], "leg_l": [[123.90884651807325, 116.7540782623559], [169.21000315663483, 108.88761496586552], [165.7237734467285, 148.1300254534818], [176.6819151257377, 149.07417361643354]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[118.0, 116.0], [101.72808522765135, 76.19044808081628], [91.71636489268228, 36.44745235385585]], "head": [97.22911200902226, 17.51429293718145], "nose": [[106.64265513311928, 20.888490004746693], [113.22050347862063, 23.246258626580946]], "arm_r": [[91.71636489268228, 38.35758230819208], [99.4809362457579, 66.89511958511801], [114.4809362457579, 92.48117554369131]], "arm_l": [[97.62521141075553, 36.50693790554404], [105.38978276383115, 65.04447518246997], [120.38978276383115, 90.63053114104326]], "leg_r": [[118.0, 117.2155372436685], [163.30115663856157, 109.34907394717814], [191.3375272105556, 137.44565683383726], [199.1832821512516, 129.8527871797909]], "leg_l": [[123.90884651807325, 116.7540782623559], [169.21000315663483, 108.88761496586552], [197.24637372862887, 136.98419785252463], [205.09212866932486, 129.3913281984783]]}}}, {"name": "Leg Extension", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[118.0, 116.0], [101.72808522765135, 76.19044808081628], [91.71636489268228, 36.44745235385585]], "head": [97.22911200902226, 17.51429293718145], "nose": [[106.64265513311928, 20.888490004746693], [113.22050347862063, 23.246258626580946]], "arm_r": [[91.71636489268228, 38.35758230819208], [99.4809362457579, 66.89511958511801], [114.4809362457579, 92.48117554369131]], "arm_l": [[97.62521141075553, 36.50693790554404], [105.38978276383115, 65.04447518246997], [120.38978276383115, 90.63053114104326]], "leg_r": [[118.0, 117.2155372436685], [163.30115663856157, 109.34907394717814], [168.17593037476746, 148.44775976153034], [179.093938042822, 147.12756314489147]], "leg_l": [[123.90884651807325, 116.7540782623559], [169.21000315663483, 108.88761496586552], [174.08477689284072, 147.9863007802177], [185.00278456089526, 146.66610416357884]]}, {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[118.0, 116.0], [101.72808522765135, 76.19044808081628], [91.71636489268228, 36.44745235385585]], "head": [97.22911200902226, 17.51429293718145], "nose": [[106.64265513311928, 20.888490004746693], [113.22050347862063, 23.246258626580946]], "arm_r": [[91.71636489268228, 38.35758230819208], [99.4809362457579, 66.89511958511801], [114.4809362457579, 92.48117554369131]], "arm_l": [[97.62521141075553, 36.50693790554404], [105.38978276383115, 65.04447518246997], [120.38978276383115, 90.63053114104326]], "leg_r": [[118.0, 117.2155372436685], [163.30115663856157, 109.34907394717814], [203.20371864895455, 106.60120530053786], [202.4363974377692, 95.79470838149437]], "leg_l": [[123.90884651807325, 116.7540782623559], [169.21000315663483, 108.88761496586552], [209.1125651670278, 106.13974631922524], [208.34524395584245, 95.33324940018176]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[118.0, 116.0], [101.72808522765135, 76.19044808081628], [91.71636489268228, 36.44745235385585]], "head": [97.22911200902226, 17.51429293718145], "nose": [[106.64265513311928, 20.888490004746693], [113.22050347862063, 23.246258626580946]], "arm_r": [[91.71636489268228, 38.35758230819208], [99.4809362457579, 66.89511958511801], [114.4809362457579, 92.48117554369131]], "arm_l": [[97.62521141075553, 36.50693790554404], [105.38978276383115, 65.04447518246997], [120.38978276383115, 90.63053114104326]], "leg_r": [[118.0, 117.2155372436685], [163.30115663856157, 109.34907394717814], [194.16613997407038, 134.4056644612481], [201.16300039712578, 126.04674386776264]], "leg_l": [[123.90884651807325, 116.7540782623559], [169.21000315663483, 108.88761496586552], [200.07498649214364, 133.94420547993548], [207.07184691519905, 125.58528488645004]]}}, "orbit": [{"yaw": 37.0, "sample": {"order": ["arm_l", "spine", "arm_r", "leg_l", "leg_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[118.0, 116.0], [105.00467104633924, 74.48996656412565], [97.00895567017038, 33.70070519368849]], "head": [101.4116312751903, 15.343650354052286], "nose": [[110.06318318500227, 20.358692682071943], [115.31648645342548, 23.403873307202012]], "arm_r": [[90.38899041549786, 35.22620280402643], [96.59005281836, 64.57516948281769], [108.5695854690694, 91.72878667233499]], "arm_l": [[108.34793557759552, 33.74821246760142], [114.54899798045767, 63.097179146392676], [126.52853063116706, 90.25079633590998]], "leg_r": [[113.78729483793566, 116.97077120657869], [149.9664071757056, 113.8384637009904], [153.85957458488554, 153.4465832699093], [162.57908320756252, 153.26736302257655]], "leg_l": [[126.93171981481694, 116.60223367767217], [163.11083215258688, 113.4699261720839], [167.00399956176682, 153.0780457410028], [175.7235081844438, 152.89882549367005]], "props": {"bg": [{"kind": "poly", "pts": [[106.24251462180918, 120.85205320267173], [94.80856445333131, 44.79327452542702], [104.29131806420105, 45.344240219141625], [115.72526823267891, 121.40301889638633]], "w": 9, "color": "equipment"}, {"kind": "poly", "pts": [[101.24169339739956, 122.2128849456383], [151.17511134858344, 124.7164255628519], [160.65786495945318, 125.2673912565665], [110.7244470082693, 122.7638506393529]], "w": 9, "color": "equipment"}, {"kind": "line", "pts": [[108.96860407889773, 124.8942403560915], [106.4605061093873, 148.74851518022155]], "w": 5, "color": "equipment"}, {"kind": "line", "pts": [[150.7066587654828, 128.34059638801995], [147.98955263184652, 154.18272744749413]], "w": 5, "color": "equipment"}, {"kind": "poly", "pts": [[100.30260356580132, 148.15832207268397], [149.81800518873348, 154.63757516058593], [155.74472619552708, 154.98192871915757], [106.22932457259492, 148.5026756312556]], "w": 5, "color": "equipment"}, {"kind": "circle", "c": [117.52256409344713, 89.73985261227406], "r": 3.5, "fill": true, "w": 3, "color": "prop"}], "fg": [{"kind": "circle", "c": [166.90947233550744, 146.11361907675857], "r": 5, "fill": true, "color": "prop"}]}}}, {"yaw": 180.0, "sample": {"order": ["leg_r", "arm_r", "leg_l", "spine", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[118.0, 116.0], [134.27191477234865, 76.19044808081628], [144.2836351073177, 36.44745235385585]], "head": [138.77088799097774, 17.51429293718145], "nose": [[129.35734486688074, 20.888490004746693], [122.77949652137937, 23.246258626580946]], "arm_r": [[150.19248162539097, 36.50693790554404], [142.42791027231536, 65.04447518246997], [127.42791027231534, 90.63053114104326]], "arm_l": [[144.2836351073177, 38.35758230819208], [136.5190637542421, 66.89511958511801], [121.5190637542421, 92.48117554369131]], "leg_r": [[123.90884651807325, 116.7540782623559], [78.60768987951168, 108.88761496586552], [73.73291614330579, 147.9863007802177], [62.81490847525124, 146.66610416357884]], "leg_l": [[118.0, 117.2155372436685], [72.69884336143843, 109.34907394717814], [67.82406962523254, 148.44775976153034], [56.90606195717799, 147.12756314489147]], "props": {"bg": [{"kind": "poly", "pts": [[126.0, 118.9019945781101], [150.0, 49.364740639952885], [150.0, 54.83706293316358], [126.0, 124.3743168713208]], "w": 9, "color": "equipment"}, {"kind": "poly", "pts": [[132.0, 120.78137981968192], [70.0, 117.02260933653828], [70.0, 122.49493162974898], [132.0, 126.25370211289261]], "w": 9, "color": "equipment"}, {"kind": "line", "pts": [[128.0, 125.39692620785908], [128.0, 147.9495491067209]], "w": 5, "color": "equipment"}, {"kind": "line", "pts": [[76.0, 123.51754096628727], [76.0, 147.9495491067209]], "w": 5, "color": "equipment"}, {"kind": "poly", "pts": [[132.0, 146.23944839009255], [70.0, 146.23944839009255], [70.0, 149.65964982334924], [132.0, 149.65964982334924]], "w": 5, "color": "equipment"}, {"kind": "circle", "c": [122.0, 91.56799185956638], "r": 3.5, "fill": true, "w": 3, "color": "prop"}], "fg": [{"kind": "circle", "c": [63.706002038882666, 140.75964282059232], "r": 5, "fill": true, "color": "prop"}]}}}]}, {"name": "Leg Press", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[140.0, 116.0], [119.59179949383865, 78.0835670547446], [105.41656376460136, 39.588898523569924]], "head": [110.59294466665177, 20.56387367228598], "nose": [[120.26197316975961, 23.115321850495402], [127.02345395378309, 24.899530866204653]], "arm_r": [[105.41656376460136, 41.49902847790615], [113.18113511767699, 70.03656575483208], [141.37191374125425, 80.14128841983603]], "arm_l": [[111.32541028267461, 39.648384075258114], [119.08998163575023, 68.18592135218404], [147.2807602593275, 78.29064401718799]], "leg_r": [[140.0, 117.2155372436685], [157.1292173153023, 75.17232014967723], [185.0, 103.4282287014976], [192.89024540879532, 95.88020391213522]], "leg_l": [[145.90884651807326, 116.7540782623559], [161.69967986127625, 74.2057150715518], [189.0, 102.9966926054235], [197.03965931177694, 95.60316169903263]]}, {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[140.0, 116.0], [119.59179949383865, 78.0835670547446], [105.41656376460136, 39.588898523569924]], "head": [110.59294466665177, 20.56387367228598], "nose": [[120.26197316975961, 23.115321850495402], [127.02345395378309, 24.899530866204653]], "arm_r": [[105.41656376460136, 41.49902847790615], [113.18113511767699, 70.03656575483208], [141.37191374125425, 80.14128841983603]], "arm_l": [[111.32541028267461, 39.648384075258114], [119.08998163575023, 68.18592135218404], [147.2807602593275, 78.29064401718799]], "leg_r": [[140.0, 117.2155372436685], [180.56387328382812, 95.8524219129624], [220.0, 102.44342094848537], [221.8404858503854, 91.76324503029944]], "leg_l": [[145.90884651807326, 116.7540782623559], [185.0654782344644, 92.98043985822271], [224.0, 102.0118848524113], [226.52196163749238, 91.46755465641775]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[140.0, 116.0], [119.59179949383865, 78.0835670547446], [105.41656376460136, 39.588898523569924]], "head": [110.59294466665177, 20.56387367228598], "nose": [[120.26197316975961, 23.115321850495402], [127.02345395378309, 24.899530866204653]], "arm_r": [[105.41656376460136, 41.49902847790615], [113.18113511767699, 70.03656575483208], [141.37191374125425, 80.14128841983603]], "arm_l": [[111.32541028267461, 39.648384075258114], [119.08998163575023, 68.18592135218404], [147.2807602593275, 78.29064401718799]], "leg_r": [[140.0, 117.2155372436685], [168.72442575335768, 81.83218317512427], [202.5, 102.9358248249915], [208.39302981821828, 93.78865179534496]], "leg_l": [[145.90884651807326, 116.7540782623559], [173.37889429197503, 80.41752162098928], [206.5, 102.5042887289174], [212.66756005027608, 93.53436026427744]]}}}, {"name": "Leg Raises", "camera": 0.0, "frames": [{"order": ["arm_l", "spine", "leg_l", "arm_r", "leg_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[185.0, 148.0], [141.5, 148.0], [100.0, 148.0]], "head": [80.43704798532389, 143.90493909881587], "nose": [[80.43704798532389, 133.90493909881587], [80.43704798532389, 127.01128482773042]], "arm_r": [[100.0, 149.91012995433624], [129.98172481057287, 150.94120880220163], [159.96344962114574, 151.97228765006705]], "arm_l": [[105.90884651807325, 148.05948555168817], [135.89057132864613, 149.09056439955359], [165.872296139219, 150.121643247419]], "leg_r": [[185.0, 149.2155372436685], [208.0, 109.98358477385614], [228.0, 75.8688434957584], [218.47372055837118, 70.45240085419125]], "leg_l": [[190.90884651807326, 148.7540782623559], [213.90884651807326, 109.5221257925435], [233.90884651807326, 75.4073845144458], [224.38256707644445, 69.99094187287865]]}, {"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[185.0, 148.0], [141.5, 148.0], [100.0, 148.0]], "head": [80.43704798532389, 143.90493909881587], "nose": [[80.43704798532389, 133.90493909881587], [80.43704798532389, 127.01128482773042]], "arm_r": [[100.0, 149.91012995433624], [129.98172481057287, 150.94120880220163], [159.96344962114574, 151.97228765006705]], "arm_l": [[105.90884651807325, 148.05948555168817], [135.89057132864613, 149.09056439955359], [165.872296139219, 150.121643247419]], "leg_r": [[185.0, 149.2155372436685], [230.55233116211224, 142.91083479720226], [270.163053911775, 137.42848484375332], [268.6321498012143, 126.70102445552496]], "leg_l": [[190.90884651807326, 148.7540782623559], [236.4611776801855, 142.44937581588962], [276.07190042984826, 136.96702586244072], [274.54099631928756, 126.23956547421233]]}], "tween": {"t": 0.5, "sample": {"order": ["arm_l", "leg_l", "spine", "arm_r", "leg_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[185.0, 148.0], [141.5, 148.0], [100.0, 148.0]], "head": [80.43704798532389, 143.90493909881587], "nose": [[80.43704798532389, 133.90493909881587], [80.43704798532389, 127.01128482773042]], "arm_r": [[100.0, 149.91012995433624], [129.98172481057287, 150.94120880220163], [159.96344962114574, 151.97228765006705]], "arm_l": [[105.90884651807325, 148.05948555168817], [135.89057132864613, 149.09056439955359], [165.872296139219, 150.121643247419]], "leg_r": [[185.0, 149.2155372436685], [223.13572833753193, 123.88345193236816], [256.2972312397336, 101.85555166167222], [250.14610930155538, 92.87468274277532]], "leg_l": [[190.90884651807326, 148.7540782623559], [229.0445748556052, 123.42199295105556], [262.20607775780684, 101.39409268035959], [256.0549558196286, 92.41322376146272]]}}}, {"name": "Leg Swings", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[156.0, 66.0], [159.7912748095231, 23.3238785947173], [163.40823813355095, -17.390122286184578]], "head": [165.8456250016539, -36.939465193360675], "nose": [[175.8441478685724, -36.76759175239952], [182.84308173466712, -36.64728089622242]], "arm_r": [[163.40823813355095, -15.47999233184835], [163.93181032666945, 14.059740528793668], [169.65608018796578, 43.06116235359757]], "arm_l": [[169.3170846516242, -17.33063673449639], [191.25769570019932, 2.818481441438399], [216.9727147212627, 18.034886119475505]], "leg_r": [[156.0, 67.2155372436685], [198.34322325881226, 84.91610929892303], [232.62991528689673, 105.20464886963919], [238.93925608675823, 96.3308687444116]], "leg_l": [[161.90884651807326, 66.7540782623559], [152.7054359091143, 111.13927992793873], [124.14471158382098, 138.71890898230964], [131.846111137951, 146.45378523778777]]}, {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[156.0, 66.0], [159.7912748095231, 23.3238785947173], [163.40823813355095, -17.390122286184578]], "head": [165.8456250016539, -36.939465193360675], "nose": [[175.8441478685724, -36.76759175239952], [182.84308173466712, -36.64728089622242]], "arm_r": [[163.40823813355095, -15.47999233184835], [163.93181032666945, 14.059740528793668], [169.65608018796578, 43.06116235359757]], "arm_l": [[169.3170846516242, -17.33063673449639], [191.25769570019932, 2.818481441438399], [216.9727147212627, 18.034886119475505]], "leg_r": [[156.0, 67.2155372436685], [130.94660438930876, 105.20828401031557], [106.32014537628243, 136.24984799467126], [112.78578315149963, 145.01383628684118]], "leg_l": [[161.90884651807326, 66.7540782623559], [152.7054359091143, 111.13927992793873], [124.14471158382098, 138.71890898230964], [131.846111137951, 146.45378523778777]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[156.0, 66.0], [159.7912748095231, 23.3238785947173], [163.40823813355095, -17.390122286184578]], "head": [165.8456250016539, -36.939465193360675], "nose": [[175.8441478685724, -36.76759175239952], [182.84308173466712, -36.64728089622242]], "arm_r": [[163.40823813355095, -15.47999233184835], [163.93181032666945, 14.059740528793668], [169.65608018796578, 43.06116235359757]], "arm_l": [[169.3170846516242, -17.33063673449639], [191.25769570019932, 2.818481441438399], [216.9727147212627, 18.034886119475505]], "leg_r": [[156.0, 67.2155372436685], [169.4490984172459, 110.53724878775138], [176.7385194369318, 149.26993103400275], [187.73810059063766, 149.17539747602967]], "leg_l": [[161.90884651807326, 66.7540782623559], [152.7054359091143, 111.13927992793873], [124.14471158382098, 138.71890898230964], [131.846111137951, 146.45378523778777]]}}}, {"name": "Lunge", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 68.0], [164.54698815214294, 25.395540020190396], [168.88491937775055, -15.250094213421079]], "head": [171.66838139695184, -34.75456764656357], "nose": [[181.66247319251514, -34.41086838579267], [188.6582089816488, -34.17028332129074]], "arm_r": [[168.88491937775055, -13.339964259084852], [168.36134718463205, 16.199768601557167], [172.01742748678646, 45.523782962321306]], "arm_l": [[174.7937658958238, -15.19060866173289], [174.2701937027053, 14.349124198909129], [177.92627400485972, 43.67313855967327]], "leg_r": [[160.0, 69.2155372436685], [179.47137861645146, 110.25810678029066], [195.312257849331, 146.42974223223348], [204.99480167196958, 141.28905093810306]], "leg_l": [[165.90884651807326, 68.7540782623559], [144.50202796057948, 108.8509721197623], [124.83704329970969, 143.1540806833463], [133.08224180021978, 150.3247189060237]]}, {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 106.0], [166.05402989176287, 63.57777028291508], [171.82971358160557, 23.105987909144403]], "head": [175.30267713494416, 3.7090617012853215], "nose": [[185.2790495650788, 4.396079266744735], [192.26199791689754, 4.876956279906786]], "arm_r": [[171.82971358160557, 25.01611786348063], [170.25963489431726, 54.51986110987899], [172.87430717674698, 83.9516689755912]], "arm_l": [[177.73856009967884, 23.16547346083259], [176.16848141239052, 52.66921670723095], [178.78315369482024, 82.10102457294316]], "leg_r": [[160.0, 107.2155372436685], [205.88569496240868, 110.40713459933113], [196.0, 148.57746287018125], [206.99919882818972, 148.4467200883112]], "leg_l": [[165.90884651807326, 106.7540782623559], [163.40017792297274, 151.98781726819098], [124.0, 145.1915035150705], [115.3395832106557, 151.87059000314056]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 87.0], [165.3013164381239, 44.480179176892], [170.35889418943754, 3.915292644501605]], "head": [173.48758349024214, -15.538370063055481], "nose": [[183.47429126005107, -15.02294022259423], [190.4646980033331, -14.66215423430872]], "arm_r": [[170.35889418943754, 5.825422598837832], [169.3119092883625, 35.35165764096847], [172.4477631863921, 64.73404383394062]], "arm_l": [[176.2677407075108, 3.974778196189794], [175.22075580643576, 33.50101323832043], [178.35660970446537, 62.88339943129258]], "leg_r": [[160.0, 88.2155372436685], [200.77851063746203, 109.17860093471752], [196.0, 148.2888101774132], [206.98349694938457, 147.69563637304435]], "leg_l": [[165.90884651807326, 87.7540782623559], [162.09776564098584, 132.89949240048355], [124.0, 144.90285082230247], [122.23010981968788, 155.5945929197867]]}}}, {"name": "March in Place", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 62.0], [163.03440660786944, 19.26521672923713], [165.92930026825064, -21.504748919881465]], "head": [168.01986953360372, -41.09300638186289], "nose": [[178.01986953360372, -41.09300638186289], [185.01986953360372, -41.09300638186289]], "arm_r": [[165.92930026825064, -19.594618965545237], [150.0317223412545, 5.460311235117487], [173.01305563482384, 24.450977881902162]], "arm_l": [[171.8381467863239, -21.445263368193277], [186.8381467863239, 4.140792590380022], [215.3698422751785, -4.98887736580931]], "leg_r": [[160.0, 63.215537243668514], [201.34452612976168, 83.07425723018636], [185.07506040672968, 119.06092320680713], [194.40358946445036, 124.80147780556837]], "leg_l": [[165.90884651807326, 62.754078262355904], [164.4746241447424, 108.03321059644975], [162.0079625065513, 147.35054957930294], [172.98702739227483, 148.01857614327372]]}, {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [163.03440660786944, 21.26521672923713], [165.92930026825064, -19.504748919881465]], "head": [168.01986953360372, -39.09300638186289], "nose": [[178.01986953360372, -39.09300638186289], [185.01986953360372, -39.09300638186289]], "arm_r": [[165.92930026825064, -17.594618965545237], [165.40572807513215, 11.945113895096782], [192.1359238007832, 25.357914813218557]], "arm_l": [[171.8381467863239, -19.445263368193277], [171.3145745932054, 10.094469492448743], [198.04477031885645, 23.507270410570516]], "leg_r": [[160.0, 65.2155372436685], [168.06607636623892, 109.81480911859667], [158.0, 147.939388496694], [168.64599592855438, 150.6655047598624]], "leg_l": [[165.90884651807326, 64.7540782623559], [172.71840731660157, 109.55611956268012], [162.0, 147.50785240061987], [172.59772985997608, 150.4106343225862]]}, {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 62.0], [163.03440660786944, 19.26521672923713], [165.92930026825064, -21.504748919881465]], "head": [168.01986953360372, -41.09300638186289], "nose": [[178.01986953360372, -41.09300638186289], [185.01986953360372, -41.09300638186289]], "arm_r": [[165.92930026825064, -19.594618965545237], [180.92930026825064, 5.99143699302806], [209.46099575710525, -3.138232963161272]], "arm_l": [[171.8381467863239, -21.445263368193277], [155.94056885932775, 3.6096668324694487], [178.9219021528971, 22.60033347925412]], "leg_r": [[160.0, 63.215537243668514], [159.58457964721987, 108.51484653715457], [158.00131601773398, 147.876286549904], [168.99269577728379, 148.3050693816835]], "leg_l": [[165.90884651807326, 62.754078262355904], [207.25337264783494, 82.61279824887376], [190.98390692480294, 118.59946422549453], [200.31243598252362, 124.34001882425576]]}, {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [163.03440660786944, 21.26521672923713], [165.92930026825064, -19.504748919881465]], "head": [168.01986953360372, -39.09300638186289], "nose": [[178.01986953360372, -39.09300638186289], [185.01986953360372, -39.09300638186289]], "arm_r": [[165.92930026825064, -17.594618965545237], [165.40572807513215, 11.945113895096782], [192.1359238007832, 25.357914813218557]], "arm_l": [[171.8381467863239, -19.445263368193277], [171.3145745932054, 10.094469492448743], [198.04477031885645, 23.507270410570516]], "leg_r": [[160.0, 65.2155372436685], [168.06607636623892, 109.81480911859667], [158.0, 147.939388496694], [168.64599592855438, 150.6655047598624]], "leg_l": [[165.90884651807326, 64.7540782623559], [172.71840731660157, 109.55611956268012], [162.0, 147.50785240061987], [172.59772985997608, 150.4106343225862]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 63.0], [163.03440660786944, 20.26521672923713], [165.92930026825064, -20.504748919881465]], "head": [168.01986953360372, -40.09300638186289], "nose": [[178.01986953360372, -40.09300638186289], [185.01986953360372, -40.09300638186289]], "arm_r": [[165.92930026825064, -18.594618965545237], [157.40883992713296, 9.732974293633951], [182.425414589148, 26.03952896291895]], "arm_l": [[171.8381467863239, -20.445263368193277], [179.34954690795715, 8.157915700951795], [209.25706691995097, 10.475929473965058]], "leg_r": [[160.0, 64.2155372436685], [187.71521835779888, 100.37108340382309], [174.5027852113516, 137.55239037407463], [184.60663358284393, 141.83514346377513]], "leg_l": [[165.90884651807326, 63.754078262355904], [169.92855366558072, 108.88194074142741], [162.0, 147.49266015363207], [172.7817467986817, 149.63988796164233]]}}}, {"name": "Neck Rolls", "camera": 35.0, "frames": [{"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [160.0, 21.160862743968956], [160.0, -19.708659006037678]], "head": [171.38061648487894, -32.49312079333657], "nose": [[176.5398949555325, -23.926800366090653], [180.07013744825343, -18.065285320810304]], "arm_r": [[153.6906592001385, -18.143972149085513], [155.8324733454366, 11.548258390940887], [160.52152070437936, 41.119820372808604]], "arm_l": [[171.1495845045311, -19.659931294766643], [173.2913986498292, 10.032299245259756], [177.98044600877196, 39.603861227127474]], "leg_r": [[155.98496494554269, 64.99570981806046], [156.61457190991757, 110.40833397961296], [156.16054743205788, 149.78133298532248], [165.16982961073276, 150.99271411447162]], "leg_l": [[168.85527875912692, 64.61770475016273], [174.08875167395848, 109.83670152473152], [164.0, 147.88216467508], [174.58122841298848, 150.8132031809861]]}, {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [162.9139746738203, 19.823897743730882], [154.17205065235942, -17.03472900556153]], "head": [153.12453367726502, -35.148439987716216], "nose": [[162.23394763661693, -31.023082146611472], [166.73946689638586, -28.98267874750428]], "arm_r": [[148.05012423863502, -12.895807892791026], [154.33891855598083, 14.89374058327624], [163.11430413163794, 42.59044372578424]], "arm_l": [[165.13422077075342, -19.560235550108835], [171.42301508809922, 8.22931292595843], [180.19840066375633, 35.92601606846643]], "leg_r": [[155.98496494554269, 64.99570981806046], [156.61457190991757, 110.40833397961296], [156.16054743205788, 149.78133298532248], [165.16982961073276, 150.99271411447162]], "leg_l": [[168.85527875912692, 64.61770475016273], [174.08875167395848, 109.83670152473152], [164.0, 147.88216467508], [174.58122841298848, 150.8132031809861]]}, {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [160.0, 21.160862743968956], [160.0, -19.708659006037678]], "head": [153.33641694674432, -38.512215478516], "nose": [[161.32919767195267, -44.52182913321442], [165.9681531456001, -48.009767951558864]], "arm_r": [[153.6906592001385, -18.143972149085513], [155.8324733454366, 11.548258390940887], [160.52152070437936, 41.119820372808604]], "arm_l": [[171.1495845045311, -19.659931294766643], [173.2913986498292, 10.032299245259756], [177.98044600877196, 39.603861227127474]], "leg_r": [[155.98496494554269, 64.99570981806046], [156.61457190991757, 110.40833397961296], [156.16054743205788, 149.78133298532248], [165.16982961073276, 150.99271411447162]], "leg_l": [[168.85527875912692, 64.61770475016273], [174.08875167395848, 109.83670152473152], [164.0, 147.88216467508], [174.58122841298848, 150.8132031809861]]}, {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [157.0860253261797, 21.269201783645215], [165.82794934764058, -19.925337085390197]], "head": [170.30045449761838, -39.40798760388682], "nose": [[180.02217287718435, -37.065298604380274], [186.48013645549753, -35.509092217459234]], "arm_r": [[159.7060229339162, -21.027840257638886], [157.70085690716655, 8.818571392275864], [158.303566049395, 38.54205986896477]], "arm_l": [[176.79011946603458, -17.20941934491831], [174.78495343928498, 12.636992304996438], [175.38766258151338, 42.36048078168535]], "leg_r": [[155.98496494554269, 64.99570981806046], [156.61457190991757, 110.40833397961296], [156.16054743205788, 149.78133298532248], [165.16982961073276, 150.99271411447162]], "leg_l": [[168.85527875912692, 64.61770475016273], [174.08875167395848, 109.83670152473152], [164.0, 147.88216467508], [174.58122841298848, 150.8132031809861]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [161.4679290575061, 20.64267047800022], [157.0641418849878, -18.672274474100206]], "head": [162.72087077889134, -35.233915555558895], "nose": [[170.2285926709034, -28.62830339678036], [174.87521206467858, -24.540010568912756]], "arm_r": [[150.8018299546917, -15.799053939069722], [155.03270573465034, 13.15571890388496], [161.78026604533693, 42.00060715071375]], "arm_l": [[168.16669751995354, -19.93208044090749], [172.39757329991218, 9.02269240204719], [179.14513361059878, 37.86758064887598]], "leg_r": [[155.98496494554269, 64.99570981806046], [156.5530112803125, 110.40321098059958], [156.04541303597708, 149.76987086878293], [165.0545136399393, 150.9981459014436]], "leg_l": [[168.85527875912692, 64.61770475016273], [174.08875167395848, 109.83670152473152], [164.0, 147.88216467508], [174.58122841298845, 150.8132031809861]]}}}, {"name": "Overhead Triceps Stretch", "camera": 40.0, "frames": [{"order": ["leg_l", "leg_r", "spine", "arm_l", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [161.16295359997207, 21.356410973743678], [162.272438068911, -19.326553269696262]], "head": [164.9328825001008, -38.335831116240485], "nose": [[174.6663806423974, -36.04257711506479], [180.015629423895, -34.782271108722945]], "arm_r": [[155.20177436235906, -17.86330883254189], [166.09610938577018, -37.706394651916014], [146.89387147564, -55.93361473951907]], "arm_l": [[173.86954081587587, -19.280984693379658], [193.89556067214647, -35.16347455371521], [198.4725459338602, -64.19293966449294]], "leg_r": [[155.50048673219422, 64.93115555091643], [157.45202622343763, 110.06460929178084], [158.21239294139752, 149.183764076137], [166.64702989109946, 150.4943102984795]], "leg_l": [[169.0259523082187, 64.57765746255455], [172.35504357414234, 109.83191331245861], [162.0, 147.62941573491267], [172.53323300991636, 150.69016969434705]]}, {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [162.43438403305933, 21.02155460880003], [169.34797173752037, -20.009322276027916]], "head": [175.72916593806963, -38.48639829069877], "nose": [[185.07160735756915, -34.92005726336842], [190.24410815251892, -32.9455303735957]], "arm_r": [[162.43579379477544, -20.44083187320207], [166.34604183376157, -48.96599323044342], [151.8757374220089, -28.493607897535185]], "arm_l": [[180.78658872067825, -18.068999665382783], [189.42294106870335, -44.89422830041568], [164.0, -53.42605795442014]], "leg_r": [[155.50048673219422, 64.93115555091643], [157.45202622343763, 110.06460929178084], [158.21239294139752, 149.183764076137], [166.64702989109946, 150.4943102984795]], "leg_l": [[169.0259523082187, 64.57765746255455], [172.35504357414234, 109.83191331245861], [162.0, 147.62941573491267], [172.53323300991636, 150.69016969434705]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "leg_r", "spine", "arm_l", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [161.7954246174492, 21.207469215474156], [165.8256557275463, -19.86619066607126]], "head": [170.3699753116084, -38.7097346879485], "nose": [[179.93117163205625, -35.77996935231922], [185.21939608800238, -34.159538584160664]], "arm_r": [[158.8075288111505, -19.348352716342887], [166.39563936077067, -44.42588798396824], [145.34772804978564, -43.160878574485466]], "arm_l": [[177.370221684355, -18.87521560232866], [192.09535806067572, -41.01353840195073], [178.25562314256626, -66.99665153999453]], "leg_r": [[155.50048673219422, 64.93115555091643], [157.37713478136337, 110.05802895905501], [158.07262920713882, 149.16913898687545], [166.50707681467256, 150.50083895445817]], "leg_l": [[169.0259523082187, 64.57765746255455], [172.35504357414234, 109.83191331245861], [162.0, 147.62941573491267], [172.53323300991636, 150.69016969434705]]}}}, {"name": "Pec Deck", "camera": 90.0, "frames": [{"order": ["spine", "arm_l", "arm_r", "leg_l", "leg_r", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 116.0], [160.0, 73.16086274396895], [160.0, 32.29134099396232]], "head": [160.0, 12.59518593371817], "arm_r": [[149.0, 32.29134099396232], [119.2919579377529, 36.425901331217304], [92.367328079126, 38.40857274058021]], "arm_l": [[171.0, 32.29134099396232], [200.7080420622471, 36.425901331217304], [227.632671920874, 38.40857274058021]], "leg_r": [[153.0, 116.0], [153.0, 133.23190329713196], [153.0, 173.01277911186287], [153.0, 171.8629660159187]], "leg_l": [[167.0, 116.0], [167.0, 133.23190329713196], [167.0, 173.01277911186287], [167.0, 171.8629660159187]]}, {"order": ["spine", "leg_l", "leg_r", "arm_l", "arm_r", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 116.0], [160.0, 73.16086274396895], [160.0, 32.29134099396232]], "head": [160.0, 12.59518593371817], "arm_r": [[149.0, 32.29134099396232], [145.8641461019704, 39.50923965824833], [143.02209729517693, 33.74893662733645]], "arm_l": [[171.0, 32.29134099396232], [174.1358538980296, 39.50923965824833], [176.97790270482307, 33.74893662733645]], "leg_r": [[153.0, 116.0], [153.0, 133.23190329713196], [153.0, 173.01277911186287], [153.0, 171.8629660159187]], "leg_l": [[167.0, 116.0], [167.0, 133.23190329713196], [167.0, 173.01277911186287], [167.0, 171.8629660159187]]}], "tween": {"t": 0.5, "sample": {"order": ["spine", "arm_l", "arm_r", "leg_l", "leg_r", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 116.0], [160.0, 73.16086274396895], [160.0, 32.29134099396232]], "head": [160.0, 12.59518593371817], "arm_r": [[149.0, 32.29134099396232], [128.16024888623008, 48.32855050541065], [109.27302017191471, 54.37959395586806]], "arm_l": [[171.0, 32.29134099396232], [191.83975111376992, 48.32855050541065], [210.7269798280853, 54.37959395586806]], "leg_r": [[153.0, 116.0], [153.0, 133.23190329713196], [153.0, 173.01277911186287], [153.0, 171.8629660159187]], "leg_l": [[167.0, 116.0], [167.0, 133.23190329713196], [167.0, 173.01277911186287], [167.0, 171.8629660159187]]}}}, {"name": "Plank", "camera": 0.0, "frames": [{"order": ["arm_r", "leg_r", "spine", "leg_l", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[172.0, 114.0], [128.6655306330091, 117.73367682621827], [87.32345066220162, 121.29569035008166]], "head": [67.39955670036672, 123.0123233736303], "nose": [[58.7064746151967, 127.95502747771098], [52.64429678870563, 131.4018546132537]], "arm_r": [[93.23229718027487, 121.35517590176984], [93.23229718027487, 150.89940849213608], [63.23229718027487, 150.89940849213608]], "arm_l": [[87.32345066220162, 123.2058203044179], [87.32345066220162, 152.75005289478415], [57.32345066220162, 152.75005289478415]], "leg_r": [[177.90884651807326, 114.7540782623559], [204.2933625902214, 151.86261333148943], [244.2933625902214, 151.86261333148943], [244.2933625902214, 162.69549861462372]], "leg_l": [[172.0, 115.2155372436685], [198.38451607214813, 152.32407231280206], [238.38451607214813, 152.32407231280206], [238.38451607214813, 163.15695759593632]]}, {"order": ["arm_r", "leg_r", "spine", "leg_l", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[168.0, 137.0], [125.16086274396895, 129.56106188266668], [84.29134099396232, 122.46414390865907]], "head": [64.59518593371817, 119.04394247540239], "nose": [[55.90210384854815, 123.98664657948308], [49.839926022057085, 127.43347371502581]], "arm_r": [[90.20018751203557, 122.52362946034725], [90.20018751203557, 152.06786205071347], [60.20018751203557, 152.0678620507135]], "arm_l": [[84.29134099396232, 124.37427386299531], [84.29134099396232, 153.91850645336154], [54.29134099396232, 153.91850645336154]], "leg_r": [[173.90884651807326, 137.7540782623559], [219.21000315663483, 145.62054155884627], [258.60231327712313, 152.46094442535966], [256.6921833227869, 163.12925383968215]], "leg_l": [[168.0, 138.2155372436685], [213.30115663856157, 146.0820005401589], [252.6934667590499, 152.9224034066723], [250.78333680471366, 163.59071282099478]]}], "tween": {"t": 0.5, "sample": {"order": ["arm_r", "leg_r", "spine", "leg_l", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[170.0, 125.5], [126.54140236118918, 123.63138307763239], [85.08090116554209, 121.84867957698282]], "head": [65.09993673390494, 120.9895453598023], "nose": [[56.40685464873492, 125.932249463883], [50.344676822243855, 129.37907659942573]], "arm_r": [[90.98974768361533, 121.908165128671], [90.98974768361533, 151.45239771903724], [60.989747683615334, 151.45239771903724]], "arm_l": [[85.08090116554209, 123.75880953131905], [85.08090116554209, 153.30304212168528], [55.08090116554209, 153.30304212168528]], "leg_r": [[175.90884651807326, 126.2540782623559], [214.704853025466, 150.59437198191316], [254.55264094913582, 154.0276380290104], [253.5939277789116, 164.81930091310488]], "leg_l": [[170.0, 126.7155372436685], [208.79600650739275, 151.05583096322576], [248.64379443106256, 154.489097010323], [247.68508126083833, 165.28075989441749]]}}}, {"name": "Pull-Up", "camera": 0.0, "frames": [{"order": ["leg_l", "leg_r", "spine", "arm_l", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[156.0, 99.0], [156.0, 56.160862743968956], [156.0, 15.291340993962322]], "head": [156.69798993405, -4.392815700791431], "nose": [[166.67436236418465, -5.079833266250844], [173.6573107160034, -5.560710279412895]], "arm_r": [[156.0, 17.20147094829855], [154.4533259562162, -12.3034712212812], [151.81470930141467, -41.73320653220463]], "arm_l": [[161.90884651807326, 15.350826545650511], [160.50443880392413, -14.161015014786125], [158.00888856162837, -43.60285087153715]], "leg_r": [[156.0, 100.2155372436685], [182.38451607214813, 137.32407231280206], [142.5367281484783, 140.7573383598993], [137.8879272693306, 150.57526664807864]], "leg_l": [[161.90884651807326, 99.7540782623559], [188.2933625902214, 136.86261333148946], [148.44557466655155, 140.2958793785867], [143.79677378740385, 150.11380766676604]]}, {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[156.0, 67.0], [156.0, 24.160862743968956], [156.0, -16.708659006037678]], "head": [157.3951294748825, -36.35683522248037], "nose": [[167.3419871354356, -37.38640925124002], [174.3036404030135, -38.106992338495076]], "arm_r": [[156.0, -14.79852905170145], [181.44676676044196, -30.44619337585729], [154.0, -42.37314613604329]], "arm_l": [[161.90884651807326, -16.64917345434949], [186.16861785786944, -34.02938406879529], [158.0, -44.19386765345278]], "leg_r": [[156.0, 68.2155372436685], [182.38451607214813, 105.32407231280206], [142.5367281484783, 108.75733835989931], [137.8879272693306, 118.57526664807864]], "leg_l": [[161.90884651807326, 67.7540782623559], [188.2933625902214, 104.86261333148944], [148.44557466655155, 108.2958793785867], [143.79677378740385, 118.11380766676604]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[156.0, 83.0], [156.0, 40.160862743968956], [156.0, -0.7086590060376778]], "head": [157.04671912485887, -20.37782117030325], "nose": [[167.00980719951306, -21.236235998159508], [173.98317008615527, -21.83705755640153]], "arm_r": [[156.0, 1.2014709482985495], [134.64946364596483, -19.55332843708773], [111.97773274796863, -0.20511103877473147]], "arm_l": [[161.90884651807326, -0.6491734543494891], [139.7155670588939, -20.52810638165094], [117.76662496819401, -0.3877900207003597]], "leg_r": [[156.0, 84.2155372436685], [182.38451607214813, 121.32407231280206], [142.5367281484783, 124.75733835989931], [137.8879272693306, 134.57526664807864]], "leg_l": [[161.90884651807326, 83.7540782623559], [188.2933625902214, 120.86261333148944], [148.44557466655155, 124.2958793785867], [143.79677378740385, 134.11380766676604]]}}}, {"name": "Push-Up", "camera": 0.0, "frames": [{"order": ["leg_r", "arm_r", "leg_l", "spine", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[180.0, 115.0], [138.40074311560795, 102.4750484116029], [98.714095743142, 90.5259566663505]], "head": [79.58800062388129, 84.76735823490355], "nose": [[69.83685766497203, 86.98438574824536], [63.016267211475395, 88.53512054480812]], "arm_r": [[104.62294226121524, 90.58544221803868], [90.66949108176117, 116.73948142258237], [62.92966140223935, 127.98966973623477]], "arm_l": [[98.714095743142, 92.43608662068672], [98.08624768774057, 121.97384842742832], [93.92556209079443, 151.23256293137962]], "leg_r": [[185.90884651807326, 115.7540782623559], [216.81618561243002, 149.30606635968886], [256.0, 141.3890027259118], [253.44970263774323, 151.92672102829837]], "leg_l": [[180.0, 116.2155372436685], [221.11149742237373, 136.53803680407202], [260.0, 145.75976983403476], [253.14654590646848, 154.23313305715615]]}, {"order": ["arm_r", "leg_r", "spine", "leg_l", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[180.0, 132.0], [136.6655306330091, 128.26632317378173], [95.32345066220162, 124.70430964991834]], "head": [75.39955670036672, 122.98767662636972], "nose": [[66.7064746151967, 127.93038073045041], [60.64429678870563, 131.37720786599314]], "arm_r": [[101.23229718027487, 124.76379520160651], [126.52667189632352, 140.64928481588592], [97.0, 145.87671697909758]], "arm_l": [[95.32345066220162, 126.61443960425457], [124.54010044659273, 133.32179563010098], [100.99999999999999, 151.63666950855597]], "leg_r": [[185.90884651807326, 132.7540782623559], [221.45545402765325, 161.50706980680354], [255.99999999999997, 141.6472709247043], [257.01133983112186, 152.43427406487965]], "leg_l": [[180.0, 133.2155372436685], [220.84205541554815, 154.05827872604254], [260.0, 146.01803803282723], [257.4838828139067, 156.5637222789729]]}], "tween": {"t": 0.5, "sample": {"order": ["arm_r", "leg_r", "spine", "leg_l", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[180.0, 123.5], [137.29921752002664, 115.3259072573788], [96.56168940694857, 107.52763487074017]], "head": [76.9291457379953, 103.76943131091434], "nose": [[67.60607009461341, 107.38609559555283], [61.093147118739246, 109.91262834251884]], "arm_r": [[102.47053592502182, 107.58712042242836], [122.25277767997544, 129.79803772926599], [97.0, 145.74758287970135]], "arm_l": [[96.56168940694857, 109.43776482507641], [119.61748516508456, 128.34069448148384], [101.0, 151.50753540915974]], "leg_r": [[185.90884651807326, 124.2540782623559], [218.77913752303337, 155.94506275047252], [256.0, 141.51813682530806], [255.32534956444712, 152.3306284389917]], "leg_l": [[180.0, 124.7155372436685], [220.01754793404967, 147.05561095864437], [260.0, 145.88890393343098], [255.64850815815672, 155.8381198287707]]}}}, {"name": "Rear Delt Fly", "camera": 270.0, "frames": [{"order": ["leg_l", "leg_r", "arm_l", "arm_r", "spine", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 116.0], [160.0, 73.16086274396895], [160.0, 32.29134099396232]], "head": [160.0, 12.59518593371817], "arm_r": [[171.0, 32.29134099396232], [174.1358538980296, 29.690992159873403], [177.16485616560564, 19.444223248777263]], "arm_l": [[149.0, 32.29134099396232], [145.8641461019704, 29.690992159873403], [142.83514383439433, 19.444223248777263]], "leg_r": [[167.0, 116.0], [167.0, 117.60537684831505], [167.0, 153.55713870028174], [167.0, 148.73505608560188]], "leg_l": [[153.0, 116.0], [153.0, 117.60537684831505], [153.0, 153.55713870028174], [153.0, 148.73505608560188]]}, {"order": ["leg_l", "leg_r", "spine", "arm_l", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 116.0], [160.0, 73.16086274396895], [160.0, 32.29134099396232]], "head": [160.0, 12.59518593371817], "arm_r": [[171.0, 32.29134099396232], [200.8356568610482, 35.1988540984957], [229.6546883674333, 35.09863647252796]], "arm_l": [[149.0, 32.29134099396232], [119.1643431389518, 35.1988540984957], [90.3453116325667, 35.09863647252796]], "leg_r": [[167.0, 116.0], [167.0, 117.60537684831505], [167.0, 153.55713870028174], [167.0, 148.73505608560188]], "leg_l": [[153.0, 116.0], [153.0, 117.60537684831505], [153.0, 153.55713870028174], [153.0, 148.73505608560188]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "leg_r", "arm_l", "arm_r", "spine", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 116.0], [160.0, 73.16086274396895], [160.0, 32.29134099396232]], "head": [160.0, 12.59518593371817], "arm_r": [[171.0, 32.29134099396232], [192.21320343559643, 43.375209342088894], [212.703584492363, 47.46101864010683]], "arm_l": [[149.0, 32.29134099396232], [127.78679656440357, 43.375209342088894], [107.296415507637, 47.46101864010683]], "leg_r": [[167.0, 116.0], [167.0, 117.60537684831505], [167.0, 153.55713870028174], [167.0, 148.73505608560188]], "leg_l": [[153.0, 116.0], [153.0, 117.60537684831505], [153.0, 153.55713870028174], [153.0, 148.73505608560188]]}}}, {"name": "Reverse Crunch", "camera": 0.0, "frames": [{"order": ["arm_l", "leg_l", "spine", "arm_r", "leg_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[172.0, 148.0], [128.5, 148.0], [87.0, 148.0]], "head": [67.43704798532389, 143.90493909881587], "nose": [[67.43704798532389, 133.90493909881587], [67.43704798532389, 127.01128482773042]], "arm_r": [[87.0, 149.91012995433624], [116.98172481057287, 150.94120880220163], [146.96344962114574, 151.97228765006705]], "arm_l": [[92.90884651807325, 148.05948555168817], [122.89057132864612, 149.09056439955359], [152.872296139219, 150.121643247419]], "leg_r": [[172.0, 149.2155372436685], [164.0121838273212, 104.60260696559263], [201.59988865875755, 118.07557051893123], [205.3621102353399, 107.89598815654968]], "leg_l": [[177.90884651807326, 148.7540782623559], [169.92103034539446, 104.14114798428001], [207.5087351768308, 117.61411153761861], [211.27095675341317, 107.43452917523706]]}, {"order": ["arm_l", "leg_l", "spine", "arm_r", "leg_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[166.0, 136.0], [124.61787286428175, 154.24267631947558], [82.62501214971675, 143.35521174704758]], "head": [62.63719560933484, 144.04259764562454], "nose": [[62.63719560933484, 134.04259764562454], [62.63719560933484, 127.14894337453909]], "arm_r": [[82.62501214971675, 145.26534170138382], [112.60673696028962, 146.29642054924923], [142.58846177086252, 147.32749939711462]], "arm_l": [[88.53385866779, 143.41469729873577], [118.51558347836287, 144.44577614660116], [148.49730828893578, 145.47685499446658]], "leg_r": [[166.0, 137.2155372436685], [146.5595599599278, 96.15874622037313], [185.95187008041614, 102.99914908688652], [187.86200003475238, 92.33083967256403]], "leg_l": [[171.90884651807326, 136.7540782623559], [152.46840647800107, 95.69728723906053], [191.8607165984894, 102.5376901055739], [193.77084655282565, 91.86938069125141]]}], "tween": {"t": 0.5, "sample": {"order": ["arm_l", "leg_l", "spine", "arm_r", "leg_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[169.0, 142.0], [126.03181909460824, 151.19312771952025], [84.40784565448814, 145.72317600490808]], "head": [64.48395169265324, 144.00654298135944], "nose": [[64.48395169265324, 134.00654298135944], [64.48395169265324, 127.11288871027399]], "arm_r": [[84.40784565448814, 147.6333059592443], [114.38957046506101, 148.6643848071097], [144.37129527563388, 149.69546365497513]], "arm_l": [[90.31669217256139, 145.78266155659625], [120.29841698313426, 146.81374040446167], [150.28014179370714, 147.84481925232706]], "leg_r": [[169.0, 143.2155372436685], [155.16753322280343, 100.01105626897177], [193.80456627436615, 110.20653635873893], [196.65157577049388, 99.74277269053276]], "leg_l": [[174.90884651807326, 142.7540782623559], [161.0763797408767, 99.54959728765917], [199.7134127924394, 109.74507737742633], [202.56042228856714, 99.28131370922014]]}}}, {"name": "Romanian Deadlift", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[90.0, 92.0], [131.26145916952032, 77.91407563146277], [166.9527046464468, 56.71838660269156]], "head": [185.3628017154956, 49.02248570910264], "nose": [[195.3258897901498, 49.8809005369589], [202.299252676792, 50.48172209520092]], "arm_r": [[166.9527046464468, 58.628516557027794], [167.522630693969, 88.16741731595586], [169.12519553685217, 117.66946650593746]], "arm_l": [[172.86155116452005, 56.77787215437974], [172.53496926154835, 86.32035410289532], [173.2408596233939, 115.8564070410833]], "leg_r": [[90.0, 93.2155372436685], [125.35053290875736, 122.20202514661878], [155.3086980593084, 148.30423046258957], [160.72538022288882, 138.87577986942745]], "leg_l": [[95.90884651807325, 92.7540782623559], [130.836880114458, 122.23311800484156], [160.3993109485087, 148.76930488988666], [165.9571573187271, 139.4208727010124]]}, {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[158.0, 64.0], [165.55369572851146, 21.811685497906495], [172.76009510168907, -18.436936383401104]], "head": [176.91832891804427, -37.70268319925994], "nose": [[186.8947013481789, -37.01566563380052], [193.87764969999765, -36.53478862063847]], "arm_r": [[172.76009510168907, -16.526806429064877], [178.85644942928292, 12.400979889721263], [199.1744301067988, 34.137801113206514]], "arm_l": [[178.66894161976234, -18.377450831712917], [183.4474723060758, 10.789583023349335], [202.20923781110346, 33.84326058608493]], "leg_r": [[158.0, 65.2155372436685], [169.22887526493585, 109.1462686355685], [158.0, 146.95458074368176], [168.55768072289158, 149.9956086837736]], "leg_l": [[163.90884651807326, 64.7540782623559], [174.01236624892732, 108.94900788074216], [162.0, 146.52304464760766], [172.49226113348837, 149.77625928644824]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[124.0, 78.0], [152.2476749245726, 45.366861135771344], [175.74775741142687, 11.627906421285644]], "head": [188.46932181698216, -3.570131021415193], "nose": [[198.43942052699484, -2.7973880417618164], [205.41784186312674, -2.256518161392056]], "arm_r": [[175.74775741142687, 13.538036375621871], [172.91579688708677, 42.95033841992456], [171.05157770169677, 72.43747391486767]], "arm_l": [[181.65660392950014, 11.687391972973833], [178.0074482941626, 41.01224285393908], [175.3206698023005, 70.4377517450443]], "leg_r": [[124.0, 79.2155372436685], [159.67849876443978, 107.8096593131734], [158.0, 147.16727220151085], [168.97835398703234, 146.48800914018568]], "leg_l": [[129.90884651807326, 78.7540782623559], [165.468203411963, 107.49177652673711], [162.0, 146.73573610543676], [172.99823667699596, 146.54177648268518]]}}}, {"name": "Rotary", "camera": 90.0, "frames": [{"order": ["spine", "arm_l", "leg_l", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 122.0], [160.0, 79.16086274396895], [160.0, 38.29134099396232]], "head": [160.0, 18.59518593371817], "nose": [[169.63488735000612, 21.272674636344597], [173.34432219963855, 22.303508681644907]], "arm_r": [[152.92933629344807, 39.754585431116695], [174.52472560523668, 53.00593171303545], [197.15692080730136, 61.433928518814795]], "arm_l": [[167.07066370655193, 36.82809655680795], [188.66605301834056, 50.079442838726706], [211.29824822040524, 58.50743964450605]], "leg_r": [[153.0, 122.0], [140.32068163241803, 135.6641252039847], [136.5497405147205, 175.47598817361995], [133.7005822327046, 174.9683561722579]], "leg_l": [[167.0, 122.0], [179.67931836758197, 135.6641252039847], [183.4502594852795, 175.47598817361995], [186.2994177672954, 174.9683561722579]]}, {"order": ["spine", "arm_r", "leg_l", "leg_r", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 122.0], [160.0, 79.16086274396895], [160.0, 38.29134099396232]], "head": [160.0, 18.59518593371817], "nose": [[150.36511264999388, 21.272674636344597], [146.65567780036145, 22.30350868164491]], "arm_r": [[152.92933629344807, 36.82809655680795], [131.33394698165944, 50.079442838726706], [108.70175177959479, 58.50743964450605]], "arm_l": [[167.07066370655193, 39.754585431116695], [145.47527439476332, 53.00593171303545], [122.84307919269864, 61.433928518814795]], "leg_r": [[153.0, 122.0], [140.32068163241803, 135.6641252039847], [136.5497405147205, 175.47598817361995], [133.7005822327046, 174.9683561722579]], "leg_l": [[167.0, 122.0], [179.67931836758197, 135.6641252039847], [183.4502594852795, 175.47598817361995], [186.2994177672954, 174.9683561722579]]}], "tween": {"t": 0.5, "sample": {"order": ["spine", "leg_l", "leg_r", "arm_l", "arm_r", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 122.0], [160.0, 79.16086274396895], [160.0, 38.29134099396232]], "head": [160.0, 18.59518593371817], "arm_r": [[149.0, 38.29134099396232], [149.0, 53.29134099396232], [149.0, 63.55194529373239]], "arm_l": [[171.0, 38.29134099396232], [171.0, 53.29134099396232], [171.0, 63.55194529373239]], "leg_r": [[153.0, 122.0], [140.32068163241803, 135.6641252039847], [136.5497405147205, 175.47598817361995], [133.7005822327046, 174.9683561722579]], "leg_l": [[167.0, 122.0], [179.67931836758197, 135.6641252039847], [183.4502594852795, 175.47598817361995], [186.2994177672954, 174.9683561722579]]}}}, {"name": "Seated Row", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[140.0, 122.0], [142.69891028706257, 79.2013101935184], [149.59296717847204, 38.855014516213274]], "head": [153.06593073181065, 19.458088308354192], "nose": [[163.02901880646485, 18.599673480497934], [170.00238169310705, 17.998851922255916]], "arm_r": [[149.59296717847204, 40.7651444705495], [170.10755873258697, 62.32212354359968], [200.0, 64.82166477360376]], "arm_l": [[155.5018136965453, 38.914500067901464], [174.0120238448619, 62.16456016148103], [204.0, 63.000943256194205]], "leg_r": [[140.0, 123.2155372436685], [183.8850455429191, 109.63722816102359], [188.0, 148.8205388219859], [198.94163850640507, 147.7061180825139]], "leg_l": [[145.90884651807326, 122.7540782623559], [189.43323013923415, 108.09307121777746], [192.0, 147.4041949728996], [202.97732933112334, 146.70905688665255]]}, {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[140.0, 122.0], [142.69891028706257, 79.2013101935184], [149.59296717847204, 38.855014516213274]], "head": [153.06593073181065, 19.458088308354192], "nose": [[163.02901880646485, 18.599673480497934], [170.00238169310705, 17.998851922255916]], "arm_r": [[149.59296717847204, 40.7651444705495], [130.27974637330342, 63.37285383528793], [158.0, 74.66974230372583]], "arm_l": [[155.5018136965453, 38.914500067901464], [134.82908862986338, 60.32446610131683], [162.0, 72.84902078631629]], "leg_r": [[140.0, 123.2155372436685], [183.8850455429191, 109.63722816102359], [188.0, 148.8205388219859], [198.94163850640507, 147.7061180825139]], "leg_l": [[145.90884651807326, 122.7540782623559], [189.43323013923415, 108.09307121777746], [192.0, 147.4041949728996], [202.97732933112334, 146.70905688665255]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[140.0, 122.0], [142.69891028706257, 79.2013101935184], [149.59296717847204, 38.855014516213274]], "head": [153.06593073181065, 19.458088308354192], "nose": [[163.02901880646485, 18.599673480497934], [170.00238169310705, 17.998851922255916]], "arm_r": [[149.59296717847204, 40.7651444705495], [149.00535132812342, 70.30370907354869], [179.0, 69.74570353866478]], "arm_l": [[155.5018136965453, 38.914500067901464], [153.0031939480439, 68.35608343607598], [182.99999999999997, 67.92498202125525]], "leg_r": [[140.0, 123.2155372436685], [183.8850455429191, 109.63722816102359], [188.0, 148.8205388219859], [198.94163850640507, 147.7061180825139]], "leg_l": [[145.90884651807326, 122.7540782623559], [189.43323013923415, 108.09307121777746], [192.0, 147.4041949728996], [202.97732933112334, 146.70905688665255]]}}}, {"name": "Shoulder Press", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[120.0, 130.0], [103.0200605630874, 90.37728052628361], [95.1324142218102, 50.073787462921175]], "head": [98.60537777514881, 30.676861255062093], "nose": [[108.5109515671687, 29.305873344521444], [115.4428280483597, 28.346462102667882]], "arm_r": [[95.1324142218102, 51.9839174172574], [110.1324142218102, 77.5699733758307], [110.1324142218102, 48.02574078546445]], "arm_l": [[101.04126073988344, 50.133273014609365], [116.04126073988344, 75.71932897318266], [116.04126073988344, 46.17509638281641]], "leg_r": [[120.0, 131.2155372436685], [161.917285943973, 112.5576236573147], [164.0, 151.89650005692488], [174.98507904390829, 151.3324549957621]], "leg_l": [[125.90884651807325, 130.7540782623559], [167.3506724370636, 111.09303668974187], [168.0, 150.48015620783858], [178.99855056415495, 150.3043039328269]]}, {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[120.0, 130.0], [103.0200605630874, 90.37728052628361], [95.1324142218102, 50.073787462921175]], "head": [98.60537777514881, 30.676861255062093], "nose": [[108.5109515671687, 29.305873344521444], [115.4428280483597, 28.346462102667882]], "arm_r": [[95.1324142218102, 51.9839174172574], [112.76597179058439, 28.082131165884945], [133.6057229043543, 6.829788793172227]], "arm_l": [[101.04126073988344, 50.133273014609365], [118.67481830865763, 26.231486763236905], [139.51456942242757, 4.979144390524189]], "leg_r": [[120.0, 131.2155372436685], [161.917285943973, 112.5576236573147], [164.0, 151.89650005692488], [174.98507904390829, 151.3324549957621]], "leg_l": [[125.90884651807325, 130.7540782623559], [167.3506724370636, 111.09303668974187], [168.0, 150.48015620783858], [178.99855056415495, 150.3043039328269]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[120.0, 130.0], [103.0200605630874, 90.37728052628361], [95.1324142218102, 50.073787462921175]], "head": [98.60537777514881, 30.676861255062093], "nose": [[108.5109515671687, 29.305873344521444], [115.4428280483597, 28.346462102667882]], "arm_r": [[95.1324142218102, 51.9839174172574], [125.09130026444741, 53.53014308133817], [136.32949806692477, 26.137207627984708]], "arm_l": [[101.04126073988344, 50.133273014609365], [131.00014678252066, 51.67949867869013], [142.23834458499803, 24.286563225336668]], "leg_r": [[120.0, 131.2155372436685], [161.917285943973, 112.5576236573147], [164.0, 151.89650005692488], [174.98507904390829, 151.3324549957621]], "leg_l": [[125.90884651807325, 130.7540782623559], [167.3506724370636, 111.09303668974187], [168.0, 150.48015620783858], [178.99855056415495, 150.3043039328269]]}}}, {"name": "Side Plank", "camera": 0.0, "frames": [{"order": ["arm_r", "leg_r", "spine", "leg_l", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[174.0, 150.0], [132.6290415411608, 136.76197856352545], [93.16019611491195, 124.13260179079685]], "head": [74.13906578900888, 118.0461551533373], "nose": [[64.17597771435469, 117.18774032548103], [57.20261482771247, 116.58691876723901]], "arm_r": [[99.0690426329852, 124.19208734248504], [127.25982125656246, 134.296810007489], [156.2375960452345, 141.94342007481436]], "arm_l": [[93.16019611491195, 126.0427317451331], [89.95929250465917, 155.4183141839495], [60.0, 153.87974546036065]], "leg_r": [[179.90884651807326, 150.7540782623559], [225.90184049526727, 151.5446924600911], [265.8957483015229, 152.23218306681738], [265.70377183071275, 163.0634184490528]], "leg_l": [[174.0, 151.2155372436685], [219.992993977194, 152.00615144140374], [259.98690178344964, 152.69364204813002], [259.7949253126395, 163.52487743036542]]}, {"order": ["leg_r", "arm_r", "spine", "leg_l", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[174.0, 137.0], [131.16086274396895, 129.56106188266668], [90.29134099396232, 122.46414390865907]], "head": [70.59518593371817, 119.04394247540239], "nose": [[60.59518593371817, 119.04394247540239], [53.59518593371817, 119.04394247540239]], "arm_r": [[96.20018751203557, 122.52362946034725], [111.20018751203557, 96.93757350177395], [126.20018751203557, 71.35151754320066]], "arm_l": [[90.29134099396232, 124.37427386299531], [89.99905209869154, 153.91710417204732], [60.0, 153.68224624951935]], "leg_r": [[179.90884651807326, 137.7540782623559], [225.21000315663483, 145.62054155884627], [264.60231327712313, 152.46094442535966], [262.6921833227869, 163.12925383968215]], "leg_l": [[174.0, 138.2155372436685], [219.30115663856157, 146.0820005401589], [258.6934667590499, 152.9224034066723], [256.78333680471366, 163.59071282099478]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_r", "arm_r", "spine", "leg_l", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[174.0, 143.5], [131.79213590699416, 133.13627470916663], [91.52486326654031, 123.24904253515317]], "head": [72.11894874102038, 118.48411136695391], "nose": [[62.128179926165075, 118.05453193528285], [55.13484237509207, 117.75383495926968]], "arm_r": [[97.43370978461355, 123.30852808684135], [125.6244884081908, 113.20380542183739], [153.34087438352944, 101.89771708756348]], "arm_l": [[91.52486326654031, 125.1591724894894], [89.98658163375015, 154.66454016997503], [59.999999999999986, 153.78099585494002]], "leg_r": [[179.90884651807326, 144.2540782623559], [225.6970716429635, 148.59600171041538], [265.5129195776506, 152.3715873174236], [264.45861629992817, 163.15460014560324]], "leg_l": [[174.0, 144.7155372436685], [219.78822512489023, 149.05746069172798], [259.6040730595774, 152.83304629873624], [258.54976978185493, 163.61605912691584]]}}}, {"name": "Standing Calf Raise", "camera": 0.0, "frames": [{"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [161.5181281065588, 21.186959188910578], [162.9664572197126, -19.657665952703468]], "head": [164.36158669459508, -39.30584216914616], "nose": [[174.36158669459508, -39.30584216914616], [181.36158669459508, -39.30584216914616]], "arm_r": [[162.9664572197126, -17.74753599836724], [164.0134421207876, 11.778699043763396], [168.18863514958957, 41.035409193477136]], "arm_l": [[168.87530373778586, -19.59818040101528], [169.92228863886086, 9.928054641115358], [174.09748166766283, 39.1847647908291]], "leg_r": [[160.0, 65.2155372436685], [168.06607636623892, 109.81480911859667], [158.0, 147.939388496694], [168.64599592855438, 150.6655047598624]], "leg_l": [[165.90884651807326, 64.7540782623559], [172.71840731660157, 109.55611956268012], [162.00000000000003, 147.5078524006199], [172.5977298599761, 150.4106343225862]]}, {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 55.0], [161.5181281065588, 12.186959188910578], [162.9664572197126, -28.657665952703468]], "head": [164.36158669459508, -48.30584216914616], "nose": [[174.36158669459508, -48.30584216914616], [181.36158669459508, -48.30584216914616]], "arm_r": [[162.9664572197126, -26.74753599836724], [164.0134421207876, 2.778699043763396], [168.18863514958957, 32.035409193477136]], "arm_l": [[168.87530373778586, -28.59818040101528], [169.92228863886086, 0.9280546411153574], [174.09748166766283, 30.184764790829092]], "leg_r": [[160.0, 56.215537243668514], [168.06607636623892, 100.81480911859667], [158.0, 138.939388496694], [163.06640975574908, 148.5548393941648]], "leg_l": [[165.90884651807326, 55.754078262355904], [172.71840731660157, 100.55611956268012], [162.00000000000003, 138.5078524006199], [166.90079995019303, 148.20619194420698]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 59.5], [161.5181281065588, 16.686959188910578], [162.9664572197126, -24.157665952703468]], "head": [164.36158669459508, -43.80584216914616], "nose": [[174.36158669459508, -43.80584216914616], [181.36158669459508, -43.80584216914616]], "arm_r": [[162.9664572197126, -22.24753599836724], [164.0134421207876, 7.278699043763396], [168.18863514958957, 36.535409193477136]], "arm_l": [[168.87530373778586, -24.09818040101528], [169.92228863886086, 5.428054641115358], [174.09748166766283, 34.6847647908291]], "leg_r": [[160.0, 60.715537243668514], [168.06607636623892, 105.31480911859667], [158.0, 143.439388496694], [166.5996846423216, 150.19415207052123]], "leg_l": [[165.90884651807326, 60.254078262355904], [172.71840731660157, 105.05611956268012], [162.00000000000003, 143.0078524006199], [170.4826264968593, 149.9046747536479]]}}}, {"name": "Standing Calf Stretch", "camera": 0.0, "frames": [{"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 66.0], [170.59618644035737, 24.374309027589845], [186.21725231980906, -13.573789038579548]], "head": [194.98467525559062, -31.27657594171221], "nose": [[204.60832220826177, -28.5589631417822], [211.33715407983, -26.658814506583642]], "arm_r": [[186.21725231980906, -11.66365908424332], [188.3099465321328, 17.808605240420718], [209.89014054229233, 38.33175370810613]], "arm_l": [[192.12609883788232, -13.51430348689136], [194.21879305020607, 15.95796083777268], [215.7989870603656, 36.48110930545809]], "leg_r": [[160.0, 67.2155372436685], [175.16556540504365, 109.98393208966951], [187.22985964654342, 147.54182987483443], [197.31336898935066, 143.21283640822708]], "leg_l": [[165.90884651807326, 66.7540782623559], [157.78606467079257, 111.34336773614788], [142.0, 147.53823689459549], [152.70742489329234, 150.02007985883768]]}, {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 68.0], [177.8449674978852, 28.74769124941882], [202.39643095943165, -4.4202453757258695]], "head": [215.7790430866088, -19.057341090546714], "nose": [[224.86662519788973, -14.884110762524257], [231.2107797071463, -11.970726577436773]], "arm_r": [[202.39643095943165, -2.510115421389642], [198.72420173956954, 26.811941847485613], [195.78470647585493, 56.2140096981627]], "arm_l": [[208.30527747750492, -4.360759824037681], [204.15369854281798, 24.89920922697154], [200.73277082933996, 54.25073071932251]], "leg_r": [[160.0, 69.2155372436685], [179.93274697663705, 110.04274491100989], [196.21451686919335, 146.0240134993241], [205.54108250286276, 140.28036566038193]], "leg_l": [[165.90884651807326, 68.7540782623559], [148.56714690316622, 110.71273056909666], [132.3754437974776, 146.7333951610521], [143.13990766102046, 148.96313513172313]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 67.0], [174.26917504841117, 26.407887228155587], [194.49986089975675, -9.416353188154432]], "head": [205.6837189691717, -25.74520576796695], "nose": [[215.06780908160056, -22.28994126842204], [221.62451440633933, -19.875732658837062]], "arm_r": [[194.49986089975675, -7.506223233818204], [193.70641989937332, 22.02767447620785], [204.05970368705914, 49.75678161626014]], "arm_l": [[200.40870741783002, -9.356867636466243], [199.373637753329, 20.169774839066125], [209.49937694165087, 47.980254176067106]], "leg_r": [[160.0, 68.2155372436685], [177.60126700830682, 110.06923414430075], [191.8168250129063, 146.88995559085808], [201.54566768152094, 141.8347521274287]], "leg_l": [[165.90884651807326, 67.7540782623559], [151.06952728568317, 110.63332035745131], [137.015040258231, 147.51396171963245], [147.86193480658685, 149.31507754712842]]}}}, {"name": "Standing Forward Fold", "camera": 0.0, "frames": [{"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [163.7912748095231, 21.3238785947173], [167.40823813355095, -19.390122286184578]], "head": [169.8456250016539, -38.939465193360675], "nose": [[179.8441478685724, -38.76759175239952], [186.84308173466712, -38.64728089622242]], "arm_r": [[167.40823813355095, -17.47999233184835], [167.40823813355095, 12.064240258517891], [170.02291041598068, 41.4960481242301]], "arm_l": [[173.3170846516242, -19.33063673449639], [173.3170846516242, 10.213595855869853], [175.93175693405394, 39.64540372158206]], "leg_r": [[160.0, 65.2155372436685], [168.06607636623892, 109.81480911859668], [158.0, 147.939388496694], [168.64599592855438, 150.6655047598624]], "leg_l": [[165.90884651807326, 64.7540782623559], [172.71840731660157, 109.55611956268012], [162.0, 147.50785240061987], [172.59772985997608, 150.4106343225862]]}, {"order": ["arm_l", "leg_l", "spine", "arm_r", "leg_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[147.0, 66.0], [176.1289416829083, 98.8575650078507], [187.3972636865633, 139.0385228514936]], "head": [189.48783295191635, 158.62678031347502], "nose": [[184.73694397530744, 167.42615835581051], [181.4506430358062, 173.51289380046708]], "arm_r": [[187.3972636865633, 140.94865280582985], [182.4192496006142, 170.08331055850218], [210.68489359821646, 160.183500587288]], "arm_l": [[193.30611020463655, 139.09800840318178], [186.02056319437705, 167.75778889244543], [215.38942014795313, 161.72949102185854]], "leg_r": [[147.0, 67.2155372436685], [141.31274251864636, 112.16912679040732], [116.21554710763508, 142.84285757112133], [125.8492956904415, 148.07170342364373]], "leg_l": [[152.90884651807326, 66.7540782623559], [145.81726113426228, 111.51366557248716], [119.2597840340148, 140.9707851128171], [128.62869772062507, 146.64719652698653]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[153.5, 65.0], [195.0328826650446, 51.61662929027126], [236.57347679163044, 47.926177033458714]], "head": [256.5727152529139, 48.09805622977341], "nose": [[261.7058802321052, 56.68005001306202], [265.2586487728301, 62.61982355503152]], "arm_r": [[236.57347679163044, 49.83630698779495], [234.07579830025136, 79.27796781424398], [259.31796266290564, 95.24379849787402]], "arm_l": [[242.4823233097037, 47.9856625851469], [238.81197686747683, 77.30794847930343], [262.8923626362581, 94.92844135448324]], "leg_r": [[153.5, 66.2155372436685], [166.9043301898599, 109.55070334848604], [158.0, 147.95458074368176], [168.8965972088972, 149.4364364079613]], "leg_l": [[159.40884651807326, 65.7540782623559], [171.89384470090084, 109.35476432290021], [162.0, 147.52304464760766], [172.85477539736297, 149.27750973779763]]}}}, {"name": "Standing Quad Stretch", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [161.5181281065588, 21.186959188910578], [162.9664572197126, -19.657665952703468]], "head": [164.36158669459508, -39.30584216914616], "nose": [[174.35567849015837, -39.649541429917065], [181.35141427929204, -39.89012649441899]], "arm_r": [[162.9664572197126, -17.74753599836724], [147.06887929271645, 7.307394202295484], [151.2440723215184, 36.564104352009224]], "arm_l": [[168.87530373778586, -19.59818040101528], [195.24990093602662, -33.34960627592398], [218.31387531576726, -52.3214550379599]], "leg_r": [[160.0, 65.2155372436685], [153.598037355837, 110.07582613989624], [122.07760721156811, 85.82349837463056], [111.87858481133344, 89.88156862741086]], "leg_l": [[165.90884651807326, 64.7540782623559], [172.71840731660157, 109.55611956268012], [162.00000000000003, 147.5078524006199], [172.5977298599761, 150.4106343225862]]}, {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [161.5181281065588, 21.186959188910578], [162.9664572197126, -19.657665952703468]], "head": [164.36158669459508, -39.30584216914616], "nose": [[174.35567849015837, -39.649541429917065], [181.35141427929204, -39.89012649441899]], "arm_r": [[162.9664572197126, -17.74753599836724], [150.76798165630018, 9.244052078465458], [156.0, 38.3355128699944]], "arm_l": [[168.87530373778586, -19.59818040101528], [194.08432618194038, -35.19178059954181], [216.3056442657986, -54.9914509633138]], "leg_r": [[160.0, 65.2155372436685], [116.01483426203507, 51.955138515520545], [154.0, 39.6105356653511], [159.45624794549292, 30.204236892595254]], "leg_l": [[165.90884651807326, 64.7540782623559], [172.71840731660157, 109.55611956268012], [162.00000000000003, 147.5078524006199], [172.5977298599761, 150.4106343225862]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [161.5181281065588, 21.186959188910578], [162.9664572197126, -19.657665952703468]], "head": [164.36158669459508, -39.30584216914616], "nose": [[174.35567849015837, -39.649541429917065], [181.35141427929204, -39.89012649441899]], "arm_r": [[162.9664572197126, -17.74753599836724], [148.88406149406435, 8.339389938618353], [153.5884153863569, 37.518116152632096]], "arm_l": [[168.87530373778586, -19.59818040101528], [194.68483483546345, -34.28473592696265], [217.33279023539066, -53.6789032423792]], "leg_r": [[160.0, 65.2155372436685], [121.19953747183251, 89.54894130397363], [128.05510569102682, 50.73950342195268], [120.81943634843438, 42.5801415318936]], "leg_l": [[165.90884651807326, 64.7540782623559], [172.71840731660157, 109.55611956268012], [162.00000000000003, 147.5078524006199], [172.5977298599761, 150.4106343225862]]}}}, {"name": "Standing Side Bend", "camera": 90.0, "frames": [{"order": ["leg_l", "leg_r", "spine", "arm_r", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 72.0], [153.03719012291472, 28.66760595178478], [126.51599455364676, -4.1603828869694155]], "head": [116.21523305544568, -21.043282954169158], "arm_r": [[117.08715424592353, 1.4189654949775277], [128.77181618709193, 28.91334251867918], [139.84864615181866, 56.420863887144534]], "arm_l": [[135.94483486137, -9.739731268916358], [147.93654155006897, -37.233957687641194], [152.7812111713494, -55.72796157898259]], "leg_r": [[153.0, 72.0], [138.36855694632547, 115.55868611102298], [155.0, 149.09377549913083], [146.55707891051017, 154.93895289329112]], "leg_l": [[167.0, 72.0], [181.63144305367453, 115.55868611102298], [165.0, 149.09377549913083], [173.44292108948983, 154.93895289329112]]}, {"order": ["leg_l", "leg_r", "spine", "arm_l", "arm_r", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 72.0], [160.0, 29.160862743968956], [160.0, -11.708659006037678]], "head": [160.0, -31.40481406628183], "arm_r": [[149.0, -11.708659006037678], [144.82480697119803, 17.8366392933887], [140.77363501897415, 47.26294768420651]], "arm_l": [[171.0, -11.708659006037678], [175.17519302880197, 17.8366392933887], [179.22636498102585, 47.26294768420651]], "leg_r": [[153.0, 72.0], [138.36855694632547, 115.55868611102298], [155.0, 149.09377549913083], [146.55707891051017, 154.93895289329112]], "leg_l": [[167.0, 72.0], [181.63144305367453, 115.55868611102298], [165.0, 149.09377549913083], [173.44292108948983, 154.93895289329112]]}, {"order": ["leg_l", "leg_r", "spine", "arm_l", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 72.0], [166.96280987708528, 28.66760595178478], [193.48400544635325, -4.1603828869694155]], "head": [203.78476694455432, -21.043282954169158], "arm_r": [[184.05516513863, -9.739731268916358], [172.06345844993103, -37.233957687641194], [167.2187888286506, -55.72796157898259]], "arm_l": [[202.91284575407647, 1.4189654949775277], [191.22818381290807, 28.91334251867918], [180.15135384818134, 56.420863887144534]], "leg_r": [[153.0, 72.0], [138.36855694632547, 115.55868611102298], [155.0, 149.09377549913083], [146.55707891051017, 154.93895289329112]], "leg_l": [[167.0, 72.0], [181.63144305367453, 115.55868611102298], [165.0, 149.09377549913083], [173.44292108948983, 154.93895289329112]]}, {"order": ["leg_l", "leg_r", "spine", "arm_l", "arm_r", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 72.0], [160.0, 29.160862743968956], [160.0, -11.708659006037678]], "head": [160.0, -31.40481406628183], "arm_r": [[149.0, -11.708659006037678], [144.82480697119803, 17.8366392933887], [140.77363501897415, 47.26294768420651]], "arm_l": [[171.0, -11.708659006037678], [175.17519302880197, 17.8366392933887], [179.22636498102585, 47.26294768420651]], "leg_r": [[153.0, 72.0], [138.36855694632547, 115.55868611102298], [155.0, 149.09377549913083], [146.55707891051017, 154.93895289329112]], "leg_l": [[167.0, 72.0], [181.63144305367453, 115.55868611102298], [165.0, 149.09377549913083], [173.44292108948983, 154.93895289329112]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "leg_r", "spine", "arm_r", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 72.0], [156.63534088571913, 29.02687874664438], [142.7915448634304, -9.792229018314188]], "head": [137.44677734186524, -28.772043845484575], "arm_r": [[132.19160987813555, -6.897266347007331], [136.08805632571406, 22.68539136798885], [139.7334672760993, 52.166082466652526]], "arm_l": [[153.39147984872525, -12.687191689621045], [183.09625879428003, -8.649429316315874], [207.62273713211505, -4.858687263051138]], "leg_r": [[153.0, 72.0], [138.36855694632547, 115.55868611102298], [155.0, 149.09377549913083], [146.55707891051017, 154.93895289329112]], "leg_l": [[167.0, 72.0], [181.63144305367453, 115.55868611102298], [165.0, 149.09377549913083], [173.44292108948983, 154.93895289329112]]}}}, {"name": "Torso Twist", "camera": 90.0, "frames": [{"order": ["leg_l", "leg_r", "spine", "arm_r", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 64.0], [160.0, 21.160862743968956], [160.0, -19.708659006037678]], "head": [160.0, -39.40481406628183], "nose": [[150.68351813315385, -35.77119801271661], [147.83636163162325, -34.660749485167834]], "arm_r": [[150.6714709422793, -20.720873666029334], [140.45270482677998, 4.751067330158591], [126.41597666385152, 22.521983399674568]], "arm_l": [[169.3285290577207, -18.69644434604602], [159.10976294222135, 6.775496650141903], [145.0730347792929, 24.54641271965788]], "leg_r": [[153.0, 64.0], [152.38216056651342, 109.45365967810031], [151.8451597222945, 148.77302494282907], [151.8406455123998, 150.78833858281408]], "leg_l": [[167.0, 64.0], [167.61783943348658, 109.45365967810031], [168.1548402777055, 148.77302494282907], [168.1593544876002, 150.78833858281408]]}, {"order": ["leg_l", "leg_r", "spine", "arm_l", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 64.0], [160.0, 21.160862743968956], [160.0, -19.708659006037678]], "head": [160.0, -39.40481406628183], "nose": [[169.31648186684615, -35.77119801271661], [172.16363836837675, -34.660749485167834]], "arm_r": [[150.6714709422793, -18.69644434604602], [160.89023705777865, 6.775496650141903], [174.92696522070713, 24.54641271965788]], "arm_l": [[169.3285290577207, -20.720873666029334], [179.54729517322002, 4.751067330158591], [193.5840233361485, 22.521983399674568]], "leg_r": [[153.0, 64.0], [152.38216056651342, 109.45365967810031], [151.8451597222945, 148.77302494282907], [151.8406455123998, 150.78833858281408]], "leg_l": [[167.0, 64.0], [167.61783943348658, 109.45365967810031], [168.1548402777055, 148.77302494282907], [168.1593544876002, 150.78833858281408]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "leg_r", "spine", "arm_l", "arm_r", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 64.0], [160.0, 21.160862743968956], [160.0, -19.708659006037678]], "head": [160.0, -39.40481406628183], "arm_r": [[149.0, -19.708659006037678], [149.0, 6.272103107495482], [149.0, 24.741947367265226]], "arm_l": [[171.0, -19.708659006037678], [171.0, 6.272103107495482], [171.0, 24.741947367265226]], "leg_r": [[153.0, 64.0], [152.30227750929063, 109.43468725833473], [151.6958455907651, 148.7345183160037], [151.69074752238387, 150.77701284803751]], "leg_l": [[167.0, 64.0], [167.69772249070937, 109.43468725833473], [168.3041544092349, 148.73451831600366], [168.30925247761613, 150.7770128480375]]}}}, {"name": "Tricep Press", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[118.0, 124.0], [109.72232766064437, 81.92461008445406], [104.68785811999925, 41.33721444350374]], "head": [106.08298759488176, 21.689038227061047], "nose": [[116.02984525543485, 20.659464198301396], [122.99149852301275, 19.93888111104634]], "arm_r": [[104.68785811999925, 43.24734439783997], [94.67819740520659, 71.09854849438184], [124.0, 64.85204926757933]], "arm_l": [[110.5967046380725, 41.39669999519193], [98.50612246687119, 68.43532854979176], [128.0, 63.03132775016979]], "leg_r": [[118.0, 125.2155372436685], [162.26738385552633, 112.89958304064265], [156.0, 151.8053465749981], [166.8641356033426, 153.50269283330525]], "leg_l": [[123.90884651807325, 124.7540782623559], [167.98485944606693, 111.78953829173301], [160.00000000000003, 150.38900272591184], [170.7786039325257, 152.55147938544167]]}, {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[118.0, 124.0], [109.72232766064437, 81.92461008445406], [104.68785811999925, 41.33721444350374]], "head": [106.08298759488176, 21.689038227061047], "nose": [[116.02984525543485, 20.659464198301396], [122.99149852301275, 19.93888111104634]], "arm_r": [[104.68785811999925, 43.24734439783997], [109.6655943384287, 72.38204819466002], [124.0, 98.3355128699944]], "arm_l": [[110.5967046380725, 41.39669999519193], [113.17159954067507, 70.83190879489234], [128.0, 96.51479135258487]], "leg_r": [[118.0, 125.2155372436685], [162.26738385552633, 112.89958304064265], [156.0, 151.8053465749981], [166.8641356033426, 153.50269283330525]], "leg_l": [[123.90884651807325, 124.7540782623559], [167.98485944606693, 111.78953829173301], [160.00000000000003, 150.38900272591184], [170.7786039325257, 152.55147938544167]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[118.0, 124.0], [109.72232766064437, 81.92461008445406], [104.68785811999925, 41.33721444350374]], "head": [106.08298759488176, 21.689038227061047], "nose": [[116.02984525543485, 20.659464198301396], [122.99149852301275, 19.93888111104634]], "arm_r": [[104.68785811999925, 43.24734439783997], [95.81620416576652, 71.47018477789757], [124.0, 81.59378106878685]], "arm_l": [[110.5967046380725, 41.39669999519193], [100.04454561797358, 69.05300441583627], [128.0, 79.77305955137733]], "leg_r": [[118.0, 125.2155372436685], [162.26738385552633, 112.89958304064265], [156.0, 151.8053465749981], [166.8641356033426, 153.50269283330525]], "leg_l": [[123.90884651807325, 124.7540782623559], [167.98485944606693, 111.78953829173301], [160.0, 150.38900272591184], [170.7786039325257, 152.5514793854417]]}}}, {"name": "Triceps Pushdown", "camera": 0.0, "frames": [{"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 66.0], [155.33005328091872, 23.464845696367505], [163.2776668032939, -16.66816291776044]], "head": [168.79041391963386, -35.60132233443484], "nose": [[178.69598771165374, -34.23033442389419], [185.62786419284475, -33.270923182040626]], "arm_r": [[163.2776668032939, -14.758032963424213], [147.83339766715355, 10.57038294758685], [172.0, -6.935259274591587]], "arm_l": [[169.18651332136716, -16.608677366072254], [150.1367514394947, 6.21479360962754], [176.0, -8.75598079200112]], "leg_r": [[150.0, 67.2155372436685], [160.60371763953367, 111.29667164972889], [146.0, 147.96977299066958], [156.24068183654285, 151.9247829380784]], "leg_l": [[155.90884651807326, 66.7540782623559], [165.23395514054224, 111.11463772661938], [150.0, 147.53823689459549], [160.17101027135118, 151.6639291057432]]}, {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 66.0], [155.33005328091872, 23.464845696367505], [163.2776668032939, -16.66816291776044]], "head": [168.79041391963386, -35.60132233443484], "nose": [[178.69598771165374, -34.23033442389419], [185.62786419284475, -33.270923182040626]], "arm_r": [[163.2776668032939, -14.758032963424213], [158.24284519048118, 14.367155954155876], [168.0, 42.30512837601883]], "arm_l": [[169.18651332136716, -16.608677366072254], [163.00090098657685, 12.300726187737686], [172.0, 40.4844068586093]], "leg_r": [[150.0, 67.2155372436685], [160.60371763953367, 111.29667164972889], [146.0, 147.96977299066958], [156.24068183654285, 151.9247829380784]], "leg_l": [[155.90884651807326, 66.7540782623559], [165.23395514054224, 111.11463772661938], [150.0, 147.53823689459549], [160.17101027135118, 151.6639291057432]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 66.0], [155.33005328091872, 23.464845696367505], [163.2776668032939, -16.66816291776044]], "head": [168.79041391963386, -35.60132233443484], "nose": [[178.69598771165374, -34.23033442389419], [185.62786419284475, -33.270923182040626]], "arm_r": [[163.2776668032939, -14.758032963424213], [142.29329874083328, 6.3558478668007865], [170.0, 17.68493455071362]], "arm_l": [[169.18651332136716, -16.608677366072254], [146.90717119470105, 3.1766695627598747], [174.0, 15.864213033304088]], "leg_r": [[150.0, 67.2155372436685], [160.60371763953367, 111.29667164972889], [146.0, 147.96977299066958], [156.24068183654285, 151.9247829380784]], "leg_l": [[155.90884651807326, 66.7540782623559], [165.23395514054224, 111.11463772661938], [150.0, 147.53823689459549], [160.17101027135118, 151.6639291057432]]}}}]}
\ No newline at end of file
+{"profile": "neutral", "pitch": 10.0, "exercises": [{"name": "Abdominal", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[128.0, 126.0], [123.50070744350393, 83.31559451522868], [125.00047162900262, 42.39449550813546]], "head": [127.78393364820391, 22.89002207499297], "nose": [[137.6895074402238, 24.26100998553362], [144.6213839214148, 25.22042122738718]], "arm_r": [[125.00047162900262, 44.30462546247169], [153.5959458166933, 35.37057588667207], [137.46787065175624, 10.458926161567888]], "arm_l": [[130.90931814707588, 42.453981059823654], [159.51563713035878, 33.55366614061362], [143.51615744049056, 8.561724009237087]], "leg_r": [[128.0, 127.2155372436685], [171.17430886688956, 111.58284855334222], [168.0, 150.8509233159615], [178.9653082306577, 151.71059641116776]], "leg_l": [[133.90884651807326, 126.7540782623559], [176.35384130990658, 109.29150625391821], [172.0, 148.449771713863], [182.93464482641144, 149.6288883001427]]}, {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[128.0, 126.0], [123.11337415401846, 76.7383796324037], [167.64073825067507, 57.70967534760443]], "head": [186.18441534201082, 50.33136579709479], "nose": [[193.66654290527876, 56.965954490223126], [198.8685566836205, 61.57870955266287]], "arm_r": [[167.64073825067507, 59.61980530194066], [195.90006093783734, 49.70250854036303], [172.72817683159388, 30.93775236749825]], "arm_l": [[173.54958476874833, 57.76916089929262], [201.81359837899106, 47.864837419955926], [178.71564429723324, 29.01188919273017]], "leg_r": [[128.0, 127.2155372436685], [171.1743088668896, 111.58284855334223], [168.0, 150.8509233159615], [178.9653082306577, 151.71059641116778]], "leg_l": [[133.90884651807326, 126.7540782623559], [176.35384130990658, 109.29150625391821], [172.0, 148.449771713863], [182.93464482641144, 149.62888830014273]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[128.0, 126.0], [122.50068568111516, 81.31026966131319], [148.00041311155596, 46.22466606468976]], "head": [160.31364261806914, 30.70388407251191], "nose": [[169.4012247293501, 34.87711440053437], [175.74537923860663, 37.79049858562185]], "arm_r": [[148.00041311155596, 48.134796019025984], [177.4772385118675, 42.64130176976346], [162.0341655680046, 17.312178498974532]], "arm_l": [[153.90925962962922, 46.28415161637795], [183.38904849856343, 40.8061016600034], [168.05011894867513, 15.415678998609911]], "leg_r": [[128.0, 127.2155372436685], [171.17430886688956, 111.58284855334223], [168.0, 150.8509233159615], [178.96530823065774, 151.71059641116776]], "leg_l": [[133.90884651807326, 126.7540782623559], [176.35384130990658, 109.29150625391821], [172.0, 148.449771713863], [182.93464482641144, 149.62888830014273]]}}}, {"name": "Abductor", "camera": 90.0, "frames": [{"order": ["spine", "arm_l", "arm_r", "leg_l", "leg_r", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 116.0], [160.0, 73.16086274396895], [160.0, 32.29134099396232]], "head": [160.0, 12.59518593371817], "arm_r": [[149.0, 32.29134099396232], [139.72949016875157, 60.82303648281693], [131.3275549188913, 86.68153428172431]], "arm_l": [[171.0, 32.29134099396232], [180.27050983124843, 60.82303648281693], [188.6724450811087, 86.68153428172431]], "leg_r": [[153.0, 116.0], [147.39401020336322, 133.10345930302373], [145.10544255919902, 172.83189963719428], [143.92179588176742, 171.6549669223465]], "leg_l": [[167.0, 116.0], [172.60598979663678, 133.10345930302373], [174.89455744080098, 172.83189963719428], [176.07820411823258, 171.6549669223465]]}, {"order": ["spine", "arm_l", "arm_r", "leg_l", "leg_r", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 116.0], [160.0, 73.16086274396895], [160.0, 32.29134099396232]], "head": [160.0, 12.59518593371817], "arm_r": [[149.0, 32.29134099396232], [139.72949016875157, 60.82303648281693], [131.3275549188913, 86.68153428172431]], "arm_l": [[171.0, 32.29134099396232], [180.27050983124843, 60.82303648281693], [188.6724450811087, 86.68153428172431]], "leg_r": [[153.0, 116.0], [130.0, 130.9232660108731], [120.61056874428219, 169.76167264761645], [115.7543569835581, 168.1244147020733]], "leg_l": [[167.0, 116.0], [190.0, 130.9232660108731], [199.38943125571782, 169.76167264761645], [204.24564301644193, 168.1244147020733]]}], "tween": {"t": 0.5, "sample": {"order": ["spine", "arm_l", "arm_r", "leg_l", "leg_r", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 116.0], [160.0, 73.16086274396895], [160.0, 32.29134099396232]], "head": [160.0, 12.59518593371817], "arm_r": [[149.0, 32.29134099396232], [139.72949016875157, 60.82303648281693], [131.3275549188913, 86.68153428172431]], "arm_l": [[171.0, 32.29134099396232], [180.27050983124843, 60.82303648281693], [188.6724450811087, 86.68153428172431]], "leg_r": [[153.0, 116.0], [138.40398580536578, 132.34142152089595], [132.44536528849622, 171.75877049117304], [129.36356808016237, 170.42094139199114]], "leg_l": [[167.0, 116.0], [181.59601419463425, 132.34142152089595], [187.5546347115038, 171.75877049117304], [190.63643191983766, 170.42094139199114]]}}, "orbit": [{"yaw": 127.0, "sample": {"order": ["spine", "arm_r", "leg_r", "arm_l", "leg_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 116.0], [160.0, 73.16086274396895], [160.0, 32.29134099396232]], "head": [160.0, 12.59518593371817], "nose": [[150.25538327938614, 14.840724837485546], [146.0426781173218, 15.811496044064228]], "arm_r": [[154.77104199355594, 32.327140292628755], [144.38560499931648, 59.71678856368911], [127.45898099051918, 84.10364976890271]], "arm_l": [[168.78499061052023, 33.440885896654606], [173.2070703112358, 62.76814640805501], [169.70061398975147, 88.91108067494635]], "leg_r": [[157.9655840337451, 116.45381562691817], [126.61174048168456, 131.4098395903588], [118.23109913779213, 170.5183789577097], [110.96489412668988, 168.85049407976638]], "leg_l": [[165.59044857033106, 116.73152857444055], [143.19089005938437, 132.85925017338457], [138.46571149104156, 172.44611886252008], [133.09011101583206, 171.0256258036004]], "props": {"bg": [{"kind": "poly", "pts": [[170.17558907270748, 64.59122072790889], [164.32336047718314, 120.25119531754564], [171.43542568533545, 120.66441958783159], [177.28765428085978, 65.00444499819484]], "w": 16, "color": "equipment"}, {"kind": "poly", "pts": [[131.35448580972135, 127.88309544322196], [171.286261312086, 133.1082995463687], [185.5103917283906, 133.9347480869406], [145.57861622602596, 128.70954398379385]], "w": 10, "color": "equipment"}, {"kind": "line", "pts": [[157.9099183587413, 135.8785623534417], [156.44686120986023, 149.79355600085088]], "w": 6, "color": "equipment"}], "fg": [{"kind": "line", "pts": [[125.25318741486642, 144.3309051200959], [127.97029354850271, 118.48877406062171]], "w": 6, "color": "prop"}, {"kind": "line", "pts": [[141.8323369925662, 145.78031570312166], [144.54944312620253, 119.93818464364746]], "w": 6, "color": "prop"}]}}}, {"yaw": 270.0, "sample": {"order": ["leg_l", "leg_r", "arm_l", "arm_r", "spine", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 116.0], [160.0, 73.16086274396895], [160.0, 32.29134099396232]], "head": [160.0, 12.59518593371817], "arm_r": [[171.0, 32.29134099396232], [180.27050983124843, 59.10236470334958], [188.67244508110866, 79.06508551601353]], "arm_l": [[149.0, 32.29134099396232], [139.72949016875157, 59.102364703349586], [131.3275549188913, 79.06508551601354]], "leg_r": [[167.0, 116.0], [172.60598979663678, 117.59341061272917], [174.89455744080098, 153.54028741485138], [176.07820411823258, 148.71567825347657]], "leg_l": [[153.0, 116.0], [147.39401020336322, 117.59341061272917], [145.10544255919902, 153.54028741485138], [143.9217958817674, 148.71567825347657]], "props": {"bg": [{"kind": "poly", "pts": [[160.0, 69.87214486573811], [160.0, 122.49493162974898], [160.0, 126.59917334965701], [160.0, 73.97638658564614]], "w": 16, "color": "equipment"}, {"kind": "poly", "pts": [[185.0, 125.9911475918806], [135.0, 125.9911475918806], [135.0, 134.19963103169664], [185.0, 134.19963103169664]], "w": 10, "color": "equipment"}, {"kind": "line", "pts": [[160.0, 134.79385241571816], [160.0, 147.9495491067209]], "w": 6, "color": "equipment"}], "fg": [{"kind": "line", "pts": [[172.60598979663678, 129.80941468294597], [172.60598979663678, 105.37740654251236]], "w": 6, "color": "prop"}, {"kind": "line", "pts": [[147.39401020336322, 129.80941468294597], [147.39401020336322, 105.37740654251236]], "w": 6, "color": "prop"}]}}}]}, {"name": "Adductor", "camera": 90.0, "frames": [{"order": ["spine", "arm_l", "arm_r", "leg_l", "leg_r", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 116.0], [160.0, 73.16086274396895], [160.0, 32.29134099396232]], "head": [160.0, 12.59518593371817], "arm_r": [[149.0, 32.29134099396232], [139.72949016875157, 60.82303648281693], [131.3275549188913, 86.68153428172431]], "arm_l": [[171.0, 32.29134099396232], [180.27050983124843, 60.82303648281693], [188.6724450811087, 86.68153428172431]], "leg_r": [[153.0, 116.0], [130.0, 130.9232660108731], [120.61056874428219, 169.76167264761645], [115.7543569835581, 168.1244147020733]], "leg_l": [[167.0, 116.0], [190.0, 130.9232660108731], [199.38943125571782, 169.76167264761645], [204.24564301644193, 168.1244147020733]]}, {"order": ["spine", "arm_l", "arm_r", "leg_l", "leg_r", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 116.0], [160.0, 73.16086274396895], [160.0, 32.29134099396232]], "head": [160.0, 12.59518593371817], "arm_r": [[149.0, 32.29134099396232], [139.72949016875157, 60.82303648281693], [131.3275549188913, 86.68153428172431]], "arm_l": [[171.0, 32.29134099396232], [180.27050983124843, 60.82303648281693], [188.6724450811087, 86.68153428172431]], "leg_r": [[153.0, 116.0], [147.39401020336322, 133.10345930302373], [145.10544255919902, 172.83189963719428], [143.92179588176742, 171.6549669223465]], "leg_l": [[167.0, 116.0], [172.60598979663678, 133.10345930302373], [174.89455744080098, 172.83189963719428], [176.07820411823258, 171.6549669223465]]}], "tween": {"t": 0.5, "sample": {"order": ["spine", "arm_l", "arm_r", "leg_l", "leg_r", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 116.0], [160.0, 73.16086274396895], [160.0, 32.29134099396232]], "head": [160.0, 12.59518593371817], "arm_r": [[149.0, 32.29134099396232], [139.72949016875157, 60.82303648281693], [131.3275549188913, 86.68153428172431]], "arm_l": [[171.0, 32.29134099396232], [180.27050983124843, 60.82303648281693], [188.6724450811087, 86.68153428172431]], "leg_r": [[153.0, 116.0], [138.40398580536578, 132.34142152089595], [132.44536528849622, 171.75877049117304], [129.36356808016237, 170.42094139199114]], "leg_l": [[167.0, 116.0], [181.59601419463425, 132.34142152089595], [187.5546347115038, 171.75877049117304], [190.63643191983766, 170.42094139199114]]}}}, {"name": "Arm Circles", "camera": 40.0, "frames": [{"order": ["leg_l", "leg_r", "spine", "arm_l", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 66.0], [160.0, 23.160862743968956], [160.0, -17.708659006037678]], "head": [160.534691310332, -37.31490683420151], "nose": [[170.48425726540742, -36.311842875321474], [175.84330179220183, -35.771571626994444]], "arm_r": [[152.92933629344807, -16.245414568883305], [171.3991805532178, -12.140315628510749], [189.7987414793807, -10.625787364292506]], "arm_l": [[171.59710274696485, -17.663090429721073], [198.08553053273266, -15.217403989394604], [224.47316185370062, -15.355973659669438]], "leg_r": [[155.50048673219422, 66.93115555091643], [164.01536833498193, 112.0157431100973], [156.0, 149.83419719709872], [165.63509355919305, 153.02761752718845]], "leg_l": [[169.0259523082187, 66.57765746255455], [178.07794715293392, 111.28365043871992], [164.0, 147.99480835522493], [174.22144670557157, 152.02318799487986]]}, {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 66.0], [160.0, 23.160862743968956], [160.0, -17.708659006037678]], "head": [160.534691310332, -37.31490683420151], "nose": [[170.48425726540742, -36.311842875321474], [175.84330179220183, -35.771571626994444]], "arm_r": [[152.92933629344807, -16.245414568883305], [146.05740343975492, -39.183541265595636], [137.2281578147836, -62.68175177962408]], "arm_l": [[171.59710274696485, -17.663090429721073], [184.0087981838679, -44.59188377321808], [194.38980082196048, -72.06557524262496]], "leg_r": [[155.50048673219422, 66.93115555091643], [164.01536833498193, 112.0157431100973], [156.0, 149.83419719709872], [165.63509355919305, 153.02761752718845]], "leg_l": [[169.0259523082187, 66.57765746255455], [178.07794715293392, 111.28365043871992], [164.0, 147.99480835522493], [174.22144670557157, 152.02318799487986]]}, {"order": ["arm_l", "leg_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 66.0], [160.0, 23.160862743968956], [160.0, -17.708659006037678]], "head": [160.534691310332, -37.31490683420151], "nose": [[170.48425726540742, -36.311842875321474], [175.84330179220183, -35.771571626994444]], "arm_r": [[152.92933629344807, -16.245414568883305], [132.85541810268234, -12.374042228280558], [112.85788723111405, -11.092351163556003]], "arm_l": [[171.59710274696485, -17.663090429721073], [190.0669470067346, -21.76818937009363], [208.46650793289749, -28.432616704957745]], "leg_r": [[155.50048673219422, 66.93115555091643], [164.01536833498193, 112.0157431100973], [156.0, 149.83419719709872], [165.63509355919305, 153.02761752718845]], "leg_l": [[169.0259523082187, 66.57765746255455], [178.07794715293392, 111.28365043871992], [164.0, 147.99480835522493], [174.22144670557157, 152.02318799487986]]}, {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 66.0], [160.0, 23.160862743968956], [160.0, -17.708659006037678]], "head": [160.534691310332, -37.31490683420151], "nose": [[170.48425726540742, -36.311842875321474], [175.84330179220183, -35.771571626994444]], "arm_r": [[152.92933629344807, -16.245414568883305], [148.67105145730744, 12.807960658166763], [146.4124332310594, 41.68142191233785]], "arm_l": [[171.59710274696485, -17.663090429721073], [181.7863665161021, 8.400424721307992], [193.92031961085468, 34.295403219710764]], "leg_r": [[155.50048673219422, 66.93115555091643], [164.01536833498193, 112.0157431100973], [156.0, 149.83419719709872], [165.63509355919305, 153.02761752718845]], "leg_l": [[169.0259523082187, 66.57765746255455], [178.07794715293392, 111.28365043871992], [164.0, 147.99480835522493], [174.22144670557157, 152.02318799487986]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "leg_r", "spine", "arm_l", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 66.0], [160.0, 23.160862743968956], [160.0, -17.708659006037678]], "head": [160.534691310332, -37.31490683420151], "nose": [[170.48425726540742, -36.311842875321474], [175.84330179220183, -35.771571626994444]], "arm_r": [[152.92933629344807, -16.245414568883305], [162.2109378471654, -30.551313123154785], [170.1431632791755, -46.9375812492998]], "arm_l": [[171.59710274696485, -17.663090429721073], [194.69997296127158, -34.82924303104664], [216.40087295386246, -54.06488107406239]], "leg_r": [[155.50048673219422, 66.93115555091643], [164.01536833498193, 112.0157431100973], [156.00000000000003, 149.83419719709875], [165.63509355919308, 153.02761752718848]], "leg_l": [[169.0259523082187, 66.57765746255455], [178.07794715293392, 111.28365043871992], [164.0, 147.99480835522493], [174.22144670557157, 152.02318799487986]]}}}, {"name": "Arm Curl", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[130.0, 124.0], [130.7861315886847, 81.14917968668395], [134.4304502554608, 40.41789305174976]], "head": [137.2139122746621, 20.913419618607264], "nose": [[146.73819953969002, 23.961033782707986], [153.3955951537561, 26.091290105818835]], "arm_r": [[134.4304502554608, 42.328023006085985], [159.00501158413056, 59.27389864999469], [180.9456226327057, 79.4230168259295]], "arm_l": [[140.33929677353407, 40.47737860343795], [164.91385810220382, 57.423254247346655], [186.85446915077895, 77.57237242328145]], "leg_r": [[130.0, 125.2155372436685], [173.8364696761423, 111.48589278988837], [176.0, 150.82053882198593], [186.98389774627694, 150.2346069268626]], "leg_l": [[135.90884651807326, 124.7540782623559], [179.40646070778647, 110.0162218015468], [180.0, 149.4041949728996], [190.99878894026918, 149.24345139631006]]}, {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[130.0, 124.0], [130.7861315886847, 81.14917968668395], [134.4304502554608, 40.41789305174976]], "head": [137.2139122746621, 20.913419618607264], "nose": [[146.73819953969002, 23.961033782707986], [153.3955951537561, 26.091290105818835]], "arm_r": [[134.4304502554608, 42.328023006085985], [159.00501158413056, 59.27389864999469], [178.28863987472675, 36.64170344793001]], "arm_l": [[140.33929677353407, 40.47737860343795], [164.91385810220382, 57.423254247346655], [184.19748639280002, 34.791059045281976]], "leg_r": [[130.0, 125.2155372436685], [173.8364696761423, 111.48589278988837], [176.0, 150.82053882198593], [186.98389774627694, 150.2346069268626]], "leg_l": [[135.90884651807326, 124.7540782623559], [179.40646070778647, 110.0162218015468], [180.0, 149.4041949728996], [190.99878894026918, 149.24345139631006]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[130.0, 124.0], [130.7861315886847, 81.14917968668395], [134.4304502554608, 40.41789305174976]], "head": [137.2139122746621, 20.913419618607264], "nose": [[146.73819953969002, 23.961033782707986], [153.3955951537561, 26.091290105818835]], "arm_r": [[134.4304502554608, 42.328023006085985], [159.00501158413056, 59.27389864999469], [188.94905553678657, 57.47026639867471]], "arm_l": [[140.33929677353407, 40.47737860343795], [164.91385810220382, 57.423254247346655], [194.85790205485984, 55.61962199602667]], "leg_r": [[130.0, 125.2155372436685], [173.8364696761423, 111.48589278988837], [176.0, 150.82053882198593], [186.98389774627694, 150.2346069268626]], "leg_l": [[135.90884651807326, 124.7540782623559], [179.40646070778647, 110.0162218015468], [180.0, 149.4041949728996], [190.99878894026918, 149.24345139631006]]}}}, {"name": "Back Extension", "camera": 0.0, "frames": [{"order": ["arm_l", "leg_l", "spine", "arm_r", "leg_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 102.0], [192.3935695959019, 114.23040065827821], [224.68848379368362, 140.99266081782648]], "head": [239.07527980045666, 154.6747597962834], "nose": [[236.62097246800994, 164.3689010949183], [234.92751919881226, 171.05778437257678]], "arm_r": [[224.68848379368362, 142.90279077216272], [194.76156228588889, 140.84188928718248], [216.3417562960484, 161.3650377548679]], "arm_l": [[230.59733031175688, 141.05214636951465], [200.67040880396215, 138.99124488453444], [222.25060281412166, 159.51439335221986]], "leg_r": [[150.0, 103.2155372436685], [110.70964035040065, 126.77422264541534], [72.00000000000003, 136.6990008460836], [76.92410730224489, 146.3858838484451]], "leg_l": [[155.90884651807326, 102.7540782623559], [113.84739023733808, 121.09462279932025], [76.79945624721068, 135.94696647498034], [82.97447053750626, 144.9119197054579]]}, {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 102.0], [190.45633894911956, 85.80672899774845], [224.97226147206464, 62.800530599496355]], "head": [242.948142398048, 54.16630451840163], "nose": [[252.82867196388915, 55.70745198004789], [259.7424903480551, 56.78585709811745]], "arm_r": [[224.97226147206464, 64.71066055383258], [210.42797286467453, 90.55062860967689], [238.4353856595906, 79.96292255537792]], "arm_l": [[230.8811079901379, 62.86001615118454], [216.3368193827478, 88.69998420702885], [244.34423217766386, 78.11227815272986]], "leg_r": [[150.0, 103.2155372436685], [110.70964035040065, 126.77422264541532], [72.00000000000003, 136.69900084608358], [76.92410730224489, 146.38588384844508]], "leg_l": [[155.90884651807326, 102.7540782623559], [113.84739023733808, 121.09462279932026], [76.79945624721073, 135.94696647498048], [82.97447053750635, 144.91191970545802]]}], "tween": {"t": 0.5, "sample": {"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 102.0], [193.48509363643677, 99.79587312945566], [234.97087262292237, 101.97422073835621]], "head": [254.72463943482512, 105.05537821855494], "nose": [[260.3028000822107, 113.35502422256974], [264.1663589793951, 119.10354478146067]], "arm_r": [[234.97087262292237, 103.88435069269244], [208.60635924306342, 117.98164008833311], [238.0041003816883, 123.87181271381567]], "arm_l": [[240.87971914099563, 102.03370629004439], [214.51520576113668, 116.13099568568506], [243.91294689976155, 122.02116831116763]], "leg_r": [[150.0, 103.2155372436685], [110.70964035040063, 126.77422264541534], [72.0, 136.6990008460836], [76.92410730224486, 146.3858838484451]], "leg_l": [[155.90884651807326, 102.7540782623559], [113.84739023733808, 121.09462279932026], [76.79945624721068, 135.94696647498034], [82.97447053750628, 144.9119197054579]]}}}, {"name": "Barbell Squat", "camera": 0.0, "frames": [{"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [164.54698815214294, 21.395540020190396], [168.88491937775055, -19.25009421342108]], "head": [171.66838139695184, -38.75456764656357], "nose": [[181.66247319251514, -38.41086838579267], [188.6582089816488, -38.17028332129074]], "arm_r": [[168.88491937775055, -17.33996425908485], [192.65203640748376, 0.688316816865914], [172.53258338641072, -21.22686681380081]], "arm_l": [[174.7937658958238, -19.19060866173289], [192.95847995970774, 4.322244384596905], [180.4718928817179, -22.541266754049982]], "leg_r": [[160.0, 65.2155372436685], [169.19599513541698, 109.6022295401473], [160.0, 147.93938849669397], [170.70535714285714, 150.42986750584978]], "leg_l": [[165.90884651807326, 64.7540782623559], [173.95930503181157, 109.35608682232831], [164.0, 147.50785240061987], [174.65358746612156, 150.2050526233538]]}, {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[117.0, 108.0], [143.17988057153133, 73.6944252453616], [163.29538590931725, 37.85749664208181]], "head": [175.05109095516673, 21.92297247450017], "nose": [[184.9566647471866, 23.29396038504082], [191.88854122837762, 24.25337162689438]], "arm_r": [[163.29538590931725, 39.76762659641804], [192.92714924506498, 35.152816875860026], [163.0, 33.095125219384]], "arm_l": [[169.2042324273905, 37.916982193770004], [196.41794186727452, 25.4828719446184], [167.0, 31.274403701974496]], "leg_r": [[117.0, 109.2155372436685], [162.9998620689883, 109.32647413907678], [160.0, 148.60784736415684], [170.47933607337416, 145.31447656703358]], "leg_l": [[122.90884651807325, 108.7540782623559], [168.90764416620357, 109.08161372334803], [164.00000000000003, 148.17631126808274], [174.6275376973515, 145.38121892992268]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[138.5, 86.0], [154.42497830672875, 46.114735055074036], [166.88571296317096, 7.1105000413806465]], "head": [174.3778448314892, -11.151456927521664], "nose": [[184.3409329061434, -10.293042099665406], [191.3142957927856, -9.692220541423389]], "arm_r": [[166.88571296317096, 9.020629995716874], [178.30322698758025, 36.3415537826384], [169.3591627143218, 8.140887535208648]], "arm_l": [[172.79455948124422, 7.169985593068835], [178.3035324959653, 36.21181789545084], [177.20992304137073, 6.687222030755185]], "leg_r": [[138.5, 87.2155372436685], [176.68686825538467, 112.47279577877578], [159.99999999999997, 148.27361793042542], [170.68903086130587, 150.83120352836477]], "leg_l": [[144.40884651807326, 86.7540782623559], [182.10526165656407, 112.71605172140461], [164.0, 147.8420818343513], [174.57848897743455, 150.81215017210735]]}}}, {"name": "Bench Press", "camera": 0.0, "frames": [{"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 116.0], [106.5, 116.0], [65.0, 116.0]], "head": [45.194638625168594, 113.25882502327553], "nose": [[46.60741399901631, 103.35912473775934], [47.58162570573677, 96.53255903615947]], "arm_r": [[65.0, 117.91012995433623], [91.55143264884377, 131.6629772522244], [93.0, 102.15320590614091]], "arm_l": [[70.90884651807325, 116.05948555168817], [97.42740111345603, 129.87371855164292], [96.99999999999999, 100.33248438873136]], "leg_r": [[150.0, 117.2155372436685], [195.30008769797305, 109.34310611741422], [196.0, 148.72938534005917], [206.93968862700535, 147.59655128433957]], "leg_l": [[155.90884651807326, 116.7540782623559], [201.21645940182967, 108.92375949218865], [200.0, 148.29784924398507], [210.98222903324333, 147.68232827813742]]}, {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 116.0], [106.5, 116.0], [65.0, 116.0]], "head": [45.194638625168594, 113.25882502327553], "nose": [[46.60741399901631, 103.35912473775934], [47.58162570573677, 96.53255903615947]], "arm_r": [[65.0, 117.91012995433623], [60.64117536816883, 88.67940591693508], [46.121662397853754, 62.825928192786506]], "arm_l": [[70.90884651807325, 116.05948555168817], [65.41872444969624, 87.01419136030722], [49.604935830325346, 61.907887764618856]], "leg_r": [[150.0, 117.2155372436685], [195.30008769797305, 109.34310611741422], [196.0, 148.72938534005917], [206.93968862700535, 147.59655128433957]], "leg_l": [[155.90884651807326, 116.7540782623559], [201.21645940182967, 108.92375949218865], [200.0, 148.29784924398507], [210.98222903324333, 147.68232827813742]]}], "tween": {"t": 0.5, "sample": {"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 116.0], [106.5, 116.0], [65.0, 116.0]], "head": [45.194638625168594, 113.25882502327553], "nose": [[46.60741399901631, 103.35912473775934], [47.58162570573677, 96.53255903615947]], "arm_r": [[65.0, 117.91012995433623], [93.09853317509568, 107.55922265396269], [80.0, 80.97983921637844]], "arm_l": [[70.90884651807325, 116.05948555168817], [98.69349816980488, 104.91708615451506], [84.0, 79.1591176989689]], "leg_r": [[150.0, 117.2155372436685], [195.30008769797305, 109.34310611741422], [196.0, 148.72938534005917], [206.93968862700535, 147.59655128433957]], "leg_l": [[155.90884651807326, 116.7540782623559], [201.21645940182967, 108.92375949218865], [200.0, 148.29784924398507], [210.98222903324333, 147.68232827813742]]}}}, {"name": "Bird Dog", "camera": 0.0, "frames": [{"order": ["arm_r", "leg_r", "leg_l", "spine", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[190.0, 106.0], [147.03555718411147, 99.29848248056776], [106.04649104941329, 92.90508070915538]], "head": [86.19556801658685, 95.30543819395331], "nose": [[77.1079859053059, 99.47866852197576], [70.76383139604934, 102.39205270706324]], "arm_r": [[111.95533756748654, 92.96456626084355], [115.93122267253811, 122.24819097380495], [111.0, 151.39056507548827]], "arm_l": [[106.04649104941329, 94.81521066349163], [110.07168565309334, 124.09230142764625], [105.0, 153.2112865928978]], "leg_r": [[195.90884651807326, 106.7540782623559], [201.51483631471004, 151.7175669488609], [241.51483631471004, 151.7175669488609], [250.52550880188895, 157.93105468496077]], "leg_l": [[190.0, 107.2155372436685], [195.60598979663678, 152.17902593017354], [235.60598979663678, 152.17902593017354], [244.61666228381569, 158.3925136662734]]}, {"order": ["arm_r", "leg_r", "leg_l", "spine", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[190.0, 106.0], [147.03555718411147, 99.29848248056776], [106.04649104941329, 92.90508070915538]], "head": [86.19556801658685, 95.30543819395331], "nose": [[76.52653951347901, 97.85688637216273], [69.76505872945553, 99.64109538787199]], "arm_r": [[111.95533756748654, 92.96456626084355], [115.93122267253811, 122.24819097380495], [111.0, 151.39056507548827]], "arm_l": [[106.04649104941329, 94.81521066349163], [77.6809337814338, 85.1965493841931], [49.31537651345428, 75.57788810489458]], "leg_r": [[195.90884651807326, 106.7540782623559], [240.3414345273704, 95.02927615912368], [278.9784675789331, 84.83379606935654], [286.04913128548503, 93.13226764344691]], "leg_l": [[190.0, 107.2155372436685], [195.60598979663678, 152.17902593017354], [235.60598979663678, 152.17902593017354], [244.61666228381569, 158.3925136662734]]}, {"order": ["arm_r", "leg_r", "leg_l", "spine", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[190.0, 106.0], [147.03555718411147, 99.29848248056776], [106.04649104941329, 92.90508070915538]], "head": [86.19556801658685, 95.30543819395331], "nose": [[77.1079859053059, 99.47866852197576], [70.76383139604934, 102.39205270706324]], "arm_r": [[111.95533756748654, 92.96456626084355], [115.93122267253811, 122.24819097380495], [111.0, 151.39056507548827]], "arm_l": [[106.04649104941329, 94.81521066349163], [110.07168565309334, 124.09230142764625], [105.0, 153.2112865928978]], "leg_r": [[195.90884651807326, 106.7540782623559], [201.51483631471004, 151.7175669488609], [241.51483631471004, 151.7175669488609], [250.52550880188895, 157.93105468496077]], "leg_l": [[190.0, 107.2155372436685], [195.60598979663678, 152.17902593017354], [235.60598979663678, 152.17902593017354], [244.61666228381569, 158.3925136662734]]}, {"order": ["leg_r", "arm_r", "spine", "arm_l", "leg_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[190.0, 106.0], [147.03555718411147, 99.29848248056776], [106.04649104941329, 92.90508070915538]], "head": [86.19556801658685, 95.30543819395331], "nose": [[76.52653951347901, 97.85688637216273], [69.76505872945553, 99.64109538787199]], "arm_r": [[111.95533756748654, 92.96456626084355], [83.58978029950704, 83.34590498154503], [55.22422303152753, 73.72724370224651]], "arm_l": [[106.04649104941329, 94.81521066349163], [110.07168565309334, 124.09230142764625], [105.0, 153.2112865928978]], "leg_r": [[195.90884651807326, 106.7540782623559], [201.51483631471004, 151.7175669488609], [241.51483631471004, 151.7175669488609], [250.52550880188895, 157.93105468496077]], "leg_l": [[190.0, 107.2155372436685], [234.43258800929715, 95.49073514043629], [273.06962106085984, 85.29525505066914], [280.1402847674118, 93.59372662475951]]}], "tween": {"t": 0.5, "sample": {"order": ["arm_r", "leg_r", "leg_l", "spine", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[190.0, 106.0], [147.03555718411147, 99.29848248056776], [106.04649104941329, 92.90508070915538]], "head": [86.19556801658685, 95.30543819395331], "nose": [[76.78202489248983, 98.67963526151854], [70.20417654698848, 101.0374038833528]], "arm_r": [[111.95533756748654, 92.96456626084355], [115.93122267253811, 122.24819097380495], [111.0, 151.39056507548827]], "arm_l": [[106.04649104941329, 94.81521066349163], [82.84966999647663, 113.55006179251055], [57.03634935368697, 128.60417717799652]], "leg_r": [[195.90884651807326, 106.7540782623559], [234.0445748556052, 132.08616357365625], [273.7023693105576, 126.9444353308775], [281.8124200154689, 134.26302654838682]], "leg_l": [[190.0, 107.2155372436685], [195.60598979663678, 152.17902593017354], [235.60598979663678, 152.17902593017354], [244.61666228381569, 158.3925136662734]]}}, "orbit": [{"yaw": 37.0, "sample": {"order": ["arm_r", "arm_l", "spine", "leg_r", "leg_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[190.0, 106.0], [155.68707029783513, 94.80852282274789], [122.95174655898823, 84.13159632031196]], "head": [107.09809451775733, 84.4574513154624], "nose": [[97.95900332573831, 88.5166399269906], [92.89233625341937, 90.7670340686228]], "arm_r": [[134.2907264664134, 84.17910359422488], [137.46600949517568, 113.87822453047738], [133.5277599609364, 142.5052657333192]], "arm_l": [[116.3317813043157, 85.65709393064989], [119.54644464966532, 115.35483396198678], [115.49601639130758, 143.94380727355005]], "leg_r": [[198.93171981481694, 106.60223367767217], [203.40886233537387, 152.15157118192892], [235.35428273726558, 156.3317344644463], [242.55052575493283, 163.48687425758854]], "leg_l": [[185.78729483793566, 106.97077120657869], [190.2644373584926, 152.52010871083542], [222.2098577603843, 156.7002719933528], [229.40610077805155, 163.85541178649504]], "props": {"bg": [], "fg": []}}}, {"yaw": 180.0, "sample": {"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[190.0, 106.0], [232.96444281588853, 99.29848248056776], [273.9535089505867, 92.90508070915538]], "head": [293.80443198341317, 95.30543819395332], "nose": [[302.8920140946941, 99.47866852197578], [309.23616860395066, 102.39205270706326]], "arm_r": [[273.9535089505867, 94.81521066349163], [269.97762384553516, 124.09883537645301], [274.90884651807323, 153.24120947813634]], "arm_l": [[279.8623554686599, 92.96456626084357], [275.83716086497986, 122.2416570249982], [280.90884651807323, 151.36064219024973]], "leg_r": [[190.0, 107.2155372436685], [184.39401020336322, 152.17902593017354], [144.39401020336322, 152.17902593017354], [135.38333771618431, 158.39251366627337]], "leg_l": [[195.90884651807326, 106.7540782623559], [190.30285672143648, 151.7175669488609], [150.30285672143648, 151.7175669488609], [141.29218423425758, 157.93105468496077]], "props": {"bg": [], "fg": []}}}]}, {"name": "Bodyweight Squat", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [163.03440660786944, 21.26521672923713], [165.92930026825064, -19.504748919881465]], "head": [168.01986953360372, -39.09300638186289], "nose": [[178.01986953360372, -39.09300638186289], [185.01986953360372, -39.09300638186289]], "arm_r": [[165.92930026825064, -17.594618965545237], [166.97628516932568, 11.9316160765854], [170.6323654714801, 41.25563043734954]], "arm_l": [[171.8381467863239, -19.445263368193277], [172.88513168739894, 10.080971673937361], [176.54121198955337, 39.4049860347015]], "leg_r": [[160.0, 65.2155372436685], [160.0, 111.19909733724643], [160.0, 147.939388496694], [160.0, 152.2888332649806]], "leg_l": [[165.90884651807326, 64.7540782623559], [173.95930503181157, 109.35608682232831], [164.0, 147.50785240061987], [174.65358746612154, 150.2050526233538]]}, {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[146.0, 106.0], [169.0442059174294, 69.57790593735669], [185.91158816514124, 32.150800629466346]], "head": [196.21234966334234, 15.267900562266604], "nose": [[206.14003446926995, 16.468348143027285], [213.0878575307592, 17.30847326270656]], "arm_r": [[185.91158816514124, 34.06093058380257], [215.8933129757141, 33.02985173593716], [245.43754556608036, 27.899549586052117]], "arm_l": [[191.8204346832145, 32.21028618115454], [221.80215949378737, 31.179207333289117], [251.34639208415362, 26.048905183404077]], "leg_r": [[146.0, 107.2155372436685], [189.49303604512522, 121.96649178745926], [160.0, 148.57746287018125], [169.92809767204204, 153.24184306651708]], "leg_l": [[151.90884651807326, 106.7540782623559], [194.83033076218078, 123.04799530614652], [164.0, 148.14592677410715], [173.6741219853583, 153.3019727452271]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[153.0, 85.0], [166.42277500746357, 44.23170229834066], [176.44238319738355, 4.551065281965364]], "head": [182.7884763254854, -14.12726447827346], "nose": [[192.77038521149322, -13.526019942799008], [199.7573288004463, -13.105172417491014]], "arm_r": [[176.44238319738355, 6.461195236301592], [198.38299424595868, 26.610313412236394], [222.4987000644752, 44.18389617406784]], "arm_l": [[182.3512297154568, 4.610550833653553], [204.29184076403195, 24.759669009588354], [228.40754658254846, 42.3332517714198]], "leg_r": [[153.0, 86.2155372436685], [178.8862823773017, 124.14007916459117], [160.0, 144.6419652845282], [168.23298591666023, 151.5226164668142]], "leg_l": [[158.90884651807326, 85.7540782623559], [190.7002154063726, 118.4951797068221], [164.0, 147.82688958736352], [173.4411956216626, 153.38593746786563]]}}}, {"name": "Butterfly Stretch", "camera": 52.0, "frames": [{"order": ["leg_l", "spine", "leg_r", "arm_l", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 118.0], [167.50046261033052, 77.00750092244417], [188.06995283948805, 54.81524765398234]], "head": [198.03156476589623, 45.452201386939706], "nose": [[203.41726561244826, 53.87800855251433], [206.71863397655784, 59.04292469083349]], "arm_r": [[179.4018345498141, 55.99124107973272], [179.3792722923378, 85.04096750172937], [179.7792766637896, 113.93409674302181]], "arm_l": [[200.37592029395185, 54.85187061649525], [200.26403873897775, 84.5670097002087], [200.5936451686343, 114.1932218141525]], "leg_r": [[154.48392472475294, 118.74835945275024], [153.99624906356505, 134.04977472288715], [176.83193200832542, 158.80286328447318], [178.74974735954865, 164.86196005792743]], "leg_l": [[169.15392444003692, 118.46425693551305], [213.29985218754484, 127.71028366316729], [191.32252689605835, 158.54310040943946], [200.45869159355496, 163.62289220662225]]}, {"order": ["leg_l", "spine", "leg_r", "arm_l", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 118.0], [169.39539005652387, 77.23415963036267], [194.15610676495888, 64.60624353918908]], "head": [205.57273436097324, 59.76538829462121], "nose": [[209.6047010216403, 68.91652182262033], [212.25797438704146, 74.938510816213]], "arm_r": [[185.48798847528494, 65.78223696493946], [183.36407363225595, 94.71822023026505], [181.67844369976953, 123.52169374631076]], "arm_l": [[206.46207421942268, 64.64286650170199], [205.1884194198, 94.35768290107595], [204.37141029440315, 124.00281258392798]], "leg_r": [[154.48392472475294, 118.74835945275024], [149.23043809783283, 133.36867530000643], [174.0881495814435, 157.71001146004073], [175.23930996841958, 163.86691836751203]], "leg_l": [[169.15392444003692, 118.46425693551305], [214.26107828554112, 126.25220384951403], [193.48786929959434, 156.78423338611003], [202.8873875451986, 161.82209527241005]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "spine", "leg_r", "arm_l", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 118.0], [168.41999929273476, 77.11286671031178], [191.23476868132948, 59.53447897382967]], "head": [202.0041618662562, 52.379193807430994], "nose": [[206.70285021478765, 61.20655625628143], [209.69657099521638, 66.8308191226959]], "arm_r": [[182.56665039165554, 60.71047239958005], [181.08833447545757, 90.58306392738217], [180.06729563247913, 120.42215403788741]], "arm_l": [[203.54073613579325, 59.57110193634258], [202.3592043555531, 89.54289707323235], [201.64032860894966, 119.51249717266958]], "leg_r": [[154.48392472475294, 118.74835945275024], [151.6094041501542, 133.7297564954217], [175.4705656164614, 158.27213348395813], [177.00298826323856, 164.3849879986275]], "leg_l": [[169.15392444003692, 118.46425693551305], [213.84170829120612, 126.99293200911858], [192.42833487469392, 157.67765196031715], [201.7026563745822, 162.74017215214639]]}}}, {"name": "Calfs", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[140.0, 118.0], [130.9947902691037, 76.01258331473494], [128.14293290308055, 35.16126798294198]], "head": [131.61589645641916, 15.764341775082897], "nose": [[141.46845419959308, 17.475220472543253], [148.36210847067855, 18.672290974183092]], "arm_r": [[128.14293290308055, 37.071397937278206], [155.6576522621167, 48.84552396045507], [183.3010064025833, 37.36733835695226]], "arm_l": [[134.0517794211538, 35.22075353463017], [161.85257237368958, 46.324036719323196], [187.80927886043239, 31.51096362388954]], "leg_r": [[140.0, 119.2155372436685], [185.99829621354849, 118.82564110752023], [212.0, 148.75976983403476], [218.68954801152353, 140.16030879539852]], "leg_l": [[145.90884651807326, 118.7540782623559], [191.8707292881921, 116.9102687478058], [216.0, 148.32823373796066], [223.2019010505247, 140.1399483462988]]}, {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[140.0, 118.0], [130.9947902691037, 76.01258331473494], [128.14293290308055, 35.16126798294198]], "head": [131.61589645641916, 15.764341775082897], "nose": [[141.46845419959308, 17.475220472543253], [148.36210847067855, 18.672290974183092]], "arm_r": [[128.14293290308055, 37.071397937278206], [155.6576522621167, 48.84552396045507], [183.3010064025833, 37.36733835695226]], "arm_l": [[134.0517794211538, 35.22075353463017], [161.85257237368958, 46.324036719323196], [187.80927886043239, 31.51096362388954]], "leg_r": [[140.0, 119.2155372436685], [185.1751715569397, 110.6752678976193], [212.0, 139.89650005692488], [222.040000508351, 135.47051611931823]], "leg_l": [[145.90884651807326, 118.7540782623559], [190.80108243714875, 108.87222292080564], [216.0, 139.46496396085078], [226.26606991421917, 135.57431163727887]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[140.0, 118.0], [130.9947902691037, 76.01258331473494], [128.14293290308055, 35.16126798294198]], "head": [131.61589645641916, 15.764341775082897], "nose": [[141.46845419959308, 17.475220472543253], [148.36210847067855, 18.672290974183092]], "arm_r": [[128.14293290308055, 37.071397937278206], [155.6576522621167, 48.84552396045507], [183.3010064025833, 37.36733835695226]], "arm_l": [[134.0517794211538, 35.22075353463017], [161.85257237368958, 46.324036719323196], [187.80927886043239, 31.51096362388954]], "leg_r": [[140.0, 119.2155372436685], [185.76022286869107, 114.59616916585809], [212.0, 144.3281349454798], [220.6686979213759, 137.6594754981849]], "leg_l": [[145.90884651807326, 118.7540782623559], [191.50375298446497, 112.75527283004915], [216.0, 143.8965988494057], [225.03661802939996, 137.7197628895158]]}}}, {"name": "Cardio", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 62.0], [163.03440660786944, 19.26521672923713], [165.92930026825064, -21.504748919881465]], "head": [168.01986953360372, -41.09300638186289], "nose": [[178.01986953360372, -41.09300638186289], [185.01986953360372, -41.09300638186289]], "arm_r": [[165.92930026825064, -19.594618965545237], [150.0317223412545, 5.460311235117487], [173.01305563482384, 24.450977881902162]], "arm_l": [[171.8381467863239, -21.445263368193277], [186.8381467863239, 4.140792590380022], [215.3698422751785, -4.98887736580931]], "leg_r": [[160.0, 63.215537243668514], [201.34452612976168, 83.07425723018636], [185.07506040672968, 119.06092320680713], [194.40358946445036, 124.80147780556837]], "leg_l": [[165.90884651807326, 62.754078262355904], [164.4746241447424, 108.03321059644975], [162.0079625065513, 147.35054957930294], [172.98702739227483, 148.01857614327372]]}, {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [163.03440660786944, 21.26521672923713], [165.92930026825064, -19.504748919881465]], "head": [168.01986953360372, -39.09300638186289], "nose": [[178.01986953360372, -39.09300638186289], [185.01986953360372, -39.09300638186289]], "arm_r": [[165.92930026825064, -17.594618965545237], [165.40572807513215, 11.945113895096782], [192.1359238007832, 25.357914813218557]], "arm_l": [[171.8381467863239, -19.445263368193277], [171.3145745932054, 10.094469492448743], [198.04477031885645, 23.507270410570516]], "leg_r": [[160.0, 65.2155372436685], [168.06607636623892, 109.81480911859667], [158.0, 147.939388496694], [168.64599592855438, 150.6655047598624]], "leg_l": [[165.90884651807326, 64.7540782623559], [172.71840731660157, 109.55611956268012], [162.0, 147.50785240061987], [172.59772985997608, 150.4106343225862]]}, {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 62.0], [163.03440660786944, 19.26521672923713], [165.92930026825064, -21.504748919881465]], "head": [168.01986953360372, -41.09300638186289], "nose": [[178.01986953360372, -41.09300638186289], [185.01986953360372, -41.09300638186289]], "arm_r": [[165.92930026825064, -19.594618965545237], [180.92930026825064, 5.99143699302806], [209.46099575710525, -3.138232963161272]], "arm_l": [[171.8381467863239, -21.445263368193277], [155.94056885932775, 3.6096668324694487], [178.9219021528971, 22.60033347925412]], "leg_r": [[160.0, 63.215537243668514], [159.58457964721987, 108.51484653715457], [158.00131601773398, 147.876286549904], [168.99269577728379, 148.3050693816835]], "leg_l": [[165.90884651807326, 62.754078262355904], [207.25337264783494, 82.61279824887376], [190.98390692480294, 118.59946422549453], [200.31243598252362, 124.34001882425576]]}, {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [163.03440660786944, 21.26521672923713], [165.92930026825064, -19.504748919881465]], "head": [168.01986953360372, -39.09300638186289], "nose": [[178.01986953360372, -39.09300638186289], [185.01986953360372, -39.09300638186289]], "arm_r": [[165.92930026825064, -17.594618965545237], [165.40572807513215, 11.945113895096782], [192.1359238007832, 25.357914813218557]], "arm_l": [[171.8381467863239, -19.445263368193277], [171.3145745932054, 10.094469492448743], [198.04477031885645, 23.507270410570516]], "leg_r": [[160.0, 65.2155372436685], [168.06607636623892, 109.81480911859667], [158.0, 147.939388496694], [168.64599592855438, 150.6655047598624]], "leg_l": [[165.90884651807326, 64.7540782623559], [172.71840731660157, 109.55611956268012], [162.0, 147.50785240061987], [172.59772985997608, 150.4106343225862]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 63.0], [163.03440660786944, 20.26521672923713], [165.92930026825064, -20.504748919881465]], "head": [168.01986953360372, -40.09300638186289], "nose": [[178.01986953360372, -40.09300638186289], [185.01986953360372, -40.09300638186289]], "arm_r": [[165.92930026825064, -18.594618965545237], [157.40883992713296, 9.732974293633951], [182.425414589148, 26.03952896291895]], "arm_l": [[171.8381467863239, -20.445263368193277], [179.34954690795715, 8.157915700951795], [209.25706691995097, 10.475929473965058]], "leg_r": [[160.0, 64.2155372436685], [187.71521835779888, 100.37108340382309], [174.5027852113516, 137.55239037407463], [184.60663358284393, 141.83514346377513]], "leg_l": [[165.90884651807326, 63.754078262355904], [169.92855366558072, 108.88194074142741], [162.0, 147.49266015363207], [172.7817467986817, 149.63988796164233]]}}}, {"name": "Cat-Cow", "camera": 0.0, "frames": [{"order": ["arm_r", "leg_r", "leg_l", "spine", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[190.0, 106.0], [145.1601730939384, 123.58044185519884], [109.35354598857317, 94.41322108943268]], "head": [89.42965202673827, 96.1298541129813], "nose": [[79.80600507406713, 93.4122413130513], [73.0771732024989, 91.51209267785273]], "arm_r": [[115.26239250664642, 94.47270664112087], [120.87972526065431, 123.49440084545728], [111.0, 151.39056507548827]], "arm_l": [[109.35354598857317, 96.32335104376892], [114.9679818582322, 125.3455889056239], [105.0, 153.2112865928978]], "leg_r": [[195.90884651807326, 106.7540782623559], [201.51483631471004, 151.7175669488609], [241.51483631471004, 151.7175669488609], [250.52550880188895, 157.93105468496077]], "leg_l": [[190.0, 107.2155372436685], [195.60598979663678, 152.17902593017354], [235.60598979663678, 152.17902593017354], [244.61666228381569, 158.3925136662734]]}, {"order": ["arm_r", "leg_r", "leg_l", "spine", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[190.0, 106.0], [152.08424735154603, 74.85543177687914], [109.94656592660422, 96.70312504123498]], "head": [93.56352504082439, 108.00037547050745], "nose": [[86.43854210873769, 115.01711390210609], [81.48879464043186, 119.89166358434622]], "arm_r": [[115.85541244467747, 96.76261059292317], [124.5590964138107, 125.03612878182669], [111.0, 151.39056507548827]], "arm_l": [[109.94656592660422, 98.61325499557122], [118.63058159288913, 126.89263803630388], [105.0, 153.2112865928978]], "leg_r": [[195.90884651807326, 106.7540782623559], [201.51483631471004, 151.7175669488609], [241.51483631471004, 151.7175669488609], [250.52550880188895, 157.93105468496077]], "leg_l": [[190.0, 107.2155372436685], [195.60598979663678, 152.17902593017354], [235.60598979663678, 152.17902593017354], [244.61666228381569, 158.39251366627337]]}], "tween": {"t": 0.5, "sample": {"order": ["arm_r", "leg_r", "leg_l", "spine", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[190.0, 106.0], [147.07348578236468, 98.95228286813989], [105.7422999370209, 95.05845136735941]], "head": [86.94844752130274, 101.79493314402872], "nose": [[77.25780748643307, 104.26302870834161], [70.48077400378631, 105.98906189268591]], "arm_r": [[111.65114645509415, 95.1179369190476], [120.46983670889635, 123.3568712620384], [111.0, 151.39056507548824]], "arm_l": [[105.7422999370209, 96.96858132169565], [114.56083205483813, 125.20756355913188], [105.0, 153.21128659289778]], "leg_r": [[195.90884651807326, 106.7540782623559], [201.51483631471007, 151.7175669488609], [241.51483631471004, 151.7175669488609], [250.52550880188895, 157.93105468496077]], "leg_l": [[190.0, 107.2155372436685], [195.60598979663678, 152.17902593017354], [235.60598979663678, 152.17902593017354], [244.61666228381569, 158.39251366627337]]}}}, {"name": "Chest Opener", "camera": 0.0, "frames": [{"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 66.0], [161.1256802152048, 23.170537162876016], [160.7504534768032, -17.70220939343298]], "head": [160.05246354275317, -37.38636608818675], "nose": [[169.95803733477305, -38.757353998727396], [176.88991381596406, -39.71676524058096]], "arm_r": [[160.7504534768032, -15.792079439096753], [147.59931907313086, 10.762100915602247], [151.77451210193283, 40.018811065315994]], "arm_l": [[166.65929999487645, -17.642723841744793], [153.50816559120412, 8.911456512954208], [157.6833586200061, 38.16816666266796]], "leg_r": [[160.0, 67.2155372436685], [168.06607636623892, 111.81480911859667], [158.0, 149.939388496694], [168.64599592855438, 152.6655047598624]], "leg_l": [[165.90884651807326, 66.7540782623559], [176.5209189274121, 110.83326271847169], [162.0, 147.53823689459546], [172.24958208092917, 151.47082311825417]]}, {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 66.0], [154.58807701263999, 23.32473036716258], [140.95635941600804, -15.463026121913273]], "head": [131.8765494212171, -33.012428782016556], "nose": [[139.35867698448504, -39.64701747514489], [144.5606907628268, -44.25977253758464]], "arm_r": [[140.95635941600804, -13.552896167577046], [138.72959228006906, 15.909837994427669], [150.0, 43.289936129031]], "arm_l": [[146.8652059340813, -15.403540570225084], [141.44082956091827, 13.653731466709052], [154.0, 40.48440685860927]], "leg_r": [[160.0, 67.2155372436685], [168.06607636623892, 111.81480911859667], [158.0, 149.939388496694], [168.64599592855438, 152.6655047598624]], "leg_l": [[165.90884651807326, 66.7540782623559], [176.5209189274121, 110.83326271847169], [162.0, 147.53823689459546], [172.24958208092917, 151.47082311825417]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 66.0], [157.86202344571535, 23.15540830526413], [150.7809886433799, -17.175586742622897]], "head": [145.7733885622911, -36.2443727887196], "nose": [[154.86097067357207, -40.41760311674206], [161.2051251828286, -43.330987301829545]], "arm_r": [[150.7809886433799, -15.26545678828667], [142.95151607473076, 13.254877696829503], [150.73286795258437, 41.78798182584565]], "arm_l": [[156.68983516145317, -17.11610119093471], [147.31553692727067, 10.948717387584443], [155.7728940366232, 39.29464460290591]], "leg_r": [[160.0, 67.2155372436685], [168.06607636623892, 111.81480911859668], [158.0, 149.939388496694], [168.64599592855438, 152.6655047598624]], "leg_l": [[165.90884651807326, 66.7540782623559], [176.5209189274121, 110.8332627184717], [162.0, 147.53823689459546], [172.24958208092917, 151.47082311825417]]}}}, {"name": "Chest Press", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[116.0, 122.0], [104.04264946482697, 80.73349088839885], [98.30413502720322, 40.17760054359378]], "head": [100.39470429255628, 20.589343081612355], "nose": [[110.35779236721048, 19.730928253756097], [117.3311552538527, 19.130106695514076]], "arm_r": [[98.30413502720322, 42.08773049793001], [93.0946896971953, 71.18311980971865], [116.07602299076464, 52.19245316293397]], "arm_l": [[104.21298154527646, 40.237086095281974], [99.00353621526855, 69.3324754070706], [121.98486950883789, 50.341808760285936]], "leg_r": [[116.0, 123.2155372436685], [160.6875870401012, 112.47166989865372], [158.0, 151.77496208102255], [168.97514242459204, 152.5028201333689]], "leg_l": [[121.90884651807325, 122.7540782623559], [166.38862646620098, 111.20411892847166], [162.0, 150.35861823193625], [172.93359315614495, 151.5471554084083]]}, {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[116.0, 122.0], [104.04264946482697, 80.73349088839885], [98.30413502720322, 40.17760054359378]], "head": [100.39470429255628, 20.589343081612355], "nose": [[110.35779236721048, 19.730928253756097], [117.3311552538527, 19.130106695514076]], "arm_r": [[98.30413502720322, 42.08773049793001], [128.1397918882514, 45.17594372902312], [157.2486636765313, 52.323340481322006]], "arm_l": [[104.21298154527646, 40.237086095281974], [134.04863840632467, 43.32529932637508], [163.15751019460455, 50.47269607867397]], "leg_r": [[116.0, 123.2155372436685], [160.6875870401012, 112.47166989865372], [158.0, 151.77496208102255], [168.97514242459204, 152.5028201333689]], "leg_l": [[121.90884651807325, 122.7540782623559], [166.38862646620098, 111.20411892847166], [162.0, 150.35861823193625], [172.93359315614495, 151.5471554084083]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[116.0, 122.0], [104.04264946482697, 80.73349088839885], [98.30413502720322, 40.17760054359378]], "head": [100.39470429255628, 20.589343081612355], "nose": [[110.35779236721048, 19.730928253756097], [117.3311552538527, 19.130106695514076]], "arm_r": [[98.30413502720322, 42.08773049793001], [116.35858572176467, 65.68280376169301], [145.58968766532172, 59.036797490709745]], "arm_l": [[104.21298154527646, 40.237086095281974], [122.26743223983792, 63.83215935904497], [151.49853418339498, 57.18615308806171]], "leg_r": [[116.0, 123.2155372436685], [160.6875870401012, 112.47166989865372], [158.0, 151.77496208102255], [168.97514242459204, 152.5028201333689]], "leg_l": [[121.90884651807325, 122.7540782623559], [166.38862646620098, 111.20411892847166], [162.0, 150.35861823193625], [172.93359315614495, 151.5471554084083]]}}}, {"name": "Child's Pose", "camera": 0.0, "frames": [{"order": ["arm_r", "leg_r", "spine", "arm_l", "leg_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[201.0, 108.0], [155.82571467885413, 111.8022553753272], [124.45770572282038, 141.37389729585914]], "head": [111.33652514301022, 156.23877421337863], "nose": [[117.27325277234276, 164.28583983105068], [121.38774953839007, 169.86292328970427]], "arm_r": [[130.36655224089364, 141.4333828475473], [107.12136215799222, 160.10999998477317], [79.99999999999999, 147.481718557415]], "arm_l": [[124.45770572282038, 143.28402725019538], [108.99208153799717, 168.5998024546127], [83.99999999999999, 152.2568633338612]], "leg_r": [[206.90884651807326, 108.7540782623559], [209.31630050524868, 153.9931512401667], [247.56849074377007, 142.47595437727284], [257.8378754352393, 146.35811326384913]], "leg_l": [[201.0, 109.2155372436685], [203.40745398717542, 154.45461022147933], [241.6596442256968, 142.93741335858545], [251.92902891716605, 146.81957224516174]]}, {"order": ["arm_r", "leg_r", "spine", "arm_l", "leg_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[202.0, 110.0], [156.74378800446112, 116.33532375305779], [128.93070318831133, 149.67279357603658]], "head": [118.03792248801076, 166.19137912675268], "nose": [[124.78564870801182, 173.57163595561101], [129.46956295252383, 178.69461945579832]], "arm_r": [[134.8395497063846, 149.73227912772478], [106.04161630328194, 158.0116388932225], [78.00000000000001, 147.5121030513906]], "arm_l": [[128.93070318831133, 151.58292353037282], [105.75011328400946, 170.33724855225375], [82.0, 152.28724782783678]], "leg_r": [[207.90884651807326, 110.7540782623559], [209.5142233663883, 156.02763866028954], [247.10192819782466, 142.55467510695092], [257.5635498770713, 145.9022207575537]], "leg_l": [[202.0, 111.2155372436685], [203.60537684831505, 156.48909764160214], [241.1930816797514, 143.01613408826356], [251.65470335899806, 146.36367973886632]]}], "tween": {"t": 0.5, "sample": {"order": ["arm_r", "leg_r", "spine", "leg_l", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[201.5, 109.0], [156.26996632297158, 114.06239831006303], [126.62305565266601, 145.56456984475278]], "head": [114.58675518962505, 161.29461868726145], "nose": [[120.93805820162584, 169.01866879389266], [125.34330093897469, 174.37604437034733]], "arm_r": [[132.53190217073927, 145.62405539644095], [105.28609458574061, 133.25830599136376], [99.20171180988373, 104.32807532258633]], "arm_l": [[126.62305565266601, 147.47469979908902], [102.5416627187038, 129.85554190324453], [118.69316943490644, 104.95861963534743]], "leg_r": [[207.40884651807326, 109.7540782623559], [209.41533833687873, 155.012118237712], [247.3482845451267, 142.51275480992763], [257.71734094714066, 146.12884622273043]], "leg_l": [[201.5, 110.2155372436685], [203.50649181880547, 155.4735772190246], [241.43943802705343, 142.97421379124023], [251.8084944290674, 146.59030520404303]]}}}, {"name": "Cobra Stretch", "camera": 0.0, "frames": [{"order": ["arm_r", "leg_r", "spine", "leg_l", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[188.0, 147.0], [146.2115550987712, 134.51133070811812], [104.6860248829862, 130.99893053725023]], "head": [84.69820834260429, 130.3115446386733], "nose": [[77.21608077933637, 136.94613333180163], [72.0140670009946, 141.5588883942414]], "arm_r": [[110.59487140105945, 131.0584160889384], [133.96056156318676, 149.58862856981378], [104.00000000000001, 148.07421618993888]], "arm_l": [[104.6860248829862, 132.90906049158647], [78.52170000775969, 147.36354601159968], [89.13411237249052, 119.7296105142682]], "leg_r": [[193.90884651807326, 147.7540782623559], [235.07408286009883, 167.9708002769172], [270.0, 148.76880890060664], [280.80420398805575, 150.8036171891036]], "leg_l": [[188.0, 148.2155372436685], [233.91528622289013, 150.9635728393661], [273.8970875772516, 152.15171001244428], [282.10647978204673, 159.3620820259009]]}, {"order": ["leg_r", "arm_r", "leg_l", "spine", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[188.0, 147.0], [148.3804213554066, 126.63849409496028], [126.34500567148017, 90.56665811833622]], "head": [116.64881326655342, 73.34001274777336], "nose": [[106.82714865461051, 75.22014685216816], [99.95575837047687, 76.5355180981072]], "arm_r": [[132.2538521895534, 90.6261436700244], [118.68593676222457, 116.976177686032], [104.21670561230384, 142.85698070747202]], "arm_l": [[126.34500567148017, 92.47678807267246], [117.22115287129492, 120.6215411178757], [107.1206945871057, 148.44093505140947]], "leg_r": [[193.90884651807326, 147.7540782623559], [235.07408286009883, 167.9708002769172], [270.0, 148.76880890060664], [280.80420398805575, 150.8036171891036]], "leg_l": [[188.0, 148.2155372436685], [233.91528622289013, 150.9635728393661], [273.8970875772516, 152.15171001244428], [282.10647978204673, 159.3620820259009]]}], "tween": {"t": 0.5, "sample": {"order": ["arm_r", "leg_r", "spine", "leg_l", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[188.0, 147.0], [147.58496764320086, 130.84243706328948], [112.31672612283286, 109.07150914920337]], "head": [95.26392283575102, 98.78029639980302], "nose": [[86.28759453505705, 103.18773672704067], [80.02305400384289, 106.2636701871341]], "arm_r": [[118.22557264090611, 109.13099470089155], [131.2607963421022, 135.74054653115266], [104.0, 148.0742161899389]], "arm_l": [[112.31672612283286, 110.9816391035396], [89.20688866169617, 129.8204583254148], [69.74512702248809, 107.33663014299796]], "leg_r": [[193.90884651807326, 147.7540782623559], [235.07408286009883, 167.97080027691717], [270.0, 148.76880890060661], [280.80420398805575, 150.8036171891036]], "leg_l": [[188.0, 148.2155372436685], [233.91528622289013, 150.9635728393661], [273.8970875772516, 152.15171001244425], [282.10647978204673, 159.3620820259009]]}}}, {"name": "Crunch", "camera": 0.0, "frames": [{"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[166.0, 146.0], [122.5, 146.0], [81.0, 146.0]], "head": [61.437047985323886, 141.90493909881587], "nose": [[63.546821344204844, 132.13002956602958], [65.00220317992915, 125.387018180479]], "arm_r": [[81.0, 147.91012995433624], [53.810766388900504, 135.42419773253272], [66.48931424112148, 162.20036579120364]], "arm_l": [[86.90884651807325, 146.05948555168817], [59.71961290697375, 133.57355332988467], [72.39816075919472, 160.34972138855557]], "leg_r": [[166.0, 147.2155372436685], [194.0368351722767, 111.30128659027031], [205.0, 149.18515274969292], [215.27575990220512, 145.31938923995287]], "leg_l": [[171.90884651807326, 146.7540782623559], [199.48140422111373, 110.49289964731176], [208.99999999999997, 148.75361665361882], [219.41521534843125, 145.26855340182198]]}, {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[166.0, 144.0], [117.46766838873427, 144.00204748311933], [96.1737983030601, 103.01749698724782]], "head": [92.0155644867049, 83.75175017138899], "nose": [[101.80342162264763, 81.70288397605682], [108.65045482778427, 80.26961266064238]], "arm_r": [[96.1737983030601, 104.92762694158407], [89.93644757852732, 76.02900671779581], [75.8523006949506, 102.11501576632395]], "arm_l": [[102.08264482113334, 103.076982538936], [95.84529409660057, 74.17836231514775], [81.76114721302385, 100.26437136367588]], "leg_r": [[166.0, 145.2155372436685], [195.90537209116957, 110.79416451246408], [205.0, 149.15476825571733], [215.45316340216056, 145.78169389384772]], "leg_l": [[171.90884651807326, 144.7540782623559], [201.45982836379503, 110.03713295240527], [208.99999999999997, 148.72323215964326], [219.5809665807253, 145.76173557962537]]}], "tween": {"t": 0.5, "sample": {"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[166.0, 145.0], [121.16522782962937, 145.67055250176602], [84.95160625843393, 123.01582062720574]], "head": [70.80947063470298, 109.08853582080536], "nose": [[77.93445356678967, 102.07179738920672], [82.8842010350955, 97.19724770696659]], "arm_r": [[84.95160625843393, 124.92595058154197], [66.27616715930534, 101.80439316837644], [65.49085871006913, 131.33850168811765]], "arm_l": [[90.86045277650717, 123.07530617889392], [72.18501367737859, 99.95374876572838], [71.39970522814238, 129.48785728546957]], "leg_r": [[166.0, 146.2155372436685], [194.97879634694823, 111.03391792015745], [205.0, 149.16996050270512], [215.36848703280944, 145.55228605768735]], "leg_l": [[171.90884651807326, 145.7540782623559], [200.47950405948097, 110.25018764814351], [209.0, 148.73842440663103], [219.50242901519758, 145.51718692476985]]}}}, {"name": "Dead Bug", "camera": 0.0, "frames": [{"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[185.0, 148.0], [141.5, 148.0], [100.0, 148.0]], "head": [80.43704798532389, 143.90493909881587], "nose": [[80.43704798532389, 133.90493909881587], [80.43704798532389, 127.01128482773042]], "arm_r": [[100.0, 149.91012995433624], [100.0, 120.36589736396999], [100.0, 90.82166477360374]], "arm_l": [[105.90884651807325, 148.05948555168817], [105.90884651807325, 118.51525296132193], [105.90884651807325, 88.97102037095569]], "leg_r": [[185.0, 149.2155372436685], [205.16507275229756, 108.4991273664634], [244.55738287278587, 115.33953023297677], [246.4675128271221, 104.67122081865428]], "leg_l": [[190.90884651807326, 148.7540782623559], [211.07391927037082, 108.03766838515077], [250.46622939085913, 114.87807125166415], [252.37635934519537, 104.20976183734166]]}, {"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[185.0, 148.0], [141.5, 148.0], [100.0, 148.0]], "head": [80.43704798532389, 143.90493909881587], "nose": [[80.43704798532389, 133.90493909881587], [80.43704798532389, 127.01128482773042]], "arm_r": [[100.0, 149.91012995433624], [78.78679656440357, 129.01920274473565], [57.57359312880715, 108.12827553513509]], "arm_l": [[105.90884651807325, 148.05948555168817], [105.90884651807325, 118.51525296132193], [105.90884651807325, 88.97102037095569]], "leg_r": [[185.0, 149.2155372436685], [205.16507275229756, 108.4991273664634], [244.55738287278587, 115.33953023297677], [246.4675128271221, 104.67122081865428]], "leg_l": [[190.90884651807326, 148.7540782623559], [235.12688453123593, 136.2673872310511], [273.57735236876863, 125.40939502991648], [270.5453414547817, 114.99615735074556]]}, {"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[185.0, 148.0], [141.5, 148.0], [100.0, 148.0]], "head": [80.43704798532389, 143.90493909881587], "nose": [[80.43704798532389, 133.90493909881587], [80.43704798532389, 127.01128482773042]], "arm_r": [[100.0, 149.91012995433624], [100.0, 120.36589736396999], [100.0, 90.82166477360374]], "arm_l": [[105.90884651807325, 148.05948555168817], [105.90884651807325, 118.51525296132193], [105.90884651807325, 88.97102037095569]], "leg_r": [[185.0, 149.2155372436685], [205.16507275229756, 108.4991273664634], [244.55738287278587, 115.33953023297677], [246.4675128271221, 104.67122081865428]], "leg_l": [[190.90884651807326, 148.7540782623559], [211.07391927037082, 108.03766838515077], [250.46622939085913, 114.87807125166415], [252.37635934519537, 104.20976183734166]]}, {"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[185.0, 148.0], [141.5, 148.0], [100.0, 148.0]], "head": [80.43704798532389, 143.90493909881587], "nose": [[80.43704798532389, 133.90493909881587], [80.43704798532389, 127.01128482773042]], "arm_r": [[100.0, 149.91012995433624], [100.0, 120.36589736396999], [100.0, 90.82166477360374]], "arm_l": [[105.90884651807325, 148.05948555168817], [84.69564308247682, 127.16855834208761], [63.482439646880394, 106.27763113248703]], "leg_r": [[185.0, 149.2155372436685], [229.21803801316267, 136.7288462123637], [267.6685058506954, 125.87085401122908], [264.63649493670846, 115.45761633205817]], "leg_l": [[190.90884651807326, 148.7540782623559], [211.07391927037082, 108.03766838515077], [250.46622939085913, 114.87807125166415], [252.37635934519537, 104.20976183734166]]}], "tween": {"t": 0.5, "sample": {"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[185.0, 148.0], [141.5, 148.0], [100.0, 148.0]], "head": [80.43704798532389, 143.90493909881587], "nose": [[80.43704798532389, 133.90493909881587], [80.43704798532389, 127.01128482773042]], "arm_r": [[100.0, 149.91012995433624], [88.51949702904732, 122.61481816034394], [77.03899405809463, 95.31950636635166]], "arm_l": [[105.90884651807325, 148.05948555168817], [105.90884651807325, 118.51525296132193], [105.90884651807325, 88.97102037095569]], "leg_r": [[185.0, 149.2155372436685], [205.16507275229756, 108.4991273664634], [244.55738287278587, 115.33953023297677], [246.4675128271221, 104.67122081865428]], "leg_l": [[190.90884651807326, 148.7540782623559], [226.14689090154624, 119.63505607061941], [266.0920722917292, 117.57342185184505], [265.5163767730568, 106.755382661499]]}}}, {"name": "Deadlift", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[92.0, 90.0], [135.1183334051322, 83.18419756301371], [174.00472051595008, 68.41408112378903]], "head": [193.49212181165478, 63.983410276466856], "nose": [[203.4198066175824, 65.18385785722754], [210.36762967907163, 66.02398297690681]], "arm_r": [[174.00472051595008, 70.32421107812527], [166.15560263227576, 98.83930806126465], [159.29930173624587, 127.60161513162308]], "arm_l": [[179.91356703402334, 68.4735666754772], [171.25570462090087, 96.76072587520919], [163.5755155861773, 125.32040024801982]], "leg_r": [[92.0, 91.2155372436685], [113.48276422122781, 131.27301891844934], [106.37742255819141, 170.03886151266857], [117.33935559069171, 169.138408861487]], "leg_l": [[97.90884651807325, 90.7540782623559], [116.99012526171511, 131.97395705768585], [104.17165311229812, 169.28877339739455], [115.1488380723927, 169.98611906473377]]}, {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 64.0], [157.55369572851146, 21.811685497906495], [164.76009510168907, -18.436936383401104]], "head": [168.91832891804427, -37.70268319925994], "nose": [[178.8947013481789, -37.01566563380052], [185.87764969999765, -36.53478862063847]], "arm_r": [[164.76009510168907, -16.526806429064877], [170.85644942928292, 12.400979889721263], [191.1744301067988, 34.137801113206514]], "arm_l": [[170.66894161976234, -18.377450831712917], [175.4474723060758, 10.789583023349335], [194.20923781110346, 33.84326058608493]], "leg_r": [[150.0, 65.2155372436685], [161.22887526493585, 109.1462686355685], [150.0, 146.95458074368176], [160.55768072289158, 149.9956086837736]], "leg_l": [[155.90884651807326, 64.7540782623559], [166.01236624892732, 108.94900788074216], [154.0, 146.52304464760766], [164.49226113348837, 149.77625928644824]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[121.0, 77.0], [152.0282252858047, 46.91558466206313], [178.42478914733817, 15.322067738987485]], "head": [192.44297443333522, 1.2737762956579388], "nose": [[202.3983153476498, 2.217803456164566], [209.36608873622006, 2.878530937391008]], "arm_r": [[178.42478914733817, 17.23219769332372], [170.69935078808214, 45.78003344335117], [163.8809515483398, 74.551077451591]], "arm_l": [[184.33363566541144, 15.381553290675667], [175.8851979024652, 43.73005997979728], [168.3328137749549, 72.32277049751575]], "leg_r": [[121.0, 78.2155372436685], [155.53625674578984, 108.1389004808649], [150.0, 147.1520799545231], [160.9996521719293, 147.2382272564324]], "leg_l": [[126.90884651807325, 77.7540782623559], [161.1827791149295, 107.96854417466466], [154.0, 146.72054385844896], [164.98644510033364, 147.25816596777275]]}}}, {"name": "Dip", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 86.0], [166.05402989176287, 43.57777028291508], [171.82971358160557, 3.105987909144403]], "head": [176.32873466848287, -16.08535597292574], "nose": [[186.3154424382918, -15.569926132464488], [193.30584918157382, -15.209140144178978]], "arm_r": [[171.82971358160557, 5.0161178634806305], [165.65325797831116, 33.92742009448894], [174.0, 62.30512837601881]], "arm_l": [[177.73856009967884, 3.165473460832592], [170.58370649634907, 31.857168685039003], [178.0, 60.4844068586093]], "leg_r": [[160.0, 87.2155372436685], [187.68349106499423, 123.39464958143844], [147.73830967481126, 121.3330153626641], [143.44026726142926, 131.30473884268272]], "leg_l": [[165.90884651807326, 86.7540782623559], [193.5923375830675, 122.93319060012583], [153.64715619288452, 120.87155638135147], [149.34911377950252, 130.8432798613701]]}, {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 116.0], [172.06485767544996, 74.76384973073141], [189.02120768159105, 37.37575364089446]], "head": [199.02120768159102, 20.318383001845604], "nose": [[208.69023618469885, 22.86983118005503], [215.45171696872234, 24.65404019576428]], "arm_r": [[189.02120768159105, 39.285883595230686], [159.1021538806009, 37.11702038250827], [174.0, 62.76089578565258]], "arm_l": [[194.9300541996643, 37.43523919258265], [165.103239794315, 34.26526825327232], [178.0, 60.94017426824305]], "leg_r": [[160.0, 117.2155372436685], [199.83716857408416, 139.8661155629493], [159.86153549332033, 141.24088736010316], [156.46234855519592, 151.54357349890591]], "leg_l": [[165.90884651807326, 116.7540782623559], [205.74601509215742, 139.4046565816367], [165.7703820113936, 140.77942837879056], [162.37119507326918, 151.0821145175933]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 101.0], [169.07414305802632, 59.084192074171945], [180.54329523109206, 19.78605846585289]], "head": [187.873319765578, 1.460409776103674], "nose": [[197.75384933141916, 3.0015572377499415], [204.66766771558514, 4.079962355819498]], "arm_r": [[180.54329523109206, 21.696188420189117], [156.09947277307057, 38.82447106310195], [174.0, 62.533012080835704]], "arm_l": [[186.45214174916532, 19.845544017541076], [161.40189587227587, 36.1018888687581], [178.0, 60.71229056342617]], "leg_r": [[160.0, 102.2155372436685], [194.45196315629408, 132.23299183974086], [154.45348623372723, 131.88923344711145], [150.6012050398731, 142.0360958211416]], "leg_l": [[165.90884651807326, 101.7540782623559], [200.36080967436735, 131.77153285842823], [160.3623327518005, 131.42777446579885], [156.51005155794635, 141.574636839829]]}}}, {"name": "Dumbbell Bench Press", "camera": 0.0, "frames": [{"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 116.0], [106.5, 116.0], [65.0, 116.0]], "head": [45.194638625168594, 113.25882502327553], "nose": [[46.60741399901631, 103.35912473775934], [47.58162570573677, 96.53255903615947]], "arm_r": [[65.0, 117.91012995433623], [89.01219176211681, 135.62064948104614], [90.0, 106.09243691818973]], "arm_l": [[70.90884651807325, 116.05948555168817], [94.99848677843022, 133.66770585501038], [97.99999999999999, 104.2717154007802]], "leg_r": [[150.0, 117.2155372436685], [195.30008769797305, 109.34310611741422], [196.0, 148.72938534005917], [206.93968862700535, 147.59655128433957]], "leg_l": [[155.90884651807326, 116.7540782623559], [201.21645940182967, 108.92375949218865], [200.0, 148.29784924398507], [210.98222903324333, 147.68232827813742]]}, {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 116.0], [106.5, 116.0], [65.0, 116.0]], "head": [45.194638625168594, 113.25882502327553], "nose": [[46.60741399901631, 103.35912473775934], [47.58162570573677, 96.53255903615947]], "arm_r": [[65.0, 117.91012995433623], [60.069389734334294, 88.76765535754025], [44.92401672772445, 63.264791219785906]], "arm_l": [[70.90884651807325, 116.05948555168817], [65.94642267092193, 86.9222484977981], [50.637339472650595, 61.51436169540154]], "leg_r": [[150.0, 117.2155372436685], [195.30008769797305, 109.34310611741422], [196.0, 148.72938534005917], [206.93968862700535, 147.59655128433957]], "leg_l": [[155.90884651807326, 116.7540782623559], [201.21645940182967, 108.92375949218865], [200.0, 148.29784924398507], [210.98222903324333, 147.68232827813742]]}], "tween": {"t": 0.5, "sample": {"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 116.0], [106.5, 116.0], [65.0, 116.0]], "head": [45.194638625168594, 113.25882502327553], "nose": [[46.60741399901631, 103.35912473775934], [47.58162570573677, 96.53255903615947]], "arm_r": [[65.0, 117.91012995433623], [93.37505541110951, 108.318677244387], [78.0, 82.94945472240286]], "arm_l": [[70.90884651807325, 116.05948555168817], [99.46709581175503, 107.01069392508388], [85.0, 81.12873320499331]], "leg_r": [[150.0, 117.2155372436685], [195.30008769797305, 109.34310611741422], [196.0, 148.72938534005917], [206.93968862700535, 147.59655128433957]], "leg_l": [[155.90884651807326, 116.7540782623559], [201.21645940182967, 108.92375949218865], [200.0, 148.29784924398507], [210.98222903324333, 147.68232827813742]]}}}, {"name": "Dumbbell Curl", "camera": 0.0, "frames": [{"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [162.27661409656804, 21.21957229272239], [164.44855628065022, -19.59393919812868]], "head": [166.1916711356034, -39.215144441936815], "nose": [[176.1901940025219, -39.04327100097566], [183.18912786861665, -38.92296014479856]], "arm_r": [[164.44855628065022, -17.683809243792453], [164.44855628065022, 11.860423346573782], [169.65800161065815, 40.955812658362404]], "arm_l": [[170.35740279872348, -19.534453646440493], [170.35740279872348, 10.009778943925744], [175.5668481287314, 39.10516825571437]], "leg_r": [[160.0, 65.2155372436685], [168.06607636623892, 109.81480911859667], [158.0, 147.939388496694], [168.64599592855438, 150.6655047598624]], "leg_l": [[165.90884651807326, 64.7540782623559], [172.71840731660157, 109.55611956268012], [162.0, 147.50785240061987], [172.59772985997608, 150.4106343225862]]}, {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [162.27661409656804, 21.21957229272239], [164.44855628065022, -19.59393919812868]], "head": [166.1916711356034, -39.215144441936815], "nose": [[176.1901940025219, -39.04327100097566], [183.18912786861665, -38.92296014479856]], "arm_r": [[164.44855628065022, -17.683809243792453], [166.01863496793854, 11.819934002605898], [188.65992237462171, -7.562826542899472]], "arm_l": [[170.35740279872348, -19.534453646440493], [171.9274814860118, 9.96928959995786], [194.56876889269498, -9.41347094554751]], "leg_r": [[160.0, 65.2155372436685], [168.06607636623892, 109.81480911859667], [158.0, 147.939388496694], [168.64599592855438, 150.6655047598624]], "leg_l": [[165.90884651807326, 64.7540782623559], [172.71840731660157, 109.55611956268012], [162.0, 147.50785240061987], [172.59772985997608, 150.4106343225862]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [162.27661409656804, 21.21957229272239], [164.44855628065022, -19.59393919812868]], "head": [166.1916711356034, -39.215144441936815], "nose": [[176.1901940025219, -39.04327100097566], [183.18912786861665, -38.92296014479856]], "arm_r": [[164.44855628065022, -17.683809243792453], [165.23386472988642, 11.850299275948743], [193.51310946265176, 21.71236676541092]], "arm_l": [[170.35740279872348, -19.534453646440493], [171.14271124795968, 9.999654873300704], [199.42195598072502, 19.86172236276288]], "leg_r": [[160.0, 65.2155372436685], [168.06607636623892, 109.81480911859667], [158.0, 147.939388496694], [168.64599592855438, 150.6655047598624]], "leg_l": [[165.90884651807326, 64.7540782623559], [172.71840731660157, 109.55611956268012], [162.0, 147.50785240061987], [172.59772985997608, 150.4106343225862]]}}}, {"name": "Dumbbell Fly", "camera": 90.0, "frames": [{"order": ["arm_l", "arm_r", "spine", "leg_l", "leg_r", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 116.0], [160.0, 108.44630427148853], [160.0, 101.23990489831093]], "head": [160.0, 95.05956501081197], "nose": [[160.0, 85.05956501081197], [160.0, 78.40216939674589]], "arm_r": [[149.0, 101.23990489831093], [149.0, 71.89547687629675], [149.0, 41.895476876296755]], "arm_l": [[171.0, 101.23990489831093], [171.0, 71.89547687629675], [171.0, 41.895476876296755]], "leg_r": [[153.0, 116.0], [139.2057898989263, 116.18285705273352], [139.66261958055435, 156.14743421830946], [136.36799117671453, 155.65043725393338]], "leg_l": [[167.0, 116.0], [180.82470091432793, 118.16195384834933], [180.79806668035621, 158.1090965367697], [184.09202372933197, 157.69091950032282]]}, {"order": ["arm_l", "arm_r", "spine", "leg_l", "leg_r", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 116.0], [160.0, 108.44630427148853], [160.0, 101.23990489831093]], "head": [160.0, 95.05956501081197], "nose": [[160.0, 85.05956501081197], [160.0, 78.40216939674589]], "arm_r": [[149.0, 101.23990489831093], [119.16434313895179, 100.912118909318], [91.5012038069931, 89.43156725352722]], "arm_l": [[171.0, 101.23990489831093], [200.8356568610482, 100.912118909318], [228.49879619300688, 89.43156725352722]], "leg_r": [[153.0, 116.0], [139.2057898989263, 116.18285705273352], [139.66261958055435, 156.14743421830946], [136.36799117671453, 155.65043725393338]], "leg_l": [[167.0, 116.0], [180.82470091432793, 118.16195384834933], [180.79806668035621, 158.1090965367697], [184.09202372933197, 157.69091950032282]]}], "tween": {"t": 0.5, "sample": {"order": ["arm_l", "arm_r", "spine", "leg_l", "leg_r", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 116.0], [160.0, 108.44630427148853], [160.0, 101.23990489831093]], "head": [160.0, 95.05956501081197], "nose": [[160.0, 85.05956501081197], [160.0, 78.40216939674589]], "arm_r": [[149.0, 101.23990489831093], [128.92608180923423, 86.32207646778681], [109.72929837259207, 65.53785060661484]], "arm_l": [[171.0, 101.23990489831093], [191.07391819076574, 86.32207646778681], [210.2707016274079, 65.53785060661484]], "leg_r": [[153.0, 116.0], [139.20037079553018, 116.08135134071809], [139.88445590137957, 156.01219734499898], [136.58605196402783, 155.6991971878592]], "leg_l": [[167.0, 116.0], [180.83203697997072, 118.04697952897166], [180.53021656989924, 157.94616690783334], [183.8314873318548, 157.75202692624796]]}}}, {"name": "Dumbbell Row", "camera": 0.0, "frames": [{"order": ["arm_r", "leg_r", "leg_l", "spine", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[168.0, 82.0], [125.16086274396895, 74.5610618826667], [84.29134099396232, 67.46414390865907]], "head": [64.29134099396232, 67.46414390865907], "nose": [[56.582726094680645, 73.83417187834434], [51.2204149928478, 78.26532742926075]], "arm_r": [[90.20018751203557, 67.52362946034725], [102.09645322030373, 94.64572241081939], [92.0, 122.46652631042721]], "arm_l": [[84.29134099396232, 69.37427386299531], [85.51514091393564, 98.89391400097236], [66.0, 121.33282456880013]], "leg_r": [[173.90884651807326, 82.7540782623559], [211.1451698158023, 109.35220659139566], [241.63376726871167, 83.85261792608881], [247.1902245701585, 93.20185091031925]], "leg_l": [[168.0, 83.2155372436685], [162.4022022511762, 128.18001577127842], [202.0, 122.60784736415684], [211.34192807714578, 128.32723060939875]]}, {"order": ["arm_r", "leg_r", "leg_l", "spine", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[168.0, 82.0], [125.16086274396895, 74.5610618826667], [84.29134099396232, 67.46414390865907]], "head": [64.29134099396232, 67.46414390865907], "nose": [[56.582726094680645, 73.83417187834434], [51.2204149928478, 78.26532742926075]], "arm_r": [[90.20018751203557, 67.52362946034725], [119.61204459175761, 61.70220240985699], [99.99999999999999, 84.0590239429511]], "arm_l": [[84.29134099396232, 69.37427386299531], [85.51514091393564, 98.89391400097236], [66.0, 121.33282456880013]], "leg_r": [[173.90884651807326, 82.7540782623559], [211.1451698158023, 109.35220659139566], [241.63376726871167, 83.85261792608881], [247.1902245701585, 93.20185091031925]], "leg_l": [[168.0, 83.2155372436685], [162.4022022511762, 128.18001577127842], [202.0, 122.60784736415684], [211.34192807714578, 128.32723060939875]]}], "tween": {"t": 0.5, "sample": {"order": ["arm_r", "leg_r", "leg_l", "spine", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[168.0, 82.0], [125.16086274396895, 74.5610618826667], [84.29134099396232, 67.46414390865907]], "head": [64.29134099396232, 67.46414390865907], "nose": [[56.582726094680645, 73.83417187834434], [51.2204149928478, 78.26532742926075]], "arm_r": [[90.20018751203557, 67.52362946034725], [116.5166611402472, 81.70770635791291], [96.0, 103.26277512668918]], "arm_l": [[84.29134099396232, 69.37427386299531], [85.51514091393564, 98.89391400097236], [66.0, 121.33282456880013]], "leg_r": [[173.90884651807326, 82.7540782623559], [211.1451698158023, 109.35220659139566], [241.63376726871167, 83.85261792608881], [247.1902245701585, 93.20185091031925]], "leg_l": [[168.0, 83.2155372436685], [162.40220225117616, 128.18001577127842], [202.0, 122.60784736415684], [211.34192807714578, 128.32723060939878]]}}}, {"name": "Dumbbell Shoulder Press", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[130.0, 124.0], [130.7861315886847, 81.14917968668395], [134.4304502554608, 40.41789305174976]], "head": [137.2139122746621, 20.913419618607264], "nose": [[147.2080040702254, 21.25711887937817], [154.20373985935908, 21.497703943880097]], "arm_r": [[134.4304502554608, 42.328023006085985], [158.22858921859793, 60.31657064382028], [146.0, 33.338201171188686]], "arm_l": [[140.33929677353407, 40.47737860343795], [165.22803832271347, 56.97255699205585], [150.0, 31.51747965377915]], "leg_r": [[130.0, 125.2155372436685], [173.8364696761423, 111.48589278988837], [176.0, 150.82053882198593], [186.98389774627694, 150.2346069268626]], "leg_l": [[135.90884651807326, 124.7540782623559], [179.40646070778647, 110.0162218015468], [180.0, 149.4041949728996], [190.99878894026918, 149.24345139631006]]}, {"order": ["leg_l", "spine", "leg_r", "arm_l", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[130.0, 124.0], [130.7861315886847, 81.14917968668395], [134.4304502554608, 40.41789305174976]], "head": [137.2139122746621, 20.913419618607264], "nose": [[147.2080040702254, 21.25711887937817], [154.20373985935908, 21.497703943880097]], "arm_r": [[134.4304502554608, 42.328023006085985], [143.41856866823082, 14.140944212067012], [138.0, -14.91737872640951]], "arm_l": [[140.33929677353407, 40.47737860343795], [148.61393171247587, 12.079197208400549], [142.0, -16.738100243819016]], "leg_r": [[130.0, 125.2155372436685], [173.8364696761423, 111.48589278988837], [176.0, 150.82053882198593], [186.98389774627694, 150.2346069268626]], "leg_l": [[135.90884651807326, 124.7540782623559], [179.40646070778647, 110.0162218015468], [180.0, 149.4041949728996], [190.99878894026918, 149.24345139631006]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[130.0, 124.0], [130.7861315886847, 81.14917968668395], [134.4304502554608, 40.41789305174976]], "head": [137.2139122746621, 20.913419618607264], "nose": [[147.2080040702254, 21.25711887937817], [154.20373985935908, 21.497703943880097]], "arm_r": [[134.4304502554608, 42.328023006085985], [162.17101057015535, 31.079582070738752], [142.0, 9.210411222389595]], "arm_l": [[140.33929677353407, 40.47737860343795], [167.52019230026508, 27.97385205227781], [146.0, 7.3896897049800625]], "leg_r": [[130.0, 125.2155372436685], [173.8364696761423, 111.48589278988837], [176.0, 150.82053882198593], [186.98389774627694, 150.2346069268626]], "leg_l": [[135.90884651807326, 124.7540782623559], [179.40646070778647, 110.0162218015468], [180.0, 149.4041949728996], [190.99878894026918, 149.24345139631006]]}}}, {"name": "Face Pull", "camera": 0.0, "frames": [{"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 66.0], [153.06228919571583, 23.248423141253653], [158.8662093611655, -17.239211379257284]], "head": [162.6823892686964, -36.57349259579321], "nose": [[172.6454773433506, -35.715077767936954], [179.6188402299928, -35.114256209694936]], "arm_r": [[158.8662093611655, -15.329081424921057], [187.57122205368216, -6.742470038371721], [214.0, -20.722567816762492]], "arm_l": [[164.77505587923875, -17.179725827569097], [192.76969061470504, -6.559295559438974], [218.0, -22.543289334172012]], "leg_r": [[150.0, 67.2155372436685], [160.60371763953367, 111.29667164972889], [146.0, 147.96977299066958], [156.24068183654285, 151.9247829380784]], "leg_l": [[155.90884651807326, 66.7540782623559], [165.23395514054224, 111.11463772661938], [150.0, 147.5382368945955], [160.17101027135118, 151.6639291057432]]}, {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 66.0], [153.06228919571583, 23.248423141253653], [158.8662093611655, -17.239211379257284]], "head": [162.6823892686964, -36.57349259579321], "nose": [[172.6454773433506, -35.715077767936954], [179.6188402299928, -35.114256209694936]], "arm_r": [[158.8662093611655, -15.329081424921057], [179.33203423607728, 6.086650492133344], [170.2261581012055, -22.376263640994495]], "arm_l": [[164.77505587923875, -17.179725827569097], [186.60646021578427, 0.4368913694498724], [174.27938782058348, -24.463157719618334]], "leg_r": [[150.0, 67.2155372436685], [160.60371763953367, 111.29667164972889], [146.0, 147.96977299066958], [156.24068183654285, 151.9247829380784]], "leg_l": [[155.90884651807326, 66.7540782623559], [165.23395514054224, 111.11463772661938], [150.0, 147.5382368945955], [160.17101027135118, 151.6639291057432]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 66.0], [153.06228919571583, 23.248423141253653], [158.8662093611655, -17.239211379257284]], "head": [162.6823892686964, -36.57349259579321], "nose": [[172.6454773433506, -35.715077767936954], [179.6188402299928, -35.114256209694936]], "arm_r": [[158.8662093611655, -15.329081424921057], [179.91401379749405, 6.0440244594031185], [192.09814735171608, -21.172187554907964]], "arm_l": [[164.77505587923875, -17.179725827569097], [185.30308924151055, 3.421335695414859], [196.06151469937672, -23.838774964749273]], "leg_r": [[150.0, 67.2155372436685], [160.60371763953367, 111.29667164972889], [146.0, 147.96977299066958], [156.24068183654285, 151.9247829380784]], "leg_l": [[155.90884651807326, 66.7540782623559], [165.23395514054224, 111.11463772661938], [150.0, 147.5382368945955], [160.17101027135118, 151.6639291057432]]}}}, {"name": "Glute Bridge", "camera": 0.0, "frames": [{"order": ["arm_l", "leg_l", "spine", "arm_r", "leg_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[168.0, 144.0], [124.5, 144.0], [83.0, 144.0]], "head": [64.20614758428184, 137.2635182233307], "nose": [[66.66045491672855, 127.56937692469582], [68.35390818592623, 120.88049364703733]], "arm_r": [[83.0, 145.91012995433624], [112.92692150779473, 147.97103143931645], [142.85384301558946, 150.03193292429665]], "arm_l": [[88.90884651807325, 144.05948555168817], [118.83576802586798, 146.12038703666838], [148.76268953366272, 148.1812885216486]], "leg_r": [[168.0, 145.2155372436685], [197.5044798913556, 110.46025450639627], [205.0, 149.15476825571733], [215.58437753584877, 146.20511660320886]], "leg_l": [[173.90884651807326, 144.7540782623559], [203.11952081133066, 109.7589308468124], [208.99999999999997, 148.72323215964326], [219.69813598891116, 146.20283871368702]]}, {"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[172.0, 116.0], [130.5742805698035, 129.31344833951914], [93.2243080852167, 147.3068466003177]], "head": [74.95339893236468, 139.29569860961357], "nose": [[78.08659203536632, 129.79922026344212], [80.24971099599095, 123.24296544784039]], "arm_r": [[93.2243080852167, 149.21697655465394], [122.76854067558295, 144.0866744047689], [152.3127732659492, 138.9563722548839]], "arm_l": [[99.13315460328995, 147.3663321520059], [128.67738719365622, 142.23603000212086], [158.22161978402247, 137.10572785223582]], "leg_r": [[172.0, 117.2155372436685], [217.61231681635738, 111.34649988822532], [205.0, 148.72938534005917], [215.7014503494387, 151.23609550620586]], "leg_l": [[177.90884651807326, 116.7540782623559], [223.61346075114872, 111.62851219693835], [209.00000000000003, 148.29784924398507], [219.55091873231555, 151.36155338463374]]}], "tween": {"t": 0.5, "sample": {"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[170.0, 130.0], [127.02099954612248, 136.73033567600945], [86.57106738965523, 145.95306097398856]], "head": [68.02739029831947, 138.57475142347892], "nose": [[70.82298545434472, 128.97346777369498], [72.75244694506371, 122.34686197785895]], "arm_r": [[86.57106738965523, 147.86319092832477], [116.52995343229244, 146.316965264244], [146.48883947492965, 144.77073960016327]], "arm_l": [[92.47991390772847, 146.01254652567673], [122.43879995036569, 144.46632086159596], [152.3976859930029, 142.9200951975152]], "leg_r": [[170.0, 131.2155372436685], [210.61089504167376, 109.93924028266386], [205.0, 148.94207679788823], [215.98427899607043, 149.52103566600155]], "leg_l": [[175.90884651807326, 130.7540782623559], [216.72361168994397, 109.85955478107854], [209.0, 148.51054070181414], [219.9370385291809, 149.6679258108433]]}}}, {"name": "Goblet Squat", "camera": 0.0, "frames": [{"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 66.0], [163.7912748095231, 23.3238785947173], [167.40823813355095, -17.390122286184578]], "head": [169.8456250016539, -36.939465193360675], "nose": [[179.8441478685724, -36.76759175239952], [186.84308173466712, -36.64728089622242]], "arm_r": [[167.40823813355095, -15.47999233184835], [161.58327027278116, 13.501977080510542], [185.0, -4.965643768567148]], "arm_l": [[173.3170846516242, -17.33063673449639], [165.11280321558735, 11.087334645808118], [189.0, -6.7863652859766805]], "leg_r": [[160.0, 67.2155372436685], [173.9610906185993, 110.37987505887354], [162.0, 147.96977299066955], [172.496690241954, 151.20910105398104]], "leg_l": [[165.90884651807326, 66.7540782623559], [178.83656733035028, 110.22945395799843], [166.0, 147.53823689459546], [176.4181910389946, 151.0146634275683]]}, {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 104.0], [167.67276839345243, 64.77395322209524], [179.086797280632, 25.399263623249595]], "head": [186.57892914895024, 7.1373066543472845], "nose": [[196.55530157908487, 7.824324219806698], [203.53824993090362, 8.305201232968749]], "arm_r": [[179.086797280632, 27.309393577585823], [171.41227156937236, 55.87054440998645], [196.0, 38.9432027495061]], "arm_l": [[184.99564379870526, 25.458749174937783], [174.87887031847484, 53.27239289284542], [200.0, 37.12248123209657]], "leg_r": [[150.0, 105.2155372436685], [192.35367092323057, 122.89185045923996], [162.0, 148.54707837620566], [171.58691261951185, 153.8587544047633]], "leg_l": [[155.90884651807326, 104.7540782623559], [197.5802282343205, 123.93877901322136], [166.0, 148.11554228013156], [175.3143251279279, 153.87842212811603]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[155.0, 85.0], [165.87049934594478, 43.50075172802916], [173.41160389132622, 3.2917374825404693]], "head": [178.41920397241503, -15.777048563556235], "nose": [[188.40997278727033, -15.347469131885184], [195.40331033834335, -15.046772155872004]], "arm_r": [[173.41160389132622, 5.201867436876697], [165.81924442435937, 33.78430844561579], [190.5, 16.98877949046944]], "arm_l": [[179.32045040939948, 3.351223034228658], [169.30026135410338, 31.198755194030664], [194.5, 15.168057973059907]], "leg_r": [[155.0, 86.2155372436685], [187.70233881532357, 118.07459372668339], [162.0, 148.25842568343762], [171.52792396979862, 153.672062223347]], "leg_l": [[160.90884651807326, 85.7540782623559], [192.7002154063726, 118.49517970682211], [166.0, 147.82688958736352], [175.34124224802244, 153.54735913060154]]}}}, {"name": "Hip Circles", "camera": 45.0, "frames": [{"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 72.0], [159.4441210227361, 29.052652315278735], [156.867198580654, -12.126112205712047]], "head": [155.88069306833134, -31.945593306627956], "nose": [[165.85174201672737, -31.185209937076426], [170.78943214897384, -30.80866605688989]], "arm_r": [[149.08902398760196, -10.775446362053344], [131.8875970946982, 10.825618436816505], [159.08844008591993, 3.134054379460707]], "arm_l": [[168.82355861562613, -12.084049568730713], [184.12264494790446, 3.873360335346298], [192.6819593838462, -0.5811362594590932]], "leg_r": [[155.05025253169416, 72.85951462778281], [170.27809415713818, 116.23629730305997], [154.0, 149.79871604858775], [162.52693632902785, 155.58828277543466]], "leg_l": [[169.12793291022595, 72.53321385285722], [185.70098094242752, 115.44046187013774], [166.0, 148.10084546808693], [174.8346073365813, 154.25803608271772]]}, {"order": ["leg_l", "leg_r", "spine", "arm_l", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[145.0, 67.0], [142.7455452100055, 24.369372531538616], [132.582347706998, -12.53794996469125]], "head": [128.68424683806742, -30.794211269113987], "nose": [[138.53680458124134, -29.08333257165363], [143.48655204954719, -28.223817943870824]], "arm_r": [[125.10548640637649, -8.253658769857907], [111.30892979662221, 17.798644034020015], [137.0966365575342, 8.96567608177466]], "arm_l": [[144.23739444953964, -15.429512678884556], [161.6823363014897, -7.278796034128369], [169.55065570847063, -5.843835992193469]], "leg_r": [[140.05025253169416, 67.85951462778281], [156.0161760072335, 110.05383320510516], [154.0, 149.7227548136488], [164.73580332864196, 149.98477051650372]], "leg_l": [[154.12793291022595, 67.53321385285722], [172.15197971679743, 108.65115236715383], [166.0, 148.024884233148], [176.5089784339167, 149.4096928711608]]}, {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 63.0], [160.5558789772639, 20.245707058089195], [163.132801419346, -20.038101690788437]], "head": [164.11930693166866, -39.51497302275793], "nose": [[173.77107388531098, -36.8989847443863], [178.70876401755748, -35.560686837875664]], "arm_r": [[155.35462682629398, -18.687435847129734], [136.26937282033094, 2.2177744861050854], [164.21333813717047, -1.7897275383506326]], "arm_l": [[175.08916145431814, -19.996039053807102], [188.5044206735372, -4.734483615365129], [197.80685743509673, -5.504918177270433]], "leg_r": [[155.05025253169416, 63.85951462778281], [155.05915304318088, 109.27844419331313], [154.21378610932078, 148.62017241502275], [161.9880118482433, 150.1782104752242]], "leg_l": [[169.12793291022595, 63.53321385285723], [170.96275690184086, 109.06990579364435], [167.57786367283106, 147.57341467764627], [175.3006823934713, 150.2235671631545]]}, {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[175.0, 67.0], [177.2544547899945, 23.586408599708555], [187.417652293002, -16.850555347853984]], "head": [191.31575316193258, -36.46061287678006], "nose": [[201.1683109051065, -34.74973417931971], [206.11805837341234, -33.890219551536894]], "arm_r": [[179.94079099238047, -18.53815986364468], [158.70043603116835, -3.669811924778571], [187.42453366575347, -5.856419765892781]], "arm_l": [[199.07269903554362, -13.77022235142325], [209.07384253603584, 8.149653195646259], [219.8785528166899, 1.9184552781877287]], "leg_r": [[170.05025253169416, 67.85951462778281], [169.1103843231303, 113.37006357914862], [154.0, 149.7227548136488], [164.0955116276239, 154.04375169498837]], "leg_l": [[184.12793291022595, 67.53321385285722], [184.15710984832504, 113.073700371891], [166.0, 148.02488423314801], [175.72854456534554, 153.14721573509743]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[152.5, 69.5], [151.1060183087905, 26.75662358296359], [144.68658356094633, -12.85629329445861]], "head": [142.22797809252467, -32.09226080032802], "nose": [[152.15022272696726, -30.847647842708234], [157.090932702051, -30.227901819365403]], "arm_r": [[137.01245872650412, -10.006774915193887], [121.4328142515516, 14.075330717271115], [147.9656772757711, 5.706404729478997]], "arm_l": [[156.53889383730868, -14.313083193083294], [173.02499855449702, -2.137474215169938], [181.16570342816195, -3.677114282418678]], "leg_r": [[147.55025253169416, 70.35951462778281], [165.4435261192274, 112.61080343795655], [154.0, 150.2506348449002], [164.40256499711734, 153.63504937924387]], "leg_l": [[161.62793291022595, 70.03321385285722], [180.5764911170924, 111.62790675116207], [166.0, 148.40794644497956], [176.233437431852, 152.43961324032162]]}}}, {"name": "Hip Thrust", "camera": 0.0, "frames": [{"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 136.0], [107.30297316079633, 126.97118259325977], [69.2548115566876, 110.21707042748173]], "head": [60.175001561896664, 92.66766776737846], "nose": [[68.781162701048, 87.5751251365347], [74.78133380596279, 84.02463071165936]], "arm_r": [[69.2548115566876, 112.12720038181797], [99.25399859551796, 112.34470027256535], [124.24702183306655, 96.00315788768978]], "arm_l": [[75.16365807476085, 110.27655597916991], [105.08630887497687, 108.15636549973091], [124.9919151500325, 86.05260088982135]], "leg_r": [[150.0, 137.2155372436685], [187.9153388743531, 111.56461136348543], [203.0, 148.0484225268028], [212.7875096801391, 143.104270330527]], "leg_l": [[155.90884651807326, 136.7540782623559], [193.43783057440342, 110.55790777790622], [207.0, 147.6168864307287], [216.98400043067375, 143.06970414754042]]}, {"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[152.0, 114.0], [108.48104389225882, 113.9737793615104], [67.06410864858307, 111.09623166805618]], "head": [54.47770082758632, 95.78944430675715], "nose": [[61.84307038470169, 89.02547126666317], [66.96254629603588, 84.32401035894505]], "arm_r": [[67.06410864858307, 113.0063616223924], [96.52714518752813, 107.4415897188251], [121.83726912974487, 91.58044756166865]], "arm_l": [[72.97295516665632, 111.15571721974436], [101.48303474405499, 101.96078907057003], [118.96500119444003, 77.9512477713769]], "leg_r": [[152.0, 115.2155372436685], [197.52072750459536, 108.69321211492046], [203.0, 147.71419309307137], [213.72351917568668, 145.30068334308834]], "leg_l": [[157.90884651807326, 114.7540782623559], [203.4023403087058, 108.05000190891487], [207.0, 147.28265699699728], [217.82750079408547, 145.37171715459846]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[151.0, 125.0], [107.66880918157864, 120.45952464630693], [67.33627381329264, 110.50684590755573]], "head": [56.44349311299209, 93.98826035683962], "nose": [[64.47381860088683, 88.02890866739654], [70.06426717121788, 83.88020396264102]], "arm_r": [[67.33627381329264, 112.41697586189196], [97.22215189743444, 109.8424444392641], [122.61084176884933, 94.1035518711111]], "arm_l": [[73.24512033136588, 110.56633145924391], [102.70102224857601, 104.96504811148836], [121.73528941335792, 82.12904288061985]], "leg_r": [[151.0, 126.2155372436685], [193.77452083569057, 109.55101958727552], [203.0, 147.88130780993708], [213.4415940186742, 144.47365747936155]], "leg_l": [[156.90884651807326, 125.7540782623559], [199.54443640867484, 108.7477721024602], [207.0, 147.449771713863], [217.5874176218285, 144.51072057311362]]}}}, {"name": "Hollow Body Hold", "camera": 0.0, "frames": [{"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[185.0, 148.0], [141.5, 148.0], [100.0, 148.0]], "head": [80.43704798532389, 143.90493909881587], "nose": [[80.43704798532389, 133.90493909881587], [80.43704798532389, 127.01128482773042]], "arm_r": [[100.0, 149.91012995433624], [70.0, 149.9101299543362], [40.0, 149.9101299543362]], "arm_l": [[105.90884651807325, 148.05948555168817], [75.90884651807325, 148.05948555168817], [45.90884651807325, 148.05948555168817]], "leg_r": [[185.0, 149.2155372436685], [231.0, 149.2155372436685], [271.0, 149.2155372436685], [271.0, 138.38265196053422]], "leg_l": [[190.90884651807326, 148.7540782623559], [236.90884651807326, 148.7540782623559], [276.90884651807323, 148.7540782623559], [276.90884651807323, 137.92119297922162]]}, {"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[185.0, 148.0], [140.58688278525045, 148.43364741865258], [102.76846842615609, 129.42694930442767]], "head": [87.44757956377653, 116.76650487323789], "nose": [[93.93309205085959, 109.15479496153674], [98.43260531866537, 103.8739494143883]], "arm_r": [[102.76846842615609, 131.3370792587639], [75.5792348150566, 118.85114703696041], [48.3900012039571, 106.3652148151569]], "arm_l": [[108.67731494422934, 129.48643485611586], [81.48808133312984, 117.00050263431234], [54.298847722030345, 104.51457041250885]], "leg_r": [[185.0, 149.2155372436685], [229.99478963375506, 139.79689717094502], [269.12069366310726, 131.60677536857676], [266.8336650641119, 121.01061461985441]], "leg_l": [[190.90884651807326, 148.7540782623559], [235.90363615182832, 139.3354381896324], [275.0295401811805, 131.14531638726413], [272.74251158218516, 120.54915563854179]]}], "tween": {"t": 0.5, "sample": {"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[185.0, 148.0], [141.26935782158634, 148.2668263271459], [100.6970043790406, 138.60132020497852]], "head": [82.72112345305727, 129.96709412388378], "nose": [[86.18778863202375, 120.58720966100572], [88.58192963530344, 114.10929361221746]], "arm_r": [[100.6970043790406, 140.51145015931476], [71.40812416544262, 134.11690786335836], [42.11924395184462, 127.72236556740197]], "arm_l": [[106.60585089711385, 138.6608057566667], [77.31697068351586, 132.26626346071032], [48.02809046991787, 125.87172116475392]], "leg_r": [[185.0, 149.2155372436685], [230.74800718694058, 144.48027695599242], [270.5288830016715, 140.36265931453494], [269.3790699057273, 129.58911771044515]], "leg_l": [[190.90884651807326, 148.7540782623559], [236.65685370501384, 144.01881797467982], [276.4377295197447, 139.90120033322233], [275.28791642380054, 129.12765872913255]]}}}, {"name": "Incline Bench Press", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[148.0, 122.0], [121.21872582333387, 88.24229916701316], [95.66877459731904, 56.03667653324413]], "head": [88.82837173080566, 37.52834496527767], "nose": [[97.98669786795362, 33.51274259880355], [104.38151607145183, 30.7088408020571]], "arm_r": [[95.66877459731904, 57.94680648758036], [125.026216615892, 64.02878958265757], [106.0, 41.18627870131074]], "arm_l": [[101.57762111539229, 56.096162084932324], [131.28947904201019, 60.18109651350132], [110.0, 39.365557183901196]], "leg_r": [[148.0, 123.2155372436685], [191.96163186669585, 109.8796648279379], [198.0, 148.8205388219859], [208.68783472873713, 146.25810977141558]], "leg_l": [[153.90884651807326, 122.7540782623559], [197.8060125288814, 109.21381982938716], [201.99999999999997, 148.3890027259118], [212.79722051369865, 146.31855615798008]]}, {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[148.0, 122.0], [121.21872582333387, 88.24229916701316], [95.66877459731904, 56.03667653324413]], "head": [88.82837173080566, 37.52834496527767], "nose": [[97.98669786795362, 33.51274259880355], [104.38151607145183, 30.7088408020571]], "arm_r": [[95.66877459731904, 57.94680648758036], [104.38394002450866, 29.676718628853195], [97.99999999999999, 0.8091608278101887]], "arm_l": [[101.57762111539229, 56.096162084932324], [109.48815338938583, 27.5975292847013], [102.0, -1.01156068959933]], "leg_r": [[148.0, 123.2155372436685], [191.96163186669585, 109.8796648279379], [198.0, 148.8205388219859], [208.68783472873713, 146.25810977141558]], "leg_l": [[153.90884651807326, 122.7540782623559], [197.8060125288814, 109.21381982938716], [201.99999999999997, 148.3890027259118], [212.79722051369865, 146.31855615798008]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[148.0, 122.0], [121.21872582333387, 88.24229916701316], [95.66877459731904, 56.03667653324413]], "head": [88.82837173080566, 37.52834496527767], "nose": [[97.98669786795362, 33.51274259880355], [104.38151607145183, 30.7088408020571]], "arm_r": [[95.66877459731904, 57.94680648758036], [121.70710419240449, 43.273323253551595], [102.0, 20.997719764560486]], "arm_l": [[101.57762111539229, 56.096162084932324], [126.94689567822562, 40.32693556903181], [105.99999999999999, 19.17699824715095]], "leg_r": [[148.0, 123.2155372436685], [191.96163186669585, 109.8796648279379], [198.0, 148.8205388219859], [208.68783472873713, 146.25810977141558]], "leg_l": [[153.90884651807326, 122.7540782623559], [197.8060125288814, 109.21381982938716], [201.99999999999997, 148.3890027259118], [212.79722051369865, 146.31855615798008]]}}}, {"name": "Kneeling Hip Flexor Stretch", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 108.0], [167.48803432654196, 65.23222110289855], [189.54080710454963, 29.92034407316092]], "head": [200.72466517396458, 13.5914914933484], "nose": [[210.01618295035192, 17.288476401496972], [216.50646993231942, 19.87088474417535]], "arm_r": [[189.54080710454963, 31.830474027497146], [194.52030489904934, 60.96488587692039], [200.33667189428212, 89.9485305307548]], "arm_l": [[195.4496536226229, 29.979829624849106], [199.76126870996237, 59.21734341322912], [204.91426784576808, 88.32247976825207]], "leg_r": [[160.0, 109.2155372436685], [205.95650014293446, 111.18517604332762], [206.0, 150.57746287018125], [216.93848396248202, 149.43340250036692]], "leg_l": [[165.90884651807326, 108.7540782623559], [150.1759199250925, 151.32324086867877], [110.27335791469952, 148.57537222203848], [100.84451760697628, 154.15472060398542]]}, {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[162.0, 110.0], [172.77124244752997, 67.43012826213044], [202.09680242673178, 37.003417838858354]], "head": [216.23893805046274, 23.076133032457975], "nose": [[224.75555327463707, 28.31705519590993], [230.69188994773205, 31.970135395121623]], "arm_r": [[202.09680242673178, 38.91354779319458], [204.2210542863234, 68.38362245013457], [207.26105622745138, 97.77577678161127]], "arm_l": [[208.00564894480505, 37.062903390546545], [209.3595764755619, 66.57703280205601], [211.63136747047088, 96.03643315671671]], "leg_r": [[162.0, 111.2155372436685], [207.99997279057675, 111.26480978658026], [210.0, 150.60784736415684], [220.86856580612752, 148.93823445938295]], "leg_l": [[167.90884651807326, 110.7540782623559], [142.18597295841892, 148.3104391959247], [102.1859729584189, 148.3104391959247], [93.17530047123999, 154.52392693202455]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[161.0, 109.0], [170.11579656407298, 66.31583998394368], [195.919329904345, 33.27177313446985]], "head": [208.6408943099003, 18.073735691769016], "nose": [[217.57877685273942, 22.558627624738115], [223.815822522058, 25.688281172299863]], "arm_r": [[195.919329904345, 35.18190308880608], [199.52031557113526, 64.51252850700274], [203.99416062595552, 93.72639378717997]], "arm_l": [[201.82817642241827, 33.331258686158044], [204.71258284238976, 62.73861748692094], [208.4733956607904, 92.04978347246244]], "leg_r": [[161.0, 110.2155372436685], [206.98884942993953, 111.21293316469936], [208.0, 150.59265511716904], [218.9071791020003, 149.18833029841562]], "leg_l": [[166.90884651807326, 109.7540782623559], [146.0252835300541, 150.11770438059347], [106.04965044929027, 148.74293258343957], [96.8242742018906, 154.64294477047432]]}}}, {"name": "Lat Pull Down", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 122.0], [146.62433082233014, 79.24791483349992], [147.74955388155342, 38.34937572031629]], "head": [149.49266873650657, 18.728170476508154], "nose": [[155.28735587215587, 10.578225630072646], [159.30239092661319, 4.931274641291457]], "arm_r": [[147.74955388155342, 40.259505674652516], [174.77136716083461, 27.425922423252715], [194.0, 4.748391839859067]], "arm_l": [[153.6584003996267, 38.40886127200448], [181.60676431735172, 27.670890621268786], [198.0, 2.9276703224495346]], "leg_r": [[150.0, 123.2155372436685], [194.41395436835177, 111.42246307048075], [193.0, 150.79015432801035], [203.99312537163428, 151.17308446470886]], "leg_l": [[155.90884651807326, 122.7540782623559], [200.07727426166045, 110.09824525529687], [197.0, 149.373810478924], [207.96739988435445, 150.20720445545675]]}, {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 122.0], [146.62433082233014, 79.24791483349992], [147.74955388155342, 38.34937572031629]], "head": [149.49266873650657, 18.728170476508154], "nose": [[158.18575082167658, 13.785466372427464], [164.24792864816766, 10.338639236884735]], "arm_r": [[147.74955388155342, 40.259505674652516], [171.72345780323056, 58.020259740573785], [194.0, 38.231855442274124]], "arm_l": [[153.6584003996267, 38.40886127200448], [176.75165320875354, 57.2673942535663], [198.0, 36.41113392486461]], "leg_r": [[150.0, 123.2155372436685], [194.41395436835177, 111.42246307048075], [193.0, 150.79015432801035], [203.99312537163428, 151.17308446470886]], "leg_l": [[155.90884651807326, 122.7540782623559], [200.07727426166045, 110.09824525529687], [197.0, 149.373810478924], [207.96739988435445, 150.20720445545675]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 122.0], [146.62433082233014, 79.24791483349992], [147.74955388155342, 38.34937572031629]], "head": [149.49266873650657, 18.728170476508154], "nose": [[156.91670692063303, 12.028643848407304], [162.0776482783039, 7.371358528174099]], "arm_r": [[147.74955388155342, 40.259505674652516], [177.18620166864008, 45.95811146381125], [194.0, 21.49012364106659]], "arm_l": [[153.6584003996267, 38.40886127200448], [182.86086618586543, 45.17585880681934], [198.0, 19.66940212365707]], "leg_r": [[150.0, 123.2155372436685], [194.41395436835177, 111.42246307048075], [193.0, 150.79015432801035], [203.99312537163428, 151.17308446470886]], "leg_l": [[155.90884651807326, 122.7540782623559], [200.07727426166045, 110.09824525529687], [197.0, 149.373810478924], [207.96739988435445, 150.20720445545675]]}}, "orbit": [{"yaw": 37.0, "sample": {"order": ["spine", "leg_l", "arm_l", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 122.0], [147.3040707245407, 78.89514362473935], [148.20271381636047, 38.114194914475945]], "head": [149.59482723761698, 18.67515228851495], "nose": [[154.82362622309998, 10.15108863498608], [158.03017579167425, 4.923725199021456]], "arm_r": [[141.58274856168794, 39.63969252481388], [163.16332819238937, 29.629999065841375], [178.51999718740836, 8.961939106629913]], "arm_l": [[159.54169372378558, 38.16170218838887], [181.8622495962053, 30.344449654035888], [194.95446973694257, 7.314389402273139]], "leg_r": [[145.78729483793566, 122.97077120657869], [181.25785593812145, 115.81913656544059], [180.12862176996924, 155.0390638196267], [188.90812205815823, 156.57082043229062]], "leg_l": [[158.93171981481694, 122.60223367767217], [194.20619463380368, 114.56218166811979], [191.74857413428708, 153.51615916977624], [200.50752913482114, 155.49569120384058]], "props": {"bg": [{"kind": "poly", "pts": [[125.25533829692662, 127.16784412500894], [154.00621665862917, 130.9299910792746], [163.4889702694989, 131.4809567729892], [134.73809190779636, 127.71880981872354]], "w": 8, "color": "equipment"}, {"kind": "line", "pts": [[144.1631461190869, 131.31225668434325], [142.28207264195407, 149.20296280244077]], "w": 5, "color": "equipment"}, {"kind": "circle", "c": [258.197452678252, 32.37490189145329], "r": 4, "fill": true, "w": 3, "color": "prop"}, {"kind": "line", "pts": [[258.197452678252, 32.37490189145329], [186.73723346217548, 8.138164254451526]], "w": 4, "color": "equipment"}], "fg": [{"kind": "line", "pts": [[201.18079401782464, 4.809805831896043], [172.29367290652633, 11.46652267700701]], "w": 4, "color": "prop"}]}}}, {"yaw": 180.0, "sample": {"order": ["leg_r", "leg_l", "spine", "arm_r", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[150.0, 122.0], [153.37566917766986, 79.24791483349992], [152.25044611844658, 38.34937572031629]], "head": [150.50733126349343, 18.728170476508154], "nose": [[144.71264412784413, 10.578225630072646], [140.69760907338681, 4.931274641291457]], "arm_r": [[158.15929263651984, 38.40886127200448], [131.13747935723865, 25.575278020604674], [111.90884651807325, 2.897747437211029]], "arm_l": [[152.25044611844658, 40.259505674652516], [124.30208220072154, 29.521535023916826], [107.90884651807326, 4.778314725097573]], "leg_r": [[155.90884651807326, 122.7540782623559], [111.49489214972148, 110.96100408916814], [112.90884651807325, 150.3286953466977], [101.91572114643897, 150.71162548339626]], "leg_l": [[150.0, 123.2155372436685], [105.83157225641283, 110.55970423660949], [108.90884651807326, 149.83526946023665], [97.94144663371881, 150.66866343676938]], "props": {"bg": [{"kind": "poly", "pts": [[174.0, 126.78137981968192], [138.0, 126.78137981968192], [138.0, 132.25370211289263], [174.0, 132.25370211289263]], "w": 8, "color": "equipment"}, {"kind": "line", "pts": [[156.0, 131.39692620785908], [156.0, 148.31139338200543]], "w": 5, "color": "equipment"}, {"kind": "circle", "c": [28.0, 25.21166005905144], "r": 4, "fill": true, "w": 3, "color": "prop"}, {"kind": "line", "pts": [[28.0, 25.21166005905144], [109.90884651807326, 3.838031081154301]], "w": 4, "color": "equipment"}], "fg": [{"kind": "line", "pts": [[109.90884651807326, 8.005587345160627], [109.90884651807326, -0.32952518285202625]], "w": 4, "color": "prop"}]}}}]}, {"name": "Lateral Raise", "camera": 90.0, "frames": [{"order": ["leg_l", "leg_r", "spine", "arm_l", "arm_r", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 64.0], [160.0, 21.160862743968956], [160.0, -19.708659006037678]], "head": [160.0, -39.40481406628183], "arm_r": [[149.0, -19.708659006037678], [144.82480697119803, 9.8366392933887], [140.71304450611134, 39.47761343854478]], "arm_l": [[171.0, -19.708659006037678], [175.17519302880197, 9.8366392933887], [179.28695549388866, 39.47761343854478]], "leg_r": [[153.0, 64.0], [148.19169068968793, 109.05299216255729], [144.0105521589818, 148.22950708652013], [144.0105521589818, 150.13963704085637]], "leg_l": [[167.0, 64.0], [171.80830931031207, 109.05299216255729], [175.9894478410182, 148.22950708652013], [175.9894478410182, 150.13963704085637]]}, {"order": ["leg_l", "leg_r", "spine", "arm_l", "arm_r", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 64.0], [160.0, 21.160862743968956], [160.0, -19.708659006037678]], "head": [160.0, -39.40481406628183], "arm_r": [[149.0, -19.708659006037678], [119.01827518942713, -18.667409597798567], [89.69172300010406, -16.99693330126719]], "arm_l": [[171.0, -19.708659006037678], [200.98172481057287, -18.667409597798567], [230.30827699989595, -16.99693330126719]], "leg_r": [[153.0, 64.0], [148.19169068968793, 109.05299216255729], [144.0105521589818, 148.22950708652013], [144.0105521589818, 150.13963704085637]], "leg_l": [[167.0, 64.0], [171.80830931031207, 109.05299216255729], [175.9894478410182, 148.22950708652013], [175.9894478410182, 150.13963704085637]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "leg_r", "spine", "arm_l", "arm_r", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 64.0], [160.0, 21.160862743968956], [160.0, -19.708659006037678]], "head": [160.0, -39.40481406628183], "arm_r": [[149.0, -19.708659006037678], [126.70565523567817, 0.25529216051033654], [104.82092037786576, 20.450798446546216]], "arm_l": [[171.0, -19.708659006037678], [193.29434476432183, 0.25529216051033643], [215.17907962213422, 20.450798446546216]], "leg_r": [[153.0, 64.0], [148.19169068968793, 109.05299216255729], [144.0105521589818, 148.22950708652013], [144.0105521589818, 150.13963704085637]], "leg_l": [[167.0, 64.0], [171.80830931031207, 109.05299216255729], [175.9894478410182, 148.22950708652013], [175.9894478410182, 150.13963704085637]]}}}, {"name": "Leg Curl", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[118.0, 116.0], [101.72808522765135, 76.19044808081628], [91.71636489268228, 36.44745235385585]], "head": [97.22911200902226, 17.51429293718145], "nose": [[106.64265513311928, 20.888490004746693], [113.22050347862063, 23.246258626580946]], "arm_r": [[91.71636489268228, 38.35758230819208], [99.4809362457579, 66.89511958511801], [114.4809362457579, 92.48117554369131]], "arm_l": [[97.62521141075553, 36.50693790554404], [105.38978276383115, 65.04447518246997], [120.38978276383115, 90.63053114104326]], "leg_r": [[118.0, 117.2155372436685], [163.30115663856157, 109.34907394717814], [203.20371864895455, 106.60120530053786], [202.4363974377692, 95.79470838149437]], "leg_l": [[123.90884651807325, 116.7540782623559], [169.21000315663483, 108.88761496586552], [209.1125651670278, 106.13974631922524], [208.34524395584245, 95.33324940018176]]}, {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[118.0, 116.0], [101.72808522765135, 76.19044808081628], [91.71636489268228, 36.44745235385585]], "head": [97.22911200902226, 17.51429293718145], "nose": [[106.64265513311928, 20.888490004746693], [113.22050347862063, 23.246258626580946]], "arm_r": [[91.71636489268228, 38.35758230819208], [99.4809362457579, 66.89511958511801], [114.4809362457579, 92.48117554369131]], "arm_l": [[97.62521141075553, 36.50693790554404], [105.38978276383115, 65.04447518246997], [120.38978276383115, 90.63053114104326]], "leg_r": [[118.0, 117.2155372436685], [163.30115663856157, 109.34907394717814], [159.81492692865524, 148.59148443479444], [170.77306860766444, 149.53563259774617]], "leg_l": [[123.90884651807325, 116.7540782623559], [169.21000315663483, 108.88761496586552], [165.7237734467285, 148.1300254534818], [176.6819151257377, 149.07417361643354]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[118.0, 116.0], [101.72808522765135, 76.19044808081628], [91.71636489268228, 36.44745235385585]], "head": [97.22911200902226, 17.51429293718145], "nose": [[106.64265513311928, 20.888490004746693], [113.22050347862063, 23.246258626580946]], "arm_r": [[91.71636489268228, 38.35758230819208], [99.4809362457579, 66.89511958511801], [114.4809362457579, 92.48117554369131]], "arm_l": [[97.62521141075553, 36.50693790554404], [105.38978276383115, 65.04447518246997], [120.38978276383115, 90.63053114104326]], "leg_r": [[118.0, 117.2155372436685], [163.30115663856157, 109.34907394717814], [191.3375272105556, 137.44565683383726], [199.1832821512516, 129.8527871797909]], "leg_l": [[123.90884651807325, 116.7540782623559], [169.21000315663483, 108.88761496586552], [197.24637372862887, 136.98419785252463], [205.09212866932486, 129.3913281984783]]}}}, {"name": "Leg Extension", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[118.0, 116.0], [101.72808522765135, 76.19044808081628], [91.71636489268228, 36.44745235385585]], "head": [97.22911200902226, 17.51429293718145], "nose": [[106.64265513311928, 20.888490004746693], [113.22050347862063, 23.246258626580946]], "arm_r": [[91.71636489268228, 38.35758230819208], [99.4809362457579, 66.89511958511801], [114.4809362457579, 92.48117554369131]], "arm_l": [[97.62521141075553, 36.50693790554404], [105.38978276383115, 65.04447518246997], [120.38978276383115, 90.63053114104326]], "leg_r": [[118.0, 117.2155372436685], [163.30115663856157, 109.34907394717814], [168.17593037476746, 148.44775976153034], [179.093938042822, 147.12756314489147]], "leg_l": [[123.90884651807325, 116.7540782623559], [169.21000315663483, 108.88761496586552], [174.08477689284072, 147.9863007802177], [185.00278456089526, 146.66610416357884]]}, {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[118.0, 116.0], [101.72808522765135, 76.19044808081628], [91.71636489268228, 36.44745235385585]], "head": [97.22911200902226, 17.51429293718145], "nose": [[106.64265513311928, 20.888490004746693], [113.22050347862063, 23.246258626580946]], "arm_r": [[91.71636489268228, 38.35758230819208], [99.4809362457579, 66.89511958511801], [114.4809362457579, 92.48117554369131]], "arm_l": [[97.62521141075553, 36.50693790554404], [105.38978276383115, 65.04447518246997], [120.38978276383115, 90.63053114104326]], "leg_r": [[118.0, 117.2155372436685], [163.30115663856157, 109.34907394717814], [203.20371864895455, 106.60120530053786], [202.4363974377692, 95.79470838149437]], "leg_l": [[123.90884651807325, 116.7540782623559], [169.21000315663483, 108.88761496586552], [209.1125651670278, 106.13974631922524], [208.34524395584245, 95.33324940018176]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[118.0, 116.0], [101.72808522765135, 76.19044808081628], [91.71636489268228, 36.44745235385585]], "head": [97.22911200902226, 17.51429293718145], "nose": [[106.64265513311928, 20.888490004746693], [113.22050347862063, 23.246258626580946]], "arm_r": [[91.71636489268228, 38.35758230819208], [99.4809362457579, 66.89511958511801], [114.4809362457579, 92.48117554369131]], "arm_l": [[97.62521141075553, 36.50693790554404], [105.38978276383115, 65.04447518246997], [120.38978276383115, 90.63053114104326]], "leg_r": [[118.0, 117.2155372436685], [163.30115663856157, 109.34907394717814], [194.16613997407038, 134.4056644612481], [201.16300039712578, 126.04674386776264]], "leg_l": [[123.90884651807325, 116.7540782623559], [169.21000315663483, 108.88761496586552], [200.07498649214364, 133.94420547993548], [207.07184691519905, 125.58528488645004]]}}, "orbit": [{"yaw": 37.0, "sample": {"order": ["arm_l", "spine", "arm_r", "leg_l", "leg_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[118.0, 116.0], [105.00467104633924, 74.48996656412565], [97.00895567017038, 33.70070519368849]], "head": [101.4116312751903, 15.343650354052286], "nose": [[110.06318318500227, 20.358692682071943], [115.31648645342548, 23.403873307202012]], "arm_r": [[90.38899041549786, 35.22620280402643], [96.59005281836, 64.57516948281769], [108.5695854690694, 91.72878667233499]], "arm_l": [[108.34793557759552, 33.74821246760142], [114.54899798045767, 63.097179146392676], [126.52853063116706, 90.25079633590998]], "leg_r": [[113.78729483793566, 116.97077120657869], [149.9664071757056, 113.8384637009904], [153.85957458488554, 153.4465832699093], [162.57908320756252, 153.26736302257655]], "leg_l": [[126.93171981481694, 116.60223367767217], [163.11083215258688, 113.4699261720839], [167.00399956176682, 153.0780457410028], [175.7235081844438, 152.89882549367005]], "props": {"bg": [{"kind": "poly", "pts": [[106.24251462180918, 120.85205320267173], [94.80856445333131, 44.79327452542702], [104.29131806420105, 45.344240219141625], [115.72526823267891, 121.40301889638633]], "w": 9, "color": "equipment"}, {"kind": "poly", "pts": [[101.24169339739956, 122.2128849456383], [151.17511134858344, 124.7164255628519], [160.65786495945318, 125.2673912565665], [110.7244470082693, 122.7638506393529]], "w": 9, "color": "equipment"}, {"kind": "line", "pts": [[108.96860407889773, 124.8942403560915], [106.4605061093873, 148.74851518022155]], "w": 5, "color": "equipment"}, {"kind": "line", "pts": [[150.7066587654828, 128.34059638801995], [147.98955263184652, 154.18272744749413]], "w": 5, "color": "equipment"}, {"kind": "poly", "pts": [[100.30260356580132, 148.15832207268397], [149.81800518873348, 154.63757516058593], [155.74472619552708, 154.98192871915757], [106.22932457259492, 148.5026756312556]], "w": 5, "color": "equipment"}, {"kind": "circle", "c": [117.52256409344713, 89.73985261227406], "r": 3.5, "fill": true, "w": 3, "color": "prop"}], "fg": [{"kind": "circle", "c": [166.90947233550744, 146.11361907675857], "r": 5, "fill": true, "color": "prop"}]}}}, {"yaw": 180.0, "sample": {"order": ["leg_r", "arm_r", "leg_l", "spine", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[118.0, 116.0], [134.27191477234865, 76.19044808081628], [144.2836351073177, 36.44745235385585]], "head": [138.77088799097774, 17.51429293718145], "nose": [[129.35734486688074, 20.888490004746693], [122.77949652137937, 23.246258626580946]], "arm_r": [[150.19248162539097, 36.50693790554404], [142.42791027231536, 65.04447518246997], [127.42791027231534, 90.63053114104326]], "arm_l": [[144.2836351073177, 38.35758230819208], [136.5190637542421, 66.89511958511801], [121.5190637542421, 92.48117554369131]], "leg_r": [[123.90884651807325, 116.7540782623559], [78.60768987951168, 108.88761496586552], [73.73291614330579, 147.9863007802177], [62.81490847525124, 146.66610416357884]], "leg_l": [[118.0, 117.2155372436685], [72.69884336143843, 109.34907394717814], [67.82406962523254, 148.44775976153034], [56.90606195717799, 147.12756314489147]], "props": {"bg": [{"kind": "poly", "pts": [[126.0, 118.9019945781101], [150.0, 49.364740639952885], [150.0, 54.83706293316358], [126.0, 124.3743168713208]], "w": 9, "color": "equipment"}, {"kind": "poly", "pts": [[132.0, 120.78137981968192], [70.0, 117.02260933653828], [70.0, 122.49493162974898], [132.0, 126.25370211289261]], "w": 9, "color": "equipment"}, {"kind": "line", "pts": [[128.0, 125.39692620785908], [128.0, 147.9495491067209]], "w": 5, "color": "equipment"}, {"kind": "line", "pts": [[76.0, 123.51754096628727], [76.0, 147.9495491067209]], "w": 5, "color": "equipment"}, {"kind": "poly", "pts": [[132.0, 146.23944839009255], [70.0, 146.23944839009255], [70.0, 149.65964982334924], [132.0, 149.65964982334924]], "w": 5, "color": "equipment"}, {"kind": "circle", "c": [122.0, 91.56799185956638], "r": 3.5, "fill": true, "w": 3, "color": "prop"}], "fg": [{"kind": "circle", "c": [63.706002038882666, 140.75964282059232], "r": 5, "fill": true, "color": "prop"}]}}}]}, {"name": "Leg Press", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[140.0, 116.0], [119.59179949383865, 78.0835670547446], [105.41656376460136, 39.588898523569924]], "head": [110.59294466665177, 20.56387367228598], "nose": [[120.26197316975961, 23.115321850495402], [127.02345395378309, 24.899530866204653]], "arm_r": [[105.41656376460136, 41.49902847790615], [113.18113511767699, 70.03656575483208], [141.37191374125425, 80.14128841983603]], "arm_l": [[111.32541028267461, 39.648384075258114], [119.08998163575023, 68.18592135218404], [147.2807602593275, 78.29064401718799]], "leg_r": [[140.0, 117.2155372436685], [157.1292173153023, 75.17232014967723], [185.0, 103.4282287014976], [192.89024540879532, 95.88020391213522]], "leg_l": [[145.90884651807326, 116.7540782623559], [161.69967986127625, 74.2057150715518], [189.0, 102.9966926054235], [197.03965931177694, 95.60316169903263]]}, {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[140.0, 116.0], [119.59179949383865, 78.0835670547446], [105.41656376460136, 39.588898523569924]], "head": [110.59294466665177, 20.56387367228598], "nose": [[120.26197316975961, 23.115321850495402], [127.02345395378309, 24.899530866204653]], "arm_r": [[105.41656376460136, 41.49902847790615], [113.18113511767699, 70.03656575483208], [141.37191374125425, 80.14128841983603]], "arm_l": [[111.32541028267461, 39.648384075258114], [119.08998163575023, 68.18592135218404], [147.2807602593275, 78.29064401718799]], "leg_r": [[140.0, 117.2155372436685], [180.56387328382812, 95.8524219129624], [220.0, 102.44342094848537], [221.8404858503854, 91.76324503029944]], "leg_l": [[145.90884651807326, 116.7540782623559], [185.0654782344644, 92.98043985822271], [224.0, 102.0118848524113], [226.52196163749238, 91.46755465641775]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[140.0, 116.0], [119.59179949383865, 78.0835670547446], [105.41656376460136, 39.588898523569924]], "head": [110.59294466665177, 20.56387367228598], "nose": [[120.26197316975961, 23.115321850495402], [127.02345395378309, 24.899530866204653]], "arm_r": [[105.41656376460136, 41.49902847790615], [113.18113511767699, 70.03656575483208], [141.37191374125425, 80.14128841983603]], "arm_l": [[111.32541028267461, 39.648384075258114], [119.08998163575023, 68.18592135218404], [147.2807602593275, 78.29064401718799]], "leg_r": [[140.0, 117.2155372436685], [168.72442575335768, 81.83218317512427], [202.5, 102.9358248249915], [208.39302981821828, 93.78865179534496]], "leg_l": [[145.90884651807326, 116.7540782623559], [173.37889429197503, 80.41752162098928], [206.5, 102.5042887289174], [212.66756005027608, 93.53436026427744]]}}}, {"name": "Leg Raises", "camera": 0.0, "frames": [{"order": ["arm_l", "spine", "leg_l", "arm_r", "leg_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[185.0, 148.0], [141.5, 148.0], [100.0, 148.0]], "head": [80.43704798532389, 143.90493909881587], "nose": [[80.43704798532389, 133.90493909881587], [80.43704798532389, 127.01128482773042]], "arm_r": [[100.0, 149.91012995433624], [129.98172481057287, 150.94120880220163], [159.96344962114574, 151.97228765006705]], "arm_l": [[105.90884651807325, 148.05948555168817], [135.89057132864613, 149.09056439955359], [165.872296139219, 150.121643247419]], "leg_r": [[185.0, 149.2155372436685], [208.0, 109.98358477385614], [228.0, 75.8688434957584], [218.47372055837118, 70.45240085419125]], "leg_l": [[190.90884651807326, 148.7540782623559], [213.90884651807326, 109.5221257925435], [233.90884651807326, 75.4073845144458], [224.38256707644445, 69.99094187287865]]}, {"order": ["arm_l", "leg_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[185.0, 148.0], [141.5, 148.0], [100.0, 148.0]], "head": [80.43704798532389, 143.90493909881587], "nose": [[80.43704798532389, 133.90493909881587], [80.43704798532389, 127.01128482773042]], "arm_r": [[100.0, 149.91012995433624], [129.98172481057287, 150.94120880220163], [159.96344962114574, 151.97228765006705]], "arm_l": [[105.90884651807325, 148.05948555168817], [135.89057132864613, 149.09056439955359], [165.872296139219, 150.121643247419]], "leg_r": [[185.0, 149.2155372436685], [230.55233116211224, 142.91083479720226], [270.163053911775, 137.42848484375332], [268.6321498012143, 126.70102445552496]], "leg_l": [[190.90884651807326, 148.7540782623559], [236.4611776801855, 142.44937581588962], [276.07190042984826, 136.96702586244072], [274.54099631928756, 126.23956547421233]]}], "tween": {"t": 0.5, "sample": {"order": ["arm_l", "leg_l", "spine", "arm_r", "leg_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[185.0, 148.0], [141.5, 148.0], [100.0, 148.0]], "head": [80.43704798532389, 143.90493909881587], "nose": [[80.43704798532389, 133.90493909881587], [80.43704798532389, 127.01128482773042]], "arm_r": [[100.0, 149.91012995433624], [129.98172481057287, 150.94120880220163], [159.96344962114574, 151.97228765006705]], "arm_l": [[105.90884651807325, 148.05948555168817], [135.89057132864613, 149.09056439955359], [165.872296139219, 150.121643247419]], "leg_r": [[185.0, 149.2155372436685], [223.13572833753193, 123.88345193236816], [256.2972312397336, 101.85555166167222], [250.14610930155538, 92.87468274277532]], "leg_l": [[190.90884651807326, 148.7540782623559], [229.0445748556052, 123.42199295105556], [262.20607775780684, 101.39409268035959], [256.0549558196286, 92.41322376146272]]}}}, {"name": "Leg Swings", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[156.0, 66.0], [159.7912748095231, 23.3238785947173], [163.40823813355095, -17.390122286184578]], "head": [165.8456250016539, -36.939465193360675], "nose": [[175.8441478685724, -36.76759175239952], [182.84308173466712, -36.64728089622242]], "arm_r": [[163.40823813355095, -15.47999233184835], [163.93181032666945, 14.059740528793668], [169.65608018796578, 43.06116235359757]], "arm_l": [[169.3170846516242, -17.33063673449639], [191.25769570019932, 2.818481441438399], [216.9727147212627, 18.034886119475505]], "leg_r": [[156.0, 67.2155372436685], [198.34322325881226, 84.91610929892303], [232.62991528689673, 105.20464886963919], [238.93925608675823, 96.3308687444116]], "leg_l": [[161.90884651807326, 66.7540782623559], [152.7054359091143, 111.13927992793873], [124.14471158382098, 138.71890898230964], [131.846111137951, 146.45378523778777]]}, {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[156.0, 66.0], [159.7912748095231, 23.3238785947173], [163.40823813355095, -17.390122286184578]], "head": [165.8456250016539, -36.939465193360675], "nose": [[175.8441478685724, -36.76759175239952], [182.84308173466712, -36.64728089622242]], "arm_r": [[163.40823813355095, -15.47999233184835], [163.93181032666945, 14.059740528793668], [169.65608018796578, 43.06116235359757]], "arm_l": [[169.3170846516242, -17.33063673449639], [191.25769570019932, 2.818481441438399], [216.9727147212627, 18.034886119475505]], "leg_r": [[156.0, 67.2155372436685], [130.94660438930876, 105.20828401031557], [106.32014537628243, 136.24984799467126], [112.78578315149963, 145.01383628684118]], "leg_l": [[161.90884651807326, 66.7540782623559], [152.7054359091143, 111.13927992793873], [124.14471158382098, 138.71890898230964], [131.846111137951, 146.45378523778777]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[156.0, 66.0], [159.7912748095231, 23.3238785947173], [163.40823813355095, -17.390122286184578]], "head": [165.8456250016539, -36.939465193360675], "nose": [[175.8441478685724, -36.76759175239952], [182.84308173466712, -36.64728089622242]], "arm_r": [[163.40823813355095, -15.47999233184835], [163.93181032666945, 14.059740528793668], [169.65608018796578, 43.06116235359757]], "arm_l": [[169.3170846516242, -17.33063673449639], [191.25769570019932, 2.818481441438399], [216.9727147212627, 18.034886119475505]], "leg_r": [[156.0, 67.2155372436685], [169.4490984172459, 110.53724878775138], [176.7385194369318, 149.26993103400275], [187.73810059063766, 149.17539747602967]], "leg_l": [[161.90884651807326, 66.7540782623559], [152.7054359091143, 111.13927992793873], [124.14471158382098, 138.71890898230964], [131.846111137951, 146.45378523778777]]}}}, {"name": "Lunge", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 68.0], [164.54698815214294, 25.395540020190396], [168.88491937775055, -15.250094213421079]], "head": [171.66838139695184, -34.75456764656357], "nose": [[181.66247319251514, -34.41086838579267], [188.6582089816488, -34.17028332129074]], "arm_r": [[168.88491937775055, -13.339964259084852], [168.36134718463205, 16.199768601557167], [172.01742748678646, 45.523782962321306]], "arm_l": [[174.7937658958238, -15.19060866173289], [174.2701937027053, 14.349124198909129], [177.92627400485972, 43.67313855967327]], "leg_r": [[160.0, 69.2155372436685], [179.47137861645146, 110.25810678029066], [195.312257849331, 146.42974223223348], [204.99480167196958, 141.28905093810306]], "leg_l": [[165.90884651807326, 68.7540782623559], [144.50202796057948, 108.8509721197623], [124.83704329970969, 143.1540806833463], [133.08224180021978, 150.3247189060237]]}, {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 106.0], [166.05402989176287, 63.57777028291508], [171.82971358160557, 23.105987909144403]], "head": [175.30267713494416, 3.7090617012853215], "nose": [[185.2790495650788, 4.396079266744735], [192.26199791689754, 4.876956279906786]], "arm_r": [[171.82971358160557, 25.01611786348063], [170.25963489431726, 54.51986110987899], [172.87430717674698, 83.9516689755912]], "arm_l": [[177.73856009967884, 23.16547346083259], [176.16848141239052, 52.66921670723095], [178.78315369482024, 82.10102457294316]], "leg_r": [[160.0, 107.2155372436685], [205.88569496240868, 110.40713459933113], [196.0, 148.57746287018125], [206.99919882818972, 148.4467200883112]], "leg_l": [[165.90884651807326, 106.7540782623559], [163.40017792297274, 151.98781726819098], [124.0, 145.1915035150705], [115.3395832106557, 151.87059000314056]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 87.0], [165.3013164381239, 44.480179176892], [170.35889418943754, 3.915292644501605]], "head": [173.48758349024214, -15.538370063055481], "nose": [[183.47429126005107, -15.02294022259423], [190.4646980033331, -14.66215423430872]], "arm_r": [[170.35889418943754, 5.825422598837832], [169.3119092883625, 35.35165764096847], [172.4477631863921, 64.73404383394062]], "arm_l": [[176.2677407075108, 3.974778196189794], [175.22075580643576, 33.50101323832043], [178.35660970446537, 62.88339943129258]], "leg_r": [[160.0, 88.2155372436685], [200.77851063746203, 109.17860093471752], [196.0, 148.2888101774132], [206.98349694938457, 147.69563637304435]], "leg_l": [[165.90884651807326, 87.7540782623559], [162.09776564098584, 132.89949240048355], [124.0, 144.90285082230247], [122.23010981968788, 155.5945929197867]]}}}, {"name": "March in Place", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 62.0], [163.03440660786944, 19.26521672923713], [165.92930026825064, -21.504748919881465]], "head": [168.01986953360372, -41.09300638186289], "nose": [[178.01986953360372, -41.09300638186289], [185.01986953360372, -41.09300638186289]], "arm_r": [[165.92930026825064, -19.594618965545237], [150.0317223412545, 5.460311235117487], [173.01305563482384, 24.450977881902162]], "arm_l": [[171.8381467863239, -21.445263368193277], [186.8381467863239, 4.140792590380022], [215.3698422751785, -4.98887736580931]], "leg_r": [[160.0, 63.215537243668514], [201.34452612976168, 83.07425723018636], [185.07506040672968, 119.06092320680713], [194.40358946445036, 124.80147780556837]], "leg_l": [[165.90884651807326, 62.754078262355904], [164.4746241447424, 108.03321059644975], [162.0079625065513, 147.35054957930294], [172.98702739227483, 148.01857614327372]]}, {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [163.03440660786944, 21.26521672923713], [165.92930026825064, -19.504748919881465]], "head": [168.01986953360372, -39.09300638186289], "nose": [[178.01986953360372, -39.09300638186289], [185.01986953360372, -39.09300638186289]], "arm_r": [[165.92930026825064, -17.594618965545237], [165.40572807513215, 11.945113895096782], [192.1359238007832, 25.357914813218557]], "arm_l": [[171.8381467863239, -19.445263368193277], [171.3145745932054, 10.094469492448743], [198.04477031885645, 23.507270410570516]], "leg_r": [[160.0, 65.2155372436685], [168.06607636623892, 109.81480911859667], [158.0, 147.939388496694], [168.64599592855438, 150.6655047598624]], "leg_l": [[165.90884651807326, 64.7540782623559], [172.71840731660157, 109.55611956268012], [162.0, 147.50785240061987], [172.59772985997608, 150.4106343225862]]}, {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 62.0], [163.03440660786944, 19.26521672923713], [165.92930026825064, -21.504748919881465]], "head": [168.01986953360372, -41.09300638186289], "nose": [[178.01986953360372, -41.09300638186289], [185.01986953360372, -41.09300638186289]], "arm_r": [[165.92930026825064, -19.594618965545237], [180.92930026825064, 5.99143699302806], [209.46099575710525, -3.138232963161272]], "arm_l": [[171.8381467863239, -21.445263368193277], [155.94056885932775, 3.6096668324694487], [178.9219021528971, 22.60033347925412]], "leg_r": [[160.0, 63.215537243668514], [159.58457964721987, 108.51484653715457], [158.00131601773398, 147.876286549904], [168.99269577728379, 148.3050693816835]], "leg_l": [[165.90884651807326, 62.754078262355904], [207.25337264783494, 82.61279824887376], [190.98390692480294, 118.59946422549453], [200.31243598252362, 124.34001882425576]]}, {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [163.03440660786944, 21.26521672923713], [165.92930026825064, -19.504748919881465]], "head": [168.01986953360372, -39.09300638186289], "nose": [[178.01986953360372, -39.09300638186289], [185.01986953360372, -39.09300638186289]], "arm_r": [[165.92930026825064, -17.594618965545237], [165.40572807513215, 11.945113895096782], [192.1359238007832, 25.357914813218557]], "arm_l": [[171.8381467863239, -19.445263368193277], [171.3145745932054, 10.094469492448743], [198.04477031885645, 23.507270410570516]], "leg_r": [[160.0, 65.2155372436685], [168.06607636623892, 109.81480911859667], [158.0, 147.939388496694], [168.64599592855438, 150.6655047598624]], "leg_l": [[165.90884651807326, 64.7540782623559], [172.71840731660157, 109.55611956268012], [162.0, 147.50785240061987], [172.59772985997608, 150.4106343225862]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 63.0], [163.03440660786944, 20.26521672923713], [165.92930026825064, -20.504748919881465]], "head": [168.01986953360372, -40.09300638186289], "nose": [[178.01986953360372, -40.09300638186289], [185.01986953360372, -40.09300638186289]], "arm_r": [[165.92930026825064, -18.594618965545237], [157.40883992713296, 9.732974293633951], [182.425414589148, 26.03952896291895]], "arm_l": [[171.8381467863239, -20.445263368193277], [179.34954690795715, 8.157915700951795], [209.25706691995097, 10.475929473965058]], "leg_r": [[160.0, 64.2155372436685], [187.71521835779888, 100.37108340382309], [174.5027852113516, 137.55239037407463], [184.60663358284393, 141.83514346377513]], "leg_l": [[165.90884651807326, 63.754078262355904], [169.92855366558072, 108.88194074142741], [162.0, 147.49266015363207], [172.7817467986817, 149.63988796164233]]}}}, {"name": "Meditation", "camera": 45.0, "frames": [{"order": ["leg_l", "spine", "leg_r", "arm_l", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 143.0], [163.7689168190232, 101.11931123376914], [169.3887282349446, 61.96217263084776]], "head": [172.8100204928652, 43.44517871767739], "nose": [[182.33417946595938, 46.493193785755224], [187.25681169973322, 48.0685829897296]], "arm_r": [[161.6105536418926, 63.312838474506464], [151.15557297328326, 91.01247836372625], [174.0, 101.80912446929467]], "arm_l": [[181.34508826991674, 62.004235267829095], [190.67139017123895, 90.51411717867276], [176.0, 102.30287951513768]], "leg_r": [[155.05025253169416, 143.8595146277828], [154.99422568767014, 154.07209387802303], [171.43922706632657, 161.54015235787284], [178.1199652511979, 168.95216757827504]], "leg_l": [[169.12793291022595, 143.53321385285724], [215.0719060662019, 145.75797693041866], [181.22073988924336, 161.9598732499316], [187.0667241307739, 169.51684197139522]]}, {"order": ["leg_l", "spine", "arm_l", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 142.0], [162.66324919089547, 99.76546460013566], [163.1574490241838, 58.96797734479105]], "head": [164.14395453650644, 39.49110601282156], "nose": [[174.0667173929071, 40.731580637969245], [179.01344960879294, 41.34998660816219]], "arm_r": [[155.3792744311318, 60.318643188449755], [146.96348412260292, 88.25235828569913], [174.0, 99.20859541467401]], "arm_l": [[175.11380905915593, 59.01003998177239], [188.95848691690378, 85.4274806757818], [176.0, 99.66143129529777]], "leg_r": [[155.05025253169416, 142.8595146277828], [155.01873183104937, 152.51832215512067], [171.63230181855693, 160.22516072259677], [178.39224582806628, 167.4977154549932]], "leg_l": [[169.12793291022595, 142.53321385285724], [215.09641220958113, 144.20420520751628], [181.41381464147372, 160.64488161465553], [187.3390047076423, 168.06238984811338]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "spine", "arm_l", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 142.5], [163.21520615635515, 100.45385471006496], [166.28258674230318, 60.34086552541427]], "head": [168.49490416312793, 41.27136770640422], "nose": [[178.25664843118622, 43.44124070845555], [183.20338064707204, 44.540816807652355]], "arm_r": [[158.50441214925115, 61.691531369072976], [148.40811435988226, 89.92949865975466], [174.0, 100.46842362144986]], "arm_l": [[178.2389467772753, 60.38292816239561], [189.90484899006998, 87.96148452086629], [176.0, 100.98262552040433]], "leg_r": [[155.05025253169416, 143.3595146277828], [155.0073528953971, 153.29543427016455], [171.53631038856068, 160.88333799684227], [178.25689149245025, 168.2259053153988]], "leg_l": [[169.12793291022595, 143.03321385285724], [215.08503327392887, 144.9813173225602], [181.31782321147747, 161.30305888890103], [187.2036503720263, 168.79057970851895]]}}}, {"name": "Neck Rolls", "camera": 35.0, "frames": [{"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [160.0, 21.160862743968956], [160.0, -19.708659006037678]], "head": [171.38061648487894, -32.49312079333657], "nose": [[176.5398949555325, -23.926800366090653], [180.07013744825343, -18.065285320810304]], "arm_r": [[153.6906592001385, -18.143972149085513], [155.8324733454366, 11.548258390940887], [160.52152070437936, 41.119820372808604]], "arm_l": [[171.1495845045311, -19.659931294766643], [173.2913986498292, 10.032299245259756], [177.98044600877196, 39.603861227127474]], "leg_r": [[155.98496494554269, 64.99570981806046], [156.61457190991757, 110.40833397961296], [156.16054743205788, 149.78133298532248], [165.16982961073276, 150.99271411447162]], "leg_l": [[168.85527875912692, 64.61770475016273], [174.08875167395848, 109.83670152473152], [164.0, 147.88216467508], [174.58122841298848, 150.8132031809861]]}, {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [162.9139746738203, 19.823897743730882], [154.17205065235942, -17.03472900556153]], "head": [153.12453367726502, -35.148439987716216], "nose": [[162.23394763661693, -31.023082146611472], [166.73946689638586, -28.98267874750428]], "arm_r": [[148.05012423863502, -12.895807892791026], [154.33891855598083, 14.89374058327624], [163.11430413163794, 42.59044372578424]], "arm_l": [[165.13422077075342, -19.560235550108835], [171.42301508809922, 8.22931292595843], [180.19840066375633, 35.92601606846643]], "leg_r": [[155.98496494554269, 64.99570981806046], [156.61457190991757, 110.40833397961296], [156.16054743205788, 149.78133298532248], [165.16982961073276, 150.99271411447162]], "leg_l": [[168.85527875912692, 64.61770475016273], [174.08875167395848, 109.83670152473152], [164.0, 147.88216467508], [174.58122841298848, 150.8132031809861]]}, {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [160.0, 21.160862743968956], [160.0, -19.708659006037678]], "head": [153.33641694674432, -38.512215478516], "nose": [[161.32919767195267, -44.52182913321442], [165.9681531456001, -48.009767951558864]], "arm_r": [[153.6906592001385, -18.143972149085513], [155.8324733454366, 11.548258390940887], [160.52152070437936, 41.119820372808604]], "arm_l": [[171.1495845045311, -19.659931294766643], [173.2913986498292, 10.032299245259756], [177.98044600877196, 39.603861227127474]], "leg_r": [[155.98496494554269, 64.99570981806046], [156.61457190991757, 110.40833397961296], [156.16054743205788, 149.78133298532248], [165.16982961073276, 150.99271411447162]], "leg_l": [[168.85527875912692, 64.61770475016273], [174.08875167395848, 109.83670152473152], [164.0, 147.88216467508], [174.58122841298848, 150.8132031809861]]}, {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [157.0860253261797, 21.269201783645215], [165.82794934764058, -19.925337085390197]], "head": [170.30045449761838, -39.40798760388682], "nose": [[180.02217287718435, -37.065298604380274], [186.48013645549753, -35.509092217459234]], "arm_r": [[159.7060229339162, -21.027840257638886], [157.70085690716655, 8.818571392275864], [158.303566049395, 38.54205986896477]], "arm_l": [[176.79011946603458, -17.20941934491831], [174.78495343928498, 12.636992304996438], [175.38766258151338, 42.36048078168535]], "leg_r": [[155.98496494554269, 64.99570981806046], [156.61457190991757, 110.40833397961296], [156.16054743205788, 149.78133298532248], [165.16982961073276, 150.99271411447162]], "leg_l": [[168.85527875912692, 64.61770475016273], [174.08875167395848, 109.83670152473152], [164.0, 147.88216467508], [174.58122841298848, 150.8132031809861]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [161.4679290575061, 20.64267047800022], [157.0641418849878, -18.672274474100206]], "head": [162.72087077889134, -35.233915555558895], "nose": [[170.2285926709034, -28.62830339678036], [174.87521206467858, -24.540010568912756]], "arm_r": [[150.8018299546917, -15.799053939069722], [155.03270573465034, 13.15571890388496], [161.78026604533693, 42.00060715071375]], "arm_l": [[168.16669751995354, -19.93208044090749], [172.39757329991218, 9.02269240204719], [179.14513361059878, 37.86758064887598]], "leg_r": [[155.98496494554269, 64.99570981806046], [156.5530112803125, 110.40321098059958], [156.04541303597708, 149.76987086878293], [165.0545136399393, 150.9981459014436]], "leg_l": [[168.85527875912692, 64.61770475016273], [174.08875167395848, 109.83670152473152], [164.0, 147.88216467508], [174.58122841298845, 150.8132031809861]]}}}, {"name": "Overhead Triceps Stretch", "camera": 40.0, "frames": [{"order": ["leg_l", "leg_r", "spine", "arm_l", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [161.16295359997207, 21.356410973743678], [162.272438068911, -19.326553269696262]], "head": [164.9328825001008, -38.335831116240485], "nose": [[174.6663806423974, -36.04257711506479], [180.015629423895, -34.782271108722945]], "arm_r": [[155.20177436235906, -17.86330883254189], [166.09610938577018, -37.706394651916014], [146.89387147564, -55.93361473951907]], "arm_l": [[173.86954081587587, -19.280984693379658], [193.89556067214647, -35.16347455371521], [198.4725459338602, -64.19293966449294]], "leg_r": [[155.50048673219422, 64.93115555091643], [157.45202622343763, 110.06460929178084], [158.21239294139752, 149.183764076137], [166.64702989109946, 150.4943102984795]], "leg_l": [[169.0259523082187, 64.57765746255455], [172.35504357414234, 109.83191331245861], [162.0, 147.62941573491267], [172.53323300991636, 150.69016969434705]]}, {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [162.43438403305933, 21.02155460880003], [169.34797173752037, -20.009322276027916]], "head": [175.72916593806963, -38.48639829069877], "nose": [[185.07160735756915, -34.92005726336842], [190.24410815251892, -32.9455303735957]], "arm_r": [[162.43579379477544, -20.44083187320207], [166.34604183376157, -48.96599323044342], [151.8757374220089, -28.493607897535185]], "arm_l": [[180.78658872067825, -18.068999665382783], [189.42294106870335, -44.89422830041568], [164.0, -53.42605795442014]], "leg_r": [[155.50048673219422, 64.93115555091643], [157.45202622343763, 110.06460929178084], [158.21239294139752, 149.183764076137], [166.64702989109946, 150.4943102984795]], "leg_l": [[169.0259523082187, 64.57765746255455], [172.35504357414234, 109.83191331245861], [162.0, 147.62941573491267], [172.53323300991636, 150.69016969434705]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "leg_r", "spine", "arm_l", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [161.7954246174492, 21.207469215474156], [165.8256557275463, -19.86619066607126]], "head": [170.3699753116084, -38.7097346879485], "nose": [[179.93117163205625, -35.77996935231922], [185.21939608800238, -34.159538584160664]], "arm_r": [[158.8075288111505, -19.348352716342887], [166.39563936077067, -44.42588798396824], [145.34772804978564, -43.160878574485466]], "arm_l": [[177.370221684355, -18.87521560232866], [192.09535806067572, -41.01353840195073], [178.25562314256626, -66.99665153999453]], "leg_r": [[155.50048673219422, 64.93115555091643], [157.37713478136337, 110.05802895905501], [158.07262920713882, 149.16913898687545], [166.50707681467256, 150.50083895445817]], "leg_l": [[169.0259523082187, 64.57765746255455], [172.35504357414234, 109.83191331245861], [162.0, 147.62941573491267], [172.53323300991636, 150.69016969434705]]}}}, {"name": "Pec Deck", "camera": 90.0, "frames": [{"order": ["spine", "arm_l", "arm_r", "leg_l", "leg_r", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 116.0], [160.0, 73.16086274396895], [160.0, 32.29134099396232]], "head": [160.0, 12.59518593371817], "arm_r": [[149.0, 32.29134099396232], [119.2919579377529, 36.425901331217304], [92.367328079126, 38.40857274058021]], "arm_l": [[171.0, 32.29134099396232], [200.7080420622471, 36.425901331217304], [227.632671920874, 38.40857274058021]], "leg_r": [[153.0, 116.0], [153.0, 133.23190329713196], [153.0, 173.01277911186287], [153.0, 171.8629660159187]], "leg_l": [[167.0, 116.0], [167.0, 133.23190329713196], [167.0, 173.01277911186287], [167.0, 171.8629660159187]]}, {"order": ["spine", "leg_l", "leg_r", "arm_l", "arm_r", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 116.0], [160.0, 73.16086274396895], [160.0, 32.29134099396232]], "head": [160.0, 12.59518593371817], "arm_r": [[149.0, 32.29134099396232], [145.8641461019704, 39.50923965824833], [143.02209729517693, 33.74893662733645]], "arm_l": [[171.0, 32.29134099396232], [174.1358538980296, 39.50923965824833], [176.97790270482307, 33.74893662733645]], "leg_r": [[153.0, 116.0], [153.0, 133.23190329713196], [153.0, 173.01277911186287], [153.0, 171.8629660159187]], "leg_l": [[167.0, 116.0], [167.0, 133.23190329713196], [167.0, 173.01277911186287], [167.0, 171.8629660159187]]}], "tween": {"t": 0.5, "sample": {"order": ["spine", "arm_l", "arm_r", "leg_l", "leg_r", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 116.0], [160.0, 73.16086274396895], [160.0, 32.29134099396232]], "head": [160.0, 12.59518593371817], "arm_r": [[149.0, 32.29134099396232], [128.16024888623008, 48.32855050541065], [109.27302017191471, 54.37959395586806]], "arm_l": [[171.0, 32.29134099396232], [191.83975111376992, 48.32855050541065], [210.7269798280853, 54.37959395586806]], "leg_r": [[153.0, 116.0], [153.0, 133.23190329713196], [153.0, 173.01277911186287], [153.0, 171.8629660159187]], "leg_l": [[167.0, 116.0], [167.0, 133.23190329713196], [167.0, 173.01277911186287], [167.0, 171.8629660159187]]}}}, {"name": "Plank", "camera": 0.0, "frames": [{"order": ["arm_r", "leg_r", "spine", "leg_l", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[172.0, 114.0], [128.6655306330091, 117.73367682621827], [87.32345066220162, 121.29569035008166]], "head": [67.39955670036672, 123.0123233736303], "nose": [[58.7064746151967, 127.95502747771098], [52.64429678870563, 131.4018546132537]], "arm_r": [[93.23229718027487, 121.35517590176984], [93.23229718027487, 150.89940849213608], [63.23229718027487, 150.89940849213608]], "arm_l": [[87.32345066220162, 123.2058203044179], [87.32345066220162, 152.75005289478415], [57.32345066220162, 152.75005289478415]], "leg_r": [[177.90884651807326, 114.7540782623559], [204.2933625902214, 151.86261333148943], [244.2933625902214, 151.86261333148943], [244.2933625902214, 162.69549861462372]], "leg_l": [[172.0, 115.2155372436685], [198.38451607214813, 152.32407231280206], [238.38451607214813, 152.32407231280206], [238.38451607214813, 163.15695759593632]]}, {"order": ["arm_r", "leg_r", "spine", "leg_l", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[168.0, 137.0], [125.16086274396895, 129.56106188266668], [84.29134099396232, 122.46414390865907]], "head": [64.59518593371817, 119.04394247540239], "nose": [[55.90210384854815, 123.98664657948308], [49.839926022057085, 127.43347371502581]], "arm_r": [[90.20018751203557, 122.52362946034725], [90.20018751203557, 152.06786205071347], [60.20018751203557, 152.0678620507135]], "arm_l": [[84.29134099396232, 124.37427386299531], [84.29134099396232, 153.91850645336154], [54.29134099396232, 153.91850645336154]], "leg_r": [[173.90884651807326, 137.7540782623559], [219.21000315663483, 145.62054155884627], [258.60231327712313, 152.46094442535966], [256.6921833227869, 163.12925383968215]], "leg_l": [[168.0, 138.2155372436685], [213.30115663856157, 146.0820005401589], [252.6934667590499, 152.9224034066723], [250.78333680471366, 163.59071282099478]]}], "tween": {"t": 0.5, "sample": {"order": ["arm_r", "leg_r", "spine", "leg_l", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[170.0, 125.5], [126.54140236118918, 123.63138307763239], [85.08090116554209, 121.84867957698282]], "head": [65.09993673390494, 120.9895453598023], "nose": [[56.40685464873492, 125.932249463883], [50.344676822243855, 129.37907659942573]], "arm_r": [[90.98974768361533, 121.908165128671], [90.98974768361533, 151.45239771903724], [60.989747683615334, 151.45239771903724]], "arm_l": [[85.08090116554209, 123.75880953131905], [85.08090116554209, 153.30304212168528], [55.08090116554209, 153.30304212168528]], "leg_r": [[175.90884651807326, 126.2540782623559], [214.704853025466, 150.59437198191316], [254.55264094913582, 154.0276380290104], [253.5939277789116, 164.81930091310488]], "leg_l": [[170.0, 126.7155372436685], [208.79600650739275, 151.05583096322576], [248.64379443106256, 154.489097010323], [247.68508126083833, 165.28075989441749]]}}}, {"name": "Pull-Up", "camera": 0.0, "frames": [{"order": ["leg_l", "leg_r", "spine", "arm_l", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[156.0, 99.0], [156.0, 56.160862743968956], [156.0, 15.291340993962322]], "head": [156.69798993405, -4.392815700791431], "nose": [[166.67436236418465, -5.079833266250844], [173.6573107160034, -5.560710279412895]], "arm_r": [[156.0, 17.20147094829855], [154.4533259562162, -12.3034712212812], [151.81470930141467, -41.73320653220463]], "arm_l": [[161.90884651807326, 15.350826545650511], [160.50443880392413, -14.161015014786125], [158.00888856162837, -43.60285087153715]], "leg_r": [[156.0, 100.2155372436685], [182.38451607214813, 137.32407231280206], [142.5367281484783, 140.7573383598993], [137.8879272693306, 150.57526664807864]], "leg_l": [[161.90884651807326, 99.7540782623559], [188.2933625902214, 136.86261333148946], [148.44557466655155, 140.2958793785867], [143.79677378740385, 150.11380766676604]]}, {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[156.0, 67.0], [156.0, 24.160862743968956], [156.0, -16.708659006037678]], "head": [157.3951294748825, -36.35683522248037], "nose": [[167.3419871354356, -37.38640925124002], [174.3036404030135, -38.106992338495076]], "arm_r": [[156.0, -14.79852905170145], [181.44676676044196, -30.44619337585729], [154.0, -42.37314613604329]], "arm_l": [[161.90884651807326, -16.64917345434949], [186.16861785786944, -34.02938406879529], [158.0, -44.19386765345278]], "leg_r": [[156.0, 68.2155372436685], [182.38451607214813, 105.32407231280206], [142.5367281484783, 108.75733835989931], [137.8879272693306, 118.57526664807864]], "leg_l": [[161.90884651807326, 67.7540782623559], [188.2933625902214, 104.86261333148944], [148.44557466655155, 108.2958793785867], [143.79677378740385, 118.11380766676604]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[156.0, 83.0], [156.0, 40.160862743968956], [156.0, -0.7086590060376778]], "head": [157.04671912485887, -20.37782117030325], "nose": [[167.00980719951306, -21.236235998159508], [173.98317008615527, -21.83705755640153]], "arm_r": [[156.0, 1.2014709482985495], [134.64946364596483, -19.55332843708773], [111.97773274796863, -0.20511103877473147]], "arm_l": [[161.90884651807326, -0.6491734543494891], [139.7155670588939, -20.52810638165094], [117.76662496819401, -0.3877900207003597]], "leg_r": [[156.0, 84.2155372436685], [182.38451607214813, 121.32407231280206], [142.5367281484783, 124.75733835989931], [137.8879272693306, 134.57526664807864]], "leg_l": [[161.90884651807326, 83.7540782623559], [188.2933625902214, 120.86261333148944], [148.44557466655155, 124.2958793785867], [143.79677378740385, 134.11380766676604]]}}}, {"name": "Push-Up", "camera": 0.0, "frames": [{"order": ["leg_r", "arm_r", "leg_l", "spine", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[180.0, 115.0], [138.40074311560795, 102.4750484116029], [98.714095743142, 90.5259566663505]], "head": [79.58800062388129, 84.76735823490355], "nose": [[69.83685766497203, 86.98438574824536], [63.016267211475395, 88.53512054480812]], "arm_r": [[104.62294226121524, 90.58544221803868], [90.66949108176117, 116.73948142258237], [62.92966140223935, 127.98966973623477]], "arm_l": [[98.714095743142, 92.43608662068672], [98.08624768774057, 121.97384842742832], [93.92556209079443, 151.23256293137962]], "leg_r": [[185.90884651807326, 115.7540782623559], [216.81618561243002, 149.30606635968886], [256.0, 141.3890027259118], [253.44970263774323, 151.92672102829837]], "leg_l": [[180.0, 116.2155372436685], [221.11149742237373, 136.53803680407202], [260.0, 145.75976983403476], [253.14654590646848, 154.23313305715615]]}, {"order": ["arm_r", "leg_r", "spine", "leg_l", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[180.0, 132.0], [136.6655306330091, 128.26632317378173], [95.32345066220162, 124.70430964991834]], "head": [75.39955670036672, 122.98767662636972], "nose": [[66.7064746151967, 127.93038073045041], [60.64429678870563, 131.37720786599314]], "arm_r": [[101.23229718027487, 124.76379520160651], [126.52667189632352, 140.64928481588592], [97.0, 145.87671697909758]], "arm_l": [[95.32345066220162, 126.61443960425457], [124.54010044659273, 133.32179563010098], [100.99999999999999, 151.63666950855597]], "leg_r": [[185.90884651807326, 132.7540782623559], [221.45545402765325, 161.50706980680354], [255.99999999999997, 141.6472709247043], [257.01133983112186, 152.43427406487965]], "leg_l": [[180.0, 133.2155372436685], [220.84205541554815, 154.05827872604254], [260.0, 146.01803803282723], [257.4838828139067, 156.5637222789729]]}], "tween": {"t": 0.5, "sample": {"order": ["arm_r", "leg_r", "spine", "leg_l", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[180.0, 123.5], [137.29921752002664, 115.3259072573788], [96.56168940694857, 107.52763487074017]], "head": [76.9291457379953, 103.76943131091434], "nose": [[67.60607009461341, 107.38609559555283], [61.093147118739246, 109.91262834251884]], "arm_r": [[102.47053592502182, 107.58712042242836], [122.25277767997544, 129.79803772926599], [97.0, 145.74758287970135]], "arm_l": [[96.56168940694857, 109.43776482507641], [119.61748516508456, 128.34069448148384], [101.0, 151.50753540915974]], "leg_r": [[185.90884651807326, 124.2540782623559], [218.77913752303337, 155.94506275047252], [256.0, 141.51813682530806], [255.32534956444712, 152.3306284389917]], "leg_l": [[180.0, 124.7155372436685], [220.01754793404967, 147.05561095864437], [260.0, 145.88890393343098], [255.64850815815672, 155.8381198287707]]}}}, {"name": "Rear Delt Fly", "camera": 270.0, "frames": [{"order": ["leg_l", "leg_r", "arm_l", "arm_r", "spine", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 116.0], [160.0, 73.16086274396895], [160.0, 32.29134099396232]], "head": [160.0, 12.59518593371817], "arm_r": [[171.0, 32.29134099396232], [174.1358538980296, 29.690992159873403], [177.16485616560564, 19.444223248777263]], "arm_l": [[149.0, 32.29134099396232], [145.8641461019704, 29.690992159873403], [142.83514383439433, 19.444223248777263]], "leg_r": [[167.0, 116.0], [167.0, 117.60537684831505], [167.0, 153.55713870028174], [167.0, 148.73505608560188]], "leg_l": [[153.0, 116.0], [153.0, 117.60537684831505], [153.0, 153.55713870028174], [153.0, 148.73505608560188]]}, {"order": ["leg_l", "leg_r", "spine", "arm_l", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 116.0], [160.0, 73.16086274396895], [160.0, 32.29134099396232]], "head": [160.0, 12.59518593371817], "arm_r": [[171.0, 32.29134099396232], [200.8356568610482, 35.1988540984957], [229.6546883674333, 35.09863647252796]], "arm_l": [[149.0, 32.29134099396232], [119.1643431389518, 35.1988540984957], [90.3453116325667, 35.09863647252796]], "leg_r": [[167.0, 116.0], [167.0, 117.60537684831505], [167.0, 153.55713870028174], [167.0, 148.73505608560188]], "leg_l": [[153.0, 116.0], [153.0, 117.60537684831505], [153.0, 153.55713870028174], [153.0, 148.73505608560188]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "leg_r", "arm_l", "arm_r", "spine", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 116.0], [160.0, 73.16086274396895], [160.0, 32.29134099396232]], "head": [160.0, 12.59518593371817], "arm_r": [[171.0, 32.29134099396232], [192.21320343559643, 43.375209342088894], [212.703584492363, 47.46101864010683]], "arm_l": [[149.0, 32.29134099396232], [127.78679656440357, 43.375209342088894], [107.296415507637, 47.46101864010683]], "leg_r": [[167.0, 116.0], [167.0, 117.60537684831505], [167.0, 153.55713870028174], [167.0, 148.73505608560188]], "leg_l": [[153.0, 116.0], [153.0, 117.60537684831505], [153.0, 153.55713870028174], [153.0, 148.73505608560188]]}}}, {"name": "Reverse Crunch", "camera": 0.0, "frames": [{"order": ["arm_l", "leg_l", "spine", "arm_r", "leg_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[172.0, 148.0], [128.5, 148.0], [87.0, 148.0]], "head": [67.43704798532389, 143.90493909881587], "nose": [[67.43704798532389, 133.90493909881587], [67.43704798532389, 127.01128482773042]], "arm_r": [[87.0, 149.91012995433624], [116.98172481057287, 150.94120880220163], [146.96344962114574, 151.97228765006705]], "arm_l": [[92.90884651807325, 148.05948555168817], [122.89057132864612, 149.09056439955359], [152.872296139219, 150.121643247419]], "leg_r": [[172.0, 149.2155372436685], [164.0121838273212, 104.60260696559263], [201.59988865875755, 118.07557051893123], [205.3621102353399, 107.89598815654968]], "leg_l": [[177.90884651807326, 148.7540782623559], [169.92103034539446, 104.14114798428001], [207.5087351768308, 117.61411153761861], [211.27095675341317, 107.43452917523706]]}, {"order": ["arm_l", "leg_l", "spine", "arm_r", "leg_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[166.0, 136.0], [124.61787286428175, 154.24267631947558], [82.62501214971675, 143.35521174704758]], "head": [62.63719560933484, 144.04259764562454], "nose": [[62.63719560933484, 134.04259764562454], [62.63719560933484, 127.14894337453909]], "arm_r": [[82.62501214971675, 145.26534170138382], [112.60673696028962, 146.29642054924923], [142.58846177086252, 147.32749939711462]], "arm_l": [[88.53385866779, 143.41469729873577], [118.51558347836287, 144.44577614660116], [148.49730828893578, 145.47685499446658]], "leg_r": [[166.0, 137.2155372436685], [146.5595599599278, 96.15874622037313], [185.95187008041614, 102.99914908688652], [187.86200003475238, 92.33083967256403]], "leg_l": [[171.90884651807326, 136.7540782623559], [152.46840647800107, 95.69728723906053], [191.8607165984894, 102.5376901055739], [193.77084655282565, 91.86938069125141]]}], "tween": {"t": 0.5, "sample": {"order": ["arm_l", "leg_l", "spine", "arm_r", "leg_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[169.0, 142.0], [126.03181909460824, 151.19312771952025], [84.40784565448814, 145.72317600490808]], "head": [64.48395169265324, 144.00654298135944], "nose": [[64.48395169265324, 134.00654298135944], [64.48395169265324, 127.11288871027399]], "arm_r": [[84.40784565448814, 147.6333059592443], [114.38957046506101, 148.6643848071097], [144.37129527563388, 149.69546365497513]], "arm_l": [[90.31669217256139, 145.78266155659625], [120.29841698313426, 146.81374040446167], [150.28014179370714, 147.84481925232706]], "leg_r": [[169.0, 143.2155372436685], [155.16753322280343, 100.01105626897177], [193.80456627436615, 110.20653635873893], [196.65157577049388, 99.74277269053276]], "leg_l": [[174.90884651807326, 142.7540782623559], [161.0763797408767, 99.54959728765917], [199.7134127924394, 109.74507737742633], [202.56042228856714, 99.28131370922014]]}}}, {"name": "Romanian Deadlift", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[90.0, 92.0], [131.26145916952032, 77.91407563146277], [166.9527046464468, 56.71838660269156]], "head": [185.3628017154956, 49.02248570910264], "nose": [[195.3258897901498, 49.8809005369589], [202.299252676792, 50.48172209520092]], "arm_r": [[166.9527046464468, 58.628516557027794], [167.522630693969, 88.16741731595586], [169.12519553685217, 117.66946650593746]], "arm_l": [[172.86155116452005, 56.77787215437974], [172.53496926154835, 86.32035410289532], [173.2408596233939, 115.8564070410833]], "leg_r": [[90.0, 93.2155372436685], [125.35053290875736, 122.20202514661878], [155.3086980593084, 148.30423046258957], [160.72538022288882, 138.87577986942745]], "leg_l": [[95.90884651807325, 92.7540782623559], [130.836880114458, 122.23311800484156], [160.3993109485087, 148.76930488988666], [165.9571573187271, 139.4208727010124]]}, {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[158.0, 64.0], [165.55369572851146, 21.811685497906495], [172.76009510168907, -18.436936383401104]], "head": [176.91832891804427, -37.70268319925994], "nose": [[186.8947013481789, -37.01566563380052], [193.87764969999765, -36.53478862063847]], "arm_r": [[172.76009510168907, -16.526806429064877], [178.85644942928292, 12.400979889721263], [199.1744301067988, 34.137801113206514]], "arm_l": [[178.66894161976234, -18.377450831712917], [183.4474723060758, 10.789583023349335], [202.20923781110346, 33.84326058608493]], "leg_r": [[158.0, 65.2155372436685], [169.22887526493585, 109.1462686355685], [158.0, 146.95458074368176], [168.55768072289158, 149.9956086837736]], "leg_l": [[163.90884651807326, 64.7540782623559], [174.01236624892732, 108.94900788074216], [162.0, 146.52304464760766], [172.49226113348837, 149.77625928644824]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[124.0, 78.0], [152.2476749245726, 45.366861135771344], [175.74775741142687, 11.627906421285644]], "head": [188.46932181698216, -3.570131021415193], "nose": [[198.43942052699484, -2.7973880417618164], [205.41784186312674, -2.256518161392056]], "arm_r": [[175.74775741142687, 13.538036375621871], [172.91579688708677, 42.95033841992456], [171.05157770169677, 72.43747391486767]], "arm_l": [[181.65660392950014, 11.687391972973833], [178.0074482941626, 41.01224285393908], [175.3206698023005, 70.4377517450443]], "leg_r": [[124.0, 79.2155372436685], [159.67849876443978, 107.8096593131734], [158.0, 147.16727220151085], [168.97835398703234, 146.48800914018568]], "leg_l": [[129.90884651807326, 78.7540782623559], [165.468203411963, 107.49177652673711], [162.0, 146.73573610543676], [172.99823667699596, 146.54177648268518]]}}}, {"name": "Rotary", "camera": 90.0, "frames": [{"order": ["spine", "arm_l", "leg_l", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 122.0], [160.0, 79.16086274396895], [160.0, 38.29134099396232]], "head": [160.0, 18.59518593371817], "nose": [[169.63488735000612, 21.272674636344597], [173.34432219963855, 22.303508681644907]], "arm_r": [[152.92933629344807, 39.754585431116695], [174.52472560523668, 53.00593171303545], [197.15692080730136, 61.433928518814795]], "arm_l": [[167.07066370655193, 36.82809655680795], [188.66605301834056, 50.079442838726706], [211.29824822040524, 58.50743964450605]], "leg_r": [[153.0, 122.0], [140.32068163241803, 135.6641252039847], [136.5497405147205, 175.47598817361995], [133.7005822327046, 174.9683561722579]], "leg_l": [[167.0, 122.0], [179.67931836758197, 135.6641252039847], [183.4502594852795, 175.47598817361995], [186.2994177672954, 174.9683561722579]]}, {"order": ["spine", "arm_r", "leg_l", "leg_r", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 122.0], [160.0, 79.16086274396895], [160.0, 38.29134099396232]], "head": [160.0, 18.59518593371817], "nose": [[150.36511264999388, 21.272674636344597], [146.65567780036145, 22.30350868164491]], "arm_r": [[152.92933629344807, 36.82809655680795], [131.33394698165944, 50.079442838726706], [108.70175177959479, 58.50743964450605]], "arm_l": [[167.07066370655193, 39.754585431116695], [145.47527439476332, 53.00593171303545], [122.84307919269864, 61.433928518814795]], "leg_r": [[153.0, 122.0], [140.32068163241803, 135.6641252039847], [136.5497405147205, 175.47598817361995], [133.7005822327046, 174.9683561722579]], "leg_l": [[167.0, 122.0], [179.67931836758197, 135.6641252039847], [183.4502594852795, 175.47598817361995], [186.2994177672954, 174.9683561722579]]}], "tween": {"t": 0.5, "sample": {"order": ["spine", "leg_l", "leg_r", "arm_l", "arm_r", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 122.0], [160.0, 79.16086274396895], [160.0, 38.29134099396232]], "head": [160.0, 18.59518593371817], "arm_r": [[149.0, 38.29134099396232], [149.0, 53.29134099396232], [149.0, 63.55194529373239]], "arm_l": [[171.0, 38.29134099396232], [171.0, 53.29134099396232], [171.0, 63.55194529373239]], "leg_r": [[153.0, 122.0], [140.32068163241803, 135.6641252039847], [136.5497405147205, 175.47598817361995], [133.7005822327046, 174.9683561722579]], "leg_l": [[167.0, 122.0], [179.67931836758197, 135.6641252039847], [183.4502594852795, 175.47598817361995], [186.2994177672954, 174.9683561722579]]}}}, {"name": "Seated Row", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[140.0, 122.0], [142.69891028706257, 79.2013101935184], [149.59296717847204, 38.855014516213274]], "head": [153.06593073181065, 19.458088308354192], "nose": [[163.02901880646485, 18.599673480497934], [170.00238169310705, 17.998851922255916]], "arm_r": [[149.59296717847204, 40.7651444705495], [170.10755873258697, 62.32212354359968], [200.0, 64.82166477360376]], "arm_l": [[155.5018136965453, 38.914500067901464], [174.0120238448619, 62.16456016148103], [204.0, 63.000943256194205]], "leg_r": [[140.0, 123.2155372436685], [183.8850455429191, 109.63722816102359], [188.0, 148.8205388219859], [198.94163850640507, 147.7061180825139]], "leg_l": [[145.90884651807326, 122.7540782623559], [189.43323013923415, 108.09307121777746], [192.0, 147.4041949728996], [202.97732933112334, 146.70905688665255]]}, {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[140.0, 122.0], [142.69891028706257, 79.2013101935184], [149.59296717847204, 38.855014516213274]], "head": [153.06593073181065, 19.458088308354192], "nose": [[163.02901880646485, 18.599673480497934], [170.00238169310705, 17.998851922255916]], "arm_r": [[149.59296717847204, 40.7651444705495], [130.27974637330342, 63.37285383528793], [158.0, 74.66974230372583]], "arm_l": [[155.5018136965453, 38.914500067901464], [134.82908862986338, 60.32446610131683], [162.0, 72.84902078631629]], "leg_r": [[140.0, 123.2155372436685], [183.8850455429191, 109.63722816102359], [188.0, 148.8205388219859], [198.94163850640507, 147.7061180825139]], "leg_l": [[145.90884651807326, 122.7540782623559], [189.43323013923415, 108.09307121777746], [192.0, 147.4041949728996], [202.97732933112334, 146.70905688665255]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[140.0, 122.0], [142.69891028706257, 79.2013101935184], [149.59296717847204, 38.855014516213274]], "head": [153.06593073181065, 19.458088308354192], "nose": [[163.02901880646485, 18.599673480497934], [170.00238169310705, 17.998851922255916]], "arm_r": [[149.59296717847204, 40.7651444705495], [149.00535132812342, 70.30370907354869], [179.0, 69.74570353866478]], "arm_l": [[155.5018136965453, 38.914500067901464], [153.0031939480439, 68.35608343607598], [182.99999999999997, 67.92498202125525]], "leg_r": [[140.0, 123.2155372436685], [183.8850455429191, 109.63722816102359], [188.0, 148.8205388219859], [198.94163850640507, 147.7061180825139]], "leg_l": [[145.90884651807326, 122.7540782623559], [189.43323013923415, 108.09307121777746], [192.0, 147.4041949728996], [202.97732933112334, 146.70905688665255]]}}}, {"name": "Shoulder Press", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[120.0, 130.0], [103.0200605630874, 90.37728052628361], [95.1324142218102, 50.073787462921175]], "head": [98.60537777514881, 30.676861255062093], "nose": [[108.5109515671687, 29.305873344521444], [115.4428280483597, 28.346462102667882]], "arm_r": [[95.1324142218102, 51.9839174172574], [110.1324142218102, 77.5699733758307], [110.1324142218102, 48.02574078546445]], "arm_l": [[101.04126073988344, 50.133273014609365], [116.04126073988344, 75.71932897318266], [116.04126073988344, 46.17509638281641]], "leg_r": [[120.0, 131.2155372436685], [161.917285943973, 112.5576236573147], [164.0, 151.89650005692488], [174.98507904390829, 151.3324549957621]], "leg_l": [[125.90884651807325, 130.7540782623559], [167.3506724370636, 111.09303668974187], [168.0, 150.48015620783858], [178.99855056415495, 150.3043039328269]]}, {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[120.0, 130.0], [103.0200605630874, 90.37728052628361], [95.1324142218102, 50.073787462921175]], "head": [98.60537777514881, 30.676861255062093], "nose": [[108.5109515671687, 29.305873344521444], [115.4428280483597, 28.346462102667882]], "arm_r": [[95.1324142218102, 51.9839174172574], [112.76597179058439, 28.082131165884945], [133.6057229043543, 6.829788793172227]], "arm_l": [[101.04126073988344, 50.133273014609365], [118.67481830865763, 26.231486763236905], [139.51456942242757, 4.979144390524189]], "leg_r": [[120.0, 131.2155372436685], [161.917285943973, 112.5576236573147], [164.0, 151.89650005692488], [174.98507904390829, 151.3324549957621]], "leg_l": [[125.90884651807325, 130.7540782623559], [167.3506724370636, 111.09303668974187], [168.0, 150.48015620783858], [178.99855056415495, 150.3043039328269]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[120.0, 130.0], [103.0200605630874, 90.37728052628361], [95.1324142218102, 50.073787462921175]], "head": [98.60537777514881, 30.676861255062093], "nose": [[108.5109515671687, 29.305873344521444], [115.4428280483597, 28.346462102667882]], "arm_r": [[95.1324142218102, 51.9839174172574], [125.09130026444741, 53.53014308133817], [136.32949806692477, 26.137207627984708]], "arm_l": [[101.04126073988344, 50.133273014609365], [131.00014678252066, 51.67949867869013], [142.23834458499803, 24.286563225336668]], "leg_r": [[120.0, 131.2155372436685], [161.917285943973, 112.5576236573147], [164.0, 151.89650005692488], [174.98507904390829, 151.3324549957621]], "leg_l": [[125.90884651807325, 130.7540782623559], [167.3506724370636, 111.09303668974187], [168.0, 150.48015620783858], [178.99855056415495, 150.3043039328269]]}}}, {"name": "Side Plank", "camera": 0.0, "frames": [{"order": ["arm_r", "leg_r", "spine", "leg_l", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[174.0, 150.0], [132.6290415411608, 136.76197856352545], [93.16019611491195, 124.13260179079685]], "head": [74.13906578900888, 118.0461551533373], "nose": [[64.17597771435469, 117.18774032548103], [57.20261482771247, 116.58691876723901]], "arm_r": [[99.0690426329852, 124.19208734248504], [127.25982125656246, 134.296810007489], [156.2375960452345, 141.94342007481436]], "arm_l": [[93.16019611491195, 126.0427317451331], [89.95929250465917, 155.4183141839495], [60.0, 153.87974546036065]], "leg_r": [[179.90884651807326, 150.7540782623559], [225.90184049526727, 151.5446924600911], [265.8957483015229, 152.23218306681738], [265.70377183071275, 163.0634184490528]], "leg_l": [[174.0, 151.2155372436685], [219.992993977194, 152.00615144140374], [259.98690178344964, 152.69364204813002], [259.7949253126395, 163.52487743036542]]}, {"order": ["leg_r", "arm_r", "spine", "leg_l", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[174.0, 137.0], [131.16086274396895, 129.56106188266668], [90.29134099396232, 122.46414390865907]], "head": [70.59518593371817, 119.04394247540239], "nose": [[60.59518593371817, 119.04394247540239], [53.59518593371817, 119.04394247540239]], "arm_r": [[96.20018751203557, 122.52362946034725], [111.20018751203557, 96.93757350177395], [126.20018751203557, 71.35151754320066]], "arm_l": [[90.29134099396232, 124.37427386299531], [89.99905209869154, 153.91710417204732], [60.0, 153.68224624951935]], "leg_r": [[179.90884651807326, 137.7540782623559], [225.21000315663483, 145.62054155884627], [264.60231327712313, 152.46094442535966], [262.6921833227869, 163.12925383968215]], "leg_l": [[174.0, 138.2155372436685], [219.30115663856157, 146.0820005401589], [258.6934667590499, 152.9224034066723], [256.78333680471366, 163.59071282099478]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_r", "arm_r", "spine", "leg_l", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[174.0, 143.5], [131.79213590699416, 133.13627470916663], [91.52486326654031, 123.24904253515317]], "head": [72.11894874102038, 118.48411136695391], "nose": [[62.128179926165075, 118.05453193528285], [55.13484237509207, 117.75383495926968]], "arm_r": [[97.43370978461355, 123.30852808684135], [125.6244884081908, 113.20380542183739], [153.34087438352944, 101.89771708756348]], "arm_l": [[91.52486326654031, 125.1591724894894], [89.98658163375015, 154.66454016997503], [59.999999999999986, 153.78099585494002]], "leg_r": [[179.90884651807326, 144.2540782623559], [225.6970716429635, 148.59600171041538], [265.5129195776506, 152.3715873174236], [264.45861629992817, 163.15460014560324]], "leg_l": [[174.0, 144.7155372436685], [219.78822512489023, 149.05746069172798], [259.6040730595774, 152.83304629873624], [258.54976978185493, 163.61605912691584]]}}}, {"name": "Standing Calf Raise", "camera": 0.0, "frames": [{"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [161.5181281065588, 21.186959188910578], [162.9664572197126, -19.657665952703468]], "head": [164.36158669459508, -39.30584216914616], "nose": [[174.36158669459508, -39.30584216914616], [181.36158669459508, -39.30584216914616]], "arm_r": [[162.9664572197126, -17.74753599836724], [164.0134421207876, 11.778699043763396], [168.18863514958957, 41.035409193477136]], "arm_l": [[168.87530373778586, -19.59818040101528], [169.92228863886086, 9.928054641115358], [174.09748166766283, 39.1847647908291]], "leg_r": [[160.0, 65.2155372436685], [168.06607636623892, 109.81480911859667], [158.0, 147.939388496694], [168.64599592855438, 150.6655047598624]], "leg_l": [[165.90884651807326, 64.7540782623559], [172.71840731660157, 109.55611956268012], [162.00000000000003, 147.5078524006199], [172.5977298599761, 150.4106343225862]]}, {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 55.0], [161.5181281065588, 12.186959188910578], [162.9664572197126, -28.657665952703468]], "head": [164.36158669459508, -48.30584216914616], "nose": [[174.36158669459508, -48.30584216914616], [181.36158669459508, -48.30584216914616]], "arm_r": [[162.9664572197126, -26.74753599836724], [164.0134421207876, 2.778699043763396], [168.18863514958957, 32.035409193477136]], "arm_l": [[168.87530373778586, -28.59818040101528], [169.92228863886086, 0.9280546411153574], [174.09748166766283, 30.184764790829092]], "leg_r": [[160.0, 56.215537243668514], [168.06607636623892, 100.81480911859667], [158.0, 138.939388496694], [163.06640975574908, 148.5548393941648]], "leg_l": [[165.90884651807326, 55.754078262355904], [172.71840731660157, 100.55611956268012], [162.00000000000003, 138.5078524006199], [166.90079995019303, 148.20619194420698]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 59.5], [161.5181281065588, 16.686959188910578], [162.9664572197126, -24.157665952703468]], "head": [164.36158669459508, -43.80584216914616], "nose": [[174.36158669459508, -43.80584216914616], [181.36158669459508, -43.80584216914616]], "arm_r": [[162.9664572197126, -22.24753599836724], [164.0134421207876, 7.278699043763396], [168.18863514958957, 36.535409193477136]], "arm_l": [[168.87530373778586, -24.09818040101528], [169.92228863886086, 5.428054641115358], [174.09748166766283, 34.6847647908291]], "leg_r": [[160.0, 60.715537243668514], [168.06607636623892, 105.31480911859667], [158.0, 143.439388496694], [166.5996846423216, 150.19415207052123]], "leg_l": [[165.90884651807326, 60.254078262355904], [172.71840731660157, 105.05611956268012], [162.00000000000003, 143.0078524006199], [170.4826264968593, 149.9046747536479]]}}}, {"name": "Standing Calf Stretch", "camera": 0.0, "frames": [{"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 66.0], [170.59618644035737, 24.374309027589845], [186.21725231980906, -13.573789038579548]], "head": [194.98467525559062, -31.27657594171221], "nose": [[204.60832220826177, -28.5589631417822], [211.33715407983, -26.658814506583642]], "arm_r": [[186.21725231980906, -11.66365908424332], [188.3099465321328, 17.808605240420718], [209.89014054229233, 38.33175370810613]], "arm_l": [[192.12609883788232, -13.51430348689136], [194.21879305020607, 15.95796083777268], [215.7989870603656, 36.48110930545809]], "leg_r": [[160.0, 67.2155372436685], [175.16556540504365, 109.98393208966951], [187.22985964654342, 147.54182987483443], [197.31336898935066, 143.21283640822708]], "leg_l": [[165.90884651807326, 66.7540782623559], [157.78606467079257, 111.34336773614788], [142.0, 147.53823689459549], [152.70742489329234, 150.02007985883768]]}, {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 68.0], [177.8449674978852, 28.74769124941882], [202.39643095943165, -4.4202453757258695]], "head": [215.7790430866088, -19.057341090546714], "nose": [[224.86662519788973, -14.884110762524257], [231.2107797071463, -11.970726577436773]], "arm_r": [[202.39643095943165, -2.510115421389642], [198.72420173956954, 26.811941847485613], [195.78470647585493, 56.2140096981627]], "arm_l": [[208.30527747750492, -4.360759824037681], [204.15369854281798, 24.89920922697154], [200.73277082933996, 54.25073071932251]], "leg_r": [[160.0, 69.2155372436685], [179.93274697663705, 110.04274491100989], [196.21451686919335, 146.0240134993241], [205.54108250286276, 140.28036566038193]], "leg_l": [[165.90884651807326, 68.7540782623559], [148.56714690316622, 110.71273056909666], [132.3754437974776, 146.7333951610521], [143.13990766102046, 148.96313513172313]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 67.0], [174.26917504841117, 26.407887228155587], [194.49986089975675, -9.416353188154432]], "head": [205.6837189691717, -25.74520576796695], "nose": [[215.06780908160056, -22.28994126842204], [221.62451440633933, -19.875732658837062]], "arm_r": [[194.49986089975675, -7.506223233818204], [193.70641989937332, 22.02767447620785], [204.05970368705914, 49.75678161626014]], "arm_l": [[200.40870741783002, -9.356867636466243], [199.373637753329, 20.169774839066125], [209.49937694165087, 47.980254176067106]], "leg_r": [[160.0, 68.2155372436685], [177.60126700830682, 110.06923414430075], [191.8168250129063, 146.88995559085808], [201.54566768152094, 141.8347521274287]], "leg_l": [[165.90884651807326, 67.7540782623559], [151.06952728568317, 110.63332035745131], [137.015040258231, 147.51396171963245], [147.86193480658685, 149.31507754712842]]}}}, {"name": "Standing Forward Fold", "camera": 0.0, "frames": [{"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [163.7912748095231, 21.3238785947173], [167.40823813355095, -19.390122286184578]], "head": [169.8456250016539, -38.939465193360675], "nose": [[179.8441478685724, -38.76759175239952], [186.84308173466712, -38.64728089622242]], "arm_r": [[167.40823813355095, -17.47999233184835], [167.40823813355095, 12.064240258517891], [170.02291041598068, 41.4960481242301]], "arm_l": [[173.3170846516242, -19.33063673449639], [173.3170846516242, 10.213595855869853], [175.93175693405394, 39.64540372158206]], "leg_r": [[160.0, 65.2155372436685], [168.06607636623892, 109.81480911859668], [158.0, 147.939388496694], [168.64599592855438, 150.6655047598624]], "leg_l": [[165.90884651807326, 64.7540782623559], [172.71840731660157, 109.55611956268012], [162.0, 147.50785240061987], [172.59772985997608, 150.4106343225862]]}, {"order": ["arm_l", "leg_l", "spine", "arm_r", "leg_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[147.0, 66.0], [176.1289416829083, 98.8575650078507], [187.3972636865633, 139.0385228514936]], "head": [189.48783295191635, 158.62678031347502], "nose": [[184.73694397530744, 167.42615835581051], [181.4506430358062, 173.51289380046708]], "arm_r": [[187.3972636865633, 140.94865280582985], [182.4192496006142, 170.08331055850218], [210.68489359821646, 160.183500587288]], "arm_l": [[193.30611020463655, 139.09800840318178], [186.02056319437705, 167.75778889244543], [215.38942014795313, 161.72949102185854]], "leg_r": [[147.0, 67.2155372436685], [141.31274251864636, 112.16912679040732], [116.21554710763508, 142.84285757112133], [125.8492956904415, 148.07170342364373]], "leg_l": [[152.90884651807326, 66.7540782623559], [145.81726113426228, 111.51366557248716], [119.2597840340148, 140.9707851128171], [128.62869772062507, 146.64719652698653]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[153.5, 65.0], [195.0328826650446, 51.61662929027126], [236.57347679163044, 47.926177033458714]], "head": [256.5727152529139, 48.09805622977341], "nose": [[261.7058802321052, 56.68005001306202], [265.2586487728301, 62.61982355503152]], "arm_r": [[236.57347679163044, 49.83630698779495], [234.07579830025136, 79.27796781424398], [259.31796266290564, 95.24379849787402]], "arm_l": [[242.4823233097037, 47.9856625851469], [238.81197686747683, 77.30794847930343], [262.8923626362581, 94.92844135448324]], "leg_r": [[153.5, 66.2155372436685], [166.9043301898599, 109.55070334848604], [158.0, 147.95458074368176], [168.8965972088972, 149.4364364079613]], "leg_l": [[159.40884651807326, 65.7540782623559], [171.89384470090084, 109.35476432290021], [162.0, 147.52304464760766], [172.85477539736297, 149.27750973779763]]}}}, {"name": "Standing Quad Stretch", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "leg_r", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [161.5181281065588, 21.186959188910578], [162.9664572197126, -19.657665952703468]], "head": [164.36158669459508, -39.30584216914616], "nose": [[174.35567849015837, -39.649541429917065], [181.35141427929204, -39.89012649441899]], "arm_r": [[162.9664572197126, -17.74753599836724], [147.06887929271645, 7.307394202295484], [151.2440723215184, 36.564104352009224]], "arm_l": [[168.87530373778586, -19.59818040101528], [195.24990093602662, -33.34960627592398], [218.31387531576726, -52.3214550379599]], "leg_r": [[160.0, 65.2155372436685], [153.598037355837, 110.07582613989624], [122.07760721156811, 85.82349837463056], [111.87858481133344, 89.88156862741086]], "leg_l": [[165.90884651807326, 64.7540782623559], [172.71840731660157, 109.55611956268012], [162.00000000000003, 147.5078524006199], [172.5977298599761, 150.4106343225862]]}, {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [161.5181281065588, 21.186959188910578], [162.9664572197126, -19.657665952703468]], "head": [164.36158669459508, -39.30584216914616], "nose": [[174.35567849015837, -39.649541429917065], [181.35141427929204, -39.89012649441899]], "arm_r": [[162.9664572197126, -17.74753599836724], [150.76798165630018, 9.244052078465458], [156.0, 38.3355128699944]], "arm_l": [[168.87530373778586, -19.59818040101528], [194.08432618194038, -35.19178059954181], [216.3056442657986, -54.9914509633138]], "leg_r": [[160.0, 65.2155372436685], [116.01483426203507, 51.955138515520545], [154.0, 39.6105356653511], [159.45624794549292, 30.204236892595254]], "leg_l": [[165.90884651807326, 64.7540782623559], [172.71840731660157, 109.55611956268012], [162.00000000000003, 147.5078524006199], [172.5977298599761, 150.4106343225862]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[160.0, 64.0], [161.5181281065588, 21.186959188910578], [162.9664572197126, -19.657665952703468]], "head": [164.36158669459508, -39.30584216914616], "nose": [[174.35567849015837, -39.649541429917065], [181.35141427929204, -39.89012649441899]], "arm_r": [[162.9664572197126, -17.74753599836724], [148.88406149406435, 8.339389938618353], [153.5884153863569, 37.518116152632096]], "arm_l": [[168.87530373778586, -19.59818040101528], [194.68483483546345, -34.28473592696265], [217.33279023539066, -53.6789032423792]], "leg_r": [[160.0, 65.2155372436685], [121.19953747183251, 89.54894130397363], [128.05510569102682, 50.73950342195268], [120.81943634843438, 42.5801415318936]], "leg_l": [[165.90884651807326, 64.7540782623559], [172.71840731660157, 109.55611956268012], [162.00000000000003, 147.5078524006199], [172.5977298599761, 150.4106343225862]]}}}, {"name": "Standing Side Bend", "camera": 90.0, "frames": [{"order": ["leg_l", "leg_r", "spine", "arm_r", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 72.0], [153.03719012291472, 28.66760595178478], [126.51599455364676, -4.1603828869694155]], "head": [116.21523305544568, -21.043282954169158], "arm_r": [[117.08715424592353, 1.4189654949775277], [128.77181618709193, 28.91334251867918], [139.84864615181866, 56.420863887144534]], "arm_l": [[135.94483486137, -9.739731268916358], [147.93654155006897, -37.233957687641194], [152.7812111713494, -55.72796157898259]], "leg_r": [[153.0, 72.0], [138.36855694632547, 115.55868611102298], [155.0, 149.09377549913083], [146.55707891051017, 154.93895289329112]], "leg_l": [[167.0, 72.0], [181.63144305367453, 115.55868611102298], [165.0, 149.09377549913083], [173.44292108948983, 154.93895289329112]]}, {"order": ["leg_l", "leg_r", "spine", "arm_l", "arm_r", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 72.0], [160.0, 29.160862743968956], [160.0, -11.708659006037678]], "head": [160.0, -31.40481406628183], "arm_r": [[149.0, -11.708659006037678], [144.82480697119803, 17.8366392933887], [140.77363501897415, 47.26294768420651]], "arm_l": [[171.0, -11.708659006037678], [175.17519302880197, 17.8366392933887], [179.22636498102585, 47.26294768420651]], "leg_r": [[153.0, 72.0], [138.36855694632547, 115.55868611102298], [155.0, 149.09377549913083], [146.55707891051017, 154.93895289329112]], "leg_l": [[167.0, 72.0], [181.63144305367453, 115.55868611102298], [165.0, 149.09377549913083], [173.44292108948983, 154.93895289329112]]}, {"order": ["leg_l", "leg_r", "spine", "arm_l", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 72.0], [166.96280987708528, 28.66760595178478], [193.48400544635325, -4.1603828869694155]], "head": [203.78476694455432, -21.043282954169158], "arm_r": [[184.05516513863, -9.739731268916358], [172.06345844993103, -37.233957687641194], [167.2187888286506, -55.72796157898259]], "arm_l": [[202.91284575407647, 1.4189654949775277], [191.22818381290807, 28.91334251867918], [180.15135384818134, 56.420863887144534]], "leg_r": [[153.0, 72.0], [138.36855694632547, 115.55868611102298], [155.0, 149.09377549913083], [146.55707891051017, 154.93895289329112]], "leg_l": [[167.0, 72.0], [181.63144305367453, 115.55868611102298], [165.0, 149.09377549913083], [173.44292108948983, 154.93895289329112]]}, {"order": ["leg_l", "leg_r", "spine", "arm_l", "arm_r", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 72.0], [160.0, 29.160862743968956], [160.0, -11.708659006037678]], "head": [160.0, -31.40481406628183], "arm_r": [[149.0, -11.708659006037678], [144.82480697119803, 17.8366392933887], [140.77363501897415, 47.26294768420651]], "arm_l": [[171.0, -11.708659006037678], [175.17519302880197, 17.8366392933887], [179.22636498102585, 47.26294768420651]], "leg_r": [[153.0, 72.0], [138.36855694632547, 115.55868611102298], [155.0, 149.09377549913083], [146.55707891051017, 154.93895289329112]], "leg_l": [[167.0, 72.0], [181.63144305367453, 115.55868611102298], [165.0, 149.09377549913083], [173.44292108948983, 154.93895289329112]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "leg_r", "spine", "arm_r", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 72.0], [156.63534088571913, 29.02687874664438], [142.7915448634304, -9.792229018314188]], "head": [137.44677734186524, -28.772043845484575], "arm_r": [[132.19160987813555, -6.897266347007331], [136.08805632571406, 22.68539136798885], [139.7334672760993, 52.166082466652526]], "arm_l": [[153.39147984872525, -12.687191689621045], [183.09625879428003, -8.649429316315874], [207.62273713211505, -4.858687263051138]], "leg_r": [[153.0, 72.0], [138.36855694632547, 115.55868611102298], [155.0, 149.09377549913083], [146.55707891051017, 154.93895289329112]], "leg_l": [[167.0, 72.0], [181.63144305367453, 115.55868611102298], [165.0, 149.09377549913083], [173.44292108948983, 154.93895289329112]]}}}, {"name": "Torso Twist", "camera": 90.0, "frames": [{"order": ["leg_l", "leg_r", "spine", "arm_r", "arm_l", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 64.0], [160.0, 21.160862743968956], [160.0, -19.708659006037678]], "head": [160.0, -39.40481406628183], "nose": [[150.68351813315385, -35.77119801271661], [147.83636163162325, -34.660749485167834]], "arm_r": [[150.6714709422793, -20.720873666029334], [140.45270482677998, 4.751067330158591], [126.41597666385152, 22.521983399674568]], "arm_l": [[169.3285290577207, -18.69644434604602], [159.10976294222135, 6.775496650141903], [145.0730347792929, 24.54641271965788]], "leg_r": [[153.0, 64.0], [152.38216056651342, 109.45365967810031], [151.8451597222945, 148.77302494282907], [151.8406455123998, 150.78833858281408]], "leg_l": [[167.0, 64.0], [167.61783943348658, 109.45365967810031], [168.1548402777055, 148.77302494282907], [168.1593544876002, 150.78833858281408]]}, {"order": ["leg_l", "leg_r", "spine", "arm_l", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 64.0], [160.0, 21.160862743968956], [160.0, -19.708659006037678]], "head": [160.0, -39.40481406628183], "nose": [[169.31648186684615, -35.77119801271661], [172.16363836837675, -34.660749485167834]], "arm_r": [[150.6714709422793, -18.69644434604602], [160.89023705777865, 6.775496650141903], [174.92696522070713, 24.54641271965788]], "arm_l": [[169.3285290577207, -20.720873666029334], [179.54729517322002, 4.751067330158591], [193.5840233361485, 22.521983399674568]], "leg_r": [[153.0, 64.0], [152.38216056651342, 109.45365967810031], [151.8451597222945, 148.77302494282907], [151.8406455123998, 150.78833858281408]], "leg_l": [[167.0, 64.0], [167.61783943348658, 109.45365967810031], [168.1548402777055, 148.77302494282907], [168.1593544876002, 150.78833858281408]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "leg_r", "spine", "arm_l", "arm_r", "head"], "shade": {"arm_r": "far", "arm_l": "near", "leg_r": "far", "leg_l": "near"}, "spine": [[160.0, 64.0], [160.0, 21.160862743968956], [160.0, -19.708659006037678]], "head": [160.0, -39.40481406628183], "arm_r": [[149.0, -19.708659006037678], [149.0, 6.272103107495482], [149.0, 24.741947367265226]], "arm_l": [[171.0, -19.708659006037678], [171.0, 6.272103107495482], [171.0, 24.741947367265226]], "leg_r": [[153.0, 64.0], [152.30227750929063, 109.43468725833473], [151.6958455907651, 148.7345183160037], [151.69074752238387, 150.77701284803751]], "leg_l": [[167.0, 64.0], [167.69772249070937, 109.43468725833473], [168.3041544092349, 148.73451831600366], [168.30925247761613, 150.7770128480375]]}}}, {"name": "Tricep Press", "camera": 0.0, "frames": [{"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[118.0, 124.0], [109.72232766064437, 81.92461008445406], [104.68785811999925, 41.33721444350374]], "head": [106.08298759488176, 21.689038227061047], "nose": [[116.02984525543485, 20.659464198301396], [122.99149852301275, 19.93888111104634]], "arm_r": [[104.68785811999925, 43.24734439783997], [94.67819740520659, 71.09854849438184], [124.0, 64.85204926757933]], "arm_l": [[110.5967046380725, 41.39669999519193], [98.50612246687119, 68.43532854979176], [128.0, 63.03132775016979]], "leg_r": [[118.0, 125.2155372436685], [162.26738385552633, 112.89958304064265], [156.0, 151.8053465749981], [166.8641356033426, 153.50269283330525]], "leg_l": [[123.90884651807325, 124.7540782623559], [167.98485944606693, 111.78953829173301], [160.00000000000003, 150.38900272591184], [170.7786039325257, 152.55147938544167]]}, {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[118.0, 124.0], [109.72232766064437, 81.92461008445406], [104.68785811999925, 41.33721444350374]], "head": [106.08298759488176, 21.689038227061047], "nose": [[116.02984525543485, 20.659464198301396], [122.99149852301275, 19.93888111104634]], "arm_r": [[104.68785811999925, 43.24734439783997], [109.6655943384287, 72.38204819466002], [124.0, 98.3355128699944]], "arm_l": [[110.5967046380725, 41.39669999519193], [113.17159954067507, 70.83190879489234], [128.0, 96.51479135258487]], "leg_r": [[118.0, 125.2155372436685], [162.26738385552633, 112.89958304064265], [156.0, 151.8053465749981], [166.8641356033426, 153.50269283330525]], "leg_l": [[123.90884651807325, 124.7540782623559], [167.98485944606693, 111.78953829173301], [160.00000000000003, 150.38900272591184], [170.7786039325257, 152.55147938544167]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "arm_l", "spine", "leg_r", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[118.0, 124.0], [109.72232766064437, 81.92461008445406], [104.68785811999925, 41.33721444350374]], "head": [106.08298759488176, 21.689038227061047], "nose": [[116.02984525543485, 20.659464198301396], [122.99149852301275, 19.93888111104634]], "arm_r": [[104.68785811999925, 43.24734439783997], [95.81620416576652, 71.47018477789757], [124.0, 81.59378106878685]], "arm_l": [[110.5967046380725, 41.39669999519193], [100.04454561797358, 69.05300441583627], [128.0, 79.77305955137733]], "leg_r": [[118.0, 125.2155372436685], [162.26738385552633, 112.89958304064265], [156.0, 151.8053465749981], [166.8641356033426, 153.50269283330525]], "leg_l": [[123.90884651807325, 124.7540782623559], [167.98485944606693, 111.78953829173301], [160.0, 150.38900272591184], [170.7786039325257, 152.5514793854417]]}}}, {"name": "Triceps Pushdown", "camera": 0.0, "frames": [{"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 66.0], [155.33005328091872, 23.464845696367505], [163.2776668032939, -16.66816291776044]], "head": [168.79041391963386, -35.60132233443484], "nose": [[178.69598771165374, -34.23033442389419], [185.62786419284475, -33.270923182040626]], "arm_r": [[163.2776668032939, -14.758032963424213], [147.83339766715355, 10.57038294758685], [172.0, -6.935259274591587]], "arm_l": [[169.18651332136716, -16.608677366072254], [150.1367514394947, 6.21479360962754], [176.0, -8.75598079200112]], "leg_r": [[150.0, 67.2155372436685], [160.60371763953367, 111.29667164972889], [146.0, 147.96977299066958], [156.24068183654285, 151.9247829380784]], "leg_l": [[155.90884651807326, 66.7540782623559], [165.23395514054224, 111.11463772661938], [150.0, 147.53823689459549], [160.17101027135118, 151.6639291057432]]}, {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 66.0], [155.33005328091872, 23.464845696367505], [163.2776668032939, -16.66816291776044]], "head": [168.79041391963386, -35.60132233443484], "nose": [[178.69598771165374, -34.23033442389419], [185.62786419284475, -33.270923182040626]], "arm_r": [[163.2776668032939, -14.758032963424213], [158.24284519048118, 14.367155954155876], [168.0, 42.30512837601883]], "arm_l": [[169.18651332136716, -16.608677366072254], [163.00090098657685, 12.300726187737686], [172.0, 40.4844068586093]], "leg_r": [[150.0, 67.2155372436685], [160.60371763953367, 111.29667164972889], [146.0, 147.96977299066958], [156.24068183654285, 151.9247829380784]], "leg_l": [[155.90884651807326, 66.7540782623559], [165.23395514054224, 111.11463772661938], [150.0, 147.53823689459549], [160.17101027135118, 151.6639291057432]]}], "tween": {"t": 0.5, "sample": {"order": ["leg_l", "leg_r", "arm_l", "spine", "arm_r", "head"], "shade": {"arm_r": "near", "arm_l": "far", "leg_r": "near", "leg_l": "far"}, "spine": [[150.0, 66.0], [155.33005328091872, 23.464845696367505], [163.2776668032939, -16.66816291776044]], "head": [168.79041391963386, -35.60132233443484], "nose": [[178.69598771165374, -34.23033442389419], [185.62786419284475, -33.270923182040626]], "arm_r": [[163.2776668032939, -14.758032963424213], [142.29329874083328, 6.3558478668007865], [170.0, 17.68493455071362]], "arm_l": [[169.18651332136716, -16.608677366072254], [146.90717119470105, 3.1766695627598747], [174.0, 15.864213033304088]], "leg_r": [[150.0, 67.2155372436685], [160.60371763953367, 111.29667164972889], [146.0, 147.96977299066958], [156.24068183654285, 151.9247829380784]], "leg_l": [[155.90884651807326, 66.7540782623559], [165.23395514054224, 111.11463772661938], [150.0, 147.53823689459549], [160.17101027135118, 151.6639291057432]]}}}]}
\ No newline at end of file
diff --git a/WorkoutsTests/GoalKindTests.swift b/WorkoutsTests/GoalKindTests.swift
new file mode 100644
index 0000000..862d98a
--- /dev/null
+++ b/WorkoutsTests/GoalKindTests.swift
@@ -0,0 +1,84 @@
+import Foundation
+import Testing
+@testable import Workouts
+
+/// Locks `GoalKind`'s persisted-order contract: the stored raw-value list drives the
+/// display order, unknown raw values are dropped, and any case missing from the stored
+/// list is appended in declaration order — so a newly shipped goal kind surfaces
+/// automatically without a migration. A throwaway `UserDefaults` suite keeps each case
+/// isolated from the real domain.
+struct GoalKindTests {
+
+ /// A fresh suite scoped to one test; cleared on teardown so nothing leaks between runs.
+ private func makeDefaults(_ function: String = #function) -> UserDefaults {
+ let suite = "GoalKindTests.\(function).\(UUID().uuidString)"
+ let defaults = UserDefaults(suiteName: suite)!
+ defaults.removePersistentDomain(forName: suite)
+ return defaults
+ }
+
+ /// With nothing stored, the order is exactly declaration order (all cases present).
+ @Test func emptyDefaultsGivesDeclarationOrder() {
+ let defaults = makeDefaults()
+ #expect(GoalKind.orderedCases(defaults: defaults) == GoalKind.allCases)
+ #expect(GoalKind.orderedCases(defaults: defaults) == [.mobility, .cardio, .strength, .mindfulness])
+ }
+
+ /// A partial stored list keeps the stored ones first (in stored order), then appends
+ /// the missing cases in declaration order — the "new kind appears automatically" rule.
+ @Test func partialStoredListPutsStoredFirstThenMissing() {
+ let defaults = makeDefaults()
+ GoalKind.saveOrder([.mindfulness, .mobility], defaults: defaults)
+
+ let ordered = GoalKind.orderedCases(defaults: defaults)
+ #expect(ordered == [.mindfulness, .mobility, .cardio, .strength])
+ }
+
+ /// A full stored list is honored verbatim — a user-chosen reorder round-trips.
+ @Test func fullStoredListIsHonoredVerbatim() {
+ let defaults = makeDefaults()
+ let chosen: [GoalKind] = [.cardio, .strength, .mindfulness, .mobility]
+ GoalKind.saveOrder(chosen, defaults: defaults)
+ #expect(GoalKind.orderedCases(defaults: defaults) == chosen)
+ }
+
+ /// Unknown raw values in the stored list are dropped (e.g. a kind removed in a future
+ /// version), and the survivors still slot ahead of the missing declaration-order tail.
+ @Test func unknownRawValuesAreDropped() {
+ let defaults = makeDefaults()
+ defaults.set(["mindfulness", "flexibility", "strength"], forKey: GoalKind.orderDefaultsKey)
+
+ let ordered = GoalKind.orderedCases(defaults: defaults)
+ #expect(ordered == [.mindfulness, .strength, .mobility, .cardio])
+ }
+
+ /// Duplicate raw values collapse to a single entry (first wins), so a corrupt list
+ /// never yields a kind twice.
+ @Test func duplicateRawValuesCollapse() {
+ let defaults = makeDefaults()
+ defaults.set(["strength", "strength", "mobility"], forKey: GoalKind.orderDefaultsKey)
+
+ let ordered = GoalKind.orderedCases(defaults: defaults)
+ #expect(ordered == [.strength, .mobility, .cardio, .mindfulness])
+ #expect(Set(ordered).count == ordered.count) // no dupes
+ }
+
+ /// `saveOrder` persists `[String]` of raw values — the exact shape `orderedCases`
+ /// reads back.
+ @Test func saveOrderStoresRawValueStrings() {
+ let defaults = makeDefaults()
+ GoalKind.saveOrder([.mobility, .strength], defaults: defaults)
+ #expect(defaults.stringArray(forKey: GoalKind.orderDefaultsKey) == ["mobility", "strength"])
+ }
+
+ /// Every case carries a valid, distinct-hued color name `Color.color(from:)` maps
+ /// to a non-default color, and stable display metadata.
+ @Test func metadataIsPopulatedAndDistinct() {
+ #expect(Set(GoalKind.allCases.map(\.colorName)).count == GoalKind.allCases.count)
+ #expect(Set(GoalKind.allCases.map(\.systemImage)).count == GoalKind.allCases.count)
+ #expect(GoalKind.strength.displayName == "Strength")
+ #expect(GoalKind.mindfulness.displayName == "Mindfulness")
+ // Each color name is one the palette understands (falls back to indigo otherwise).
+ #expect(GoalKind.allCases.allSatisfy { availableColors.contains($0.colorName) })
+ }
+}
diff --git a/WorkoutsTests/ProgressPlannerTests.swift b/WorkoutsTests/ProgressPlannerTests.swift
new file mode 100644
index 0000000..211e2e5
--- /dev/null
+++ b/WorkoutsTests/ProgressPlannerTests.swift
@@ -0,0 +1,519 @@
+import Foundation
+import Testing
+@testable import Workouts
+
+/// Locks the derivation rules of `ProgressPlanner` — the Progress tab's pure adherence,
+/// streak, trend, and achievement engine (precedent: `SeedReconcilePlanner`). Every
+/// assertion below is a deterministic function of plain value snapshots (`ScheduleFacts`,
+/// `WorkoutFacts`, bare `WeekMark`/`GoalTrack`) run through a fixed UTC, Monday-first
+/// calendar, so nothing here depends on the host machine's clock, locale, or timezone.
+struct ProgressPlannerTests {
+
+ // MARK: - Fixed calendar & date helpers
+
+ /// UTC Gregorian, Monday-first weeks — matches the app's real `weekOfYear` grid
+ /// without depending on the host machine's locale or timezone.
+ private let cal: Calendar = {
+ var cal = Calendar(identifier: .gregorian)
+ cal.timeZone = TimeZone(identifier: "UTC")!
+ cal.firstWeekday = 2
+ return cal
+ }()
+
+ /// Midnight UTC on an ISO `yyyy-MM-dd` date. All the fixture dates below live in
+ /// June/July 2026, whose weekday layout (verified against two independent anchors)
+ /// is: 2026-06-29 is a Monday, so the two relevant Monday–Sunday weeks are
+ /// 06-29…07-05 and 07-06…07-12.
+ private func day(_ iso: String) -> Date {
+ let parts = iso.split(separator: "-").compactMap { Int($0) }
+ var components = DateComponents()
+ components.year = parts[0]; components.month = parts[1]; components.day = parts[2]
+ return cal.date(from: components)!
+ }
+
+ /// The same calendar day at a specific hour, for time-of-day achievements.
+ private func at(_ base: Date, hour: Int) -> Date {
+ cal.date(bySettingHour: hour, minute: 0, second: 0, of: base)!
+ }
+
+ // MARK: - Fixtures
+
+ private func schedule(
+ id: String = "SCH-1",
+ goal: GoalKind? = .strength,
+ recurrence: ScheduleRecurrence,
+ weekdays: [Int]? = nil,
+ timesPerWeek: Int? = nil,
+ onceDate: Date? = nil,
+ createdAt: Date,
+ routineID: String = "R-1",
+ routineName: String = "Push Day"
+ ) -> ScheduleFacts {
+ ScheduleFacts(id: id, goal: goal, recurrence: recurrence, weekdays: weekdays,
+ timesPerWeek: timesPerWeek, onceDate: onceDate, createdAt: createdAt,
+ routineID: routineID, routineName: routineName)
+ }
+
+ private func workout(
+ routineID: String? = "R-1",
+ start: Date,
+ end: Date? = nil,
+ completed: Bool = true,
+ volume: Double = 0,
+ activeMinutes: Double = 0,
+ energyKcal: Double = 0
+ ) -> WorkoutFacts {
+ WorkoutFacts(routineID: routineID, start: start, end: end, completed: completed,
+ volume: volume, activeMinutes: activeMinutes, energyKcal: energyKcal)
+ }
+
+ /// A bare `WeekMark` for exercising `streak`/`bestHitRun` directly — their logic
+ /// only reads `met`/`expected`, never `weekStart`, so a fixed date is fine.
+ private func mark(_ met: Int, _ expected: Int) -> WeekMark {
+ WeekMark(weekStart: day("2026-01-01"), met: met, expected: expected)
+ }
+
+ /// A bare `GoalTrack` for exercising `cleanSweepExists` (reached via `achievements`)
+ /// directly, without routing through `goalTracks`.
+ private func goalTrack(_ goal: GoalKind?, _ weeks: [WeekMark]) -> GoalTrack {
+ GoalTrack(goal: goal, scheduleTracks: [], weeks: weeks, streakWeeks: 0, streakAlive: true)
+ }
+
+ // MARK: - weekMark: daily
+
+ @Test func weekMarkDaily() {
+ let daily = schedule(recurrence: .daily, createdAt: day("2026-06-01"))
+ let now = day("2026-07-08") // Wednesday; current week is Mon 07-06 – Sun 07-12
+
+ // A fully-past week (Mon 06-29 – Sun 07-05), every day done.
+ let pastWeek = day("2026-06-29")
+ let fullyMetDays = Set((0..<7).map { cal.date(byAdding: .day, value: $0, to: pastWeek)! })
+ let fullyMet = ProgressPlanner.weekMark(schedule: daily, doneDays: fullyMetDays,
+ weekStart: pastWeek, calendar: cal, now: now)
+ #expect(fullyMet?.met == 7)
+ #expect(fullyMet?.expected == 7)
+ #expect(fullyMet?.hit == true)
+
+ // Same past week, only 4 of 7 days done — a clean met/expected count, not hit.
+ let partialDays = Set([0, 1, 3, 5].map { cal.date(byAdding: .day, value: $0, to: pastWeek)! })
+ let partial = ProgressPlanner.weekMark(schedule: daily, doneDays: partialDays,
+ weekStart: pastWeek, calendar: cal, now: now)
+ #expect(partial?.met == 4)
+ #expect(partial?.expected == 7)
+ #expect(partial?.hit == false)
+
+ // The current week (Mon 07-06 – Sun 07-12): only Mon/Tue/Wed have elapsed by
+ // `now`, so expected clamps to 3, not the full 7 — and a "done" day beyond
+ // today wouldn't count even if present in doneDays.
+ let currentWeek = day("2026-07-06")
+ let doneSoFar: Set = [day("2026-07-06"), day("2026-07-07")]
+ let current = ProgressPlanner.weekMark(schedule: daily, doneDays: doneSoFar,
+ weekStart: currentWeek, calendar: cal, now: now)
+ #expect(current?.expected == 3)
+ #expect(current?.met == 2)
+ }
+
+ // MARK: - weekMark: fixedDays
+
+ @Test func weekMarkFixedDays() {
+ // Required on Mon/Wed/Fri (weekday numbers 2, 4, 6).
+ let fixed = schedule(recurrence: .fixedDays, weekdays: [2, 4, 6], createdAt: day("2026-06-01"))
+ let now = day("2026-07-08") // Wednesday
+
+ // Met exactly on the fixed days themselves.
+ let pastWeek = day("2026-06-29")
+ let onFixedDays: Set = [day("2026-06-29"), day("2026-07-01"), day("2026-07-03")]
+ let metOnFixed = ProgressPlanner.weekMark(schedule: fixed, doneDays: onFixedDays,
+ weekStart: pastWeek, calendar: cal, now: now)
+ #expect(metOnFixed?.met == 3)
+ #expect(metOnFixed?.expected == 3)
+ #expect(metOnFixed?.hit == true)
+
+ // A catch-up on a non-fixed day (Tuesday) still counts toward met: Monday
+ // done + a Tuesday catch-up reaches met == 2 even though Wednesday and Friday
+ // were both skipped — the requirement is 3, so it still isn't hit, but the
+ // catch-up visibly moved met from what a fixed-days-only count (1) would give.
+ let withCatchUp: Set = [day("2026-06-29"), day("2026-06-30")]
+ let catchUp = ProgressPlanner.weekMark(schedule: fixed, doneDays: withCatchUp,
+ weekStart: pastWeek, calendar: cal, now: now)
+ #expect(catchUp?.met == 2)
+ #expect(catchUp?.expected == 3)
+ #expect(catchUp?.hit == false)
+
+ // The current week (Mon 07-06 – Sun 07-12): expected counts only the fixed
+ // days that have already elapsed (Mon + Wed), not the not-yet-arrived Friday.
+ let currentWeek = day("2026-07-06")
+ let current = ProgressPlanner.weekMark(schedule: fixed, doneDays: [day("2026-07-06"), day("2026-07-08")],
+ weekStart: currentWeek, calendar: cal, now: now)
+ #expect(current?.expected == 2)
+ #expect(current?.met == 2)
+ }
+
+ // MARK: - weekMark: frequency
+
+ @Test func weekMarkFrequency() {
+ let now = day("2026-07-08") // Wednesday; current week Mon 07-06 – Sun 07-12
+ let currentWeek = day("2026-07-06")
+
+ // Expected is the full weekly target even mid-week — NOT clamped down to the
+ // 3 days that have elapsed so far.
+ let twicePerWeek = schedule(recurrence: .frequency, timesPerWeek: 2, createdAt: day("2026-06-01"))
+ let untouched = ProgressPlanner.weekMark(schedule: twicePerWeek, doneDays: [],
+ weekStart: currentWeek, calendar: cal, now: now)
+ #expect(untouched?.expected == 2)
+ #expect(untouched?.met == 0)
+
+ // Met is capped at the target even when more sessions were logged.
+ let threeDone: Set = [day("2026-07-06"), day("2026-07-07"), day("2026-07-08")]
+ let overMet = ProgressPlanner.weekMark(schedule: twicePerWeek, doneDays: threeDone,
+ weekStart: currentWeek, calendar: cal, now: now)
+ #expect(overMet?.met == 2)
+ #expect(overMet?.expected == 2)
+ #expect(overMet?.hit == true)
+
+ // Target clamps to the days the schedule existed in its creation week: created
+ // Wednesday leaves only 5 days (Wed–Sun) in that week, so a 7x/week target
+ // clamps down to 5, not 7.
+ let createdMidWeek = schedule(recurrence: .frequency, timesPerWeek: 7, createdAt: day("2026-07-08"))
+ let clamped = ProgressPlanner.weekMark(schedule: createdMidWeek, doneDays: [],
+ weekStart: currentWeek, calendar: cal, now: day("2026-07-12"))
+ #expect(clamped?.expected == 5)
+ }
+
+ // MARK: - weekMark: once
+
+ @Test func weekMarkOnce() {
+ let onceDate = day("2026-07-08") // Wednesday, in week Mon 07-06 – Sun 07-12
+ let once = schedule(recurrence: .once, onceDate: onceDate, createdAt: day("2026-06-01"))
+ let now = day("2026-07-10")
+
+ // A week that doesn't contain the once date: nil, whether before or after it.
+ #expect(ProgressPlanner.weekMark(schedule: once, doneDays: [], weekStart: day("2026-06-29"),
+ calendar: cal, now: now) == nil)
+ #expect(ProgressPlanner.weekMark(schedule: once, doneDays: [], weekStart: day("2026-07-13"),
+ calendar: cal, now: now) == nil)
+
+ // Its own week: expected is always 1.
+ let ownWeek = day("2026-07-06")
+ let unmet = ProgressPlanner.weekMark(schedule: once, doneDays: [], weekStart: ownWeek,
+ calendar: cal, now: now)
+ #expect(unmet?.expected == 1)
+ #expect(unmet?.met == 0)
+
+ // Met when completed on ANY day that week — not necessarily the once date itself.
+ let anyDayDone: Set = [day("2026-07-07")]
+ let met = ProgressPlanner.weekMark(schedule: once, doneDays: anyDayDone, weekStart: ownWeek,
+ calendar: cal, now: now)
+ #expect(met?.met == 1)
+ #expect(met?.hit == true)
+ }
+
+ // MARK: - weekMark: creation-window edges
+
+ @Test func weekMarkNilBeforeCreationAndClipsCreationWeek() {
+ let createdAt = day("2026-07-08") // Wednesday
+ let daily = schedule(recurrence: .daily, createdAt: createdAt)
+ let now = day("2026-07-12")
+
+ // A week entirely before creation: nil, not a zeroed mark.
+ #expect(ProgressPlanner.weekMark(schedule: daily, doneDays: [], weekStart: day("2026-06-29"),
+ calendar: cal, now: now) == nil)
+
+ // The creation week itself: expected only counts the days since creation
+ // (Wed–Sun = 5), not the full 7-day week.
+ let creationWeek = day("2026-07-06")
+ let mark = ProgressPlanner.weekMark(schedule: daily, doneDays: [], weekStart: creationWeek,
+ calendar: cal, now: now)
+ #expect(mark?.expected == 5)
+ }
+
+ // MARK: - currentWeekBroken
+
+ @Test func currentWeekBrokenDailyMissedYesterdayVsUndoneToday() {
+ let daily = schedule(recurrence: .daily, createdAt: day("2026-06-01"))
+ let now = day("2026-07-08") // Wednesday; current week Mon 07-06 – Sun 07-12
+ let weekStart = day("2026-07-06")
+
+ // Missed Tuesday: even finishing every remaining day (Wed–Sun, 5 days) can't
+ // reach the full 7 — the week is already unmeetable.
+ let missedYesterday: Set = [day("2026-07-06")]
+ #expect(ProgressPlanner.currentWeekBroken(schedule: daily, doneDays: missedYesterday,
+ weekStart: weekStart, calendar: cal, now: now))
+
+ // Monday and Tuesday both done; today (Wednesday) simply hasn't happened yet —
+ // still fully recoverable, not broken.
+ let undoneToday: Set = [day("2026-07-06"), day("2026-07-07")]
+ #expect(!ProgressPlanner.currentWeekBroken(schedule: daily, doneDays: undoneToday,
+ weekStart: weekStart, calendar: cal, now: now))
+ }
+
+ @Test func currentWeekBrokenFrequencyDaysRemainingVsRequirement() {
+ let twicePerWeek = schedule(recurrence: .frequency, timesPerWeek: 2, createdAt: day("2026-06-01"))
+ let weekStart = day("2026-07-06")
+ let oneDone: Set = [day("2026-07-07")]
+
+ // Saturday: 2 days left (Sat, Sun). 1 done, 1 still needed — achievable.
+ #expect(!ProgressPlanner.currentWeekBroken(schedule: twicePerWeek, doneDays: oneDone,
+ weekStart: weekStart, calendar: cal, now: day("2026-07-11")))
+
+ // Sunday: only 1 day left (today itself). 1 done, 1 still needed — needs
+ // exactly 1 ≤ 1 day left, so still achievable.
+ #expect(!ProgressPlanner.currentWeekBroken(schedule: twicePerWeek, doneDays: oneDone,
+ weekStart: weekStart, calendar: cal, now: day("2026-07-12")))
+
+ // Sunday again, but nothing done yet: 2 still needed with only 1 day left — broken.
+ #expect(ProgressPlanner.currentWeekBroken(schedule: twicePerWeek, doneDays: [],
+ weekStart: weekStart, calendar: cal, now: day("2026-07-12")))
+ }
+
+ @Test func currentWeekBrokenFixedDaysCatchUpDaysRemainingNotBroken() {
+ // Mon/Wed/Fri required. Monday (a fixed day) was missed, but it's only
+ // Tuesday — catch-up days (Wed, Fri, or any other day this week) still remain.
+ let fixed = schedule(recurrence: .fixedDays, weekdays: [2, 4, 6], createdAt: day("2026-06-01"))
+ #expect(!ProgressPlanner.currentWeekBroken(schedule: fixed, doneDays: [],
+ weekStart: day("2026-07-06"), calendar: cal,
+ now: day("2026-07-07")))
+ }
+
+ // MARK: - streak
+
+ @Test func streakCountsConsecutiveHitWeeksNewestToOldest() {
+ // Three consecutive hits ending at the newest (current) week.
+ #expect(ProgressPlanner.streak(weeks: [mark(1, 1), mark(1, 1), mark(1, 1)], alive: true) == 3)
+
+ // A miss two weeks back stops the run at the newest unbroken stretch.
+ #expect(ProgressPlanner.streak(weeks: [mark(0, 1), mark(1, 1), mark(1, 1)], alive: true) == 2)
+ }
+
+ @Test func streakCurrentWeekUnhitButAliveNeitherCountsNorBreaks() {
+ // The current (last) week isn't hit yet but is still meetable ("alive") — it
+ // neither adds to the count nor zeroes it; the streak reads from the prior
+ // unbroken run.
+ let weeks = [mark(1, 1), mark(1, 1), mark(0, 1)]
+ #expect(ProgressPlanner.streak(weeks: weeks, alive: true) == 2)
+ }
+
+ @Test func streakCurrentWeekUnhitAndDeadReturnsZero() {
+ // Same shape, but the current week is already unmeetable ("dead") — the whole
+ // streak breaks, even though the two prior weeks were hits.
+ let weeks = [mark(1, 1), mark(1, 1), mark(0, 1)]
+ #expect(ProgressPlanner.streak(weeks: weeks, alive: false) == 0)
+ }
+
+ @Test func streakVacuousWeeksAreTransparent() {
+ // A vacuous (expected 0) week between two hits neither breaks the run nor
+ // resets it — it's invisible to the scan, so the hits on either side bridge
+ // across it into one streak.
+ let weeks = [mark(1, 1), mark(0, 0), mark(1, 1)]
+ #expect(ProgressPlanner.streak(weeks: weeks, alive: true) == 2)
+
+ // Alive is irrelevant when the current week is itself vacuous.
+ let vacuousCurrent = [mark(1, 1), mark(1, 1), mark(0, 0)]
+ #expect(ProgressPlanner.streak(weeks: vacuousCurrent, alive: false) == 2)
+ }
+
+ // MARK: - bestHitRun & longestDayRun
+
+ @Test func bestHitRunFindsLongestRunAroundAMissWithVacuousWeeksTransparent() {
+ // hit, miss, hit, hit, vacuous, hit, hit — the miss resets the run, but the
+ // vacuous week doesn't, so the trailing run bridges to length 4.
+ let weeks = [mark(1, 1), mark(0, 1), mark(1, 1), mark(1, 1), mark(0, 0), mark(1, 1), mark(1, 1)]
+ #expect(ProgressPlanner.bestHitRun(weeks: weeks) == 4)
+ }
+
+ @Test func longestDayRunCountsConsecutiveDaysAcrossAGap() {
+ let workouts = [
+ workout(start: day("2026-07-01")),
+ workout(start: day("2026-07-02")),
+ workout(start: day("2026-07-03")),
+ workout(start: day("2026-07-04"), completed: false), // doesn't bridge the gap
+ workout(start: day("2026-07-06")),
+ workout(start: day("2026-07-07")),
+ ]
+ #expect(ProgressPlanner.longestDayRun(workouts: workouts, calendar: cal) == 3)
+ }
+
+ // MARK: - goalTracks
+
+ @Test func goalTracksGroupsByGoalOrderWithTrailingNilGroupAndSkipsEmptyGoals() {
+ let now = day("2026-07-08")
+ let strength1 = schedule(id: "S1", goal: .strength, recurrence: .daily, createdAt: day("2026-06-01"))
+ let strength2 = schedule(id: "S2", goal: .strength, recurrence: .daily, createdAt: day("2026-06-01"))
+ let unassigned = schedule(id: "S3", goal: nil, recurrence: .daily, createdAt: day("2026-06-01"))
+
+ let tracks = ProgressPlanner.goalTracks(
+ schedules: [strength1, strength2, unassigned], workouts: [], weeksBack: 1,
+ goalOrder: [.strength, .mobility, .cardio, .mindfulness], calendar: cal, now: now)
+
+ // Only goals with member schedules produce a track (mobility/cardio/mindfulness
+ // are skipped entirely, not emitted empty); the goal-less group trails last.
+ #expect(tracks.map(\.goal) == [.strength, nil])
+ #expect(tracks[0].scheduleTracks.map(\.schedule.id) == ["S1", "S2"])
+ }
+
+ @Test func goalTracksRollupSumsAndStreakAliveFollowsBrokenMember() throws {
+ let now = day("2026-07-08") // Wednesday; current week Mon 07-06 – Sun 07-12
+ let onTrack = schedule(id: "S1", goal: .strength, recurrence: .daily,
+ createdAt: day("2026-06-01"), routineID: "R-ok")
+ let broken = schedule(id: "S2", goal: .strength, recurrence: .daily,
+ createdAt: day("2026-06-01"), routineID: "R-broken")
+
+ let onTrackWorkouts = [day("2026-07-06"), day("2026-07-07"), day("2026-07-08")]
+ .map { workout(routineID: "R-ok", start: $0) }
+ // Missed Tuesday permanently — even finishing every remaining day can't reach 7/7.
+ let brokenWorkouts = [workout(routineID: "R-broken", start: day("2026-07-06"))]
+
+ let tracks = ProgressPlanner.goalTracks(
+ schedules: [onTrack, broken], workouts: onTrackWorkouts + brokenWorkouts, weeksBack: 1,
+ calendar: cal, now: now)
+
+ let strength = try #require(tracks.first { $0.goal == .strength })
+ // Rollup sums met/expected across BOTH member schedules for the same week.
+ #expect(strength.weeks.last?.met == 4) // 3 (onTrack) + 1 (broken)
+ #expect(strength.weeks.last?.expected == 6) // 3 (onTrack) + 3 (broken, elapsed so far)
+ // One member's current week is already unmeetable, so the goal's streak is dead.
+ #expect(strength.streakAlive == false)
+ }
+
+ // MARK: - trendPoints
+
+ @Test func trendPointsFiltersCompletedOnlyAndZeroFillsDailyBucketsForWeek() {
+ let now = day("2026-07-08") // cutoff = now - 7 days = 07-01
+ let workouts = [
+ workout(start: day("2026-07-02"), completed: true, volume: 10),
+ workout(start: day("2026-07-02"), completed: true, volume: 5), // same-day sum
+ workout(start: day("2026-07-03"), completed: false, volume: 999), // excluded entirely
+ workout(start: day("2026-07-05"), completed: true, volume: 20),
+ ]
+ let points = ProgressPlanner.trendPoints(workouts: workouts, range: .week, calendar: cal, now: now)
+
+ // One point per day from the cutoff (07-01) through now (07-08) inclusive.
+ #expect(points.count == 8)
+ #expect(points.first?.date == day("2026-07-01"))
+ #expect(points.last?.date == day("2026-07-08"))
+
+ func point(_ iso: String) -> TrendPoint? { points.first { $0.date == day(iso) } }
+ #expect(point("2026-07-02")?.workouts == 2)
+ #expect(point("2026-07-02")?.volume == 15)
+ // The gap day had an incomplete workout, but it's filtered out — zero-filled,
+ // not dropped and not counted.
+ #expect(point("2026-07-03")?.workouts == 0)
+ #expect(point("2026-07-04")?.workouts == 0)
+ #expect(point("2026-07-05")?.workouts == 1)
+ #expect(point("2026-07-05")?.volume == 20)
+ }
+
+ @Test func trendPointsZeroFillsEntireWindowEvenWithNoWorkouts() {
+ // Bounded ranges (a non-nil cutoff) zero-fill the whole window regardless of
+ // data — only the unbounded `.all` range can produce an empty array.
+ let points = ProgressPlanner.trendPoints(workouts: [], range: .week, calendar: cal, now: day("2026-07-08"))
+ #expect(points.count == 8)
+ #expect(points.allSatisfy { $0.workouts == 0 })
+ }
+
+ @Test func trendPointsWeeklyBucketsForQuarter() {
+ let now = day("2026-07-08") // Wednesday, week of Mon 07-06
+ let workouts = [
+ workout(start: day("2026-07-06")), // same week as 07-08
+ workout(start: day("2026-07-08")),
+ workout(start: day("2026-06-22")), // an earlier week
+ ]
+ let points = ProgressPlanner.trendPoints(workouts: workouts, range: .quarter, calendar: cal, now: now)
+
+ let thisWeek = points.first { $0.date == day("2026-07-06") }
+ #expect(thisWeek?.workouts == 2)
+
+ let earlierWeekStart = cal.dateInterval(of: .weekOfYear, for: day("2026-06-22"))!.start
+ let earlierWeek = points.first { $0.date == earlierWeekStart }
+ #expect(earlierWeek?.workouts == 1)
+ }
+
+ @Test func trendPointsMonthlyBucketsForYear() {
+ let now = day("2026-07-08")
+ let workouts = [
+ workout(start: day("2026-07-02")),
+ workout(start: day("2026-07-20")),
+ workout(start: day("2026-06-15")),
+ ]
+ let points = ProgressPlanner.trendPoints(workouts: workouts, range: .year, calendar: cal, now: now)
+
+ let july = points.first { $0.date == cal.dateInterval(of: .month, for: day("2026-07-02"))!.start }
+ #expect(july?.workouts == 2)
+ let june = points.first { $0.date == cal.dateInterval(of: .month, for: day("2026-06-15"))!.start }
+ #expect(june?.workouts == 1)
+ }
+
+ @Test func trendPointsAllRangeStartsAtEarliestWorkout() {
+ let now = day("2026-07-08")
+ let workouts = [
+ workout(start: day("2026-05-01")),
+ workout(start: day("2026-07-08")),
+ ]
+ let points = ProgressPlanner.trendPoints(workouts: workouts, range: .all, calendar: cal, now: now)
+
+ #expect(points.first?.date == cal.dateInterval(of: .month, for: day("2026-05-01"))!.start)
+ #expect(points.last?.date == cal.dateInterval(of: .month, for: now)!.start)
+ #expect(points.count == 3) // May, June, July
+ }
+
+ @Test func trendPointsEmptyInputReturnsEmptyArrayForAllRange() {
+ let points = ProgressPlanner.trendPoints(workouts: [], range: .all, calendar: cal, now: day("2026-07-08"))
+ #expect(points.isEmpty)
+ }
+
+ // MARK: - achievements
+
+ @Test func achievementsCountMilestoneProgressReflectsCompletedCount() throws {
+ let completedWorkouts = (1...5).map { workout(start: day("2026-07-0\($0)")) }
+ let badges = ProgressPlanner.achievements(tracks: [], workouts: completedWorkouts,
+ calendar: cal, now: day("2026-07-08"))
+
+ // 5 completed, target 10 — not earned, halfway there.
+ let tenClub = try #require(badges.first { $0.id == "count-10" })
+ #expect(tenClub.earned == false)
+ #expect(tenClub.progress == 0.5)
+
+ // 5 completed clears the 1-session milestone outright.
+ let firstRep = try #require(badges.first { $0.id == "count-1" })
+ #expect(firstRep.earned == true)
+ #expect(firstRep.progress == 1)
+ }
+
+ @Test func achievementsEarlyBirdAndNightOwlFromStartHours() throws {
+ let now = day("2026-07-08")
+
+ let earlyBadges = ProgressPlanner.achievements(
+ tracks: [], workouts: [workout(start: at(day("2026-07-01"), hour: 6))], calendar: cal, now: now)
+ #expect(try #require(earlyBadges.first { $0.id == "early-bird" }).earned == true)
+ #expect(try #require(earlyBadges.first { $0.id == "night-owl" }).earned == false)
+
+ let lateBadges = ProgressPlanner.achievements(
+ tracks: [], workouts: [workout(start: at(day("2026-07-01"), hour: 22))], calendar: cal, now: now)
+ #expect(try #require(lateBadges.first { $0.id == "night-owl" }).earned == true)
+ #expect(try #require(lateBadges.first { $0.id == "early-bird" }).earned == false)
+
+ // Boundary hours: exactly 7 AM does not count as early (< 7 required); exactly
+ // 9 PM does count as late (>= 21 required).
+ let boundaryBadges = ProgressPlanner.achievements(
+ tracks: [], workouts: [workout(start: at(day("2026-07-01"), hour: 7)),
+ workout(start: at(day("2026-07-01"), hour: 21))],
+ calendar: cal, now: now)
+ #expect(try #require(boundaryBadges.first { $0.id == "early-bird" }).earned == false)
+ #expect(try #require(boundaryBadges.first { $0.id == "night-owl" }).earned == true)
+ }
+
+ @Test func achievementsCleanSweepRequiresTwoNonVacuousGoalsHitInTheSameWeek() throws {
+ let now = day("2026-07-08")
+
+ // Only one goal is non-vacuously hit that week (the other is vacuous, i.e.
+ // imposed nothing) — not a sweep, even though every present mark is "hit".
+ let noSweep = [goalTrack(.strength, [mark(1, 1)]), goalTrack(.cardio, [mark(0, 0)])]
+ let noSweepBadges = ProgressPlanner.achievements(tracks: noSweep, workouts: [], calendar: cal, now: now)
+ #expect(try #require(noSweepBadges.first { $0.id == "clean-sweep" }).earned == false)
+
+ // Two goals both non-vacuously hit the same week — a genuine sweep.
+ let sweep = [goalTrack(.strength, [mark(1, 1)]), goalTrack(.cardio, [mark(2, 2)])]
+ let sweepBadges = ProgressPlanner.achievements(tracks: sweep, workouts: [], calendar: cal, now: now)
+ #expect(try #require(sweepBadges.first { $0.id == "clean-sweep" }).earned == true)
+ }
+}
diff --git a/WorkoutsTests/SplitDocumentCodableTests.swift b/WorkoutsTests/RoutineDocumentCodableTests.swift
similarity index 88%
rename from WorkoutsTests/SplitDocumentCodableTests.swift
rename to WorkoutsTests/RoutineDocumentCodableTests.swift
index 79c820b..2c61534 100644
--- a/WorkoutsTests/SplitDocumentCodableTests.swift
+++ b/WorkoutsTests/RoutineDocumentCodableTests.swift
@@ -3,13 +3,13 @@ import Testing
import IndieSync
@testable import Workouts
-/// Round-trips `SplitDocument` (and the embedded exercise / log shapes) through
+/// Round-trips `RoutineDocument` (and the embedded exercise / log shapes) through
/// `DocumentCoder` — the exact JSON codec (ISO-8601 dates, sorted keys,
/// pretty-printed) `DocumentFileStore` uses to write and read every document in
/// this app's iCloud container. Also pins the schema-v2 migration: an old v1
-/// split file (with the removed weight-reminder / category keys) must still decode
+/// routine file (with the removed weight-reminder / category keys) must still decode
/// and round-trip at the current version.
-struct SplitDocumentCodableTests {
+struct RoutineDocumentCodableTests {
/// A whole-second epoch so ISO-8601 round-trips (no fractional-second loss).
private static let stableTimestamp = Date(timeIntervalSince1970: 1_700_000_000)
@@ -28,8 +28,8 @@ struct SplitDocumentCodableTests {
MachineSetting(name: "Back Rest Incline", value: "45°")
]
)
- let original = SplitDocument(
- schemaVersion: SplitDocument.currentSchemaVersion,
+ let original = RoutineDocument(
+ schemaVersion: RoutineDocument.currentSchemaVersion,
id: ULID.make(),
name: "Push Day",
color: "blue",
@@ -42,10 +42,11 @@ struct SplitDocumentCodableTests {
)
let data = try DocumentCoder.encode(original)
- let decoded = try DocumentCoder.decode(SplitDocument.self, from: data)
+ let decoded = try DocumentCoder.decode(RoutineDocument.self, from: data)
#expect(decoded == original)
#expect(decoded.schemaVersion == 3)
+ // PINNED: routine documents live under the on-disk "Splits/" directory.
#expect(decoded.relativePath == "Splits/\(original.id).json")
#expect(decoded.exercises.first?.machineSettings?.count == 2)
}
@@ -98,13 +99,13 @@ struct SplitDocumentCodableTests {
#expect(decoded.machineSettings == [MachineSetting(name: "Seat Height", value: "4")])
}
- /// A v1 split file carrying the since-removed keys (`weightLastUpdated`,
+ /// A v1 routine file carrying the since-removed keys (`weightLastUpdated`,
/// `weightReminderWeeks`, `category`) must still decode — Codable ignores the
/// extra keys, every remaining field is present in old files, and the integer
/// `weight` decodes natively into today's `Double`. Re-stamping `schemaVersion`
/// to the current value (what the app does on any rewrite via the
/// cache→document mapper) round-trips cleanly at the current version.
- @Test func decodesLegacyV1SplitAndRoundTripsAtCurrent() throws {
+ @Test func decodesLegacyV1RoutineAndRoundTripsAtCurrent() throws {
let json = """
{
"schemaVersion": 1,
@@ -130,7 +131,7 @@ struct SplitDocumentCodableTests {
}]
}
"""
- var decoded = try DocumentCoder.decode(SplitDocument.self, from: Data(json.utf8))
+ var decoded = try DocumentCoder.decode(RoutineDocument.self, from: Data(json.utf8))
// Removed keys are ignored; the surviving fields decode and machineSettings
// defaults to nil (the file predates it).
#expect(decoded.schemaVersion == 1)
@@ -140,9 +141,9 @@ struct SplitDocumentCodableTests {
#expect(decoded.exercises.first?.machineSettings == nil)
// The app re-stamps to the current version on rewrite; that must round-trip.
- decoded.schemaVersion = SplitDocument.currentSchemaVersion
+ decoded.schemaVersion = RoutineDocument.currentSchemaVersion
let reData = try DocumentCoder.encode(decoded)
- let reDecoded = try DocumentCoder.decode(SplitDocument.self, from: reData)
+ let reDecoded = try DocumentCoder.decode(RoutineDocument.self, from: reData)
#expect(reDecoded == decoded)
#expect(reDecoded.schemaVersion == 3)
}
diff --git a/WorkoutsTests/ScheduleDocumentTests.swift b/WorkoutsTests/ScheduleDocumentTests.swift
new file mode 100644
index 0000000..ec47152
--- /dev/null
+++ b/WorkoutsTests/ScheduleDocumentTests.swift
@@ -0,0 +1,294 @@
+import Foundation
+import SwiftData
+import Testing
+import IndieSync
+@testable import Workouts
+
+/// Round-trips `ScheduleDocument` through `DocumentCoder` (the exact JSON codec every
+/// document in this app's iCloud container is written with), pins its on-disk path and
+/// the `isReadable` forward-gate, and checks the convenience accessors + the recurrence
+/// summary shared with the `Schedule` cache entity. Also exercises the document ↔ cache
+/// mapper both directions, mirroring `WorkoutDocumentMapperTests` for the schedule aggregate.
+struct ScheduleDocumentTests {
+ /// Whole-second epochs so ISO-8601 round-trips without fractional-second loss.
+ private static let created = Date(timeIntervalSince1970: 1_699_999_000)
+ private static let updated = Date(timeIntervalSince1970: 1_700_004_000)
+ /// A fixed one-off day for `.once` schedules.
+ private static let onceDay = Date(timeIntervalSince1970: 1_752_105_600)
+
+ private static let gregorianShortWeekdays = Calendar(identifier: .gregorian).shortWeekdaySymbols
+
+ // MARK: - Codable
+
+ /// A fully-populated `.fixedDays` schedule round-trips byte-for-byte through the codec,
+ /// keeps its `Schedules/.json` path, and stays readable at the current version.
+ @Test func encodeDecodeRoundTripFixedDays() throws {
+ let original = ScheduleDocument(
+ schemaVersion: ScheduleDocument.currentSchemaVersion,
+ id: ULID.make(),
+ routineID: "01ROUTINEAAAAAAAAAAAAAAAAAA",
+ routineName: "Push Day",
+ goal: GoalKind.strength.rawValue,
+ recurrence: ScheduleRecurrence.fixedDays.rawValue,
+ weekdays: [2, 5],
+ timesPerWeek: nil,
+ order: 3,
+ createdAt: Self.created,
+ updatedAt: Self.updated
+ )
+
+ let data = try DocumentCoder.encode(original)
+ let decoded = try DocumentCoder.decode(ScheduleDocument.self, from: data)
+
+ #expect(decoded == original)
+ #expect(decoded.schemaVersion == 1)
+ #expect(decoded.relativePath == "Schedules/\(original.id).json")
+ #expect(decoded.isReadable)
+ #expect(decoded.goalKind == .strength)
+ #expect(decoded.recurrenceEnum == .fixedDays)
+ #expect(decoded.timesPerWeek == nil)
+ }
+
+ /// The nil-heavy cases round-trip too: a `.daily` schedule with no goal, no weekdays,
+ /// and no frequency count. Absent optionals decode back to nil (not phantom values).
+ @Test func encodeDecodeRoundTripDailyWithNils() throws {
+ let original = ScheduleDocument(
+ schemaVersion: ScheduleDocument.currentSchemaVersion,
+ id: ULID.make(),
+ routineID: "01ROUTINEBBBBBBBBBBBBBBBBBB",
+ routineName: "Recovery",
+ goal: nil,
+ recurrence: ScheduleRecurrence.daily.rawValue,
+ weekdays: nil,
+ timesPerWeek: nil,
+ order: 0,
+ createdAt: Self.created,
+ updatedAt: Self.updated
+ )
+
+ let data = try DocumentCoder.encode(original)
+ let json = String(decoding: data, as: UTF8.self)
+ // nil optionals are omitted from the JSON entirely. ("date" is checked as a
+ // quoted key — "updatedAt" contains it as a substring.)
+ #expect(!json.contains("goal"))
+ #expect(!json.contains("weekdays"))
+ #expect(!json.contains("timesPerWeek"))
+ #expect(!json.contains("\"date\""))
+
+ let decoded = try DocumentCoder.decode(ScheduleDocument.self, from: data)
+ #expect(decoded == original)
+ #expect(decoded.goal == nil)
+ #expect(decoded.goalKind == nil)
+ #expect(decoded.weekdays == nil)
+ #expect(decoded.timesPerWeek == nil)
+ #expect(decoded.date == nil)
+ }
+
+ /// A `.once` schedule carries its one-off day and drops the recurring-only fields.
+ @Test func encodeDecodeRoundTripOnce() throws {
+ let original = ScheduleDocument(
+ schemaVersion: ScheduleDocument.currentSchemaVersion,
+ id: ULID.make(), routineID: "R", routineName: "Trail Run",
+ goal: GoalKind.cardio.rawValue,
+ recurrence: ScheduleRecurrence.once.rawValue,
+ weekdays: nil, timesPerWeek: nil, date: Self.onceDay, order: 2,
+ createdAt: Self.created, updatedAt: Self.updated
+ )
+ let decoded = try DocumentCoder.decode(ScheduleDocument.self, from: try DocumentCoder.encode(original))
+ #expect(decoded == original)
+ #expect(decoded.recurrenceEnum == .once)
+ #expect(decoded.date == Self.onceDay)
+ #expect(decoded.weekdays == nil)
+ #expect(decoded.timesPerWeek == nil)
+ }
+
+ /// A `.frequency` schedule carries its count and drops weekdays.
+ @Test func encodeDecodeRoundTripFrequency() throws {
+ let original = ScheduleDocument(
+ schemaVersion: ScheduleDocument.currentSchemaVersion,
+ id: ULID.make(), routineID: "R", routineName: "Cardio",
+ goal: GoalKind.cardio.rawValue,
+ recurrence: ScheduleRecurrence.frequency.rawValue,
+ weekdays: nil, timesPerWeek: 3, order: 1,
+ createdAt: Self.created, updatedAt: Self.updated
+ )
+ let decoded = try DocumentCoder.decode(ScheduleDocument.self, from: try DocumentCoder.encode(original))
+ #expect(decoded == original)
+ #expect(decoded.recurrenceEnum == .frequency)
+ #expect(decoded.timesPerWeek == 3)
+ }
+
+ // MARK: - Forward-compatibility gate
+
+ /// A schedule written by a newer app version (schemaVersion + 1) fails the `isReadable`
+ /// gate, so the sync layer quarantines rather than downgrades it; at (and below) the
+ /// current version it reads.
+ @Test func forwardGateQuarantinesNewerVersions() {
+ func doc(schema: Int) -> ScheduleDocument {
+ ScheduleDocument(schemaVersion: schema, id: "01X", routineID: "R", routineName: "N",
+ goal: nil, recurrence: ScheduleRecurrence.daily.rawValue,
+ weekdays: nil, timesPerWeek: nil, order: 0,
+ createdAt: Self.created, updatedAt: Self.updated)
+ }
+ #expect(doc(schema: ScheduleDocument.currentSchemaVersion).isReadable)
+ #expect(doc(schema: ScheduleDocument.currentSchemaVersion - 1).isReadable)
+ #expect(!doc(schema: ScheduleDocument.currentSchemaVersion + 1).isReadable)
+ }
+
+ // MARK: - Recurrence summary
+
+ /// The recurrence summary formats each recurrence kind and shares its logic across the
+ /// document and the cache entity. Weekday names are pulled from a fixed Gregorian
+ /// calendar (1 = Sun … 7 = Sat) so the mapping is locale-order-stable.
+ @Test func recurrenceSummaryFormatsEachKind() {
+ let sym = Self.gregorianShortWeekdays
+
+ // Once — the one-off day, medium-date formatted (routing check; the format
+ // itself is `Date.formatDate()`'s). Degenerate nil date → the display name.
+ #expect(summary(.once, weekdays: nil, timesPerWeek: nil, date: Self.onceDay) == Self.onceDay.formatDate())
+ #expect(summary(.once, weekdays: nil, timesPerWeek: nil) == "Once")
+
+ // Daily.
+ #expect(summary(.daily, weekdays: nil, timesPerWeek: nil) == "Daily")
+
+ // Fixed days — sorted, joined with "&"; weekday 2 = Mon, 5 = Thu.
+ #expect(summary(.fixedDays, weekdays: [5, 2], timesPerWeek: nil) == "\(sym[1]) & \(sym[4])")
+ // Single day → just the name.
+ #expect(summary(.fixedDays, weekdays: [2], timesPerWeek: nil) == sym[1])
+ // Three days → Oxford-style "A, B & C".
+ #expect(summary(.fixedDays, weekdays: [2, 4, 6], timesPerWeek: nil) == "\(sym[1]), \(sym[3]) & \(sym[5])")
+ // Degenerate (no weekdays) → falls back to the recurrence display name.
+ #expect(summary(.fixedDays, weekdays: [], timesPerWeek: nil) == "Fixed Days")
+ #expect(summary(.fixedDays, weekdays: nil, timesPerWeek: nil) == "Fixed Days")
+ // Out-of-range weekday numbers are ignored.
+ #expect(summary(.fixedDays, weekdays: [0, 2, 9], timesPerWeek: nil) == sym[1])
+
+ // Frequency.
+ #expect(summary(.frequency, weekdays: nil, timesPerWeek: 2) == "2× per week")
+ #expect(summary(.frequency, weekdays: nil, timesPerWeek: nil) == "0× per week")
+ }
+
+ private func summary(_ r: ScheduleRecurrence, weekdays: [Int]?, timesPerWeek: Int?, date: Date? = nil) -> String {
+ let doc = ScheduleDocument(
+ schemaVersion: ScheduleDocument.currentSchemaVersion, id: "X", routineID: "R", routineName: "N",
+ goal: nil, recurrence: r.rawValue, weekdays: weekdays, timesPerWeek: timesPerWeek,
+ date: date, order: 0, createdAt: Self.created, updatedAt: Self.updated
+ )
+ // The entity must format identically to the document — this is the shared-logic check.
+ let entity = Schedule(id: "X", routineID: "R", routineName: "N", goalRaw: nil,
+ recurrenceRaw: r.rawValue, weekdays: weekdays, timesPerWeek: timesPerWeek,
+ date: date, order: 0, createdAt: Self.created, updatedAt: Self.updated,
+ jsonRelativePath: "")
+ #expect(doc.recurrenceSummary == entity.recurrenceSummary)
+ return doc.recurrenceSummary
+ }
+
+ // MARK: - Mapper (document ↔ cache)
+
+ @MainActor
+ private func makeContext() throws -> ModelContext {
+ let schema = Schema([Routine.self, Exercise.self, Workout.self, WorkoutLog.self, Schedule.self])
+ let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true, cloudKitDatabase: .none)
+ return ModelContext(try ModelContainer(for: schema, configurations: [config]))
+ }
+
+ /// A document pushed through `upsertSchedule` (document → cache) and rebuilt via
+ /// `ScheduleDocument(from:)` (cache → document) preserves every field in both directions.
+ @MainActor
+ @Test func mapperRoundTripPreservesEveryField() throws {
+ let context = try makeContext()
+ let original = ScheduleDocument(
+ schemaVersion: ScheduleDocument.currentSchemaVersion,
+ id: "01SCHEDULEROUNDTRIP0000001",
+ routineID: "01ROUTINEXYZ00000000000000",
+ routineName: "Leg Day",
+ goal: GoalKind.mobility.rawValue,
+ recurrence: ScheduleRecurrence.fixedDays.rawValue,
+ weekdays: [2, 4, 6],
+ timesPerWeek: nil,
+ order: 7,
+ createdAt: Self.created,
+ updatedAt: Self.updated
+ )
+
+ CacheMapper.upsertSchedule(original, relativePath: original.relativePath, into: context)
+ let entity = try #require(CacheMapper.fetchSchedule(id: original.id, in: context))
+
+ // Entity mirrors the document field-for-field.
+ #expect(entity.routineID == original.routineID)
+ #expect(entity.routineName == original.routineName)
+ #expect(entity.goalRaw == original.goal)
+ #expect(entity.goalKind == .mobility)
+ #expect(entity.recurrenceRaw == original.recurrence)
+ #expect(entity.recurrenceEnum == .fixedDays)
+ #expect(entity.weekdays == [2, 4, 6])
+ #expect(entity.timesPerWeek == nil)
+ #expect(entity.order == 7)
+ #expect(entity.jsonRelativePath == "Schedules/\(original.id).json")
+
+ // Cache → document round-trips back to the original.
+ let rebuilt = ScheduleDocument(from: entity)
+ #expect(rebuilt == original)
+ }
+
+ /// The one-off day survives document → cache (`upsertSchedule`) → document — both on
+ /// first insert and when re-upserting clears it back to nil.
+ @MainActor
+ @Test func mapperPreservesOnceDate() throws {
+ let context = try makeContext()
+ let original = ScheduleDocument(
+ schemaVersion: ScheduleDocument.currentSchemaVersion,
+ id: "01SCHEDULEONCE000000000001", routineID: "R", routineName: "Trail Run",
+ goal: nil, recurrence: ScheduleRecurrence.once.rawValue,
+ weekdays: nil, timesPerWeek: nil, date: Self.onceDay, order: 0,
+ createdAt: Self.created, updatedAt: Self.updated
+ )
+ CacheMapper.upsertSchedule(original, relativePath: original.relativePath, into: context)
+ let entity = try #require(CacheMapper.fetchSchedule(id: original.id, in: context))
+ #expect(entity.date == Self.onceDay)
+ #expect(entity.recurrenceEnum == .once)
+ #expect(ScheduleDocument(from: entity) == original)
+
+ // Editing it into a recurring schedule drops the day on re-upsert.
+ var second = original
+ second.recurrence = ScheduleRecurrence.daily.rawValue
+ second.date = nil
+ CacheMapper.upsertSchedule(second, relativePath: second.relativePath, into: context)
+ #expect(entity.date == nil)
+ #expect(ScheduleDocument(from: entity) == second)
+ }
+
+ /// Re-upserting a changed document updates the existing entity in place (matched by
+ /// id) rather than inserting a duplicate — the observer's re-import shape.
+ @MainActor
+ @Test func reUpsertUpdatesInPlace() throws {
+ let context = try makeContext()
+ let first = ScheduleDocument(
+ schemaVersion: ScheduleDocument.currentSchemaVersion,
+ id: "01SCHEDULEREUPSERT0000001", routineID: "R", routineName: "Old Name",
+ goal: nil, recurrence: ScheduleRecurrence.daily.rawValue,
+ weekdays: nil, timesPerWeek: nil, order: 0,
+ createdAt: Self.created, updatedAt: Self.created
+ )
+ CacheMapper.upsertSchedule(first, relativePath: first.relativePath, into: context)
+ try context.save()
+
+ var second = first
+ second.routineName = "New Name"
+ second.goal = GoalKind.strength.rawValue
+ second.recurrence = ScheduleRecurrence.frequency.rawValue
+ second.timesPerWeek = 4
+ second.updatedAt = Self.updated
+ CacheMapper.upsertSchedule(second, relativePath: second.relativePath, into: context)
+ try context.save()
+
+ let all = try context.fetch(FetchDescriptor())
+ #expect(all.count == 1) // updated in place, not duplicated
+ let entity = try #require(all.first)
+ #expect(entity.routineName == "New Name")
+ #expect(entity.goalKind == .strength)
+ #expect(entity.recurrenceEnum == .frequency)
+ #expect(entity.timesPerWeek == 4)
+ #expect(ScheduleDocument(from: entity) == second)
+ }
+}
diff --git a/WorkoutsTests/SeedLibraryTests.swift b/WorkoutsTests/SeedLibraryTests.swift
index ffa112b..9eeec77 100644
--- a/WorkoutsTests/SeedLibraryTests.swift
+++ b/WorkoutsTests/SeedLibraryTests.swift
@@ -4,30 +4,30 @@ import Testing
import IndieSync
@testable import Workouts
-/// Locks the bundled starter-split library end to end: the catalog decodes and
+/// Locks the bundled starter-routine library end to end: the catalog decodes and
/// orders correctly, the pinned seed ids never drift, a pristine save re-emits
/// byte-identical JSON (no phantom iCloud churn), every seed passes the
/// forward-compatibility gate, and `isPristine` tracks edits correctly through
/// both the raw document and a SwiftData cache round-trip.
struct SeedLibraryTests {
- @Test func catalogLoadsThirteenSeedsInOrder() {
+ @Test func catalogLoadsFourteenSeedsInOrder() {
let seeds = SeedLibrary.seeds
- #expect(seeds.count == 13)
- #expect(seeds.map(\.doc.order) == Array(0...12))
+ #expect(seeds.count == 14)
+ #expect(seeds.map(\.doc.order) == Array(0...13))
#expect(seeds.map(\.doc.name) == [
"Upper Body", "Core", "Lower Body", "Bodyweight Core",
"Full Body Machines", "Free Weight Basics",
"Upper Body Warm-Up", "Lower Body Warm-Up",
"Morning Wake-Up", "Morning Mobility",
- "Full Body Stretch", "Evening Stretch", "Cardio",
+ "Full Body Stretch", "Evening Stretch", "Cardio", "Meditation",
])
}
/// These ids are minted from a frozen 2020 timestamp and are already shipped
/// inside installed app bundles. Changing any of them breaks cross-device
/// convergence (two devices would mint different ids for "the same" starter
- /// split) and voids the permanent-tombstone exemption that lets an
+ /// routine) and voids the permanent-tombstone exemption that lets an
/// edited-then-deleted seed's stub veto resurrection forever — regenerate
/// only with extreme care, and never for a seed that has already shipped.
@Test func seedIDsArePinned() {
@@ -45,6 +45,7 @@ struct SeedLibraryTests {
"01DXF6DT003PRJB3FGYSSGTQ5X", // Full Body Stretch
"01DXF6DT00M5YRFPHJQ2FS2B9P", // Evening Stretch
"01DXF6DT00BXJ6N63ZBX9M9VNH", // Cardio
+ "01DXF6DT00ANFJR7GX9G808G4M", // Meditation
])
}
@@ -59,7 +60,7 @@ struct SeedLibraryTests {
@Test func allSeedsAreReadableAtCurrentSchemaVersion() {
for seed in SeedLibrary.seeds {
#expect(seed.doc.isReadable)
- #expect(seed.doc.schemaVersion == SplitDocument.currentSchemaVersion)
+ #expect(seed.doc.schemaVersion == RoutineDocument.currentSchemaVersion)
}
}
@@ -102,13 +103,13 @@ struct SeedLibraryTests {
}
/// Round-trips every seed through the SwiftData cache the way the metadata
- /// observer would (document → `CacheMapper.upsertSplit` → entity), then
+ /// observer would (document → `CacheMapper.upsertRoutine` → entity), then
/// rebuilds a document from that entity the way the write path would. The
/// result must still read as pristine, or every user's very first cache
- /// rebuild would silently fork their starter splits.
+ /// rebuild would silently fork their starter routines.
@MainActor
@Test func mapperRoundTripPreservesPristineness() throws {
- let schema = Schema([Split.self, Exercise.self, Workout.self, WorkoutLog.self])
+ let schema = Schema([Routine.self, Exercise.self, Workout.self, WorkoutLog.self])
// cloudKitDatabase: .none matches WorkoutsModelContainer — without it SwiftData
// applies CloudKit schema validation (all relationships optional), which the
// cache entities intentionally don't satisfy.
@@ -117,9 +118,9 @@ struct SeedLibraryTests {
let context = ModelContext(container)
for seed in SeedLibrary.seeds {
- CacheMapper.upsertSplit(seed.doc, relativePath: seed.doc.relativePath, into: context)
- let entity = try #require(CacheMapper.fetchSplit(id: seed.id, in: context))
- let rebuilt = SplitDocument(from: entity)
+ CacheMapper.upsertRoutine(seed.doc, relativePath: seed.doc.relativePath, into: context)
+ let entity = try #require(CacheMapper.fetchRoutine(id: seed.id, in: context))
+ let rebuilt = RoutineDocument(from: entity)
#expect(SeedLibrary.isPristine(rebuilt))
}
}
diff --git a/WorkoutsTests/SeedReconcilePlannerTests.swift b/WorkoutsTests/SeedReconcilePlannerTests.swift
index 872c2a8..55749dd 100644
--- a/WorkoutsTests/SeedReconcilePlannerTests.swift
+++ b/WorkoutsTests/SeedReconcilePlannerTests.swift
@@ -5,7 +5,7 @@ import IndieSync
/// Locks the decision table of `SeedReconcilePlanner` — the safety-critical logic that
/// upgrades stale starter-seed revisions and adds newly-shipped seeds on an existing
-/// install without ever duplicating a user's split, resurrecting a deleted seed, or
+/// install without ever duplicating a user's routine, resurrecting a deleted seed, or
/// downgrade-rewriting a newer app version's file. All fixtures are plain structs so
/// every assertion is a pure, deterministic function of the inputs.
struct SeedReconcilePlannerTests {
@@ -21,15 +21,15 @@ struct SeedReconcilePlannerTests {
)
}
- /// A seed-shaped split at the current schema version, with a fixed frozen date so
+ /// A seed-shaped routine at the current schema version, with a fixed frozen date so
/// two builds of "the same" content compare equal.
private func seed(
id: String = "01DXF6DT0038BDC2WC3EVX8ZJ5",
name: String = "Upper Body",
- schemaVersion: Int = SplitDocument.currentSchemaVersion,
+ schemaVersion: Int = RoutineDocument.currentSchemaVersion,
exercises: [ExerciseDocument]? = nil
- ) -> SplitDocument {
- SplitDocument(
+ ) -> RoutineDocument {
+ RoutineDocument(
schemaVersion: schemaVersion, id: id, name: name, color: "indigo",
systemImage: "dumbbell.fill", order: 0,
createdAt: Date(timeIntervalSince1970: 0), updatedAt: Date(timeIntervalSince1970: 0),
@@ -49,7 +49,7 @@ struct SeedReconcilePlannerTests {
live.schemaVersion = 1 // older wrapper, same content
let input = SeedReconcileInput(seedID: bundle.id, seedDoc: bundle, liveDoc: live, hasStub: false)
- #expect(SeedReconcilePlanner.decision(for: input, liveSplitNames: []) == .skip(.upToDate))
+ #expect(SeedReconcilePlanner.decision(for: input, liveRoutineNames: []) == .skip(.upToDate))
}
/// The bundle seed gained an exercise since this install wrote its file — a real
@@ -61,7 +61,7 @@ struct SeedReconcilePlannerTests {
live.schemaVersion = 1
let input = SeedReconcileInput(seedID: bundle.id, seedDoc: bundle, liveDoc: live, hasStub: false)
- #expect(SeedReconcilePlanner.decision(for: input, liveSplitNames: []) == .upgrade)
+ #expect(SeedReconcilePlanner.decision(for: input, liveRoutineNames: []) == .upgrade)
}
/// A weight tweak alone is enough of a content change to upgrade.
@@ -70,7 +70,7 @@ struct SeedReconcilePlannerTests {
let live = seed(exercises: [exercise(name: "Bench Press", weight: 115)])
let input = SeedReconcileInput(seedID: bundle.id, seedDoc: bundle, liveDoc: live, hasStub: false)
- #expect(SeedReconcilePlanner.decision(for: input, liveSplitNames: []) == .upgrade)
+ #expect(SeedReconcilePlanner.decision(for: input, liveRoutineNames: []) == .upgrade)
}
/// A live file written by a NEWER app version (schemaVersion above current) is
@@ -78,11 +78,11 @@ struct SeedReconcilePlannerTests {
@Test func newerSchemaFileIsQuarantined() {
let bundle = seed()
var live = seed(exercises: [exercise(name: "Something New")])
- live.schemaVersion = SplitDocument.currentSchemaVersion + 1
+ live.schemaVersion = RoutineDocument.currentSchemaVersion + 1
#expect(!live.isReadable) // guards the premise
let input = SeedReconcileInput(seedID: bundle.id, seedDoc: bundle, liveDoc: live, hasStub: false)
- #expect(SeedReconcilePlanner.decision(for: input, liveSplitNames: []) == .skip(.quarantined))
+ #expect(SeedReconcilePlanner.decision(for: input, liveRoutineNames: []) == .skip(.quarantined))
}
// MARK: - No live file
@@ -92,25 +92,25 @@ struct SeedReconcilePlannerTests {
let bundle = seed()
let input = SeedReconcileInput(seedID: bundle.id, seedDoc: bundle, liveDoc: nil, hasStub: true)
- #expect(SeedReconcilePlanner.decision(for: input, liveSplitNames: []) == .skip(.vetoed))
+ #expect(SeedReconcilePlanner.decision(for: input, liveRoutineNames: []) == .skip(.vetoed))
}
- /// No live file, no stub, but a live split (a legacy runtime-built or user-cloned
+ /// No live file, no stub, but a live routine (a legacy runtime-built or user-cloned
/// "Upper Body") already uses the seed's name → writing it would duplicate by name.
@Test func nameCollisionSkips() {
let bundle = seed(name: "Upper Body")
let input = SeedReconcileInput(seedID: bundle.id, seedDoc: bundle, liveDoc: nil, hasStub: false)
- #expect(SeedReconcilePlanner.decision(for: input, liveSplitNames: ["Upper Body", "Core"]) == .skip(.nameCollision))
+ #expect(SeedReconcilePlanner.decision(for: input, liveRoutineNames: ["Upper Body", "Core"]) == .skip(.nameCollision))
}
- /// No live file, no stub, no same-name split → a genuinely missing seed (newly
+ /// No live file, no stub, no same-name routine → a genuinely missing seed (newly
/// shipped, or a pre-fixed-ULID install) gets written.
@Test func missingSeedIsWritten() {
let bundle = seed(name: "Bodyweight Core")
let input = SeedReconcileInput(seedID: bundle.id, seedDoc: bundle, liveDoc: nil, hasStub: false)
- #expect(SeedReconcilePlanner.decision(for: input, liveSplitNames: ["Upper Body", "Core"]) == .write)
+ #expect(SeedReconcilePlanner.decision(for: input, liveRoutineNames: ["Upper Body", "Core"]) == .write)
}
/// A stub present but the same name also live: the veto is checked first, so it
@@ -119,7 +119,7 @@ struct SeedReconcilePlannerTests {
let bundle = seed(name: "Upper Body")
let input = SeedReconcileInput(seedID: bundle.id, seedDoc: bundle, liveDoc: nil, hasStub: true)
- #expect(SeedReconcilePlanner.decision(for: input, liveSplitNames: ["Upper Body"]) == .skip(.vetoed))
+ #expect(SeedReconcilePlanner.decision(for: input, liveRoutineNames: ["Upper Body"]) == .skip(.vetoed))
}
// MARK: - Restore
@@ -128,10 +128,10 @@ struct SeedReconcilePlannerTests {
/// the presence of a veto stub is irrelevant (restore lifts it).
@Test func shouldRestoreOnlyWhenAbsentAndNameFree() {
// Absent, name free → restore (whether or not a stub exists).
- #expect(SeedReconcilePlanner.shouldRestore(hasLiveFile: false, seedName: "Upper Body", liveSplitNames: []))
+ #expect(SeedReconcilePlanner.shouldRestore(hasLiveFile: false, seedName: "Upper Body", liveRoutineNames: []))
// Live file already present → reconcile handles any upgrade; restore leaves it.
- #expect(!SeedReconcilePlanner.shouldRestore(hasLiveFile: true, seedName: "Upper Body", liveSplitNames: []))
- // A live same-name split exists → don't duplicate it.
- #expect(!SeedReconcilePlanner.shouldRestore(hasLiveFile: false, seedName: "Upper Body", liveSplitNames: ["Upper Body"]))
+ #expect(!SeedReconcilePlanner.shouldRestore(hasLiveFile: true, seedName: "Upper Body", liveRoutineNames: []))
+ // A live same-name routine exists → don't duplicate it.
+ #expect(!SeedReconcilePlanner.shouldRestore(hasLiveFile: false, seedName: "Upper Body", liveRoutineNames: ["Upper Body"]))
}
}
diff --git a/WorkoutsTests/SyncEngineTests.swift b/WorkoutsTests/SyncEngineTests.swift
index 26b40fc..5467d27 100644
--- a/WorkoutsTests/SyncEngineTests.swift
+++ b/WorkoutsTests/SyncEngineTests.swift
@@ -1,4 +1,5 @@
import Foundation
+import IndieSync
import SwiftData
import Testing
@testable import Workouts
@@ -20,19 +21,70 @@ struct SyncEngineTests {
@MainActor
private func makeEngine() throws -> SyncEngine {
- let schema = Schema([Split.self, Exercise.self, Workout.self, WorkoutLog.self])
+ let schema = Schema([Routine.self, Exercise.self, Workout.self, WorkoutLog.self])
let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true, cloudKitDatabase: .none)
- return SyncEngine(container: try ModelContainer(for: schema, configurations: [config]))
+ let backlogURL = FileManager.default.temporaryDirectory
+ .appending(path: "SyncEngineTests-\(UUID().uuidString).json")
+ return SyncEngine(container: try ModelContainer(for: schema, configurations: [config]),
+ backlogURL: backlogURL)
}
- /// With no clone-on-edit fork recorded, `currentSplitID` is the identity — a view
- /// holding a split id resolves to that same id. This pins the redirect-follow loop's
+ /// A minimal workout document with one log, both carrying the given statuses.
+ private func makeWorkout(id: String, status: WorkoutStatus, logStatus: WorkoutStatus) -> WorkoutDocument {
+ let now = Date()
+ var log = WorkoutLogDocument(planFrom: ExerciseDocument(
+ id: ULID.make(), name: "Seated Row", order: 0, sets: 3, reps: 10,
+ weight: 90, loadType: LoadType.weight.rawValue, durationSeconds: 0), order: 0, date: now)
+ log.status = logStatus.rawValue
+ return WorkoutDocument(
+ schemaVersion: WorkoutDocument.currentSchemaVersion, id: id,
+ routineID: nil, routineName: "Upper Body", start: now, end: nil,
+ status: status.rawValue, createdAt: now, updatedAt: now, logs: [log])
+ }
+
+ /// Locks `onWorkoutBecameActive`'s trigger table: it fires exactly once, on a
+ /// phone-side `.notStarted` → `.inProgress` transition — not on draft creation,
+ /// not on a repeat in-progress save, not for a watch-originated update, and not
+ /// when an old workout is edited from `.completed` back to `.inProgress`.
+ @MainActor
+ @Test func becameActiveFiresOnlyOnLocalNotStartedToInProgress() async throws {
+ let engine = try makeEngine()
+ var fired: [String] = []
+ engine.onWorkoutBecameActive = { fired.append($0.id) }
+
+ // Draft creation: notStarted → no fire.
+ let id = ULID.make()
+ await engine.save(workout: makeWorkout(id: id, status: .notStarted, logStatus: .notStarted))
+ #expect(fired.isEmpty)
+
+ // First exercise starts: notStarted → inProgress fires once.
+ await engine.save(workout: makeWorkout(id: id, status: .inProgress, logStatus: .inProgress))
+ #expect(fired == [id])
+
+ // Re-saving an already-active run doesn't re-fire.
+ await engine.save(workout: makeWorkout(id: id, status: .inProgress, logStatus: .inProgress))
+ #expect(fired == [id])
+
+ // A watch-originated start never fires — the watch already owns a session.
+ let watchID = ULID.make()
+ await engine.ingestFromWatch(makeWorkout(id: watchID, status: .inProgress, logStatus: .inProgress))
+ #expect(fired == [id])
+
+ // Editing a finished workout back to in-progress isn't a fresh start.
+ let oldID = ULID.make()
+ await engine.save(workout: makeWorkout(id: oldID, status: .completed, logStatus: .completed))
+ await engine.save(workout: makeWorkout(id: oldID, status: .inProgress, logStatus: .inProgress))
+ #expect(fired == [id])
+ }
+
+ /// With no clone-on-edit fork recorded, `currentRoutineID` is the identity — a view
+ /// holding a routine id resolves to that same id. This pins the redirect-follow loop's
/// base case and its termination (no redirect → return input, no infinite loop).
@MainActor
- @Test func currentSplitIDIsIdentityWithoutRedirects() throws {
+ @Test func currentRoutineIDIsIdentityWithoutRedirects() throws {
let engine = try makeEngine()
- #expect(engine.currentSplitID(for: "01SEEDABCDEFGHJKMNPQRSTVWX") == "01SEEDABCDEFGHJKMNPQRSTVWX")
- #expect(engine.currentSplitID(for: "") == "")
+ #expect(engine.currentRoutineID(for: "01SEEDABCDEFGHJKMNPQRSTVWX") == "01SEEDABCDEFGHJKMNPQRSTVWX")
+ #expect(engine.currentRoutineID(for: "") == "")
}
/// A freshly constructed engine is in the `.checking` state until `connect()` runs,
diff --git a/WorkoutsTests/WCPayloadRoundTripTests.swift b/WorkoutsTests/WCPayloadRoundTripTests.swift
index 16e4e7b..fbdda5e 100644
--- a/WorkoutsTests/WCPayloadRoundTripTests.swift
+++ b/WorkoutsTests/WCPayloadRoundTripTests.swift
@@ -4,7 +4,7 @@ import IndieSync
@testable import Workouts
/// Round-trips every `WCPayload` wire shape — the iPhone↔Watch bridge's only
-/// serialization boundary. Splits/workouts ride as JSON `Data` blobs (via
+/// serialization boundary. Routines/workouts ride as JSON `Data` blobs (via
/// `DocumentCoder`, ISO-8601), settings and edit locks as native plist scalars, and
/// the ephemeral live-run frames as native values (so their timers keep sub-second
/// precision). A field dropped or a `nil`-vs-empty confusion here silently desyncs
@@ -14,9 +14,9 @@ struct WCPayloadRoundTripTests {
private static let ts = Date(timeIntervalSince1970: 1_700_000_000) // whole second: ISO-8601 safe
- private func split(id: String, name: String) -> SplitDocument {
- SplitDocument(
- schemaVersion: SplitDocument.currentSchemaVersion, id: id, name: name, color: "blue",
+ private func routine(id: String, name: String) -> RoutineDocument {
+ RoutineDocument(
+ schemaVersion: RoutineDocument.currentSchemaVersion, id: id, name: name, color: "blue",
systemImage: "dumbbell.fill", order: 0, createdAt: Self.ts, updatedAt: Self.ts,
exercises: [ExerciseDocument(id: "EX-\(id)", name: "Bench Press", order: 0, sets: 4,
reps: 10, weight: 135, loadType: 1, durationSeconds: 0,
@@ -27,7 +27,7 @@ struct WCPayloadRoundTripTests {
private func workout(id: String) -> WorkoutDocument {
WorkoutDocument(
- schemaVersion: WorkoutDocument.currentSchemaVersion, id: id, splitID: "S", splitName: "Push",
+ schemaVersion: WorkoutDocument.currentSchemaVersion, id: id, routineID: "S", routineName: "Push",
start: Self.ts, end: Self.ts, status: WorkoutStatus.completed.rawValue,
createdAt: Self.ts, updatedAt: Self.ts,
logs: [WorkoutLogDocument(id: "L-\(id)", exerciseName: "Bench Press", order: 0, sets: 4,
@@ -40,43 +40,43 @@ struct WCPayloadRoundTripTests {
// MARK: - Phone → Watch state + settings
- @Test func stateRoundTripsSplitsWorkoutsAndSettings() {
- let splits = [split(id: "SP1", name: "Upper Body"), split(id: "SP2", name: "Lower Body")]
+ @Test func stateRoundTripsRoutinesWorkoutsAndSettings() {
+ let routines = [routine(id: "SP1", name: "Upper Body"), routine(id: "SP2", name: "Lower Body")]
let workouts = [workout(id: "01WKA"), workout(id: "01WKB")]
let dict = WCPayload.encodeState(
- splits: splits, workouts: workouts, restSeconds: 90, doneCountdownSeconds: 5,
- weightUnit: "kg", editingWorkoutID: "01WKA", editingSplitID: "SP1"
+ routines: routines, workouts: workouts, restSeconds: 90, doneCountdownSeconds: 5,
+ weightUnit: "kg", editingWorkoutID: "01WKA", editingRoutineID: "SP1"
)
- #expect(WCPayload.decodeSplits(dict) == splits)
+ #expect(WCPayload.decodeRoutines(dict) == routines)
#expect(WCPayload.decodeWorkouts(dict) == workouts)
#expect(WCPayload.decodeRestSeconds(dict) == 90)
#expect(WCPayload.decodeDoneCountdownSeconds(dict) == 5)
#expect(WCPayload.decodeWeightUnit(dict) == "kg")
#expect(WCPayload.decodeEditingWorkoutID(dict) == "01WKA")
- #expect(WCPayload.decodeEditingSplitID(dict) == "SP1")
+ #expect(WCPayload.decodeEditingRoutineID(dict) == "SP1")
}
/// Nil edit locks mean "not editing" → the keys are absent, so the decoders return
- /// nil (the receiver clears the lock). An empty splits/workouts list is legitimately
+ /// nil (the receiver clears the lock). An empty routines/workouts list is legitimately
/// empty — it still encodes a key and decodes back to `[]`, never nil.
@Test func stateOmitsNilEditLocksAndKeepsEmptyListsDistinct() {
let dict = WCPayload.encodeState(
- splits: [], workouts: [], restSeconds: 60, doneCountdownSeconds: 3,
- weightUnit: "lb", editingWorkoutID: nil, editingSplitID: nil
+ routines: [], workouts: [], restSeconds: 60, doneCountdownSeconds: 3,
+ weightUnit: "lb", editingWorkoutID: nil, editingRoutineID: nil
)
#expect(WCPayload.decodeEditingWorkoutID(dict) == nil)
- #expect(WCPayload.decodeEditingSplitID(dict) == nil)
+ #expect(WCPayload.decodeEditingRoutineID(dict) == nil)
// Authoritative empty push: an empty (present) list decodes to [], not nil.
- #expect(WCPayload.decodeSplits(dict) == [])
+ #expect(WCPayload.decodeRoutines(dict) == [])
#expect(WCPayload.decodeWorkouts(dict) == [])
}
- /// An absent splits/workouts key decodes to `[]` (nothing to apply), NOT nil — nil is
+ /// An absent routines/workouts key decodes to `[]` (nothing to apply), NOT nil — nil is
/// reserved for a genuine decode failure below.
- @Test func absentSplitsWorkoutsKeysDecodeToEmpty() {
+ @Test func absentRoutinesWorkoutsKeysDecodeToEmpty() {
let empty: [String: Any] = [:]
- #expect(WCPayload.decodeSplits(empty) == [])
+ #expect(WCPayload.decodeRoutines(empty) == [])
#expect(WCPayload.decodeWorkouts(empty) == [])
}
@@ -85,8 +85,8 @@ struct WCPayloadRoundTripTests {
/// pruning against a bogus set. This must stay distinct from an empty list.
@Test func corruptStateDecodesToNilNotEmpty() {
let garbage = Data([0xFF, 0xFE, 0x00, 0x01])
- let dict: [String: Any] = [WCPayload.splitsKey: garbage, WCPayload.workoutsKey: garbage]
- #expect(WCPayload.decodeSplits(dict) == nil)
+ let dict: [String: Any] = [WCPayload.routinesKey: garbage, WCPayload.workoutsKey: garbage]
+ #expect(WCPayload.decodeRoutines(dict) == nil)
#expect(WCPayload.decodeWorkouts(dict) == nil)
}
diff --git a/WorkoutsTests/WorkoutDocumentMapperTests.swift b/WorkoutsTests/WorkoutDocumentMapperTests.swift
index 2576c08..bb5f68e 100644
--- a/WorkoutsTests/WorkoutDocumentMapperTests.swift
+++ b/WorkoutsTests/WorkoutDocumentMapperTests.swift
@@ -26,7 +26,7 @@ struct WorkoutDocumentMapperTests {
@MainActor
private func makeContext() throws -> ModelContext {
- let schema = Schema([Split.self, Exercise.self, Workout.self, WorkoutLog.self])
+ let schema = Schema([Routine.self, Exercise.self, Workout.self, WorkoutLog.self])
// cloudKitDatabase: .none matches WorkoutsModelContainer (see SeedLibraryTests).
let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true, cloudKitDatabase: .none)
return ModelContext(try ModelContainer(for: schema, configurations: [config]))
@@ -63,7 +63,7 @@ struct WorkoutDocumentMapperTests {
let context = try makeContext()
let original = WorkoutDocument(
schemaVersion: WorkoutDocument.currentSchemaVersion,
- id: "01WORKOUTROUNDTRIP00000001", splitID: "SPLIT-42", splitName: "Push Day",
+ id: "01WORKOUTROUNDTRIP00000001", routineID: "RT-42", routineName: "Push Day",
start: Self.start, end: Self.end, status: WorkoutStatus.completed.rawValue,
createdAt: Self.created, updatedAt: Self.updated,
logs: [
@@ -97,8 +97,8 @@ struct WorkoutDocumentMapperTests {
// Spot-pin the load-bearing fields the whole-document `==` rolls up, so a future
// regression names itself.
- #expect(rebuilt.splitID == "SPLIT-42")
- #expect(rebuilt.splitName == "Push Day")
+ #expect(rebuilt.routineID == "RT-42")
+ #expect(rebuilt.routineName == "Push Day")
#expect(rebuilt.start == Self.start)
#expect(rebuilt.end == Self.end)
#expect(rebuilt.createdAt == Self.created)
@@ -131,7 +131,7 @@ struct WorkoutDocumentMapperTests {
let context = try makeContext()
let original = WorkoutDocument(
schemaVersion: WorkoutDocument.currentSchemaVersion,
- id: "01WORKOUTNOMETRICS0000001", splitID: nil, splitName: nil,
+ id: "01WORKOUTNOMETRICS0000001", routineID: nil, routineName: nil,
start: Self.start, end: nil, status: WorkoutStatus.inProgress.rawValue,
createdAt: Self.created, updatedAt: Self.updated,
logs: [log(id: "LOG-X", name: "Plank", order: 0, status: .inProgress, startedAt: Self.startedAt)],
@@ -144,7 +144,7 @@ struct WorkoutDocumentMapperTests {
#expect(rebuilt == original)
#expect(rebuilt.metrics == nil)
- #expect(rebuilt.splitID == nil)
+ #expect(rebuilt.routineID == nil)
#expect(rebuilt.end == nil)
}
@@ -156,7 +156,7 @@ struct WorkoutDocumentMapperTests {
let context = try makeContext()
let scrambled = WorkoutDocument(
schemaVersion: WorkoutDocument.currentSchemaVersion,
- id: "01WORKOUTLOGORDER00000001", splitID: "S", splitName: "S",
+ id: "01WORKOUTLOGORDER00000001", routineID: "S", routineName: "S",
start: Self.start, end: Self.end, status: WorkoutStatus.completed.rawValue,
createdAt: Self.created, updatedAt: Self.updated,
logs: [
@@ -184,7 +184,7 @@ struct WorkoutDocumentMapperTests {
let context = try makeContext()
let first = WorkoutDocument(
schemaVersion: WorkoutDocument.currentSchemaVersion,
- id: "01WORKOUTREUPSERT00000001", splitID: "S", splitName: "S",
+ id: "01WORKOUTREUPSERT00000001", routineID: "S", routineName: "S",
start: Self.start, end: nil, status: WorkoutStatus.inProgress.rawValue,
createdAt: Self.created, updatedAt: Self.updated,
logs: [
diff --git a/WorkoutsTests/WorkoutMergePlannerTests.swift b/WorkoutsTests/WorkoutMergePlannerTests.swift
index 5a84aab..3b92ae4 100644
--- a/WorkoutsTests/WorkoutMergePlannerTests.swift
+++ b/WorkoutsTests/WorkoutMergePlannerTests.swift
@@ -26,8 +26,8 @@ struct WorkoutMergePlannerTests {
logs: [WorkoutLogDocument], deletedLogIDs: [String: Date]? = nil,
metrics: WorkoutMetrics? = nil, end: Date? = nil) -> WorkoutDocument {
WorkoutDocument(
- schemaVersion: WorkoutDocument.currentSchemaVersion, id: "W", splitID: nil,
- splitName: nil, start: Self.t0, end: end, status: status.rawValue,
+ schemaVersion: WorkoutDocument.currentSchemaVersion, id: "W", routineID: nil,
+ routineName: nil, start: Self.t0, end: end, status: status.rawValue,
createdAt: Self.t0, updatedAt: updatedAt, logs: logs, metrics: metrics,
deletedLogIDs: deletedLogIDs)
}
diff --git a/WorkoutsTests/WorkoutPathBucketingTests.swift b/WorkoutsTests/WorkoutPathBucketingTests.swift
index f07aeec..1c941cf 100644
--- a/WorkoutsTests/WorkoutPathBucketingTests.swift
+++ b/WorkoutsTests/WorkoutPathBucketingTests.swift
@@ -51,8 +51,8 @@ struct WorkoutPathBucketingTests {
/// document's own bucket is exactly what the engine writes it to.
@Test func instancePropertyMatchesStatic() {
let doc = WorkoutDocument(
- schemaVersion: WorkoutDocument.currentSchemaVersion, id: "01WK", splitID: nil,
- splitName: nil, start: date(year: 2024, month: 7), end: nil,
+ schemaVersion: WorkoutDocument.currentSchemaVersion, id: "01WK", routineID: nil,
+ routineName: nil, start: date(year: 2024, month: 7), end: nil,
status: WorkoutStatus.notStarted.rawValue, createdAt: date(year: 2024, month: 7),
updatedAt: date(year: 2024, month: 7), logs: [], metrics: nil
)
diff --git a/WorkoutsTests/WorkoutStatusMachineTests.swift b/WorkoutsTests/WorkoutStatusMachineTests.swift
index e84bfd5..e177be1 100644
--- a/WorkoutsTests/WorkoutStatusMachineTests.swift
+++ b/WorkoutsTests/WorkoutStatusMachineTests.swift
@@ -26,8 +26,8 @@ struct WorkoutStatusMachineTests {
status: WorkoutStatus, end: Date?, logs: [WorkoutLogDocument]
) -> WorkoutDocument {
WorkoutDocument(
- schemaVersion: WorkoutDocument.currentSchemaVersion, id: "01WK", splitID: "S",
- splitName: "S", start: Self.start, end: end, status: status.rawValue,
+ schemaVersion: WorkoutDocument.currentSchemaVersion, id: "01WK", routineID: "S",
+ routineName: "S", start: Self.start, end: end, status: status.rawValue,
createdAt: Self.start, updatedAt: Self.start, logs: logs, metrics: nil
)
}
diff --git a/WorkoutsTests/WriteBacklogTests.swift b/WorkoutsTests/WriteBacklogTests.swift
index c9f181e..ee98405 100644
--- a/WorkoutsTests/WriteBacklogTests.swift
+++ b/WorkoutsTests/WriteBacklogTests.swift
@@ -22,10 +22,10 @@ struct WriteBacklogTests {
return Calendar(identifier: .gregorian).date(from: comps)!
}
- private func workoutDoc(id: String, start: Date, updatedAt: Date, splitName: String? = nil) -> WorkoutDocument {
+ private func workoutDoc(id: String, start: Date, updatedAt: Date, routineName: String? = nil) -> WorkoutDocument {
WorkoutDocument(
- schemaVersion: WorkoutDocument.currentSchemaVersion, id: id, splitID: nil,
- splitName: splitName, start: start, end: nil,
+ schemaVersion: WorkoutDocument.currentSchemaVersion, id: id, routineID: nil,
+ routineName: routineName, start: start, end: nil,
status: WorkoutStatus.notStarted.rawValue, createdAt: updatedAt, updatedAt: updatedAt,
logs: [], metrics: nil
)
@@ -35,12 +35,12 @@ struct WriteBacklogTests {
/// bookkeeping field, so replacement/retry/backoff tests can set up exact
/// preconditions directly instead of driving them indirectly through `SyncEngine`.
private func pendingWorkout(
- id: String, start: Date, timestamp: Date, enqueuedAt: Date, splitName: String? = nil,
+ id: String, start: Date, timestamp: Date, enqueuedAt: Date, routineName: String? = nil,
attempts: Int = 0, lastAttemptAt: Date? = nil, faultMessage: String? = nil,
stalePaths: Set = []
) -> PendingWrite {
PendingWrite(
- payload: .workout(workoutDoc(id: id, start: start, updatedAt: timestamp, splitName: splitName)),
+ payload: .workout(workoutDoc(id: id, start: start, updatedAt: timestamp, routineName: routineName)),
timestamp: timestamp, stalePaths: stalePaths, enqueuedAt: enqueuedAt,
attempts: attempts, lastAttemptAt: lastAttemptAt, faultMessage: faultMessage
)
@@ -117,14 +117,14 @@ struct WriteBacklogTests {
@Test func equalTimestampIncomingWinsSlot() throws {
var backlog = WriteBacklog()
let id = "01EQUALTIMESTAMP000000001"
- backlog.enqueue(pendingWorkout(id: id, start: Self.t0, timestamp: Self.t0, enqueuedAt: Self.t0, splitName: "Original"))
- backlog.enqueue(pendingWorkout(id: id, start: Self.t0, timestamp: Self.t0, enqueuedAt: Self.t0, splitName: "Incoming"))
+ backlog.enqueue(pendingWorkout(id: id, start: Self.t0, timestamp: Self.t0, enqueuedAt: Self.t0, routineName: "Original"))
+ backlog.enqueue(pendingWorkout(id: id, start: Self.t0, timestamp: Self.t0, enqueuedAt: Self.t0, routineName: "Incoming"))
let slot = try #require(backlog.pendingWrite(for: id))
guard case .workout(let doc) = slot.payload else {
Issue.record("expected a workout payload"); return
}
- #expect(doc.splitName == "Incoming")
+ #expect(doc.routineName == "Incoming")
}
/// An incoming write strictly older than the occupant is dropped outright — the
@@ -132,14 +132,14 @@ struct WriteBacklogTests {
@Test func strictlyOlderIncomingIsDropped() throws {
var backlog = WriteBacklog()
let id = "01OLDERDROPPED0000000001"
- backlog.enqueue(pendingWorkout(id: id, start: Self.t0, timestamp: Self.t0, enqueuedAt: Self.t0, splitName: "Kept"))
- backlog.enqueue(pendingWorkout(id: id, start: Self.t0, timestamp: Self.t0.addingTimeInterval(-5), enqueuedAt: Self.t0, splitName: "Ignored"))
+ backlog.enqueue(pendingWorkout(id: id, start: Self.t0, timestamp: Self.t0, enqueuedAt: Self.t0, routineName: "Kept"))
+ backlog.enqueue(pendingWorkout(id: id, start: Self.t0, timestamp: Self.t0.addingTimeInterval(-5), enqueuedAt: Self.t0, routineName: "Ignored"))
let slot = try #require(backlog.pendingWrite(for: id))
guard case .workout(let doc) = slot.payload else {
Issue.record("expected a workout payload"); return
}
- #expect(doc.splitName == "Kept")
+ #expect(doc.routineName == "Kept")
#expect(slot.timestamp == Self.t0)
}
@@ -338,16 +338,16 @@ struct SyncEngineWriteQueueTests {
@MainActor
private func makeEngine(backlogURL: URL) throws -> (engine: SyncEngine, container: ModelContainer) {
- let schema = Schema([Split.self, Exercise.self, Workout.self, WorkoutLog.self])
+ let schema = Schema([Routine.self, Exercise.self, Workout.self, WorkoutLog.self])
let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true, cloudKitDatabase: .none)
let container = try ModelContainer(for: schema, configurations: [config])
return (SyncEngine(container: container, backlogURL: backlogURL), container)
}
- private func makeWorkoutDoc(id: String, updatedAt: Date, splitName: String? = "Push Day") -> WorkoutDocument {
+ private func makeWorkoutDoc(id: String, updatedAt: Date, routineName: String? = "Push Day") -> WorkoutDocument {
WorkoutDocument(
- schemaVersion: WorkoutDocument.currentSchemaVersion, id: id, splitID: "SPLIT-1",
- splitName: splitName, start: Self.t0, end: nil,
+ schemaVersion: WorkoutDocument.currentSchemaVersion, id: id, routineID: "RT-1",
+ routineName: routineName, start: Self.t0, end: nil,
status: WorkoutStatus.notStarted.rawValue, createdAt: Self.t0, updatedAt: updatedAt,
logs: [], metrics: nil
)
@@ -397,25 +397,25 @@ struct SyncEngineWriteQueueTests {
@Test func ingestFromWatchAppliesUpdatedAtGate() async throws {
let (engine, container) = try makeEngine(backlogURL: tempBacklogURL())
let id = "01INGESTGATE000000000001"
- await engine.save(workout: makeWorkoutDoc(id: id, updatedAt: Self.t0, splitName: "Push Day"))
+ await engine.save(workout: makeWorkoutDoc(id: id, updatedAt: Self.t0, routineName: "Push Day"))
// (a) strictly older — dropped, cache unchanged.
- await engine.ingestFromWatch(makeWorkoutDoc(id: id, updatedAt: Self.t0.addingTimeInterval(-60), splitName: "Push Day"))
+ await engine.ingestFromWatch(makeWorkoutDoc(id: id, updatedAt: Self.t0.addingTimeInterval(-60), routineName: "Push Day"))
var cached = try #require(CacheMapper.fetchWorkout(id: id, in: container.mainContext))
#expect(cached.updatedAt == Self.t0)
// (b) same updatedAt but a changed field — the echo case, ignored entirely
// (equal timestamp means duplicate, not a genuine edit).
- await engine.ingestFromWatch(makeWorkoutDoc(id: id, updatedAt: Self.t0, splitName: "Changed Name"))
+ await engine.ingestFromWatch(makeWorkoutDoc(id: id, updatedAt: Self.t0, routineName: "Changed Name"))
cached = try #require(CacheMapper.fetchWorkout(id: id, in: container.mainContext))
#expect(cached.updatedAt == Self.t0)
- #expect(cached.splitName == "Push Day")
+ #expect(cached.routineName == "Push Day")
// (c) strictly newer — accepted.
- await engine.ingestFromWatch(makeWorkoutDoc(id: id, updatedAt: Self.t0.addingTimeInterval(60), splitName: "Changed Name"))
+ await engine.ingestFromWatch(makeWorkoutDoc(id: id, updatedAt: Self.t0.addingTimeInterval(60), routineName: "Changed Name"))
cached = try #require(CacheMapper.fetchWorkout(id: id, in: container.mainContext))
#expect(cached.updatedAt == Self.t0.addingTimeInterval(60))
- #expect(cached.splitName == "Changed Name")
+ #expect(cached.routineName == "Changed Name")
}
/// A queued-but-unwritten delete vetoes resurrection: a watch push racing the