1#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11#[cfg_attr(feature = "defmt", derive(defmt::Format))]
12pub enum Axis {
13 PosX,
15 NegX,
17 PosY,
19 NegY,
21 PosZ,
23 NegZ,
25}
26
27impl Axis {
28 pub const fn index(self) -> usize {
30 match self {
31 Axis::PosX | Axis::NegX => 0,
32 Axis::PosY | Axis::NegY => 1,
33 Axis::PosZ | Axis::NegZ => 2,
34 }
35 }
36
37 pub const fn is_negative(self) -> bool {
39 matches!(self, Axis::NegX | Axis::NegY | Axis::NegZ)
40 }
41
42 pub const fn flipped(self) -> Self {
44 match self {
45 Axis::PosX => Axis::NegX,
46 Axis::NegX => Axis::PosX,
47 Axis::PosY => Axis::NegY,
48 Axis::NegY => Axis::PosY,
49 Axis::PosZ => Axis::NegZ,
50 Axis::NegZ => Axis::PosZ,
51 }
52 }
53
54 pub const fn pick(self, v: [f32; 3]) -> f32 {
56 let c = v[self.index()];
57 if self.is_negative() { -c } else { c }
58 }
59
60 pub fn dominant(v: [f32; 3], min: f32) -> Option<Self> {
65 let mag = [v[0].abs(), v[1].abs(), v[2].abs()];
66 let (i, &largest) = mag.iter().enumerate().max_by(|a, b| a.1.total_cmp(b.1))?;
67 let second = mag
68 .iter()
69 .enumerate()
70 .filter(|&(j, _)| j != i)
71 .map(|(_, m)| *m)
72 .fold(0.0f32, f32::max);
73 if largest < min || largest < 2.0 * second {
74 return None;
75 }
76 let positive = [Axis::PosX, Axis::PosY, Axis::PosZ][i];
77 Some(if v[i] < 0.0 {
78 positive.flipped()
79 } else {
80 positive
81 })
82 }
83
84 pub const fn source(self) -> &'static str {
86 match self {
87 Axis::PosX => "Axis::PosX",
88 Axis::NegX => "Axis::NegX",
89 Axis::PosY => "Axis::PosY",
90 Axis::NegY => "Axis::NegY",
91 Axis::PosZ => "Axis::PosZ",
92 Axis::NegZ => "Axis::NegZ",
93 }
94 }
95}
96
97#[derive(Clone, Copy, Debug, PartialEq, Eq)]
108#[cfg_attr(feature = "defmt", derive(defmt::Format))]
109pub struct AxisMap {
110 pub x: Axis,
112 pub y: Axis,
114 pub z: Axis,
116}
117
118#[derive(Clone, Copy, Debug, PartialEq, Eq)]
120#[cfg_attr(feature = "defmt", derive(defmt::Format))]
121pub enum GravityError {
122 Unclear(u8),
125 Repeated,
128}
129
130impl AxisMap {
131 pub const IDENTITY: Self = Self {
133 x: Axis::PosX,
134 y: Axis::PosY,
135 z: Axis::PosZ,
136 };
137
138 pub const fn apply(&self, v: [f32; 3]) -> [f32; 3] {
140 [self.x.pick(v), self.y.pick(v), self.z.pick(v)]
141 }
142
143 pub const fn is_permutation(&self) -> bool {
145 let (x, y, z) = (self.x.index(), self.y.index(), self.z.index());
146 x != y && y != z && x != z
147 }
148
149 pub const fn is_right_handed(&self) -> bool {
153 if !self.is_permutation() {
154 return false;
155 }
156 let even = matches!(
158 (self.x.index(), self.y.index(), self.z.index()),
159 (0, 1, 2) | (1, 2, 0) | (2, 0, 1)
160 );
161 let flips =
162 self.x.is_negative() as u8 + self.y.is_negative() as u8 + self.z.is_negative() as u8;
163 even == flips.is_multiple_of(2)
164 }
165
166 pub fn from_gravity(
186 x_up: [f32; 3],
187 y_up: [f32; 3],
188 z_up: [f32; 3],
189 ) -> Result<Self, GravityError> {
190 const MIN: f32 = 0.7;
191 let x = Axis::dominant(x_up, MIN).ok_or(GravityError::Unclear(0))?;
192 let y = Axis::dominant(y_up, MIN).ok_or(GravityError::Unclear(1))?;
193 let z = Axis::dominant(z_up, MIN).ok_or(GravityError::Unclear(2))?;
194 let map = Self { x, y, z };
195 if !map.is_permutation() {
196 return Err(GravityError::Repeated);
197 }
198 Ok(map)
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn identity_is_a_rotation() {
208 assert!(AxisMap::IDENTITY.is_right_handed());
209 assert_eq!(AxisMap::IDENTITY.apply([1.0, 2.0, 3.0]), [1.0, 2.0, 3.0]);
210 }
211
212 #[test]
213 fn all_four_flat_rotations_are_right_handed() {
214 for (x, y) in [
215 (Axis::PosX, Axis::PosY),
216 (Axis::NegY, Axis::PosX),
217 (Axis::NegX, Axis::NegY),
218 (Axis::PosY, Axis::NegX),
219 ] {
220 let m = AxisMap {
221 x,
222 y,
223 z: Axis::PosZ,
224 };
225 assert!(m.is_right_handed(), "{m:?}");
226 }
227 let mirror = AxisMap {
229 x: Axis::PosY,
230 y: Axis::PosX,
231 z: Axis::PosZ,
232 };
233 assert!(!mirror.is_right_handed());
234 let flipped = AxisMap {
236 x: Axis::NegX,
237 y: Axis::PosY,
238 z: Axis::PosZ,
239 };
240 assert!(!flipped.is_right_handed());
241 }
242
243 #[test]
244 fn dominant_axis_tolerates_tilt_but_not_ambiguity() {
245 assert_eq!(Axis::dominant([0.0, 0.0, 0.98], 0.7), Some(Axis::PosZ));
246 assert_eq!(Axis::dominant([-0.9, 0.4, 0.1], 0.7), Some(Axis::NegX));
247 assert_eq!(Axis::dominant([0.7, 0.7, 0.0], 0.7), None); assert_eq!(Axis::dominant([0.3, 0.1, 0.1], 0.7), None); }
250
251 #[test]
252 fn gravity_calibration_rejects_repeated_poses() {
253 let flat = [0.0, 0.0, 1.0];
254 assert_eq!(
255 AxisMap::from_gravity(flat, [0.0, 1.0, 0.0], flat),
256 Err(GravityError::Repeated)
257 );
258 assert_eq!(
259 AxisMap::from_gravity([0.6, 0.6, 0.0], [0.0, 1.0, 0.0], flat),
260 Err(GravityError::Unclear(0))
261 );
262 }
263
264 #[test]
265 fn gravity_calibration_round_trips() {
266 let truth = AxisMap {
267 x: Axis::NegX,
268 y: Axis::NegY,
269 z: Axis::PosZ,
270 };
271 let sensor_for = |board_axis: Axis| {
274 let mut v = [0.0f32; 3];
275 v[board_axis.index()] = if board_axis.is_negative() { -1.0 } else { 1.0 };
276 v
277 };
278 let map = AxisMap::from_gravity(
279 sensor_for(truth.x),
280 sensor_for(truth.y),
281 sensor_for(truth.z),
282 )
283 .unwrap();
284 assert_eq!(map, truth);
285 }
286}