re_components/
coordinates.rs

1#![allow(clippy::wrong_self_convention)] // TODO(emilk): re-enable
2
3use arrow2::datatypes::DataType;
4use arrow2_convert::{
5    deserialize::ArrowDeserialize,
6    field::{ArrowField, FixedSizeBinary},
7    serialize::ArrowSerialize,
8};
9
10/// The six cardinal directions for 3D view-space and image-space.
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
13pub enum ViewDir {
14    Up = 1,
15    Down = 2,
16    Right = 3,
17    Left = 4,
18    Forward = 5,
19    Back = 6,
20}
21
22impl ViewDir {
23    #[inline]
24    fn from_ascii_char(c: u8) -> Result<Self, String> {
25        match c {
26            b'U' => Ok(Self::Up),
27            b'D' => Ok(Self::Down),
28            b'R' => Ok(Self::Right),
29            b'L' => Ok(Self::Left),
30            b'F' => Ok(Self::Forward),
31            b'B' => Ok(Self::Back),
32            _ => Err("Expected one of UDRLFB (Up Down Right Left Forward Back)".to_owned()),
33        }
34    }
35
36    #[inline]
37    pub fn short(&self) -> &'static str {
38        match self {
39            Self::Up => "U",
40            Self::Down => "D",
41            Self::Right => "R",
42            Self::Left => "L",
43            Self::Forward => "F",
44            Self::Back => "B",
45        }
46    }
47
48    #[inline]
49    pub fn long(&self) -> &'static str {
50        match self {
51            Self::Up => "Up",
52            Self::Down => "Down",
53            Self::Right => "Right",
54            Self::Left => "Left",
55            Self::Forward => "Forward",
56            Self::Back => "Back",
57        }
58    }
59}
60
61impl TryFrom<u8> for ViewDir {
62    type Error = super::FieldError;
63
64    #[inline]
65    fn try_from(i: u8) -> super::Result<Self> {
66        match i {
67            1 => Ok(Self::Up),
68            2 => Ok(Self::Down),
69            3 => Ok(Self::Right),
70            4 => Ok(Self::Left),
71            5 => Ok(Self::Forward),
72            6 => Ok(Self::Back),
73            _ => Err(super::FieldError::BadValue),
74        }
75    }
76}
77
78// ----------------------------------------------------------------------------
79
80/// How we interpret the coordinate system of an entity/space.
81///
82/// For instance: What is "up"? What does the Z axis mean? Is this right-handed or left-handed?
83///
84/// For 3D view-space and image-space.
85///
86/// ```
87/// use re_components::ViewCoordinates;
88/// use arrow2_convert::field::ArrowField;
89/// use arrow2::datatypes::{DataType, Field};
90///
91/// assert_eq!(
92///     ViewCoordinates::data_type(),
93///     DataType::FixedSizeBinary(3)
94/// );
95/// ```
96#[derive(Clone, Copy, Debug, PartialEq, Eq)]
97#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
98pub struct ViewCoordinates(pub [ViewDir; 3]);
99
100impl re_log_types::LegacyComponent for ViewCoordinates {
101    #[inline]
102    fn legacy_name() -> re_log_types::ComponentName {
103        "rerun.view_coordinates".into()
104    }
105}
106
107impl ViewCoordinates {
108    /// Default right-handed pinhole, camera, and image coordinates: X=Right, Y=Down, Z=Forward.
109    pub const RDF: Self = Self([ViewDir::Right, ViewDir::Down, ViewDir::Forward]);
110
111    /// Default right-handed view coordinates of `re_renderer`: X=Right, Y=Up, Z=Back.
112    pub const RUB: Self = Self([ViewDir::Right, ViewDir::Up, ViewDir::Back]);
113
114    /// Choses a coordinate system based on just an up-axis.
115    pub fn from_up_and_handedness(up: SignedAxis3, handedness: Handedness) -> Self {
116        use ViewDir::{Back, Down, Forward, Right, Up};
117        match handedness {
118            Handedness::Right => match up {
119                SignedAxis3::POSITIVE_X => Self([Up, Right, Forward]),
120                SignedAxis3::NEGATIVE_X => Self([Down, Right, Back]),
121                SignedAxis3::POSITIVE_Y => Self([Right, Up, Back]),
122                SignedAxis3::NEGATIVE_Y => Self([Right, Down, Forward]),
123                SignedAxis3::POSITIVE_Z => Self([Right, Forward, Up]),
124                SignedAxis3::NEGATIVE_Z => Self([Right, Back, Down]),
125            },
126            Handedness::Left => match up {
127                SignedAxis3::POSITIVE_X => Self([Up, Right, Back]),
128                SignedAxis3::NEGATIVE_X => Self([Down, Right, Forward]),
129                SignedAxis3::POSITIVE_Y => Self([Right, Up, Forward]),
130                SignedAxis3::NEGATIVE_Y => Self([Right, Down, Back]),
131                SignedAxis3::POSITIVE_Z => Self([Right, Back, Up]),
132                SignedAxis3::NEGATIVE_Z => Self([Right, Forward, Down]),
133            },
134        }
135    }
136
137    /// Returns an error if this does not span all three dimensions.
138    pub fn sanity_check(&self) -> Result<(), String> {
139        let mut dims = [false; 3];
140        for dir in self.0 {
141            let dim = match dir {
142                ViewDir::Up | ViewDir::Down => 0,
143                ViewDir::Right | ViewDir::Left => 1,
144                ViewDir::Forward | ViewDir::Back => 2,
145            };
146            dims[dim] = true;
147        }
148        if dims == [true; 3] {
149            Ok(())
150        } else {
151            Err(format!(
152                "Coordinate system does not cover all three cardinal directions: {}",
153                self.describe()
154            ))
155        }
156    }
157
158    #[inline]
159    pub fn up(&self) -> Option<SignedAxis3> {
160        for (dim, &dir) in self.0.iter().enumerate() {
161            if dir == ViewDir::Up {
162                return Some(SignedAxis3::new(Sign::Positive, Axis3::from_dim(dim)));
163            } else if dir == ViewDir::Down {
164                return Some(SignedAxis3::new(Sign::Negative, Axis3::from_dim(dim)));
165            }
166        }
167        None
168    }
169
170    #[inline]
171    pub fn right(&self) -> Option<SignedAxis3> {
172        for (dim, &dir) in self.0.iter().enumerate() {
173            if dir == ViewDir::Right {
174                return Some(SignedAxis3::new(Sign::Positive, Axis3::from_dim(dim)));
175            } else if dir == ViewDir::Left {
176                return Some(SignedAxis3::new(Sign::Negative, Axis3::from_dim(dim)));
177            }
178        }
179        None
180    }
181
182    #[inline]
183    pub fn forward(&self) -> Option<SignedAxis3> {
184        for (dim, &dir) in self.0.iter().enumerate() {
185            if dir == ViewDir::Forward {
186                return Some(SignedAxis3::new(Sign::Positive, Axis3::from_dim(dim)));
187            } else if dir == ViewDir::Back {
188                return Some(SignedAxis3::new(Sign::Negative, Axis3::from_dim(dim)));
189            }
190        }
191        None
192    }
193
194    pub fn describe_short(&self) -> String {
195        let [x, y, z] = self.0;
196        format!("{}{}{}", x.short(), y.short(), z.short(),)
197    }
198
199    pub fn describe(&self) -> String {
200        let [x, y, z] = self.0;
201        format!(
202            "{}{}{} (X={}, Y={}, Z={})",
203            x.short(),
204            y.short(),
205            z.short(),
206            x.long(),
207            y.long(),
208            z.long()
209        )
210    }
211
212    /// Returns a matrix that transforms from another coordinate system to this (self) one.
213    #[cfg(feature = "glam")]
214    #[inline]
215    pub fn from_other(&self, other: &Self) -> glam::Mat3 {
216        self.from_rdf() * other.to_rdf()
217    }
218
219    /// Returns a matrix that transforms this coordinate system to RDF.
220    ///
221    /// (RDF: X=Right, Y=Down, Z=Forward)
222    #[cfg(feature = "glam")]
223    #[inline]
224    pub fn to_rdf(&self) -> glam::Mat3 {
225        fn rdf(dir: ViewDir) -> [f32; 3] {
226            match dir {
227                ViewDir::Right => [1.0, 0.0, 0.0],
228                ViewDir::Left => [-1.0, 0.0, 0.0],
229                ViewDir::Up => [0.0, -1.0, 0.0],
230                ViewDir::Down => [0.0, 1.0, 0.0],
231                ViewDir::Back => [0.0, 0.0, -1.0],
232                ViewDir::Forward => [0.0, 0.0, 1.0],
233            }
234        }
235
236        glam::Mat3::from_cols_array_2d(&[rdf(self.0[0]), rdf(self.0[1]), rdf(self.0[2])])
237    }
238
239    /// Returns a matrix that transforms from RDF to this coordinate system.
240    ///
241    /// (RDF: X=Right, Y=Down, Z=Forward)
242    #[cfg(feature = "glam")]
243    #[inline]
244    pub fn from_rdf(&self) -> glam::Mat3 {
245        self.to_rdf().transpose()
246    }
247
248    /// Returns a matrix that transforms this coordinate system to RUB.
249    ///
250    /// (RUB: X=Right, Y=Up, Z=Back)
251    #[cfg(feature = "glam")]
252    #[inline]
253    pub fn to_rub(&self) -> glam::Mat3 {
254        fn rub(dir: ViewDir) -> [f32; 3] {
255            match dir {
256                ViewDir::Right => [1.0, 0.0, 0.0],
257                ViewDir::Left => [-1.0, 0.0, 0.0],
258                ViewDir::Up => [0.0, 1.0, 0.0],
259                ViewDir::Down => [0.0, -1.0, 0.0],
260                ViewDir::Back => [0.0, 0.0, 1.0],
261                ViewDir::Forward => [0.0, 0.0, -1.0],
262            }
263        }
264
265        glam::Mat3::from_cols_array_2d(&[rub(self.0[0]), rub(self.0[1]), rub(self.0[2])])
266    }
267
268    /// Returns a matrix that transforms from RUB to this coordinate system.
269    ///
270    /// (RUB: X=Right, Y=Up, Z=Back)
271    #[cfg(feature = "glam")]
272    #[inline]
273    pub fn from_rub(&self) -> glam::Mat3 {
274        self.to_rub().transpose()
275    }
276
277    /// Returns a quaternion that rotates from RUB to this coordinate system.
278    ///
279    /// Errors if the coordinate system is left-handed or degenerate.
280    ///
281    /// (RUB: X=Right, Y=Up, Z=Back)
282    #[cfg(feature = "glam")]
283    #[inline]
284    pub fn from_rub_quat(&self) -> Result<glam::Quat, String> {
285        let mat3 = self.from_rub();
286
287        let det = mat3.determinant();
288        if det == 1.0 {
289            Ok(glam::Quat::from_mat3(&mat3))
290        } else if det == -1.0 {
291            Err(format!(
292                "Rerun does not yet support left-handed coordinate systems (found {})",
293                self.describe()
294            ))
295        } else {
296            Err(format!(
297                "Found a degenerate coordinate system: {}",
298                self.describe()
299            ))
300        }
301    }
302
303    #[cfg(feature = "glam")]
304    #[inline]
305    pub fn handedness(&self) -> Option<Handedness> {
306        let to_rdf = self.to_rdf();
307        let det = to_rdf.determinant();
308        if det == -1.0 {
309            Some(Handedness::Left)
310        } else if det == 0.0 {
311            None // bad system that doesn't pass the sanity check
312        } else {
313            Some(Handedness::Right)
314        }
315    }
316}
317
318impl std::str::FromStr for ViewCoordinates {
319    type Err = String;
320
321    #[inline]
322    fn from_str(s: &str) -> Result<Self, Self::Err> {
323        match s.as_bytes() {
324            [x, y, z] => {
325                let slf = Self([
326                    ViewDir::from_ascii_char(*x)?,
327                    ViewDir::from_ascii_char(*y)?,
328                    ViewDir::from_ascii_char(*z)?,
329                ]);
330                slf.sanity_check()?;
331                Ok(slf)
332            }
333            _ => Err(format!("Expected three letters, got: {s:?}")),
334        }
335    }
336}
337
338impl ArrowField for ViewCoordinates {
339    type Type = Self;
340
341    #[inline]
342    fn data_type() -> DataType {
343        <FixedSizeBinary<3> as ArrowField>::data_type()
344    }
345}
346
347impl ArrowSerialize for ViewCoordinates {
348    type MutableArrayType = <FixedSizeBinary<3> as ArrowSerialize>::MutableArrayType;
349
350    #[inline]
351    fn new_array() -> Self::MutableArrayType {
352        FixedSizeBinary::<3>::new_array()
353    }
354
355    #[inline]
356    fn arrow_serialize(v: &Self, array: &mut Self::MutableArrayType) -> arrow2::error::Result<()> {
357        let bytes = [v.0[0] as u8, v.0[1] as u8, v.0[2] as u8];
358        array.try_push(Some(bytes))
359    }
360}
361
362impl ArrowDeserialize for ViewCoordinates {
363    type ArrayType = <FixedSizeBinary<3> as ArrowDeserialize>::ArrayType;
364
365    #[inline]
366    fn arrow_deserialize(
367        bytes: <&Self::ArrayType as IntoIterator>::Item,
368    ) -> Option<<Self as ArrowField>::Type> {
369        bytes.and_then(|bytes| {
370            let dirs = [
371                bytes[0].try_into().ok()?,
372                bytes[1].try_into().ok()?,
373                bytes[2].try_into().ok()?,
374            ];
375            Some(ViewCoordinates(dirs))
376        })
377    }
378}
379
380re_log_types::component_legacy_shim!(ViewCoordinates);
381
382// ----------------------------------------------------------------------------
383
384/// One of `X`, `Y`, `Z`.
385#[derive(Clone, Copy, Debug, PartialEq, Eq)]
386#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
387pub enum Axis3 {
388    X,
389    Y,
390    Z,
391}
392
393impl Axis3 {
394    #[inline]
395    pub fn from_dim(dim: usize) -> Self {
396        match dim {
397            0 => Self::X,
398            1 => Self::Y,
399            2 => Self::Z,
400            _ => panic!("Expected a 3D axis, got {dim}"),
401        }
402    }
403}
404
405impl std::fmt::Display for Axis3 {
406    #[inline]
407    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
408        match self {
409            Self::X => "X".fmt(f),
410            Self::Y => "Y".fmt(f),
411            Self::Z => "Z".fmt(f),
412        }
413    }
414}
415
416// ----------------------------------------------------------------------------
417
418/// Positive (`+`) or Negative (`-`).
419#[derive(Clone, Copy, Debug, PartialEq, Eq)]
420#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
421pub enum Sign {
422    Positive,
423    Negative,
424}
425
426// ----------------------------------------------------------------------------
427
428/// One of: `+X`, `-X`, `+Y`, `-Y`, `+Z`, `-Z`,
429/// i.e. one of the six cardinal direction in 3D space.
430#[derive(Clone, Copy, Debug, PartialEq, Eq)]
431#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
432pub struct SignedAxis3 {
433    pub sign: Sign,
434    pub axis: Axis3,
435}
436
437impl SignedAxis3 {
438    pub const POSITIVE_X: Self = Self::new(Sign::Positive, Axis3::X);
439    pub const NEGATIVE_X: Self = Self::new(Sign::Negative, Axis3::X);
440    pub const POSITIVE_Y: Self = Self::new(Sign::Positive, Axis3::Y);
441    pub const NEGATIVE_Y: Self = Self::new(Sign::Negative, Axis3::Y);
442    pub const POSITIVE_Z: Self = Self::new(Sign::Positive, Axis3::Z);
443    pub const NEGATIVE_Z: Self = Self::new(Sign::Negative, Axis3::Z);
444
445    #[inline]
446    pub const fn new(sign: Sign, axis: Axis3) -> Self {
447        Self { sign, axis }
448    }
449
450    #[inline]
451    pub fn as_vec3(&self) -> [f32; 3] {
452        match (self.sign, self.axis) {
453            (Sign::Positive, Axis3::X) => [1.0, 0.0, 0.0],
454            (Sign::Negative, Axis3::X) => [-1.0, 0.0, 0.0],
455            (Sign::Positive, Axis3::Y) => [0.0, 1.0, 0.0],
456            (Sign::Negative, Axis3::Y) => [0.0, -1.0, 0.0],
457            (Sign::Positive, Axis3::Z) => [0.0, 0.0, 1.0],
458            (Sign::Negative, Axis3::Z) => [0.0, 0.0, -1.0],
459        }
460    }
461}
462
463impl std::fmt::Display for SignedAxis3 {
464    #[inline]
465    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
466        let sign = match self.sign {
467            Sign::Positive => "+",
468            Sign::Negative => "-",
469        };
470        write!(f, "{}{}", sign, self.axis)
471    }
472}
473
474impl std::str::FromStr for SignedAxis3 {
475    type Err = String;
476
477    fn from_str(s: &str) -> Result<Self, Self::Err> {
478        match s {
479            "+X" => Ok(Self::new(Sign::Positive, Axis3::X)),
480            "-X" => Ok(Self::new(Sign::Negative, Axis3::X)),
481            "+Y" => Ok(Self::new(Sign::Positive, Axis3::Y)),
482            "-Y" => Ok(Self::new(Sign::Negative, Axis3::Y)),
483            "+Z" => Ok(Self::new(Sign::Positive, Axis3::Z)),
484            "-Z" => Ok(Self::new(Sign::Negative, Axis3::Z)),
485            _ => Err("Expected one of: +X -X +Y -Y +Z -Z".to_owned()),
486        }
487    }
488}
489
490#[cfg(feature = "glam")]
491impl From<SignedAxis3> for glam::Vec3 {
492    #[inline]
493    fn from(signed_axis: SignedAxis3) -> Self {
494        glam::Vec3::from(signed_axis.as_vec3())
495    }
496}
497
498// ----------------------------------------------------------------------------
499
500/// Left or right handedness. Used to describe a coordinate system.
501#[derive(Clone, Copy, Debug, PartialEq, Eq)]
502#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
503pub enum Handedness {
504    Right,
505    Left,
506}
507
508impl Handedness {
509    #[inline]
510    pub const fn from_right_handed(right_handed: bool) -> Self {
511        if right_handed {
512            Handedness::Right
513        } else {
514            Handedness::Left
515        }
516    }
517
518    #[inline]
519    pub fn describe(&self) -> &'static str {
520        match self {
521            Self::Left => "left handed",
522            Self::Right => "right handed",
523        }
524    }
525}
526
527// ----------------------------------------------------------------------------
528
529#[cfg(feature = "glam")]
530#[test]
531fn view_coordinates() {
532    use glam::{vec3, Mat3};
533
534    assert_eq!(ViewCoordinates::RUB.to_rub(), Mat3::IDENTITY);
535    assert_eq!(ViewCoordinates::RUB.from_rub(), Mat3::IDENTITY);
536
537    {
538        assert!("UUDDLRLRBAStart".parse::<ViewCoordinates>().is_err());
539        assert!("UUD".parse::<ViewCoordinates>().is_err());
540
541        let rub = "RUB".parse::<ViewCoordinates>().unwrap();
542        let bru = "BRU".parse::<ViewCoordinates>().unwrap();
543
544        assert_eq!(rub, ViewCoordinates::RUB);
545
546        assert_eq!(rub.to_rub(), Mat3::IDENTITY);
547        assert_eq!(
548            bru.to_rub(),
549            Mat3::from_cols_array_2d(&[[0., 0., 1.], [1., 0., 0.], [0., 1., 0.]])
550        );
551        assert_eq!(bru.to_rub() * vec3(1.0, 0.0, 0.0), vec3(0.0, 0.0, 1.0));
552    }
553
554    {
555        let cardinal_direction = [
556            SignedAxis3::POSITIVE_X,
557            SignedAxis3::NEGATIVE_X,
558            SignedAxis3::POSITIVE_Y,
559            SignedAxis3::NEGATIVE_Y,
560            SignedAxis3::POSITIVE_Z,
561            SignedAxis3::NEGATIVE_Z,
562        ];
563
564        for axis in cardinal_direction {
565            for handedness in [Handedness::Right, Handedness::Left] {
566                let system = ViewCoordinates::from_up_and_handedness(axis, handedness);
567                assert_eq!(system.handedness(), Some(handedness));
568
569                let det = system.to_rub().determinant();
570                assert!(det == -1.0 || det == 0.0 || det == 1.0);
571
572                let short = system.describe_short();
573                assert_eq!(short.parse(), Ok(system));
574            }
575        }
576    }
577}
578
579#[test]
580fn test_viewcoordinates_roundtrip() {
581    use arrow2::array::Array;
582    use arrow2_convert::{deserialize::TryIntoCollection, serialize::TryIntoArrow};
583
584    let views_in = vec![
585        "RUB".parse::<ViewCoordinates>().unwrap(),
586        "LFD".parse::<ViewCoordinates>().unwrap(),
587    ];
588    let array: Box<dyn Array> = views_in.try_into_arrow().unwrap();
589    let views_out: Vec<ViewCoordinates> = TryIntoCollection::try_into_collection(array).unwrap();
590    assert_eq!(views_in, views_out);
591}