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