// // ExerciseMotion.swift // Workouts // // Copyright 2026 Rouslan Zenetl. All Rights Reserved. // import Foundation /// Codable mirror of the Exercise Library's rig data (see `Exercise Library/SYSTEM.md`). /// /// A **body profile** is a table of bone lengths; a **motion script** is key frames of /// absolute joint angles (degrees, y-up: 0 = right, 90 = up, −90 = down), a pelvis /// `root` in 320×180 canvas coordinates, optional IK pins for planted hands/feet, and /// hold/tween timings. The app bundles verbatim copies exported by /// `render.py --export` into `Resources/ExerciseMotions/` — `body.json` plus one /// `.motion.json` per library entry. /// Bone lengths for one figure profile (`neutral` is the default). struct ExerciseBodyProfile: Codable { let headR: Double let neck: Double let spine1: Double let spine2: Double let upperArm: Double let foreArm: Double let thigh: Double let shin: Double /// Offset separating left-limb attachments visually: `[dx, dy]` in canvas points. let leftOffset: [Double] } /// One exercise's motion script: key frames plus the parts drawn in the accent color. struct ExerciseMotion: Codable { let name: String /// 1-based frame used for the static visual (unused by the animated renderer). let primary: Int? /// Parts (`arm_r`, `leg_l`, `spine`, …) drawn in the working accent color. let working: [String]? /// Limbs fully occluded in this view — never drawn. let hide: [String]? let frames: [MotionKeyFrame] } /// A key frame of absolute joint angles. Limbs listed in the motion's `hide` may be /// absent entirely; a two-element array is `[upper, lower]` bone angles. struct MotionKeyFrame: Codable { /// Seconds held at this key frame (default 0.5). let hold: Double? /// Seconds animating to the *next* frame (default 0.6); the last frame tweens /// back to the first, looping. let tween: Double? /// Pelvis position, canvas coordinates `[x, y]`. let root: [Double] /// Pelvis→mid and mid→neck angles. let spine: [Double] /// Head direction from the neck joint. let neck: Double /// Nose-tick direction (the belly is on that side). let gaze: Double let armR: [Double]? let armL: [Double]? let legR: [Double]? let legL: [Double]? /// IK targets for planted extremities, keyed `hand_r`/`hand_l`/`foot_r`/`foot_l`. /// A pin present in two consecutive key frames stays planted through the tween. let pins: [String: [Double]]? enum CodingKeys: String, CodingKey { case hold, tween, root, spine, neck, gaze, pins case armR = "arm_r" case armL = "arm_l" case legR = "leg_r" case legL = "leg_l" } } /// Finds and decodes the bundled rig resources for an exercise, by exact name match /// against the exported `.motion.json` files. enum ExerciseMotionLibrary { struct Resources { let motion: ExerciseMotion let body: ExerciseBodyProfile } /// Every exercise with a bundled motion script, sorted alphabetically. XcodeGen /// flattens resource groups, so `.motion.json` files land in the /// bundle root alongside `body.json` — enumerate all json and filter on the /// compound suffix (which `body.json` doesn't match). static let exerciseNames: [String] = { let urls = Bundle.main.urls(forResourcesWithExtension: "json", subdirectory: nil) ?? [] return urls .map(\.lastPathComponent) .filter { $0.hasSuffix(".motion.json") } .map { String($0.dropLast(".motion.json".count)) } .sorted() }() /// The motion script plus the neutral body profile for `exerciseName`, or `nil` /// when no bundled motion matches (most exercises have none — the caller keeps /// its space empty). static func resources(for exerciseName: String) -> Resources? { guard let motionURL = Bundle.main.url(forResource: exerciseName, withExtension: "motion.json"), let bodyURL = Bundle.main.url(forResource: "body", withExtension: "json"), let motionData = try? Data(contentsOf: motionURL), let bodyData = try? Data(contentsOf: bodyURL), let motion = try? JSONDecoder().decode(ExerciseMotion.self, from: motionData), let profiles = try? JSONDecoder().decode([String: ExerciseBodyProfile].self, from: bodyData), let body = profiles["neutral"] else { return nil } return Resources(motion: motion, body: body) } }