rainbow-dragon/Sources/ForgeCamera/DebouncedPusher.swift

70 lines
2.1 KiB
Swift
Raw Permalink Normal View History

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<Void, Never>?
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
}
}