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