1use crate::float_helpers::{lit, rem_euclid};
9use crate::graph::{NeighborCandidate, NeighborValidator};
10use crate::Float;
11use crate::NeighborDirection;
12use nalgebra::Vector2;
13
14fn angle_diff_abs<F: Float>(a: F, b: F) -> F {
20 let two_pi: F = lit::<F>(2.0) * F::pi();
21 let mut diff = rem_euclid(b - a, two_pi);
22 if diff >= F::pi() {
23 diff -= two_pi;
24 }
25 diff.abs()
26}
27
28fn is_orthogonal<F: Float>(a: F, b: F, tolerance: F) -> bool {
30 let diff = angle_diff_abs(a, b);
31 (F::frac_pi_2() - diff).abs() <= tolerance.abs()
32}
33
34fn axis_vec_diff<F: Float>(axis_angle: F, vec_angle: F) -> F {
37 let two_pi: F = lit::<F>(2.0) * F::pi();
38 let mut diff = rem_euclid(vec_angle - axis_angle, two_pi);
39 if diff >= F::pi() {
40 diff -= two_pi;
41 }
42 let diff_abs = diff.abs();
43 diff_abs.min(F::pi() - diff_abs)
44}
45
46fn direction_quadrant<F: Float>(offset: &Vector2<F>) -> NeighborDirection {
48 if offset.x.abs() > offset.y.abs() {
49 if offset.x >= F::zero() {
50 NeighborDirection::Right
51 } else {
52 NeighborDirection::Left
53 }
54 } else if offset.y >= F::zero() {
55 NeighborDirection::Down
56 } else {
57 NeighborDirection::Up
58 }
59}
60
61pub struct XJunctionValidator<F: Float = f32> {
100 pub min_spacing: F,
102 pub max_spacing: F,
104 pub tolerance_rad: F,
106}
107
108impl<F: Float> NeighborValidator<F> for XJunctionValidator<F> {
109 type PointData = F;
110
111 fn validate(
112 &self,
113 _source_index: usize,
114 source_data: &F,
115 candidate: &NeighborCandidate<F>,
116 candidate_data: &F,
117 ) -> Option<(NeighborDirection, F)> {
118 if !is_orthogonal(*source_data, *candidate_data, self.tolerance_rad) {
120 return None;
121 }
122
123 if candidate.distance < self.min_spacing || candidate.distance > self.max_spacing {
125 return None;
126 }
127
128 let edge_angle = candidate.offset.y.atan2(candidate.offset.x);
130 let expected = F::frac_pi_4();
131
132 let score_src = (axis_vec_diff(*source_data, edge_angle) - expected).abs();
133 let score_cand = (axis_vec_diff(*candidate_data, edge_angle) - expected).abs();
134
135 if score_src > self.tolerance_rad || score_cand > self.tolerance_rad {
136 return None;
137 }
138
139 let direction = direction_quadrant(&candidate.offset);
141
142 let score_ortho = (F::frac_pi_2() - angle_diff_abs(*source_data, *candidate_data)).abs();
144 let score = score_src + score_cand + score_ortho;
145
146 Some((direction, score))
147 }
148}
149
150pub struct SpatialSquareValidator<F: Float = f32> {
163 pub min_spacing: F,
165 pub max_spacing: F,
167}
168
169impl<F: Float> NeighborValidator<F> for SpatialSquareValidator<F> {
170 type PointData = ();
171
172 fn validate(
173 &self,
174 _source_index: usize,
175 _source_data: &(),
176 candidate: &NeighborCandidate<F>,
177 _candidate_data: &(),
178 ) -> Option<(NeighborDirection, F)> {
179 if candidate.distance < self.min_spacing || candidate.distance > self.max_spacing {
180 return None;
181 }
182
183 let direction = direction_quadrant(&candidate.offset);
184 Some((direction, candidate.distance))
185 }
186}
187
188#[cfg(test)]
193mod tests {
194 use super::*;
195 use crate::graph::GridGraphParams;
196 use crate::traverse::{assign_grid_coordinates, connected_components};
197 use crate::{GridGraph, NodeNeighbor};
198 use nalgebra::Point2;
199 use std::collections::HashMap;
200 use std::f32::consts::FRAC_PI_4;
201
202 fn neighbor_map(neighbors: &[NodeNeighbor]) -> HashMap<NeighborDirection, &NodeNeighbor> {
203 neighbors.iter().map(|n| (n.direction, n)).collect()
204 }
205
206 fn make_chess_grid(rows: usize, cols: usize, spacing: f32) -> (Vec<Point2<f32>>, Vec<f32>) {
211 let mut positions = Vec::new();
212 let mut orientations = Vec::new();
213 for j in 0..rows {
214 for i in 0..cols {
215 positions.push(Point2::new(i as f32 * spacing, j as f32 * spacing));
216 orientations.push(if (i + j) % 2 == 0 {
217 FRAC_PI_4
218 } else {
219 3.0 * FRAC_PI_4
220 });
221 }
222 }
223 (positions, orientations)
224 }
225
226 #[test]
227 fn xjunction_regular_grid_center_has_four_neighbors() {
228 let spacing = 10.0;
229 let (positions, orientations) = make_chess_grid(3, 3, spacing);
230
231 let validator = XJunctionValidator {
232 min_spacing: 5.0,
233 max_spacing: 15.0,
234 tolerance_rad: 15.0f32.to_radians(),
235 };
236 let graph = GridGraph::build(
237 &positions,
238 &orientations,
239 &validator,
240 &GridGraphParams::default(),
241 );
242
243 let idx = |i: usize, j: usize| j * 3 + i;
244 let center = neighbor_map(&graph.neighbors[idx(1, 1)]);
245 assert_eq!(4, center.len());
246 assert_eq!(idx(0, 1), center[&NeighborDirection::Left].index);
247 assert_eq!(idx(2, 1), center[&NeighborDirection::Right].index);
248 assert_eq!(idx(1, 0), center[&NeighborDirection::Up].index);
249 assert_eq!(idx(1, 2), center[&NeighborDirection::Down].index);
250 }
251
252 #[test]
253 fn xjunction_rejects_parallel_orientations() {
254 let spacing = 10.0;
255 let positions = vec![Point2::new(0.0, 0.0), Point2::new(spacing, 0.0)];
256 let orientations = vec![FRAC_PI_4, FRAC_PI_4];
258
259 let validator = XJunctionValidator {
260 min_spacing: 5.0,
261 max_spacing: 15.0,
262 tolerance_rad: 15.0f32.to_radians(),
263 };
264 let graph = GridGraph::build(
265 &positions,
266 &orientations,
267 &validator,
268 &GridGraphParams {
269 k_neighbors: 2,
270 ..Default::default()
271 },
272 );
273
274 assert!(graph.neighbors[0].is_empty());
275 assert!(graph.neighbors[1].is_empty());
276 }
277
278 #[test]
279 fn xjunction_rejects_out_of_range_distance() {
280 let spacing = 30.0;
281 let positions = vec![Point2::new(0.0, 0.0), Point2::new(spacing, 0.0)];
282 let orientations = vec![FRAC_PI_4, 3.0 * FRAC_PI_4];
283
284 let validator = XJunctionValidator {
285 min_spacing: 5.0,
286 max_spacing: 15.0, tolerance_rad: 15.0f32.to_radians(),
288 };
289 let graph = GridGraph::build(
290 &positions,
291 &orientations,
292 &validator,
293 &GridGraphParams::default(),
294 );
295
296 assert!(graph.neighbors[0].is_empty());
297 assert!(graph.neighbors[1].is_empty());
298 }
299
300 #[test]
301 fn xjunction_rotated_grid_forms_single_component() {
302 let spacing = 20.0;
303 let angle = 40.0f32.to_radians();
304 let ax = Vector2::new(angle.cos(), angle.sin());
305 let ay = Vector2::new(-angle.sin(), angle.cos());
306 let cols = 4usize;
307 let rows = 4usize;
308
309 let diag0 = angle + FRAC_PI_4;
310 let diag1 = angle + 3.0 * FRAC_PI_4;
311
312 let mut positions = Vec::new();
313 let mut orientations = Vec::new();
314 for j in 0..rows {
315 for i in 0..cols {
316 let pos = ax * (i as f32 * spacing) + ay * (j as f32 * spacing);
317 positions.push(Point2::new(pos.x + 100.0, pos.y + 100.0));
318 orientations.push(if (i + j) % 2 == 0 { diag0 } else { diag1 });
319 }
320 }
321
322 let validator = XJunctionValidator {
323 min_spacing: spacing * 0.5,
324 max_spacing: spacing * 1.5,
325 tolerance_rad: 20.0f32.to_radians(),
326 };
327 let graph = GridGraph::build(
328 &positions,
329 &orientations,
330 &validator,
331 &GridGraphParams {
332 k_neighbors: 8,
333 ..Default::default()
334 },
335 );
336
337 let components = connected_components(&graph);
338 assert_eq!(1, components.len());
339 assert_eq!(cols * rows, components[0].len());
340
341 let coords = assign_grid_coordinates(&graph, &components[0]);
342 let coord_set: std::collections::HashSet<(i32, i32)> =
343 coords.iter().map(|&(_, g)| (g.i, g.j)).collect();
344 assert_eq!(cols * rows, coord_set.len());
345 }
346
347 #[test]
348 fn xjunction_direction_symmetry() {
349 let spacing = 20.0;
350 let (positions, orientations) = make_chess_grid(3, 3, spacing);
351
352 let validator = XJunctionValidator {
353 min_spacing: spacing * 0.5,
354 max_spacing: spacing * 1.5,
355 tolerance_rad: 15.0f32.to_radians(),
356 };
357 let graph = GridGraph::build(
358 &positions,
359 &orientations,
360 &validator,
361 &GridGraphParams::default(),
362 );
363
364 for (a, neighbors) in graph.neighbors.iter().enumerate() {
365 for n in neighbors {
366 let b = n.index;
367 let back = graph.neighbors[b].iter().find(|nn| nn.index == a);
368 assert!(
369 back.is_some(),
370 "Edge {a}->{b} exists but reverse {b}->{a} does not"
371 );
372 assert_eq!(n.direction.opposite(), back.unwrap().direction,);
373 }
374 }
375 }
376
377 #[test]
382 fn spatial_square_regular_grid_center_has_four() {
383 let spacing = 10.0;
384 let mut positions = Vec::new();
385 for j in 0..3 {
386 for i in 0..3 {
387 positions.push(Point2::new(i as f32 * spacing, j as f32 * spacing));
388 }
389 }
390 let data = vec![(); positions.len()];
391
392 let validator = SpatialSquareValidator {
393 min_spacing: 5.0,
394 max_spacing: 15.0,
395 };
396 let graph = GridGraph::build(&positions, &data, &validator, &GridGraphParams::default());
397
398 let idx = |i: usize, j: usize| j * 3 + i;
399 let center = neighbor_map(&graph.neighbors[idx(1, 1)]);
400 assert_eq!(4, center.len());
401 assert_eq!(idx(0, 1), center[&NeighborDirection::Left].index);
402 assert_eq!(idx(2, 1), center[&NeighborDirection::Right].index);
403 assert_eq!(idx(1, 0), center[&NeighborDirection::Up].index);
404 assert_eq!(idx(1, 2), center[&NeighborDirection::Down].index);
405 }
406
407 #[test]
408 fn spatial_square_rejects_out_of_range() {
409 let positions = vec![
410 Point2::new(0.0f32, 0.0),
411 Point2::new(3.0, 0.0), Point2::new(50.0, 0.0), ];
414 let data = vec![(); 3];
415
416 let validator = SpatialSquareValidator {
417 min_spacing: 5.0,
418 max_spacing: 15.0,
419 };
420 let graph = GridGraph::build(&positions, &data, &validator, &GridGraphParams::default());
421
422 assert!(graph.neighbors[0].is_empty());
423 }
424
425 #[test]
426 fn spatial_square_score_prefers_closest() {
427 let positions = vec![
428 Point2::new(0.0f32, 0.0),
429 Point2::new(8.0, 0.0), Point2::new(12.0, 0.0), ];
432 let data = vec![(); 3];
433
434 let validator = SpatialSquareValidator {
435 min_spacing: 5.0,
436 max_spacing: 15.0,
437 };
438 let graph = GridGraph::build(&positions, &data, &validator, &GridGraphParams::default());
439
440 let right = graph.neighbors[0]
441 .iter()
442 .find(|n| n.direction == NeighborDirection::Right)
443 .unwrap();
444 assert_eq!(1, right.index); }
446
447 #[test]
448 fn spatial_square_diagonal_grid_works() {
449 let spacing = 10.0;
451 let angle = 45.0f32.to_radians();
452 let ax = Vector2::new(angle.cos(), angle.sin());
453 let ay = Vector2::new(-angle.sin(), angle.cos());
454
455 let mut positions = Vec::new();
456 for j in 0..3 {
457 for i in 0..3 {
458 let pos = ax * (i as f32 * spacing) + ay * (j as f32 * spacing);
459 positions.push(Point2::new(pos.x + 50.0, pos.y + 50.0));
460 }
461 }
462 let data = vec![(); positions.len()];
463
464 let validator = SpatialSquareValidator {
465 min_spacing: spacing * 0.5,
466 max_spacing: spacing * 1.5,
467 };
468 let graph = GridGraph::build(&positions, &data, &validator, &GridGraphParams::default());
469
470 let components = connected_components(&graph);
471 assert_eq!(1, components.len());
472 assert_eq!(9, components[0].len());
473 }
474}