Skip to main content

bloq_utils/
dir.rs

1use glam::{IVec3, Vec3};
2use strum::{Display, EnumIter, EnumString, IntoEnumIterator};
3use thiserror::Error;
4
5/// Error returned when parsing a string or [`IVec3`] into a [`Direction`].
6#[derive(Debug, Clone, PartialEq, Eq, Error)]
7pub enum DirectionParseError {
8    /// The input string is not a recognized direction like `+X`, `-Z`, etc.
9    #[error("invalid direction string: {0}")]
10    InvalidString(String),
11    /// The [`IVec3`] is not a unit axis vector.
12    #[error("invalid IVec3 for Direction: {0:?}")]
13    InvalidVec(IVec3),
14}
15
16/// One of the six signed axis directions in 3D.
17///
18/// Parses from and displays as `+X`, `-X`, `+Y`, `-Y`, `+Z`, `-Z`.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Display, Default, EnumIter, EnumString)]
20#[strum(
21    parse_err_ty = DirectionParseError,
22    parse_err_fn = direction_parse_error
23)]
24pub enum Direction {
25    /// Positive X direction.
26    #[default]
27    #[strum(serialize = "+X")]
28    XPLUS,
29    /// Negative X direction.
30    #[strum(serialize = "-X")]
31    XMINUS,
32    /// Positive Y direction.
33    #[strum(serialize = "+Y")]
34    YPLUS,
35    /// Negative Y direction.
36    #[strum(serialize = "-Y")]
37    YMINUS,
38    /// Positive Z direction.
39    #[strum(serialize = "+Z")]
40    ZPLUS,
41    /// Negative Z direction.
42    #[strum(serialize = "-Z")]
43    ZMINUS,
44}
45
46impl Direction {
47    /// Iterates over all six directions.
48    pub fn iter() -> impl Iterator<Item = Self> {
49        <Self as IntoEnumIterator>::iter()
50    }
51
52    /// Returns a stable `0..6` index for this direction.
53    pub const fn index(self) -> usize {
54        match self {
55            Direction::XPLUS => 0,
56            Direction::XMINUS => 1,
57            Direction::YPLUS => 2,
58            Direction::YMINUS => 3,
59            Direction::ZPLUS => 4,
60            Direction::ZMINUS => 5,
61        }
62    }
63
64    /// Returns the unit axis vector pointing along this direction.
65    pub const fn to_ivec3(self) -> IVec3 {
66        match self {
67            Direction::XPLUS => IVec3::new(1, 0, 0),
68            Direction::XMINUS => IVec3::new(-1, 0, 0),
69            Direction::YPLUS => IVec3::new(0, 1, 0),
70            Direction::YMINUS => IVec3::new(0, -1, 0),
71            Direction::ZPLUS => IVec3::new(0, 0, 1),
72            Direction::ZMINUS => IVec3::new(0, 0, -1),
73        }
74    }
75
76    /// Returns the unit axis vector as a floating-point [`Vec3`].
77    pub fn to_vec3(self) -> Vec3 {
78        self.to_ivec3().as_vec3()
79    }
80
81    /// Returns `true` for the `X`/`Y` (spatial) directions.
82    pub const fn is_spatial(self) -> bool {
83        matches!(
84            self,
85            Direction::XPLUS | Direction::XMINUS | Direction::YPLUS | Direction::YMINUS
86        )
87    }
88
89    /// Returns the opposite direction along the same axis.
90    pub const fn negate(self) -> Self {
91        match self {
92            Direction::XPLUS => Direction::XMINUS,
93            Direction::XMINUS => Direction::XPLUS,
94            Direction::YPLUS => Direction::YMINUS,
95            Direction::YMINUS => Direction::YPLUS,
96            Direction::ZPLUS => Direction::ZMINUS,
97            Direction::ZMINUS => Direction::ZPLUS,
98        }
99    }
100
101    /// Drops the sign, returning the unsigned axis [`UDirection`].
102    pub const fn as_udirection(self) -> UDirection {
103        match self {
104            Direction::XPLUS | Direction::XMINUS => UDirection::X,
105            Direction::YPLUS | Direction::YMINUS => UDirection::Y,
106            Direction::ZPLUS | Direction::ZMINUS => UDirection::Z,
107        }
108    }
109}
110
111impl TryFrom<IVec3> for Direction {
112    type Error = DirectionParseError;
113
114    fn try_from(value: IVec3) -> Result<Self, Self::Error> {
115        for dir in Direction::iter() {
116            if value == dir.to_ivec3() {
117                return Ok(dir);
118            }
119        }
120        Err(DirectionParseError::InvalidVec(value))
121    }
122}
123
124fn direction_parse_error(value: &str) -> DirectionParseError {
125    DirectionParseError::InvalidString(value.to_string())
126}
127
128/// An unsigned axis (`X`, `Y`, or `Z`): a [`Direction`] with its sign dropped.
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Display, Default, EnumIter, EnumString)]
130pub enum UDirection {
131    /// X axis.
132    #[default]
133    X,
134    /// Y axis.
135    Y,
136    /// Z axis.
137    Z,
138}
139
140impl UDirection {
141    /// Iterates over the three unsigned axes.
142    pub fn iter() -> impl Iterator<Item = Self> {
143        <Self as IntoEnumIterator>::iter()
144    }
145
146    /// Returns the positive unit axis vector for this axis.
147    pub const fn to_ivec3(self) -> IVec3 {
148        match self {
149            UDirection::X => IVec3::new(1, 0, 0),
150            UDirection::Y => IVec3::new(0, 1, 0),
151            UDirection::Z => IVec3::new(0, 0, 1),
152        }
153    }
154
155    /// Returns the positive unit axis vector as a floating-point [`Vec3`].
156    pub fn to_vec3(self) -> Vec3 {
157        self.to_ivec3().as_vec3()
158    }
159
160    /// Returns `true` for the `X`/`Y` (spatial) axes.
161    pub const fn is_spatial(self) -> bool {
162        matches!(self, UDirection::X | UDirection::Y)
163    }
164
165    /// Returns a stable `0..3` index for this axis.
166    pub const fn index(self) -> usize {
167        match self {
168            UDirection::X => 0,
169            UDirection::Y => 1,
170            UDirection::Z => 2,
171        }
172    }
173}