Skip to main content

projective_grid/hex/
direction.rs

1//! Hex grid directions and neighbor metadata for pointy-top axial coordinates.
2
3use serde::{Deserialize, Serialize};
4
5/// Direction along a 6-connected hexagonal grid (pointy-top, axial coordinates).
6///
7/// Axial coordinate convention: `q` increases eastward, `r` increases south-eastward.
8#[non_exhaustive]
9#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub enum HexDirection {
11    East,
12    West,
13    NorthEast,
14    SouthWest,
15    NorthWest,
16    SouthEast,
17}
18
19impl HexDirection {
20    /// All six directions in clockwise order starting from East.
21    pub const ALL: [HexDirection; 6] = [
22        Self::East,
23        Self::SouthEast,
24        Self::SouthWest,
25        Self::West,
26        Self::NorthWest,
27        Self::NorthEast,
28    ];
29
30    /// The opposite direction.
31    pub fn opposite(self) -> Self {
32        match self {
33            Self::East => Self::West,
34            Self::West => Self::East,
35            Self::NorthEast => Self::SouthWest,
36            Self::SouthWest => Self::NorthEast,
37            Self::NorthWest => Self::SouthEast,
38            Self::SouthEast => Self::NorthWest,
39        }
40    }
41
42    /// Axial coordinate delta `(dq, dr)` for this direction.
43    ///
44    /// Convention (pointy-top): `q` increases eastward, `r` increases south-eastward.
45    pub fn delta(self) -> (i32, i32) {
46        match self {
47            Self::East => (1, 0),
48            Self::West => (-1, 0),
49            Self::NorthEast => (1, -1),
50            Self::SouthWest => (-1, 1),
51            Self::NorthWest => (0, -1),
52            Self::SouthEast => (0, 1),
53        }
54    }
55
56    /// Rotate 60 degrees clockwise.
57    pub fn rotate_cw_60(self) -> Self {
58        match self {
59            Self::East => Self::SouthEast,
60            Self::SouthEast => Self::SouthWest,
61            Self::SouthWest => Self::West,
62            Self::West => Self::NorthWest,
63            Self::NorthWest => Self::NorthEast,
64            Self::NorthEast => Self::East,
65        }
66    }
67
68    /// Axis index grouping opposite pairs: 0 = E/W, 1 = NE/SW, 2 = NW/SE.
69    pub fn axis_index(self) -> usize {
70        match self {
71            Self::East | Self::West => 0,
72            Self::NorthEast | Self::SouthWest => 1,
73            Self::NorthWest | Self::SouthEast => 2,
74        }
75    }
76
77    /// Slot index for `select_hex_neighbors` (one unique slot per direction).
78    pub(crate) fn slot_index(self) -> usize {
79        match self {
80            Self::East => 0,
81            Self::West => 1,
82            Self::NorthEast => 2,
83            Self::SouthWest => 3,
84            Self::NorthWest => 4,
85            Self::SouthEast => 5,
86        }
87    }
88}
89
90/// A validated hex grid neighbor with direction, distance, and quality score.
91#[derive(Debug)]
92pub struct HexNodeNeighbor<F: crate::Float = f32> {
93    /// Direction from the source node to this neighbor.
94    pub direction: HexDirection,
95    /// Index of the neighbor in the original point array.
96    pub index: usize,
97    /// Euclidean distance in pixels.
98    pub distance: F,
99    /// Quality score (lower is better).
100    pub score: F,
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn opposite_round_trips() {
109        for &d in &HexDirection::ALL {
110            assert_eq!(d.opposite().opposite(), d);
111        }
112    }
113
114    #[test]
115    fn opposite_deltas_sum_to_zero() {
116        for &d in &HexDirection::ALL {
117            let (dq1, dr1) = d.delta();
118            let (dq2, dr2) = d.opposite().delta();
119            assert_eq!(dq1 + dq2, 0);
120            assert_eq!(dr1 + dr2, 0);
121        }
122    }
123
124    #[test]
125    fn rotate_cw_60_cycles_all_six() {
126        let mut d = HexDirection::East;
127        let mut visited = Vec::new();
128        for _ in 0..6 {
129            visited.push(d);
130            d = d.rotate_cw_60();
131        }
132        assert_eq!(d, HexDirection::East);
133        // All 6 are distinct
134        for i in 0..6 {
135            for j in (i + 1)..6 {
136                assert_ne!(visited[i], visited[j]);
137            }
138        }
139    }
140
141    #[test]
142    fn axis_index_groups_opposites() {
143        for &d in &HexDirection::ALL {
144            assert_eq!(d.axis_index(), d.opposite().axis_index());
145        }
146        // Three distinct axes
147        let axes: std::collections::HashSet<usize> =
148            HexDirection::ALL.iter().map(|d| d.axis_index()).collect();
149        assert_eq!(axes.len(), 3);
150    }
151
152    #[test]
153    fn slot_indices_are_unique() {
154        let slots: Vec<usize> = HexDirection::ALL.iter().map(|d| d.slot_index()).collect();
155        let set: std::collections::HashSet<usize> = slots.iter().copied().collect();
156        assert_eq!(set.len(), 6);
157    }
158}