1
//! POD types for the motion-sensor surface
2
//! (SUPER_PLAN_2 §1 feature 5 + research/03 §"Feature 5").
3
//!
4
//! The three raw sensors apps want — accelerometer, gyroscope,
5
//! magnetometer — each delivered as an `(x, y, z)` triple in the sensor's
6
//! natural unit. Defined here in `azul-core` so the manager + accessors
7
//! cross the FFI without `azul-layout` being a dependency. The stateful
8
//! side lives in `azul_layout::managers::sensors::SensorManager`.
9
//!
10
//! Coordinate frame (research/03 §coordinate-frame): right-handed,
11
//! +X right, +Y up, +Z out of the screen toward the user, in the device's
12
//! default-portrait frame (iOS keeps the device frame regardless of UI
13
//! orientation; Android auto-rotates only fused sensors). v1 reports the
14
//! raw device frame.
15

            
16
/// Which motion sensor a [`SensorReading`] came from.
17
#[repr(C)]
18
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
19
pub enum SensorKind {
20
    /// Linear acceleration including gravity, in **m/s²**
21
    /// (iOS `CMAccelerometerData` ×9.80665, Android `TYPE_ACCELEROMETER`).
22
    Accelerometer,
23
    /// Angular velocity, in **rad/s** (iOS `CMGyroData`, Android
24
    /// `TYPE_GYROSCOPE`).
25
    Gyroscope,
26
    /// Geomagnetic field, in **µT** (iOS `magneticField`, Android
27
    /// `TYPE_MAGNETIC_FIELD`).
28
    Magnetometer,
29
}
30

            
31
/// One `(x, y, z)` sample from a motion sensor. Units depend on
32
/// [`SensorReading::kind`] (see [`SensorKind`]). All POD / `Copy`.
33
#[repr(C)]
34
#[derive(Debug, Clone, Copy, PartialEq)]
35
pub struct SensorReading {
36
    /// Which sensor produced this reading.
37
    pub kind: SensorKind,
38
    /// X axis (device frame: right), in the kind's unit.
39
    pub x: f32,
40
    /// Y axis (device frame: up), in the kind's unit.
41
    pub y: f32,
42
    /// Z axis (device frame: out of screen toward user), in the kind's unit.
43
    pub z: f32,
44
    /// Monotonic timestamp in milliseconds since program start.
45
    pub timestamp_ms: u64,
46
}
47

            
48
impl SensorReading {
49
    /// The magnitude of the `(x, y, z)` vector — e.g. total acceleration
50
    /// (≈9.81 at rest for the accelerometer) or field strength.
51
    #[allow(clippy::suboptimal_flops)] // mul_add not guaranteed faster/available without target +fma; keep explicit a*b+c
52
3481
    #[must_use] pub fn magnitude(&self) -> f32 {
53
3481
        (self.x * self.x + self.y * self.y + self.z * self.z).sqrt()
54
3481
    }
55
}
56

            
57
// FFI Option wrapper for `CallbackInfo::get_sensor_reading(kind) ->
58
// Option<SensorReading>` (mirrors `OptionLocationFix`).
59
impl_option!(
60
    SensorReading,
61
    OptionSensorReading,
62
    [Debug, Clone, Copy, PartialEq]
63
);
64

            
65
#[cfg(test)]
66
#[allow(clippy::float_cmp)] // exactness IS the invariant under test for several cases
67
mod autotest_generated {
68
    use core::{
69
        cell::Cell,
70
        hash::{Hash, Hasher},
71
    };
72

            
73
    use super::*;
74

            
75
    // --- helpers ---------------------------------------------------------
76

            
77
    /// `SensorReading` derives no `Default`, so every test constructs one.
78
    fn reading(kind: SensorKind, x: f32, y: f32, z: f32) -> SensorReading {
79
        SensorReading {
80
            kind,
81
            x,
82
            y,
83
            z,
84
            timestamp_ms: 0,
85
        }
86
    }
87

            
88
    fn accel(x: f32, y: f32, z: f32) -> SensorReading {
89
        reading(SensorKind::Accelerometer, x, y, z)
90
    }
91

            
92
    /// Relative comparison with an absolute floor, for the cases where f32
93
    /// addition is not associative and bit-exactness is not guaranteed.
94
    fn approx(a: f32, b: f32, rel: f32) -> bool {
95
        let scale = a.abs().max(b.abs()).max(1.0);
96
        (a - b).abs() <= rel * scale
97
    }
98

            
99
    /// FNV-1a, so the `Hash` invariant can be checked without `std`.
100
    struct Fnv(u64);
101
    impl Default for Fnv {
102
        fn default() -> Self {
103
            Self(0xcbf2_9ce4_8422_2325)
104
        }
105
    }
106
    impl Hasher for Fnv {
107
        fn finish(&self) -> u64 {
108
            self.0
109
        }
110
        fn write(&mut self, bytes: &[u8]) {
111
            for b in bytes {
112
                self.0 ^= u64::from(*b);
113
                self.0 = self.0.wrapping_mul(0x100_0000_01b3);
114
            }
115
        }
116
    }
117
    fn hash_of<T: Hash>(v: &T) -> u64 {
118
        let mut h = Fnv::default();
119
        v.hash(&mut h);
120
        h.finish()
121
    }
122

            
123
    // --- SensorReading::magnitude (getter) -------------------------------
124

            
125
    /// basic_access: exactly representable Pythagorean triples must come back
126
    /// bit-exact — every intermediate (square, sum, sqrt) is exact in f32.
127
    #[test]
128
    fn magnitude_pythagorean_is_exact() {
129
        assert_eq!(accel(3.0, 4.0, 0.0).magnitude(), 5.0);
130
        assert_eq!(accel(0.0, 3.0, 4.0).magnitude(), 5.0);
131
        assert_eq!(accel(1.0, 2.0, 2.0).magnitude(), 3.0);
132
        assert_eq!(accel(0.0, 0.0, 0.0).magnitude(), 0.0);
133
    }
134

            
135
    /// invariant: a single non-zero axis yields |v| exactly, for values whose
136
    /// square is exactly representable. Holds on all three axes.
137
    #[test]
138
    fn magnitude_single_axis_equals_abs() {
139
        for v in [1.0_f32, -1.0, 0.5, -3.25, 7.5, -1024.0, 65_536.0] {
140
            assert_eq!(accel(v, 0.0, 0.0).magnitude(), v.abs(), "x = {v}");
141
            assert_eq!(accel(0.0, v, 0.0).magnitude(), v.abs(), "y = {v}");
142
            assert_eq!(accel(0.0, 0.0, v).magnitude(), v.abs(), "z = {v}");
143
        }
144
    }
145

            
146
    /// edge_access: an all-negative-zero reading must not produce `-0.0`
147
    /// (squaring clears the sign, so the result is `+0.0`).
148
    #[test]
149
    fn magnitude_negative_zero_yields_positive_zero() {
150
        let m = accel(-0.0, -0.0, -0.0).magnitude();
151
        assert_eq!(m, 0.0);
152
        assert!(m.is_sign_positive(), "expected +0.0, got {m:?}");
153
    }
154

            
155
    /// invariant: magnitude depends only on |x|,|y|,|z| — all 8 sign
156
    /// combinations of the same triple agree bit-for-bit.
157
    #[test]
158
    fn magnitude_is_sign_invariant() {
159
        let base = accel(1.5, 2.5, 3.5).magnitude();
160
        for &sx in &[1.0_f32, -1.0] {
161
            for &sy in &[1.0_f32, -1.0] {
162
                for &sz in &[1.0_f32, -1.0] {
163
                    let m = accel(1.5 * sx, 2.5 * sy, 3.5 * sz).magnitude();
164
                    assert_eq!(m.to_bits(), base.to_bits(), "signs {sx}/{sy}/{sz}");
165
                }
166
            }
167
        }
168
    }
169

            
170
    /// invariant: for any non-NaN input the result is non-negative — never a
171
    /// negative number, never a `-0.0`.
172
    #[test]
173
    fn magnitude_is_never_negative() {
174
        let vals = [
175
            0.0_f32,
176
            -0.0,
177
            1.0,
178
            -1.0,
179
            9.81,
180
            -9.81,
181
            1e-20,
182
            -1e-20,
183
            1e20,
184
            -1e20,
185
            f32::MAX,
186
            f32::MIN,
187
            f32::MIN_POSITIVE,
188
            f32::INFINITY,
189
            f32::NEG_INFINITY,
190
        ];
191
        for &x in &vals {
192
            for &y in &vals {
193
                for &z in &vals {
194
                    let m = accel(x, y, z).magnitude();
195
                    assert!(!m.is_nan(), "unexpected NaN for ({x}, {y}, {z})");
196
                    assert!(m >= 0.0, "negative magnitude {m} for ({x}, {y}, {z})");
197
                    assert!(
198
                        m.is_sign_positive(),
199
                        "negatively-signed magnitude {m:?} for ({x}, {y}, {z})"
200
                    );
201
                }
202
            }
203
        }
204
    }
205

            
206
    /// edge_access: a NaN on any axis poisons the result (no panic, NaN out).
207
    #[test]
208
    fn magnitude_nan_on_any_axis_is_nan() {
209
        assert!(accel(f32::NAN, 0.0, 0.0).magnitude().is_nan());
210
        assert!(accel(0.0, f32::NAN, 0.0).magnitude().is_nan());
211
        assert!(accel(0.0, 0.0, f32::NAN).magnitude().is_nan());
212
        assert!(accel(f32::NAN, f32::NAN, f32::NAN).magnitude().is_nan());
213
        // NaN wins over infinity: inf*inf = inf, but inf + NaN = NaN.
214
        assert!(accel(f32::INFINITY, f32::NAN, 0.0).magnitude().is_nan());
215
        assert!(
216
            accel(f32::NEG_INFINITY, 0.0, f32::NAN)
217
                .magnitude()
218
                .is_nan()
219
        );
220
    }
221

            
222
    /// edge_access: an infinite axis (either sign) yields `+inf`, not NaN.
223
    #[test]
224
    fn magnitude_infinite_axis_is_positive_infinity() {
225
        for inf in [f32::INFINITY, f32::NEG_INFINITY] {
226
            assert_eq!(accel(inf, 0.0, 0.0).magnitude(), f32::INFINITY);
227
            assert_eq!(accel(0.0, inf, 0.0).magnitude(), f32::INFINITY);
228
            assert_eq!(accel(1.0, 2.0, inf).magnitude(), f32::INFINITY);
229
        }
230
        assert_eq!(
231
            accel(f32::INFINITY, f32::NEG_INFINITY, f32::INFINITY).magnitude(),
232
            f32::INFINITY
233
        );
234
    }
235

            
236
    /// overflow: the naive `sqrt(x² + y² + z²)` overflows for large-but-finite
237
    /// inputs — the squares exceed `f32::MAX` even though the true magnitude
238
    /// is finite (`|f32::MAX|` would fit). Documents the limitation: the
239
    /// result saturates to `+inf` rather than panicking or wrapping.
240
    #[test]
241
    fn magnitude_overflows_to_infinity_on_huge_finite_input() {
242
        // True magnitude is exactly f32::MAX (finite), but MAX*MAX == inf.
243
        assert!(accel(f32::MAX, 0.0, 0.0).magnitude().is_infinite());
244
        assert!(accel(0.0, f32::MIN, 0.0).magnitude().is_infinite());
245
        assert!(
246
            accel(f32::MAX, f32::MAX, f32::MAX)
247
                .magnitude()
248
                .is_infinite()
249
        );
250
    }
251

            
252
    /// boundary: the exact cliff where squaring stops fitting in f32.
253
    /// `1.8e19² ≈ 3.24e38 < f32::MAX`, `1.9e19² ≈ 3.61e38 > f32::MAX`.
254
    #[test]
255
    fn magnitude_overflow_cliff_per_term_and_in_the_sum() {
256
        // Below the cliff: finite, and accurate.
257
        let below = accel(1.8e19, 0.0, 0.0).magnitude();
258
        assert!(below.is_finite(), "expected finite, got {below}");
259
        assert!(approx(below, 1.8e19, 1e-5), "{below} !~ 1.8e19");
260

            
261
        // Above the cliff: a single term overflows.
262
        assert!(accel(1.9e19, 0.0, 0.0).magnitude().is_infinite());
263

            
264
        // Each term fits, but the *sum* is what overflows: 3 x 1.69e38.
265
        let two_terms = accel(1.3e19, 1.3e19, 0.0).magnitude();
266
        assert!(two_terms.is_finite(), "expected finite, got {two_terms}");
267
        assert!(approx(two_terms, 1.838_477e19, 1e-4), "{two_terms}");
268
        assert!(
269
            accel(1.3e19, 1.3e19, 1.3e19).magnitude().is_infinite(),
270
            "sum of three 1.69e38 squares must overflow to +inf"
271
        );
272
    }
273

            
274
    /// underflow: the mirror image — squaring flushes tiny inputs to zero, so
275
    /// the magnitude of a non-zero vector reads back as exactly 0.0. No panic,
276
    /// no NaN; the reading is simply below the formula's resolution.
277
    #[test]
278
    fn magnitude_underflows_to_zero_on_tiny_input() {
279
        assert_eq!(accel(1e-30, 0.0, 0.0).magnitude(), 0.0);
280
        assert_eq!(accel(0.0, -1e-30, 1e-30).magnitude(), 0.0);
281
        assert_eq!(accel(f32::MIN_POSITIVE, 0.0, 0.0).magnitude(), 0.0);
282
        // Smallest positive subnormal.
283
        assert_eq!(accel(f32::from_bits(1), 0.0, 0.0).magnitude(), 0.0);
284
    }
285

            
286
    /// boundary: just above the underflow cliff (x² lands in the subnormal
287
    /// range) the result is still finite and roughly right.
288
    #[test]
289
    fn magnitude_near_underflow_cliff_is_finite_and_close() {
290
        let m = accel(1e-19, 0.0, 0.0).magnitude();
291
        assert!(m.is_finite() && m > 0.0, "expected small positive, got {m}");
292
        assert!(approx(m, 1e-19, 1e-2), "{m} !~ 1e-19");
293
    }
294

            
295
    /// invariant: scaling by a power of two is exact in IEEE-754, so
296
    /// `magnitude(2v) == 2 * magnitude(v)` bit-for-bit (absent over/underflow).
297
    #[test]
298
    fn magnitude_scaling_by_power_of_two_is_exact() {
299
        let (x, y, z) = (1.5_f32, -2.25_f32, 0.75_f32);
300
        let base = accel(x, y, z).magnitude();
301
        let doubled = accel(2.0 * x, 2.0 * y, 2.0 * z).magnitude();
302
        let halved = accel(0.5 * x, 0.5 * y, 0.5 * z).magnitude();
303
        assert_eq!(doubled, 2.0 * base);
304
        assert_eq!(halved, 0.5 * base);
305
    }
306

            
307
    /// invariant: magnitude is symmetric in its axes (f32 addition is not
308
    /// associative, so only up to a relative epsilon).
309
    #[test]
310
    fn magnitude_is_permutation_stable() {
311
        let (x, y, z) = (0.1_f32, 12_345.678_f32, -0.000_31_f32);
312
        let base = accel(x, y, z).magnitude();
313
        for (a, b, c) in [(x, z, y), (y, x, z), (y, z, x), (z, x, y), (z, y, x)] {
314
            let m = accel(a, b, c).magnitude();
315
            assert!(approx(m, base, 1e-6), "{m} !~ {base} for ({a}, {b}, {c})");
316
        }
317
    }
318

            
319
    /// basic_access: the documented value — an accelerometer at rest reads
320
    /// ~9.81 m/s² total, whichever axis gravity lands on.
321
    #[test]
322
    fn magnitude_accelerometer_at_rest_is_about_9_81() {
323
        for r in [
324
            accel(0.0, -9.81, 0.0),
325
            accel(9.81, 0.0, 0.0),
326
            accel(0.0, 0.0, -9.81),
327
            accel(5.663_8, -5.663_8, -5.663_8), // tilted: 9.81 / sqrt(3) per axis
328
        ] {
329
            let m = r.magnitude();
330
            assert!(approx(m, 9.81, 1e-3), "{m} !~ 9.81 for {r:?}");
331
        }
332
    }
333

            
334
    /// invariant: `kind` and `timestamp_ms` are not part of the computation —
335
    /// including at the extremes of `u64`.
336
    #[test]
337
    fn magnitude_ignores_kind_and_timestamp() {
338
        let expect = accel(1.0, 2.0, 2.0).magnitude().to_bits();
339
        for kind in [
340
            SensorKind::Accelerometer,
341
            SensorKind::Gyroscope,
342
            SensorKind::Magnetometer,
343
        ] {
344
            for timestamp_ms in [0_u64, 1, u64::MAX / 2, u64::MAX] {
345
                let r = SensorReading {
346
                    kind,
347
                    x: 1.0,
348
                    y: 2.0,
349
                    z: 2.0,
350
                    timestamp_ms,
351
                };
352
                assert_eq!(r.magnitude().to_bits(), expect, "{kind:?} @ {timestamp_ms}");
353
            }
354
        }
355
    }
356

            
357
    /// invariant: `&self` getter — repeated calls are bit-identical and the
358
    /// reading is left untouched.
359
    #[test]
360
    fn magnitude_is_deterministic_and_leaves_the_reading_intact() {
361
        let r = accel(0.1, -0.2, 9.79);
362
        let before = r;
363
        let a = r.magnitude();
364
        let b = r.magnitude();
365
        assert_eq!(a.to_bits(), b.to_bits());
366
        assert_eq!(r, before, "magnitude() must not mutate the reading");
367
    }
368

            
369
    // --- SensorReading / SensorKind POD invariants ------------------------
370

            
371
    /// A NaN axis makes the derived `PartialEq` non-reflexive — callers that
372
    /// dedup readings by equality must not assume `r == r`.
373
    #[test]
374
    fn reading_with_nan_axis_is_not_equal_to_itself() {
375
        let nan = accel(f32::NAN, 0.0, 0.0);
376
        let bit_identical = nan; // Copy, so this is the very same bit pattern
377
        assert_ne!(nan, bit_identical, "a NaN axis makes PartialEq non-reflexive");
378

            
379
        // ... and the FFI wrapper inherits that.
380
        let opt = OptionSensorReading::Some(nan);
381
        let opt_copy = opt;
382
        assert_ne!(opt, opt_copy);
383

            
384
        // A non-NaN reading *is* reflexive.
385
        let ok = accel(1.0, 2.0, 2.0);
386
        assert_eq!(ok, accel(1.0, 2.0, 2.0));
387
    }
388

            
389
    /// The derived `Ord` follows declaration order, and `Hash` agrees with
390
    /// `Eq` (equal values hash equal, the three variants are distinguishable).
391
    #[test]
392
    fn sensor_kind_ordering_and_hash_are_consistent() {
393
        use SensorKind::{Accelerometer, Gyroscope, Magnetometer};
394
        assert!(Accelerometer < Gyroscope);
395
        assert!(Gyroscope < Magnetometer);
396
        assert!(Accelerometer < Magnetometer);
397

            
398
        assert_eq!(hash_of(&Accelerometer), hash_of(&Accelerometer));
399
        assert_ne!(hash_of(&Accelerometer), hash_of(&Gyroscope));
400
        assert_ne!(hash_of(&Gyroscope), hash_of(&Magnetometer));
401

            
402
        // Copy, not move.
403
        let k = Gyroscope;
404
        let copied = k;
405
        assert_eq!(k, copied);
406
    }
407

            
408
    /// FFI layout: `SensorKind` is a `repr(C)` field-less enum, i.e. a C `int`.
409
    #[test]
410
    fn sensor_kind_is_a_c_int() {
411
        assert_eq!(core::mem::size_of::<SensorKind>(), 4);
412
        assert!(
413
            core::mem::size_of::<OptionSensorReading>() > core::mem::size_of::<SensorReading>()
414
        );
415
    }
416

            
417
    // --- OptionSensorReading (FFI wrapper) -------------------------------
418

            
419
    #[test]
420
    fn option_default_is_none() {
421
        let d = OptionSensorReading::default();
422
        assert!(d.is_none());
423
        assert!(!d.is_some());
424
        assert_eq!(d.as_option(), None);
425
        assert_eq!(d.as_ref(), None);
426
        assert_eq!(d.into_option(), None);
427
    }
428

            
429
    /// round-trip: `Option<SensorReading> -> OptionSensorReading -> Option<_>`
430
    /// is the identity, for both variants.
431
    #[test]
432
    fn option_roundtrips_through_the_ffi_wrapper() {
433
        let r = accel(1.0, 2.0, 2.0);
434

            
435
        let wrapped: OptionSensorReading = Some(r).into();
436
        assert!(wrapped.is_some());
437
        assert_eq!(wrapped.into_option(), Some(r));
438
        assert_eq!(Option::<SensorReading>::from(wrapped), Some(r));
439
        assert_eq!(wrapped.as_option(), Some(&r));
440

            
441
        let none: OptionSensorReading = None.into();
442
        assert!(none.is_none());
443
        assert_eq!(none.into_option(), None);
444
        assert_eq!(Option::<SensorReading>::from(none), None);
445

            
446
        // is_some / is_none are strict complements.
447
        for o in [wrapped, none] {
448
            assert_ne!(o.is_some(), o.is_none());
449
        }
450
    }
451

            
452
    /// `replace` has `mem::replace` semantics: it returns the PREVIOUS value.
453
    #[test]
454
    fn option_replace_returns_the_previous_value() {
455
        let first = accel(1.0, 0.0, 0.0);
456
        let second = accel(0.0, 3.0, 4.0);
457

            
458
        let mut o = OptionSensorReading::None;
459
        let prev = o.replace(first);
460
        assert!(prev.is_none(), "replacing None must hand back None");
461
        assert_eq!(o.into_option(), Some(first));
462

            
463
        let prev = o.replace(second);
464
        assert_eq!(prev.into_option(), Some(first));
465
        assert_eq!(o.into_option(), Some(second));
466
        assert_eq!(o.as_option().map(SensorReading::magnitude), Some(5.0));
467
    }
468

            
469
    /// `as_mut` hands out a real mutable borrow of the payload.
470
    #[test]
471
    fn option_as_mut_mutates_the_payload() {
472
        let mut o = OptionSensorReading::Some(accel(0.0, 0.0, 0.0));
473
        assert_eq!(o.as_option().map(SensorReading::magnitude), Some(0.0));
474

            
475
        let slot = o.as_mut().expect("Some");
476
        slot.x = 3.0;
477
        slot.y = 4.0;
478
        assert_eq!(o.as_option().map(SensorReading::magnitude), Some(5.0));
479

            
480
        let mut none = OptionSensorReading::None;
481
        assert!(none.as_mut().is_none());
482
    }
483

            
484
    /// `map` / `and_then` must not run the closure on `None` (and must run it
485
    /// exactly once on `Some`).
486
    #[test]
487
    fn option_map_and_and_then_are_lazy_on_none() {
488
        let calls = Cell::new(0_u32);
489

            
490
        let mapped = OptionSensorReading::None.map(|r| {
491
            calls.set(calls.get() + 1);
492
            r.magnitude()
493
        });
494
        assert_eq!(mapped, None);
495
        assert_eq!(calls.get(), 0, "closure must not run on None");
496

            
497
        let chained = OptionSensorReading::None.and_then(|r| Some(r.magnitude()));
498
        assert_eq!(chained, None);
499

            
500
        let some = OptionSensorReading::Some(accel(3.0, 4.0, 0.0));
501
        let mapped = some.map(|r| {
502
            calls.set(calls.get() + 1);
503
            r.magnitude()
504
        });
505
        assert_eq!(mapped, Some(5.0));
506
        assert_eq!(calls.get(), 1, "closure must run exactly once on Some");
507

            
508
        assert_eq!(some.and_then(|r| Some(r.magnitude())), Some(5.0));
509
        assert_eq!(
510
            some.and_then(|_| Option::<f32>::None),
511
            None,
512
            "and_then must be able to collapse Some -> None"
513
        );
514
    }
515

            
516
    /// The wrapper is `Copy` — passing it by value across FFI leaves the
517
    /// source usable (and the copy independent).
518
    #[test]
519
    fn option_is_copy_and_the_source_survives() {
520
        let o = OptionSensorReading::Some(accel(0.0, 3.0, 4.0));
521
        let mut copy = o;
522
        copy.replace(accel(1.0, 0.0, 0.0));
523

            
524
        assert_eq!(o.as_option().map(SensorReading::magnitude), Some(5.0));
525
        assert_eq!(copy.as_option().map(SensorReading::magnitude), Some(1.0));
526
    }
527
}