import Foundation import ForgeGrade /// Rate-limits camera look pushes. Preview updates instantly elsewhere; /// camera gets at most one push per interval, trailing edge guaranteed — /// final panel position always lands on camera. public actor DebouncedPusher { private let driver: any CameraDriver private let interval: Duration private let latticeSize: Int private var latestGrade: GradeStack? private var lastPushAt: ContinuousClock.Instant? private var pending: Task? private var stopped = false public init(driver: any CameraDriver, interval: Duration = .milliseconds(150), latticeSize: Int = 33) { self.driver = driver self.interval = interval self.latticeSize = latticeSize } /// Record new grade; push now if interval elapsed, else schedule trailing push. public func update(grade: GradeStack) { guard !stopped else { return } latestGrade = grade let now = ContinuousClock.now if let last = lastPushAt, last.duration(to: now) < interval { scheduleTrailing(after: interval - last.duration(to: now)) } else { pushNow() } } private func scheduleTrailing(after delay: Duration) { guard pending == nil else { return } // one trailing push covers all updates pending = Task { try? await Task.sleep(for: delay) await self.firePending() } } private func firePending() { pending = nil guard !stopped else { return } pushNow() } private func pushNow() { guard let grade = latestGrade else { return } lastPushAt = ContinuousClock.now let caps = driver.capabilities let look = Flattener.flatten( stack: grade, latticeSize: caps.lut3dSizes.contains(latticeSize) ? latticeSize : (caps.lut3dSizes.max() ?? latticeSize), splitLeadingCDL: caps.supportsNativeCDL) let driver = self.driver Task { try? await driver.push(look: look) } } public func stop() { stopped = true pending?.cancel() pending = nil } }