1use crate::direction::NeighborDirection;
11use crate::float_helpers::{lit, rem_euclid, to_degrees};
12use crate::graph::{NeighborCandidate, NeighborValidator};
13use crate::hex::direction::HexDirection;
14use crate::hex::graph::HexNeighborValidator;
15use crate::Float;
16use nalgebra::Vector2;
17
18fn angle_diff_abs<F: Float>(a: F, b: F) -> F {
24 let two_pi: F = lit::<F>(2.0) * F::pi();
25 let mut diff = rem_euclid(b - a, two_pi);
26 if diff >= F::pi() {
27 diff -= two_pi;
28 }
29 diff.abs()
30}
31
32fn is_orthogonal<F: Float>(a: F, b: F, tolerance: F) -> bool {
34 let diff = angle_diff_abs(a, b);
35 (F::frac_pi_2() - diff).abs() <= tolerance.abs()
36}
37
38fn axis_vec_diff<F: Float>(axis_angle: F, vec_angle: F) -> F {
41 let two_pi: F = lit::<F>(2.0) * F::pi();
42 let mut diff = rem_euclid(vec_angle - axis_angle, two_pi);
43 if diff >= F::pi() {
44 diff -= two_pi;
45 }
46 let diff_abs = diff.abs();
47 diff_abs.min(F::pi() - diff_abs)
48}
49
50fn direction_quadrant<F: Float>(offset: &Vector2<F>) -> NeighborDirection {
52 if offset.x.abs() > offset.y.abs() {
53 if offset.x >= F::zero() {
54 NeighborDirection::Right
55 } else {
56 NeighborDirection::Left
57 }
58 } else if offset.y >= F::zero() {
59 NeighborDirection::Down
60 } else {
61 NeighborDirection::Up
62 }
63}
64
65fn direction_sextant<F: Float>(offset: &Vector2<F>) -> HexDirection {
67 let deg = to_degrees(offset.y.atan2(offset.x));
68 if deg >= lit(-30.0) && deg < lit(30.0) {
69 HexDirection::East
70 } else if deg >= lit(30.0) && deg < lit(90.0) {
71 HexDirection::SouthEast
72 } else if deg >= lit(90.0) && deg < lit(150.0) {
73 HexDirection::SouthWest
74 } else if deg < lit(-150.0) || deg >= lit(150.0) {
75 HexDirection::West
76 } else if deg >= lit(-150.0) && deg < lit(-90.0) {
77 HexDirection::NorthWest
78 } else {
79 HexDirection::NorthEast
80 }
81}
82
83pub struct XJunctionValidator<F: Float = f32> {
122 pub min_spacing: F,
124 pub max_spacing: F,
126 pub tolerance_rad: F,
128}
129
130impl<F: Float> NeighborValidator<F> for XJunctionValidator<F> {
131 type PointData = F;
132
133 fn validate(
134 &self,
135 _source_index: usize,
136 source_data: &F,
137 candidate: &NeighborCandidate<F>,
138 candidate_data: &F,
139 ) -> Option<(NeighborDirection, F)> {
140 if !is_orthogonal(*source_data, *candidate_data, self.tolerance_rad) {
142 return None;
143 }
144
145 if candidate.distance < self.min_spacing || candidate.distance > self.max_spacing {
147 return None;
148 }
149
150 let edge_angle = candidate.offset.y.atan2(candidate.offset.x);
152 let expected = F::frac_pi_4();
153
154 let score_src = (axis_vec_diff(*source_data, edge_angle) - expected).abs();
155 let score_cand = (axis_vec_diff(*candidate_data, edge_angle) - expected).abs();
156
157 if score_src > self.tolerance_rad || score_cand > self.tolerance_rad {
158 return None;
159 }
160
161 let direction = direction_quadrant(&candidate.offset);
163
164 let score_ortho = (F::frac_pi_2() - angle_diff_abs(*source_data, *candidate_data)).abs();
166 let score = score_src + score_cand + score_ortho;
167
168 Some((direction, score))
169 }
170}
171
172pub struct SpatialSquareValidator<F: Float = f32> {
185 pub min_spacing: F,
187 pub max_spacing: F,
189}
190
191impl<F: Float> NeighborValidator<F> for SpatialSquareValidator<F> {
192 type PointData = ();
193
194 fn validate(
195 &self,
196 _source_index: usize,
197 _source_data: &(),
198 candidate: &NeighborCandidate<F>,
199 _candidate_data: &(),
200 ) -> Option<(NeighborDirection, F)> {
201 if candidate.distance < self.min_spacing || candidate.distance > self.max_spacing {
202 return None;
203 }
204
205 let direction = direction_quadrant(&candidate.offset);
206 Some((direction, candidate.distance))
207 }
208}
209
210pub struct SpatialHexValidator<F: Float = f32> {
224 pub min_spacing: F,
226 pub max_spacing: F,
228}
229
230impl<F: Float> HexNeighborValidator<F> for SpatialHexValidator<F> {
231 type PointData = ();
232
233 fn validate(
234 &self,
235 _source_index: usize,
236 _source_data: &(),
237 candidate: &NeighborCandidate<F>,
238 _candidate_data: &(),
239 ) -> Option<(HexDirection, F)> {
240 if candidate.distance < self.min_spacing || candidate.distance > self.max_spacing {
241 return None;
242 }
243
244 let direction = direction_sextant(&candidate.offset);
245 Some((direction, candidate.distance))
246 }
247}
248
249#[cfg(test)]
254mod tests {
255 use super::*;
256 use crate::graph::GridGraphParams;
257 use crate::hex::graph::HexGridGraph;
258 use crate::hex::traverse::{hex_assign_grid_coordinates, hex_connected_components};
259 use crate::traverse::{assign_grid_coordinates, connected_components};
260 use crate::{GridGraph, NodeNeighbor};
261 use nalgebra::Point2;
262 use std::collections::HashMap;
263 use std::f32::consts::FRAC_PI_4;
264
265 fn neighbor_map(neighbors: &[NodeNeighbor]) -> HashMap<NeighborDirection, &NodeNeighbor> {
266 neighbors.iter().map(|n| (n.direction, n)).collect()
267 }
268
269 fn make_chess_grid(rows: usize, cols: usize, spacing: f32) -> (Vec<Point2<f32>>, Vec<f32>) {
274 let mut positions = Vec::new();
275 let mut orientations = Vec::new();
276 for j in 0..rows {
277 for i in 0..cols {
278 positions.push(Point2::new(i as f32 * spacing, j as f32 * spacing));
279 orientations.push(if (i + j) % 2 == 0 {
280 FRAC_PI_4
281 } else {
282 3.0 * FRAC_PI_4
283 });
284 }
285 }
286 (positions, orientations)
287 }
288
289 #[test]
290 fn xjunction_regular_grid_center_has_four_neighbors() {
291 let spacing = 10.0;
292 let (positions, orientations) = make_chess_grid(3, 3, spacing);
293
294 let validator = XJunctionValidator {
295 min_spacing: 5.0,
296 max_spacing: 15.0,
297 tolerance_rad: 15.0f32.to_radians(),
298 };
299 let graph = GridGraph::build(
300 &positions,
301 &orientations,
302 &validator,
303 &GridGraphParams::default(),
304 );
305
306 let idx = |i: usize, j: usize| j * 3 + i;
307 let center = neighbor_map(&graph.neighbors[idx(1, 1)]);
308 assert_eq!(4, center.len());
309 assert_eq!(idx(0, 1), center[&NeighborDirection::Left].index);
310 assert_eq!(idx(2, 1), center[&NeighborDirection::Right].index);
311 assert_eq!(idx(1, 0), center[&NeighborDirection::Up].index);
312 assert_eq!(idx(1, 2), center[&NeighborDirection::Down].index);
313 }
314
315 #[test]
316 fn xjunction_rejects_parallel_orientations() {
317 let spacing = 10.0;
318 let positions = vec![Point2::new(0.0, 0.0), Point2::new(spacing, 0.0)];
319 let orientations = vec![FRAC_PI_4, FRAC_PI_4];
321
322 let validator = XJunctionValidator {
323 min_spacing: 5.0,
324 max_spacing: 15.0,
325 tolerance_rad: 15.0f32.to_radians(),
326 };
327 let graph = GridGraph::build(
328 &positions,
329 &orientations,
330 &validator,
331 &GridGraphParams {
332 k_neighbors: 2,
333 ..Default::default()
334 },
335 );
336
337 assert!(graph.neighbors[0].is_empty());
338 assert!(graph.neighbors[1].is_empty());
339 }
340
341 #[test]
342 fn xjunction_rejects_out_of_range_distance() {
343 let spacing = 30.0;
344 let positions = vec![Point2::new(0.0, 0.0), Point2::new(spacing, 0.0)];
345 let orientations = vec![FRAC_PI_4, 3.0 * FRAC_PI_4];
346
347 let validator = XJunctionValidator {
348 min_spacing: 5.0,
349 max_spacing: 15.0, tolerance_rad: 15.0f32.to_radians(),
351 };
352 let graph = GridGraph::build(
353 &positions,
354 &orientations,
355 &validator,
356 &GridGraphParams::default(),
357 );
358
359 assert!(graph.neighbors[0].is_empty());
360 assert!(graph.neighbors[1].is_empty());
361 }
362
363 #[test]
364 fn xjunction_rotated_grid_forms_single_component() {
365 let spacing = 20.0;
366 let angle = 40.0f32.to_radians();
367 let ax = Vector2::new(angle.cos(), angle.sin());
368 let ay = Vector2::new(-angle.sin(), angle.cos());
369 let cols = 4usize;
370 let rows = 4usize;
371
372 let diag0 = angle + FRAC_PI_4;
373 let diag1 = angle + 3.0 * FRAC_PI_4;
374
375 let mut positions = Vec::new();
376 let mut orientations = Vec::new();
377 for j in 0..rows {
378 for i in 0..cols {
379 let pos = ax * (i as f32 * spacing) + ay * (j as f32 * spacing);
380 positions.push(Point2::new(pos.x + 100.0, pos.y + 100.0));
381 orientations.push(if (i + j) % 2 == 0 { diag0 } else { diag1 });
382 }
383 }
384
385 let validator = XJunctionValidator {
386 min_spacing: spacing * 0.5,
387 max_spacing: spacing * 1.5,
388 tolerance_rad: 20.0f32.to_radians(),
389 };
390 let graph = GridGraph::build(
391 &positions,
392 &orientations,
393 &validator,
394 &GridGraphParams {
395 k_neighbors: 8,
396 ..Default::default()
397 },
398 );
399
400 let components = connected_components(&graph);
401 assert_eq!(1, components.len());
402 assert_eq!(cols * rows, components[0].len());
403
404 let coords = assign_grid_coordinates(&graph, &components[0]);
405 let coord_set: std::collections::HashSet<(i32, i32)> =
406 coords.iter().map(|&(_, g)| (g.i, g.j)).collect();
407 assert_eq!(cols * rows, coord_set.len());
408 }
409
410 #[test]
411 fn xjunction_direction_symmetry() {
412 let spacing = 20.0;
413 let (positions, orientations) = make_chess_grid(3, 3, spacing);
414
415 let validator = XJunctionValidator {
416 min_spacing: spacing * 0.5,
417 max_spacing: spacing * 1.5,
418 tolerance_rad: 15.0f32.to_radians(),
419 };
420 let graph = GridGraph::build(
421 &positions,
422 &orientations,
423 &validator,
424 &GridGraphParams::default(),
425 );
426
427 for (a, neighbors) in graph.neighbors.iter().enumerate() {
428 for n in neighbors {
429 let b = n.index;
430 let back = graph.neighbors[b].iter().find(|nn| nn.index == a);
431 assert!(
432 back.is_some(),
433 "Edge {a}->{b} exists but reverse {b}->{a} does not"
434 );
435 assert_eq!(n.direction.opposite(), back.unwrap().direction,);
436 }
437 }
438 }
439
440 #[test]
445 fn spatial_square_regular_grid_center_has_four() {
446 let spacing = 10.0;
447 let mut positions = Vec::new();
448 for j in 0..3 {
449 for i in 0..3 {
450 positions.push(Point2::new(i as f32 * spacing, j as f32 * spacing));
451 }
452 }
453 let data = vec![(); positions.len()];
454
455 let validator = SpatialSquareValidator {
456 min_spacing: 5.0,
457 max_spacing: 15.0,
458 };
459 let graph = GridGraph::build(&positions, &data, &validator, &GridGraphParams::default());
460
461 let idx = |i: usize, j: usize| j * 3 + i;
462 let center = neighbor_map(&graph.neighbors[idx(1, 1)]);
463 assert_eq!(4, center.len());
464 assert_eq!(idx(0, 1), center[&NeighborDirection::Left].index);
465 assert_eq!(idx(2, 1), center[&NeighborDirection::Right].index);
466 assert_eq!(idx(1, 0), center[&NeighborDirection::Up].index);
467 assert_eq!(idx(1, 2), center[&NeighborDirection::Down].index);
468 }
469
470 #[test]
471 fn spatial_square_rejects_out_of_range() {
472 let positions = vec![
473 Point2::new(0.0f32, 0.0),
474 Point2::new(3.0, 0.0), Point2::new(50.0, 0.0), ];
477 let data = vec![(); 3];
478
479 let validator = SpatialSquareValidator {
480 min_spacing: 5.0,
481 max_spacing: 15.0,
482 };
483 let graph = GridGraph::build(&positions, &data, &validator, &GridGraphParams::default());
484
485 assert!(graph.neighbors[0].is_empty());
486 }
487
488 #[test]
489 fn spatial_square_score_prefers_closest() {
490 let positions = vec![
491 Point2::new(0.0f32, 0.0),
492 Point2::new(8.0, 0.0), Point2::new(12.0, 0.0), ];
495 let data = vec![(); 3];
496
497 let validator = SpatialSquareValidator {
498 min_spacing: 5.0,
499 max_spacing: 15.0,
500 };
501 let graph = GridGraph::build(&positions, &data, &validator, &GridGraphParams::default());
502
503 let right = graph.neighbors[0]
504 .iter()
505 .find(|n| n.direction == NeighborDirection::Right)
506 .unwrap();
507 assert_eq!(1, right.index); }
509
510 #[test]
511 fn spatial_square_diagonal_grid_works() {
512 let spacing = 10.0;
514 let angle = 45.0f32.to_radians();
515 let ax = Vector2::new(angle.cos(), angle.sin());
516 let ay = Vector2::new(-angle.sin(), angle.cos());
517
518 let mut positions = Vec::new();
519 for j in 0..3 {
520 for i in 0..3 {
521 let pos = ax * (i as f32 * spacing) + ay * (j as f32 * spacing);
522 positions.push(Point2::new(pos.x + 50.0, pos.y + 50.0));
523 }
524 }
525 let data = vec![(); positions.len()];
526
527 let validator = SpatialSquareValidator {
528 min_spacing: spacing * 0.5,
529 max_spacing: spacing * 1.5,
530 };
531 let graph = GridGraph::build(&positions, &data, &validator, &GridGraphParams::default());
532
533 let components = connected_components(&graph);
534 assert_eq!(1, components.len());
535 assert_eq!(9, components[0].len());
536 }
537
538 fn hex_lattice(radius: i32, spacing: f32) -> Vec<Point2<f32>> {
543 let sqrt3 = 3.0f32.sqrt();
544 let mut points = Vec::new();
545 for q in -radius..=radius {
546 for r in -radius..=radius {
547 if (q + r).abs() > radius {
548 continue;
549 }
550 let x = spacing * (q as f32 + r as f32 * 0.5);
551 let y = spacing * (r as f32 * sqrt3 / 2.0);
552 points.push(Point2::new(x, y));
553 }
554 }
555 points
556 }
557
558 #[test]
559 fn spatial_hex_center_has_six_neighbors() {
560 let spacing = 50.0;
561 let points = hex_lattice(2, spacing);
562 let data = vec![(); points.len()];
563
564 let validator = SpatialHexValidator {
565 min_spacing: spacing * 0.5,
566 max_spacing: spacing * 1.5,
567 };
568 let graph = HexGridGraph::build(
569 &points,
570 &data,
571 &validator,
572 &GridGraphParams {
573 k_neighbors: 12,
574 ..Default::default()
575 },
576 );
577
578 let center = points
579 .iter()
580 .position(|p| p.x.abs() < 0.01 && p.y.abs() < 0.01)
581 .unwrap();
582 assert_eq!(6, graph.neighbors[center].len());
583 }
584
585 #[test]
586 fn spatial_hex_edge_nodes_have_three() {
587 let spacing = 50.0;
588 let points = hex_lattice(1, spacing);
589 let data = vec![(); points.len()];
590
591 let validator = SpatialHexValidator {
592 min_spacing: spacing * 0.5,
593 max_spacing: spacing * 1.5,
594 };
595 let graph = HexGridGraph::build(
596 &points,
597 &data,
598 &validator,
599 &GridGraphParams {
600 k_neighbors: 12,
601 ..Default::default()
602 },
603 );
604
605 for (i, p) in points.iter().enumerate() {
606 if p.x.abs() < 0.01 && p.y.abs() < 0.01 {
607 assert_eq!(6, graph.neighbors[i].len());
608 } else {
609 assert_eq!(3, graph.neighbors[i].len());
610 }
611 }
612 }
613
614 #[test]
615 fn spatial_hex_rejects_out_of_range() {
616 let points = vec![
617 Point2::new(0.0f32, 0.0),
618 Point2::new(3.0, 0.0), ];
620 let data = vec![(); 2];
621
622 let validator = SpatialHexValidator {
623 min_spacing: 10.0,
624 max_spacing: 50.0,
625 };
626 let graph = HexGridGraph::build(&points, &data, &validator, &GridGraphParams::default());
627
628 assert!(graph.neighbors[0].is_empty());
629 }
630
631 #[test]
632 fn spatial_hex_single_component_and_correct_coordinates() {
633 let spacing = 50.0;
634 let points = hex_lattice(2, spacing);
635 let data = vec![(); points.len()];
636
637 let validator = SpatialHexValidator {
638 min_spacing: spacing * 0.5,
639 max_spacing: spacing * 1.5,
640 };
641 let graph = HexGridGraph::build(
642 &points,
643 &data,
644 &validator,
645 &GridGraphParams {
646 k_neighbors: 12,
647 ..Default::default()
648 },
649 );
650
651 let components = hex_connected_components(&graph);
652 assert_eq!(1, components.len());
653 assert_eq!(points.len(), components[0].len());
654
655 let coords = hex_assign_grid_coordinates(&graph, &components[0]);
656 assert_eq!(points.len(), coords.len());
657
658 let coord_set: std::collections::HashSet<(i32, i32)> =
660 coords.iter().map(|&(_, g)| (g.i, g.j)).collect();
661 assert_eq!(points.len(), coord_set.len());
662 }
663}