App/Sources/MotionPointer.swift
39 lines · 1447 bytes
4 symbols in this file
1import CoreMotion
2import Observation
3import SkyEngine
4import SkyRenderer
5
6/// Points the sky where the back of the phone points, using true north.
7@MainActor
8@Observable
9final class MotionPointer {
10 private(set) var active = false
11 private(set) var direction = Horizontal(altitude: 45, azimuth: 180)
12 var available: Bool { CMMotionManager.availableAttitudeReferenceFrames().contains(.xTrueNorthZVertical) }
13
14 private let manager = CMMotionManager()
15
16 func start() {
17 guard available, !active else { return }
18 active = true
19 manager.deviceMotionUpdateInterval = 1.0 / 60
20 manager.startDeviceMotionUpdates(using: .xTrueNorthZVertical, to: .main) { [weak self] motion, _ in
21 guard let self, let m = motion?.attitude.rotationMatrix else { return }
22 MainActor.assumeIsolated { self.update(Pointing.direction(m31: m.m31, m32: m.m32, m33: m.m33)) }
23 }
24 }
25
26 func stop() {
27 manager.stopDeviceMotionUpdates()
28 active = false
29 }
30
31 /// Light low-pass filter; azimuth is smoothed across the 0/360 wrap.
32 private func update(_ h: Horizontal) {
33 let k = 0.25
34 let dAz = remainder(h.azimuth - direction.azimuth, 360)
35 var az = direction.azimuth + k * dAz
36 if az < 0 { az += 360 } else if az >= 360 { az -= 360 }
37 direction = Horizontal(altitude: direction.altitude + k * (h.altitude - direction.altitude), azimuth: az)
38 }
39}