// AnimationPlayer.swift // CADisplayLink-driven playback engine matching Blender's transport controls. import UIKit import Observation @Observable final class AnimationPlayer { var animationSystem: AnimationSystem private var displayLink: CADisplayLink? private var lastTimestamp: CFTimeInterval = 0 init(system: AnimationSystem) { self.animationSystem = system } // MARK: - Transport controls (mirror Blender's timeline buttons) func play() { animationSystem.isPlaying = true startDisplayLink() } func pause() { animationSystem.isPlaying = false stopDisplayLink() } func stop() { animationSystem.isPlaying = false animationSystem.currentFrame = animationSystem.frameStart stopDisplayLink() } func togglePlayback() { if animationSystem.isPlaying { pause() } else { play() } } func jumpToStart() { animationSystem.currentFrame = animationSystem.frameStart } func jumpToEnd() { animationSystem.currentFrame = animationSystem.frameEnd } func stepForward() { animationSystem.currentFrame = min( animationSystem.currentFrame + 1, animationSystem.frameEnd ) } func stepBackward() { animationSystem.currentFrame = max( animationSystem.currentFrame - 1, animationSystem.frameStart ) } func nextKeyframe() { guard let action = animationSystem.activeAction else { return } let allFrames = action.tracks .flatMap { $0.sortedKeyframes } .map(\.frame) .sorted() if let next = allFrames.first(where: { $0 > animationSystem.currentFrame + 0.5 }) { animationSystem.currentFrame = next } } func previousKeyframe() { guard let action = animationSystem.activeAction else { return } let allFrames = action.tracks .flatMap { $0.sortedKeyframes } .map(\.frame) .sorted() if let prev = allFrames.last(where: { $0 < animationSystem.currentFrame - 0.5 }) { animationSystem.currentFrame = prev } } // MARK: - Display link private func startDisplayLink() { guard displayLink == nil else { return } lastTimestamp = 0 let link = CADisplayLink(target: self, selector: #selector(tick)) link.preferredFrameRateRange = CAFrameRateRange( minimum: 30, maximum: 120, preferred: 60 ) link.add(to: .main, forMode: .common) displayLink = link } private func stopDisplayLink() { displayLink?.invalidate() displayLink = nil } @objc private func tick(_ link: CADisplayLink) { if lastTimestamp == 0 { lastTimestamp = link.timestamp return } let dt = Float(link.timestamp - lastTimestamp) lastTimestamp = link.timestamp animationSystem.tick(dt: dt) } deinit { stopDisplayLink() } }