105 lines
4.3 KiB
Swift
105 lines
4.3 KiB
Swift
|
|
import SwiftUI
|
|||
|
|
import RealityKit
|
|||
|
|
import ARKit
|
|||
|
|
import Combine
|
|||
|
|
|
|||
|
|
struct ARViewContainer: UIViewRepresentable {
|
|||
|
|
@ObservedObject var arManager: ARManager
|
|||
|
|
@ObservedObject var estimator: AnchorEstimator
|
|||
|
|
|
|||
|
|
func makeUIView(context: Context) -> ARView {
|
|||
|
|
let arView = ARView(frame: .zero, cameraMode: .ar, automaticallyConfigureSession: false)
|
|||
|
|
arView.session = arManager.session
|
|||
|
|
|
|||
|
|
// Create the red sphere entity representing the UWB anchor
|
|||
|
|
let sphereRadius: Float = 0.05
|
|||
|
|
let mesh = MeshResource.generateSphere(radius: sphereRadius)
|
|||
|
|
var material = UnlitMaterial()
|
|||
|
|
material.color = .init(tint: .red)
|
|||
|
|
let sphereEntity = ModelEntity(mesh: mesh, materials: [material])
|
|||
|
|
sphereEntity.isEnabled = false
|
|||
|
|
|
|||
|
|
let anchorEntity = AnchorEntity(world: .zero)
|
|||
|
|
anchorEntity.addChild(sphereEntity)
|
|||
|
|
arView.scene.addAnchor(anchorEntity)
|
|||
|
|
|
|||
|
|
let coordinator = context.coordinator
|
|||
|
|
coordinator.sphereEntity = sphereEntity
|
|||
|
|
|
|||
|
|
// SceneEvents.Update fires every rendered frame on the main thread.
|
|||
|
|
// Updating sphere position here (rather than from the Combine sink) means:
|
|||
|
|
// 1. The sphere is never "frozen" between 4 Hz UWB pings — it always reflects
|
|||
|
|
// the latest world-space estimate relative to the smoothly moving camera.
|
|||
|
|
// 2. We can apply EMA smoothing to hide step-changes from the solver.
|
|||
|
|
coordinator.updateSub = arView.scene.subscribe(to: SceneEvents.Update.self) { [weak estimator, weak coordinator, weak sphereEntity, weak arView] _ in
|
|||
|
|
guard let estimator, let coordinator, let sphereEntity else { return }
|
|||
|
|
|
|||
|
|
// EMA-smooth toward the latest estimate every frame.
|
|||
|
|
// α ≈ 0.12 at 60 fps → time constant ≈ 120 ms. Hides solver step-changes
|
|||
|
|
// without making the sphere feel sluggish.
|
|||
|
|
let alpha: Float = 0.12
|
|||
|
|
if let target = estimator.anchorPosition {
|
|||
|
|
if let current = coordinator.smoothedPosition {
|
|||
|
|
coordinator.smoothedPosition = current + alpha * (target - current)
|
|||
|
|
} else {
|
|||
|
|
coordinator.smoothedPosition = target // snap on first appearance
|
|||
|
|
}
|
|||
|
|
sphereEntity.setPosition(coordinator.smoothedPosition!, relativeTo: nil)
|
|||
|
|
sphereEntity.isEnabled = true
|
|||
|
|
} else {
|
|||
|
|
coordinator.smoothedPosition = nil
|
|||
|
|
sphereEntity.isEnabled = false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Off-screen directional indicator using the smoothed position
|
|||
|
|
guard let arView else { return }
|
|||
|
|
guard let position = coordinator.smoothedPosition else {
|
|||
|
|
if coordinator.lastAngle != nil {
|
|||
|
|
DispatchQueue.main.async { estimator.offScreenAngle = nil }
|
|||
|
|
coordinator.lastAngle = nil
|
|||
|
|
}
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let isOffScreen: Bool
|
|||
|
|
if let proj = arView.project(position) {
|
|||
|
|
isOffScreen = !arView.bounds.contains(proj)
|
|||
|
|
} else {
|
|||
|
|
isOffScreen = true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if isOffScreen {
|
|||
|
|
guard let camera = arView.session.currentFrame?.camera else { return }
|
|||
|
|
let cameraTransform = camera.transform
|
|||
|
|
let localPos4 = simd_mul(simd_inverse(cameraTransform),
|
|||
|
|
simd_float4(position.x, position.y, position.z, 1.0))
|
|||
|
|
let angle = Double(atan2(localPos4.y, localPos4.x))
|
|||
|
|
if coordinator.lastAngle == nil || abs(coordinator.lastAngle! - angle) > 0.05 {
|
|||
|
|
coordinator.lastAngle = angle
|
|||
|
|
DispatchQueue.main.async { estimator.offScreenAngle = angle }
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
if coordinator.lastAngle != nil {
|
|||
|
|
DispatchQueue.main.async { estimator.offScreenAngle = nil }
|
|||
|
|
coordinator.lastAngle = nil
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return arView
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func updateUIView(_ uiView: ARView, context: Context) {}
|
|||
|
|
|
|||
|
|
func makeCoordinator() -> Coordinator {
|
|||
|
|
Coordinator()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
class Coordinator {
|
|||
|
|
var sphereEntity: ModelEntity?
|
|||
|
|
var updateSub: (any Cancellable)?
|
|||
|
|
var smoothedPosition: simd_float3? = nil
|
|||
|
|
var lastAngle: Double?
|
|||
|
|
}
|
|||
|
|
}
|