import Foundation import Testing @testable import Workouts /// Locks the Swift motion solver to the Exercise Library's reference implementation /// (`Exercise Library/render.py`): the bundled rig resources must decode, and the /// FK / IK / tween math must reproduce values computed by the Python for the same /// data — the two renderers are meant to stay in lockstep. struct ExerciseMotionTests { @Test func bundledBirdDogResourcesDecode() throws { let resources = try #require(ExerciseMotionLibrary.resources(for: "Bird Dog")) #expect(resources.motion.name == "Bird Dog") // Bird Dog alternates sides, so all four limbs are in the working set and // the loop is four key frames (lift right/left pair, then the opposite pair). #expect(resources.motion.frames.count == 4) #expect(resources.motion.working == ["arm_r", "leg_l", "arm_l", "leg_r"]) #expect(resources.body.upperArm == 30) #expect(MotionTimeline(motion: resources.motion, body: resources.body) != nil) } @Test func exerciseWithoutBundledMotionLoadsNothing() { #expect(ExerciseMotionLibrary.resources(for: "Bench Press") == nil) #expect(FigureAnimation(exerciseName: "Bench Press") == nil) } /// Shortest-path angular interpolation (reference: `lerp_angle`). @Test func angleLerpTakesShortestPath() { #expect(MotionSolver.lerpAngle(170, -170, 0.5) == 180) #expect(MotionSolver.lerpAngle(-170, 170, 0.25) == -175) #expect(MotionSolver.lerpAngle(0, 90, 0.5) == 45) } /// Normalizing Bird Dog's first key frame must reproduce the reference IK /// solution for the pinned support arm, and the pinned hand must land exactly /// on its target. @Test func normalizeMatchesReferenceIK() throws { let resources = try #require(ExerciseMotionLibrary.resources(for: "Bird Dog")) let pose = MotionSolver.normalize(resources.motion.frames[0], body: resources.body) let armR = try #require(pose.limbs[.armR]) #expect(abs(armR[0] - (-82.28919)) < 1e-4) #expect(abs(armR[1] - (-99.732948)) < 1e-4) let geo = MotionSolver.geometry(of: pose, body: resources.body, hide: []) let hand = try #require(geo.limbs[.armR]?.last) #expect(abs(hand.x - 105) < 1e-6) #expect(abs(hand.y - 152) < 1e-6) #expect(abs(geo.headCenter.x - 86.195568) < 1e-4) #expect(abs(geo.headCenter.y - 95.140457) < 1e-4) } /// Mid-tween (t = ease(0.5)): the hand pinned in BOTH key frames stays planted /// exactly; the pin present only in frame 1 releases; the lifting arm's angles /// match the reference interpolation. @Test func tweenKeepsSharedPinsAndReleasesOthers() throws { let resources = try #require(ExerciseMotionLibrary.resources(for: "Bird Dog")) let a = MotionSolver.normalize(resources.motion.frames[0], body: resources.body) let b = MotionSolver.normalize(resources.motion.frames[1], body: resources.body) let mid = MotionSolver.lerp(a, b, MotionSolver.ease(0.5)) #expect(mid.pins["hand_l"] != nil) #expect(mid.pins["hand_r"] == nil) let geo = MotionSolver.geometry(of: mid, body: resources.body, hide: []) let plantedHand = try #require(geo.limbs[.armL]?.last) #expect(abs(plantedHand.x - 111) < 1e-6) #expect(abs(plantedHand.y - 154) < 1e-6) let liftingArm = try #require(mid.limbs[.armR]) #expect(abs(liftingArm[0] - (-140.644595)) < 1e-4) #expect(abs(liftingArm[1] - (-149.366474)) < 1e-4) } /// Every exported motion in the bundle decodes and builds a playable timeline. @Test func allBundledMotionsBuildTimelines() throws { let urls = Bundle.main.urls(forResourcesWithExtension: "json", subdirectory: nil) ?? [] let motionURLs = urls.filter { $0.lastPathComponent.hasSuffix(".motion.json") } #expect(!motionURLs.isEmpty) for url in motionURLs { let name = url.lastPathComponent.replacingOccurrences(of: ".motion.json", with: "") let resources = try #require(ExerciseMotionLibrary.resources(for: name)) let timeline = try #require(MotionTimeline(motion: resources.motion, body: resources.body)) #expect(timeline.duration > 0) } } }