Slowly orbit the camera around prop-free form guides

Bodyweight motions (no equipment layer, no hide list) now turn the
camera a full revolution every 24 seconds while the motion loops, so
the exercise reads from every side; machine exercises keep their
authored view, since scene equipment is a view-locked billboard and a
hide list describes a single viewpoint.

Claude-Session: https://claude.ai/code/session_01LEoff8bXGBS83tK1c55Mf7
This commit is contained in:
2026-07-06 20:43:14 -04:00
parent 3c7a790e9d
commit fd3bcb513f
5 changed files with 52 additions and 6 deletions
+19
View File
@@ -74,6 +74,25 @@ struct ExerciseMotionTests {
#expect(hypot(releasedHand.x - 105, releasedHand.y - 152) > 1)
}
/// Prop-free bodyweight motions slowly orbit the camera while looping; motions with
/// equipment (view-locked scene shapes) or a hide list keep their authored view.
@Test func orbitAppliesOnlyToPropFreeMotions() throws {
let birdDog = try #require(FigureAnimation(exerciseName: "Bird Dog"))
#expect(birdDog.orbits)
// Same loop phase seconds apart: the orbit yaw differs, so the projected head
// moves even though the pose is identical.
let duration = birdDog.timeline.duration
let head0 = birdDog.geometry(at: 0).headCenter
let head1 = birdDog.geometry(at: duration * 2).headCenter
#expect(hypot(head0.x - head1.x, head0.y - head1.y) > 1)
let legPress = try #require(FigureAnimation(exerciseName: "Leg Press"))
#expect(!legPress.orbits)
let fixed0 = legPress.geometry(at: 0).headCenter
let fixed1 = legPress.geometry(at: legPress.timeline.duration * 2).headCenter
#expect(abs(fixed0.x - fixed1.x) < 1e-9 && abs(fixed0.y - fixed1.y) < 1e-9)
}
/// 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) ?? []