93 lines
2.8 KiB
Swift
93 lines
2.8 KiB
Swift
|
|
import ARKit
|
||
|
|
import Combine
|
||
|
|
import Foundation
|
||
|
|
import simd
|
||
|
|
|
||
|
|
class ARManager: NSObject, ObservableObject {
|
||
|
|
let session = ARSession()
|
||
|
|
|
||
|
|
@Published var trackingState: ARCamera.TrackingState = .notAvailable
|
||
|
|
|
||
|
|
private(set) var poseBuffer: [TimestampedPose] = []
|
||
|
|
private let bufferCapacity = 120
|
||
|
|
private let bufferLock = NSLock()
|
||
|
|
|
||
|
|
override init() {
|
||
|
|
super.init()
|
||
|
|
session.delegate = self
|
||
|
|
}
|
||
|
|
|
||
|
|
func start() {
|
||
|
|
let config = ARWorldTrackingConfiguration()
|
||
|
|
config.worldAlignment = .gravity
|
||
|
|
config.planeDetection = []
|
||
|
|
config.isAutoFocusEnabled = false
|
||
|
|
session.run(config, options: [.resetTracking, .removeExistingAnchors])
|
||
|
|
print("[AR] Session started")
|
||
|
|
}
|
||
|
|
|
||
|
|
func stop() {
|
||
|
|
session.pause()
|
||
|
|
print("[AR] Session paused")
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Returns the interpolated camera pose at the given system-uptime timestamp,
|
||
|
|
/// or nil if the timestamp is outside the current buffer range.
|
||
|
|
func poseAt(timestamp: TimeInterval) -> simd_float4x4? {
|
||
|
|
bufferLock.lock()
|
||
|
|
let buffer = poseBuffer
|
||
|
|
bufferLock.unlock()
|
||
|
|
|
||
|
|
guard buffer.count >= 2 else { return buffer.first?.transform }
|
||
|
|
|
||
|
|
// Outside range — clamp to edges
|
||
|
|
if timestamp <= buffer.first!.timestamp { return buffer.first?.transform }
|
||
|
|
if timestamp >= buffer.last!.timestamp { return buffer.last?.transform }
|
||
|
|
|
||
|
|
// Binary search for the bracketing pair
|
||
|
|
var lo = 0, hi = buffer.count - 1
|
||
|
|
while lo + 1 < hi {
|
||
|
|
let mid = (lo + hi) / 2
|
||
|
|
if buffer[mid].timestamp <= timestamp { lo = mid } else { hi = mid }
|
||
|
|
}
|
||
|
|
return PoseInterpolator.interpolate(from: buffer[lo], to: buffer[hi], at: timestamp)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// MARK: - ARSessionDelegate
|
||
|
|
extension ARManager: ARSessionDelegate {
|
||
|
|
func session(_ session: ARSession, didUpdate frame: ARFrame) {
|
||
|
|
let pose = TimestampedPose(transform: frame.camera.transform, timestamp: frame.timestamp)
|
||
|
|
|
||
|
|
bufferLock.lock()
|
||
|
|
poseBuffer.append(pose)
|
||
|
|
if poseBuffer.count > bufferCapacity {
|
||
|
|
poseBuffer.removeFirst()
|
||
|
|
}
|
||
|
|
bufferLock.unlock()
|
||
|
|
|
||
|
|
let state = frame.camera.trackingState
|
||
|
|
DispatchQueue.main.async {
|
||
|
|
self.trackingState = state
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func sessionShouldAttemptRelocalization(_ session: ARSession) -> Bool {
|
||
|
|
// NI camera assistance requires this to return false
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
func session(_ session: ARSession, didFailWithError error: Error) {
|
||
|
|
print("[AR] Session failed: \(error)")
|
||
|
|
}
|
||
|
|
|
||
|
|
func sessionWasInterrupted(_ session: ARSession) {
|
||
|
|
print("[AR] Session interrupted")
|
||
|
|
}
|
||
|
|
|
||
|
|
func sessionInterruptionEnded(_ session: ARSession) {
|
||
|
|
print("[AR] Session interruption ended — resetting")
|
||
|
|
start()
|
||
|
|
}
|
||
|
|
}
|