Skip to main content

projective_grid/hex/
validators.rs

1//! Ready-to-use [`HexNeighborValidator`] implementations for hex grids.
2//!
3//! | Validator | PointData | Use case |
4//! |-----------|-----------|----------|
5//! | [`SpatialHexValidator`] | `()` | Unoriented features on a hex lattice (ringgrid, etc.) |
6
7use crate::float_helpers::{lit, to_degrees};
8use crate::graph::NeighborCandidate;
9use crate::hex::direction::HexDirection;
10use crate::hex::graph::HexNeighborValidator;
11use crate::Float;
12use nalgebra::Vector2;
13
14// ---------------------------------------------------------------------------
15// Geometry helpers (private to the hex validators)
16// ---------------------------------------------------------------------------
17
18/// Classify an offset vector into one of 6 hex directions by sextant (60° sectors).
19fn direction_sextant<F: Float>(offset: &Vector2<F>) -> HexDirection {
20    let deg = to_degrees(offset.y.atan2(offset.x));
21    if deg >= lit(-30.0) && deg < lit(30.0) {
22        HexDirection::East
23    } else if deg >= lit(30.0) && deg < lit(90.0) {
24        HexDirection::SouthEast
25    } else if deg >= lit(90.0) && deg < lit(150.0) {
26        HexDirection::SouthWest
27    } else if deg < lit(-150.0) || deg >= lit(150.0) {
28        HexDirection::West
29    } else if deg >= lit(-150.0) && deg < lit(-90.0) {
30        HexDirection::NorthWest
31    } else {
32        HexDirection::NorthEast
33    }
34}
35
36// ---------------------------------------------------------------------------
37// SpatialHexValidator
38// ---------------------------------------------------------------------------
39
40/// Validator for **hex grids** with no orientation information.
41///
42/// Classifies neighbors into 6 sextant directions and filters by distance.
43/// Suitable for hex lattices of circular markers (ringgrid), blob detections,
44/// or any approximately regular hex arrangement.
45///
46/// # PointData
47///
48/// `()` — no per-point data needed.
49pub struct SpatialHexValidator<F: Float = f32> {
50    /// Minimum acceptable neighbor distance (pixels).
51    pub min_spacing: F,
52    /// Maximum acceptable neighbor distance (pixels).
53    pub max_spacing: F,
54}
55
56impl<F: Float> HexNeighborValidator<F> for SpatialHexValidator<F> {
57    type PointData = ();
58
59    fn validate(
60        &self,
61        _source_index: usize,
62        _source_data: &(),
63        candidate: &NeighborCandidate<F>,
64        _candidate_data: &(),
65    ) -> Option<(HexDirection, F)> {
66        if candidate.distance < self.min_spacing || candidate.distance > self.max_spacing {
67            return None;
68        }
69
70        let direction = direction_sextant(&candidate.offset);
71        Some((direction, candidate.distance))
72    }
73}
74
75// ===========================================================================
76// Tests
77// ===========================================================================
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use crate::graph::GridGraphParams;
83    use crate::hex::graph::HexGridGraph;
84    use crate::hex::traverse::{hex_assign_grid_coordinates, hex_connected_components};
85    use nalgebra::Point2;
86
87    fn hex_lattice(radius: i32, spacing: f32) -> Vec<Point2<f32>> {
88        let sqrt3 = 3.0f32.sqrt();
89        let mut points = Vec::new();
90        for q in -radius..=radius {
91            for r in -radius..=radius {
92                if (q + r).abs() > radius {
93                    continue;
94                }
95                let x = spacing * (q as f32 + r as f32 * 0.5);
96                let y = spacing * (r as f32 * sqrt3 / 2.0);
97                points.push(Point2::new(x, y));
98            }
99        }
100        points
101    }
102
103    #[test]
104    fn spatial_hex_center_has_six_neighbors() {
105        let spacing = 50.0;
106        let points = hex_lattice(2, spacing);
107        let data = vec![(); points.len()];
108
109        let validator = SpatialHexValidator {
110            min_spacing: spacing * 0.5,
111            max_spacing: spacing * 1.5,
112        };
113        let graph = HexGridGraph::build(
114            &points,
115            &data,
116            &validator,
117            &GridGraphParams {
118                k_neighbors: 12,
119                ..Default::default()
120            },
121        );
122
123        let center = points
124            .iter()
125            .position(|p| p.x.abs() < 0.01 && p.y.abs() < 0.01)
126            .unwrap();
127        assert_eq!(6, graph.neighbors[center].len());
128    }
129
130    #[test]
131    fn spatial_hex_edge_nodes_have_three() {
132        let spacing = 50.0;
133        let points = hex_lattice(1, spacing);
134        let data = vec![(); points.len()];
135
136        let validator = SpatialHexValidator {
137            min_spacing: spacing * 0.5,
138            max_spacing: spacing * 1.5,
139        };
140        let graph = HexGridGraph::build(
141            &points,
142            &data,
143            &validator,
144            &GridGraphParams {
145                k_neighbors: 12,
146                ..Default::default()
147            },
148        );
149
150        for (i, p) in points.iter().enumerate() {
151            if p.x.abs() < 0.01 && p.y.abs() < 0.01 {
152                assert_eq!(6, graph.neighbors[i].len());
153            } else {
154                assert_eq!(3, graph.neighbors[i].len());
155            }
156        }
157    }
158
159    #[test]
160    fn spatial_hex_rejects_out_of_range() {
161        let points = vec![
162            Point2::new(0.0f32, 0.0),
163            Point2::new(3.0, 0.0), // too close
164        ];
165        let data = vec![(); 2];
166
167        let validator = SpatialHexValidator {
168            min_spacing: 10.0,
169            max_spacing: 50.0,
170        };
171        let graph = HexGridGraph::build(&points, &data, &validator, &GridGraphParams::default());
172
173        assert!(graph.neighbors[0].is_empty());
174    }
175
176    #[test]
177    fn spatial_hex_single_component_and_correct_coordinates() {
178        let spacing = 50.0;
179        let points = hex_lattice(2, spacing);
180        let data = vec![(); points.len()];
181
182        let validator = SpatialHexValidator {
183            min_spacing: spacing * 0.5,
184            max_spacing: spacing * 1.5,
185        };
186        let graph = HexGridGraph::build(
187            &points,
188            &data,
189            &validator,
190            &GridGraphParams {
191                k_neighbors: 12,
192                ..Default::default()
193            },
194        );
195
196        let components = hex_connected_components(&graph);
197        assert_eq!(1, components.len());
198        assert_eq!(points.len(), components[0].len());
199
200        let coords = hex_assign_grid_coordinates(&graph, &components[0]);
201        assert_eq!(points.len(), coords.len());
202
203        // All coordinates should be unique
204        let coord_set: std::collections::HashSet<(i32, i32)> =
205            coords.iter().map(|&(_, g)| (g.i, g.j)).collect();
206        assert_eq!(points.len(), coord_set.len());
207    }
208}