118 lines
3.8 KiB
Swift
118 lines
3.8 KiB
Swift
|
|
import SwiftUI
|
||
|
|
import ARKit
|
||
|
|
|
||
|
|
struct ContentView: View {
|
||
|
|
@EnvironmentObject var ble: BLEManager
|
||
|
|
@EnvironmentObject var ni: NIManager
|
||
|
|
@EnvironmentObject var ar: ARManager
|
||
|
|
@EnvironmentObject var estimator: AnchorEstimator
|
||
|
|
|
||
|
|
var body: some View {
|
||
|
|
ZStack {
|
||
|
|
ARViewContainer(arManager: ar, estimator: estimator)
|
||
|
|
.ignoresSafeArea()
|
||
|
|
|
||
|
|
if let angle = estimator.offScreenAngle {
|
||
|
|
Image(systemName: "location.north.fill")
|
||
|
|
.resizable()
|
||
|
|
.aspectRatio(contentMode: .fit)
|
||
|
|
.frame(width: 40, height: 40)
|
||
|
|
.foregroundColor(.red)
|
||
|
|
.shadow(radius: 5)
|
||
|
|
// The image points "Up".
|
||
|
|
// SwiftUI rotates clockwise. Angle is mathematically CCW.
|
||
|
|
// If angle = 0 (right), we need it to point Right, so rotate 90 deg clockwise.
|
||
|
|
// Actually, if image points UP, we rotate by PI/2 - angle.
|
||
|
|
.rotationEffect(.radians(.pi / 2 - angle))
|
||
|
|
// Offset moves it towards the edge
|
||
|
|
.offset(x: cos(angle) * 140, y: -sin(angle) * 140)
|
||
|
|
.animation(.interactiveSpring(), value: angle)
|
||
|
|
}
|
||
|
|
|
||
|
|
VStack {
|
||
|
|
HUDView(ble: ble, ni: ni, estimator: estimator)
|
||
|
|
.padding()
|
||
|
|
Spacer()
|
||
|
|
Button("Reset Estimate") {
|
||
|
|
estimator.reset()
|
||
|
|
}
|
||
|
|
.buttonStyle(.borderedProminent)
|
||
|
|
.tint(.red.opacity(0.8))
|
||
|
|
.padding(.bottom, 40)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// MARK: - HUD overlay
|
||
|
|
|
||
|
|
private struct HUDView: View {
|
||
|
|
@ObservedObject var ble: BLEManager
|
||
|
|
@ObservedObject var ni: NIManager
|
||
|
|
@ObservedObject var estimator: AnchorEstimator
|
||
|
|
|
||
|
|
var body: some View {
|
||
|
|
VStack(alignment: .leading, spacing: 6) {
|
||
|
|
HUDRow(label: "BLE", value: bleStateText)
|
||
|
|
HUDRow(label: "NI", value: niStateText)
|
||
|
|
HUDRow(label: "Range", value: rangeText)
|
||
|
|
HUDRow(label: "Measurements", value: "\(estimator.measurementCount)")
|
||
|
|
HUDRow(label: "Residual", value: String(format: "%.3f m", estimator.residualError))
|
||
|
|
}
|
||
|
|
.padding(12)
|
||
|
|
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 12))
|
||
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||
|
|
}
|
||
|
|
|
||
|
|
private var bleStateText: String {
|
||
|
|
switch ble.connectionState {
|
||
|
|
case .disconnected: return "Disconnected"
|
||
|
|
case .scanning: return "Scanning…"
|
||
|
|
case .connecting: return "Connecting…"
|
||
|
|
case .connected: return "Connected"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private var niStateText: String {
|
||
|
|
switch ni.sessionState {
|
||
|
|
case .idle: return "Idle"
|
||
|
|
case .waitingForAccessory: return "Waiting for board…"
|
||
|
|
case .configuring: return "Configuring…"
|
||
|
|
case .ranging: return "Ranging"
|
||
|
|
case .error(let msg): return "Error: \(msg)"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private var rangeText: String {
|
||
|
|
if let r = ni.lastRange {
|
||
|
|
return String(format: "%.2f m", r)
|
||
|
|
}
|
||
|
|
return "—"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private struct HUDRow: View {
|
||
|
|
let label: String
|
||
|
|
let value: String
|
||
|
|
|
||
|
|
var body: some View {
|
||
|
|
HStack {
|
||
|
|
Text(label)
|
||
|
|
.font(.caption.bold())
|
||
|
|
.foregroundStyle(.secondary)
|
||
|
|
.frame(width: 100, alignment: .leading)
|
||
|
|
Text(value)
|
||
|
|
.font(.caption)
|
||
|
|
.foregroundStyle(.primary)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#Preview {
|
||
|
|
ContentView()
|
||
|
|
.environmentObject(BLEManager())
|
||
|
|
.environmentObject(NIManager())
|
||
|
|
.environmentObject(ARManager())
|
||
|
|
.environmentObject(AnchorEstimator())
|
||
|
|
}
|