h3o/
direction.rs

1use crate::{
2    CellIndex, Edge, NUM_HEX_VERTS, NUM_PENT_VERTS, Vertex, coord::CoordIJK,
3    error,
4};
5use core::{fmt, num::NonZeroU8};
6
7/// Maximum value for a direction.
8const MAX: u8 = 6;
9
10/// Hexagon direction to vertex relationships (same face).
11const TO_VERTEX_HEXAGON: [Vertex; NUM_HEX_VERTS as usize] = [
12    Vertex::new_unchecked(3),
13    Vertex::new_unchecked(1),
14    Vertex::new_unchecked(2),
15    Vertex::new_unchecked(5),
16    Vertex::new_unchecked(4),
17    Vertex::new_unchecked(0),
18];
19
20/// Pentagon direction to vertex relationships (same face).
21const TO_VERTEX_PENTAGON: [Vertex; NUM_PENT_VERTS as usize] = [
22    Vertex::new_unchecked(1),
23    Vertex::new_unchecked(2),
24    Vertex::new_unchecked(4),
25    Vertex::new_unchecked(3),
26    Vertex::new_unchecked(0),
27];
28
29// -----------------------------------------------------------------------------
30
31/// A direction within an hexagonal grid.
32///
33/// In H3, each hexagonal cell at level `N-1` is divided into 7 cells at the
34/// level `N`, with each sub-cell in one of the 7 possible directions (6 axes +
35/// the center).
36///
37/// ```text
38///              J axis
39///             ___
40///            /   \
41///        +--+  2  +--+
42///       / 3  \___/  6 \
43///       \    /   \    /
44///        +--+  0  +--+
45///       /    \___/    \
46///       \ 1  /   \  4 /
47///      K +--+  5  +--+ I
48///     axis   \___/    axis
49/// ```
50#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
51#[repr(u8)]
52#[expect(clippy::exhaustive_enums, reason = "not gonna change any time soon")]
53#[cfg_attr(
54    feature = "serde",
55    derive(serde_repr::Serialize_repr, serde_repr::Deserialize_repr)
56)]
57pub enum Direction {
58    /// Center.
59    Center = 0,
60    /// K axe.
61    K = 1,
62    /// J axe.
63    J = 2,
64    /// JK axe.
65    JK = 3,
66    /// I axe.
67    I = 4,
68    /// IK axe.
69    IK = 5,
70    /// IJ axe.
71    IJ = 6,
72}
73
74impl Direction {
75    /// Iterates over the valid directions.
76    ///
77    /// # Example
78    ///
79    /// ```
80    /// use h3o::Direction;
81    ///
82    /// let directions = Direction::iter().collect::<Vec<_>>();
83    /// ```
84    pub fn iter() -> impl Iterator<Item = Self> {
85        // SAFETY: values from 0 to MAX are valid directions.
86        (0..=MAX).map(Self::new_unchecked)
87    }
88
89    /// Returns the IJK coordinate of the direction.
90    pub(crate) fn coordinate(self) -> CoordIJK {
91        let value = u8::from(self);
92
93        CoordIJK::new(
94            i32::from((value >> 2) & 1),
95            i32::from((value >> 1) & 1),
96            i32::from(value & 1),
97        )
98    }
99
100    /// Returns the axe numerical value, if any.
101    pub(crate) fn axe(self) -> Option<NonZeroU8> {
102        NonZeroU8::new(self.into())
103    }
104
105    /// Initializes a new [`Direction`] using a value that may be out of range.
106    ///
107    /// # Safety
108    ///
109    /// The value must be a valid direction.
110    #[expect(unsafe_code, reason = "only used internally")]
111    pub(crate) const fn new_unchecked(value: u8) -> Self {
112        assert!(value <= MAX, "direction out of range");
113        // SAFETY: range checked above.
114        unsafe { core::mem::transmute::<u8, Self>(value) }
115    }
116
117    /// Returns a direction rotated `count` time, by 60 degrees step.
118    #[rustfmt::skip]
119    pub(crate) const fn rotate60<const CCW: bool>(self, count: usize) -> Self {
120        use Direction::{Center, I, IJ, IK, J, JK, K};
121
122        const CCW_SEQUENCE: [Direction; 6] = [K, IK, I, IJ, J, JK];
123        const CW_SEQUENCE:  [Direction; 6] = [K, JK, J, IJ, I, IK];
124
125        // Returns self if there is no rotation.
126        if count == 0 {
127            return self;
128        }
129
130        let offset = match self {
131            // The center is not affected by any rotations.
132            Center => return self,
133            K  => 0,
134            J  => if CCW { 4 } else { 2 },
135            JK => if CCW { 5 } else { 1 },
136            I  => if CCW { 2 } else { 4 },
137            IK => if CCW { 1 } else { 5 },
138            IJ => 3,
139        };
140        let index = (count + offset) % 6;
141
142        if CCW { CCW_SEQUENCE[index] } else { CW_SEQUENCE[index] }
143    }
144
145    /// Returns a direction rotated once, by 60 degrees step.
146    #[rustfmt::skip]
147    pub(crate) const fn rotate60_once<const CCW: bool>(self) -> Self {
148        use Direction::{Center, I, IJ, IK, J, JK, K};
149
150        // XXX: Lookup table approach is ~20% slower than explicit match
151        // statement.
152        match self {
153            Center => Center,
154            K  => if CCW { IK } else { JK },
155            J  => if CCW { JK } else { IJ },
156            JK => if CCW { K }  else { J },
157            I  => if CCW { IJ } else { IK },
158            IK => if CCW { I }  else { K },
159            IJ => if CCW { J }  else { I },
160        }
161    }
162
163    /// Returns the first topological vertex.
164    ///
165    /// Get the first vertex for this direction. The neighbor in this
166    /// direction is located between this vertex and the next in sequence.
167    pub(crate) fn vertex(self, origin: CellIndex) -> Vertex {
168        let is_pentagon = origin.is_pentagon();
169
170        // Check for invalid directions: center and deleted K axis (pentagon
171        // only).
172        assert!(self != Self::Center && !(is_pentagon && self == Self::K));
173
174        // Determine the vertex rotations for this cell.
175        let rotations = origin.vertex_rotations();
176
177        // Find the appropriate vertex, rotating CCW if necessary.
178        Vertex::new_unchecked(if is_pentagon {
179            // -2 because we don't use directions 0 (center) or 1 (deleted K
180            // axis).
181            let index = usize::from(self) - 2;
182            (u8::from(TO_VERTEX_PENTAGON[index]) + NUM_PENT_VERTS - rotations)
183                % NUM_PENT_VERTS
184        } else {
185            // -1 because we don't use direction 0 (center).
186            let index = usize::from(self) - 1;
187            (u8::from(TO_VERTEX_HEXAGON[index]) + NUM_HEX_VERTS - rotations)
188                % NUM_HEX_VERTS
189        })
190    }
191}
192
193impl TryFrom<u8> for Direction {
194    type Error = error::InvalidDirection;
195
196    fn try_from(value: u8) -> Result<Self, Self::Error> {
197        match value {
198            0 => Ok(Self::Center),
199            1 => Ok(Self::K),
200            2 => Ok(Self::J),
201            3 => Ok(Self::JK),
202            4 => Ok(Self::I),
203            5 => Ok(Self::IK),
204            6 => Ok(Self::IJ),
205            _ => Err(Self::Error::new(value, "out of range")),
206        }
207    }
208}
209
210impl From<Direction> for u8 {
211    fn from(value: Direction) -> Self {
212        value as Self
213    }
214}
215
216impl From<Direction> for u64 {
217    fn from(value: Direction) -> Self {
218        u8::from(value).into()
219    }
220}
221
222impl From<Direction> for usize {
223    fn from(value: Direction) -> Self {
224        u8::from(value).into()
225    }
226}
227
228impl From<Edge> for Direction {
229    fn from(value: Edge) -> Self {
230        // SAFETY: Edge are numbered from 1 to 6, according to which direction
231        // they face.
232        Self::new_unchecked(value.into())
233    }
234}
235
236impl fmt::Display for Direction {
237    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
238        write!(f, "{}", u8::from(*self))
239    }
240}