1use crate::geometry::Geometry2D;
24use crate::nfp_sliding::{compute_nfp_sliding, SlidingNfpConfig};
25use i_overlay::core::fill_rule::FillRule;
26use i_overlay::core::overlay_rule::OverlayRule;
27use i_overlay::float::single::SingleFloatOverlay;
28#[cfg(feature = "parallel")]
29use rayon::prelude::*;
30use std::collections::HashMap;
31use std::f64::consts::PI;
32use std::sync::{Arc, RwLock};
33use u_nesting_core::geom::polygon as geom_polygon;
34use u_nesting_core::geometry::Geometry2DExt;
35use u_nesting_core::robust::{orient2d_filtered, Orientation};
36use u_nesting_core::{Error, Result};
37
38pub fn rotate_nfp(nfp: &Nfp, angle: f64) -> Nfp {
43 if angle.abs() < 1e-10 {
44 return nfp.clone();
45 }
46
47 let cos_a = angle.cos();
48 let sin_a = angle.sin();
49
50 Nfp {
51 polygons: nfp
52 .polygons
53 .iter()
54 .map(|polygon| {
55 polygon
56 .iter()
57 .map(|&(x, y)| (x * cos_a - y * sin_a, x * sin_a + y * cos_a))
58 .collect()
59 })
60 .collect(),
61 }
62}
63
64pub fn translate_nfp(nfp: &Nfp, offset: (f64, f64)) -> Nfp {
66 Nfp {
67 polygons: nfp
68 .polygons
69 .iter()
70 .map(|polygon| {
71 polygon
72 .iter()
73 .map(|(x, y)| (x + offset.0, y + offset.1))
74 .collect()
75 })
76 .collect(),
77 }
78}
79
80#[derive(Debug, Clone)]
82pub struct Nfp {
83 pub polygons: Vec<Vec<(f64, f64)>>,
86}
87
88impl Nfp {
89 pub fn new() -> Self {
91 Self {
92 polygons: Vec::new(),
93 }
94 }
95
96 pub fn from_polygon(polygon: Vec<(f64, f64)>) -> Self {
98 Self {
99 polygons: vec![polygon],
100 }
101 }
102
103 pub fn from_polygons(polygons: Vec<Vec<(f64, f64)>>) -> Self {
105 Self { polygons }
106 }
107
108 pub fn is_empty(&self) -> bool {
110 self.polygons.is_empty()
111 }
112
113 pub fn vertex_count(&self) -> usize {
115 self.polygons.iter().map(|p| p.len()).sum()
116 }
117}
118
119impl Default for Nfp {
120 fn default() -> Self {
121 Self::new()
122 }
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
131pub enum NfpMethod {
132 #[default]
138 MinkowskiSum,
139
140 Sliding,
147}
148
149#[derive(Debug, Clone)]
151pub struct NfpConfig {
152 pub method: NfpMethod,
154 pub contact_tolerance: f64,
156 pub max_iterations: usize,
158}
159
160impl Default for NfpConfig {
161 fn default() -> Self {
162 Self {
163 method: NfpMethod::MinkowskiSum,
164 contact_tolerance: 1e-6,
165 max_iterations: 10000,
166 }
167 }
168}
169
170impl NfpConfig {
171 pub fn with_method(method: NfpMethod) -> Self {
173 Self {
174 method,
175 ..Default::default()
176 }
177 }
178
179 pub fn with_tolerance(mut self, tolerance: f64) -> Self {
181 self.contact_tolerance = tolerance;
182 self
183 }
184
185 pub fn with_max_iterations(mut self, max_iter: usize) -> Self {
187 self.max_iterations = max_iter;
188 self
189 }
190}
191
192pub fn compute_nfp_with_method(
203 stationary: &Geometry2D,
204 orbiting: &Geometry2D,
205 rotation: f64,
206 method: NfpMethod,
207) -> Result<Nfp> {
208 compute_nfp_with_config(
209 stationary,
210 orbiting,
211 rotation,
212 &NfpConfig::with_method(method),
213 )
214}
215
216pub fn compute_nfp_with_config(
227 stationary: &Geometry2D,
228 orbiting: &Geometry2D,
229 rotation: f64,
230 config: &NfpConfig,
231) -> Result<Nfp> {
232 let stat_exterior = stationary.exterior();
233 let orb_exterior = orbiting.exterior();
234
235 if stat_exterior.len() < 3 || orb_exterior.len() < 3 {
236 return Err(Error::invalid_geometry(
237 None,
238 "Polygons must have at least 3 vertices".into(),
239 ));
240 }
241
242 let rotated_orbiting = rotate_polygon(orb_exterior, rotation);
244
245 match config.method {
246 NfpMethod::MinkowskiSum => {
247 if stationary.is_convex()
249 && is_polygon_convex(&rotated_orbiting)
250 && stationary.holes().is_empty()
251 {
252 compute_nfp_convex(stat_exterior, &rotated_orbiting)
253 } else {
254 compute_nfp_general(stat_exterior, &rotated_orbiting)
255 }
256 }
257 NfpMethod::Sliding => {
258 let sliding_config = SlidingNfpConfig {
260 contact_tolerance: config.contact_tolerance,
261 max_iterations: config.max_iterations,
262 min_translation: config.contact_tolerance * 0.01,
263 };
264
265 let reflected: Vec<(f64, f64)> =
267 rotated_orbiting.iter().map(|&(x, y)| (-x, -y)).collect();
268
269 compute_nfp_sliding(stat_exterior, &reflected, &sliding_config)
270 }
271 }
272}
273
274pub fn compute_nfp(stationary: &Geometry2D, orbiting: &Geometry2D, rotation: f64) -> Result<Nfp> {
291 compute_nfp_mirrored(stationary, orbiting, rotation, false, false)
292}
293
294pub fn compute_nfp_mirrored(
310 stationary: &Geometry2D,
311 orbiting: &Geometry2D,
312 rotation: f64,
313 mirror_stationary: bool,
314 mirror_orbiting: bool,
315) -> Result<Nfp> {
316 let stat_exterior = stationary.exterior();
318 let orb_exterior = orbiting.exterior();
319
320 if stat_exterior.len() < 3 || orb_exterior.len() < 3 {
321 return Err(Error::invalid_geometry(
322 None,
323 "Polygons must have at least 3 vertices".into(),
324 ));
325 }
326
327 let base_stationary = if mirror_stationary {
331 crate::polygon_ops::mirror_polygon(stat_exterior)
332 } else {
333 stat_exterior.to_vec()
334 };
335 let base_orbiting = if mirror_orbiting {
336 crate::polygon_ops::mirror_polygon(orb_exterior)
337 } else {
338 orb_exterior.to_vec()
339 };
340 let rotated_orbiting = rotate_polygon(&base_orbiting, rotation);
341
342 if stationary.is_convex()
344 && is_polygon_convex(&rotated_orbiting)
345 && stationary.holes().is_empty()
346 {
347 compute_nfp_convex(&base_stationary, &rotated_orbiting)
349 } else {
350 compute_nfp_general(&base_stationary, &rotated_orbiting)
352 }
353}
354
355pub fn compute_ifp(
368 boundary_polygon: &[(f64, f64)],
369 geometry: &Geometry2D,
370 rotation: f64,
371) -> Result<Nfp> {
372 compute_ifp_with_margin(boundary_polygon, geometry, rotation, 0.0)
373}
374
375pub fn compute_ifp_with_margin(
390 boundary_polygon: &[(f64, f64)],
391 geometry: &Geometry2D,
392 rotation: f64,
393 margin: f64,
394) -> Result<Nfp> {
395 compute_ifp_with_margin_and_mirror(boundary_polygon, geometry, rotation, margin, false)
396}
397
398pub fn compute_ifp_with_margin_and_mirror(
406 boundary_polygon: &[(f64, f64)],
407 geometry: &Geometry2D,
408 rotation: f64,
409 margin: f64,
410 mirror: bool,
411) -> Result<Nfp> {
412 if boundary_polygon.len() < 3 {
413 return Err(Error::invalid_boundary(
414 None,
415 "Boundary must have at least 3 vertices".into(),
416 ));
417 }
418
419 let geom_exterior = geometry.exterior();
420 if geom_exterior.len() < 3 {
421 return Err(Error::invalid_geometry(
422 None,
423 "Geometry must have at least 3 vertices".into(),
424 ));
425 }
426
427 let base_geom = if mirror {
429 crate::polygon_ops::mirror_polygon(geom_exterior)
430 } else {
431 geom_exterior.to_vec()
432 };
433 let rotated_geom = rotate_polygon(&base_geom, rotation);
434
435 let effective_boundary = if margin > 0.0 {
437 shrink_polygon(boundary_polygon, margin)?
438 } else {
439 boundary_polygon.to_vec()
440 };
441
442 if effective_boundary.len() < 3 {
443 return Err(Error::invalid_boundary(
444 None,
445 "Boundary too small after applying margin".into(),
446 ));
447 }
448
449 compute_minkowski_erosion(&effective_boundary, &rotated_geom)
458}
459
460fn compute_minkowski_erosion(boundary: &[(f64, f64)], geometry: &[(f64, f64)]) -> Result<Nfp> {
466 if boundary.len() < 3 || geometry.len() < 3 {
467 return Err(Error::invalid_geometry(
468 None,
469 "Both boundary and geometry must have at least 3 vertices".into(),
470 ));
471 }
472
473 let (b_min_x, b_min_y, b_max_x, b_max_y) = bounding_box(boundary);
475 let is_rect = boundary.len() == 4
476 && boundary.iter().all(|&(x, y)| {
477 ((x - b_min_x).abs() < 1e-10 || (x - b_max_x).abs() < 1e-10)
478 && ((y - b_min_y).abs() < 1e-10 || (y - b_max_y).abs() < 1e-10)
479 });
480
481 let (g_min_x, g_min_y, g_max_x, g_max_y) = bounding_box(geometry);
483
484 if is_rect {
485 let ifp_min_x = b_min_x - g_min_x;
493 let ifp_max_x = b_max_x - g_max_x;
494 let ifp_min_y = b_min_y - g_min_y;
495 let ifp_max_y = b_max_y - g_max_y;
496
497 if ifp_min_x > ifp_max_x + 1e-10 || ifp_min_y > ifp_max_y + 1e-10 {
499 return Err(Error::invalid_geometry(
500 None,
501 "Geometry too large to fit in boundary".into(),
502 ));
503 }
504
505 let ifp_min_x = ifp_min_x.min(ifp_max_x);
507 let ifp_min_y = ifp_min_y.min(ifp_max_y);
508
509 return Ok(Nfp::from_polygon(vec![
510 (ifp_min_x, ifp_min_y),
511 (ifp_max_x, ifp_min_y),
512 (ifp_max_x, ifp_max_y),
513 (ifp_min_x, ifp_max_y),
514 ]));
515 }
516
517 compute_minkowski_erosion_general(boundary, geometry)
521}
522
523fn compute_minkowski_erosion_general(
525 boundary: &[(f64, f64)],
526 geometry: &[(f64, f64)],
527) -> Result<Nfp> {
528 if geometry.is_empty() {
529 return Ok(Nfp::from_polygon(boundary.to_vec()));
530 }
531
532 let first_g = geometry[0];
534 let mut result: Vec<[f64; 2]> = boundary
535 .iter()
536 .map(|&(x, y)| [x - first_g.0, y - first_g.1])
537 .collect();
538
539 for &(gx, gy) in geometry.iter().skip(1) {
541 let translated: Vec<[f64; 2]> = boundary.iter().map(|&(x, y)| [x - gx, y - gy]).collect();
542
543 let shapes = result.overlay(&[translated], OverlayRule::Intersect, FillRule::NonZero);
545
546 if shapes.is_empty() {
547 return Err(Error::invalid_geometry(
548 None,
549 "Geometry too large to fit in boundary".into(),
550 ));
551 }
552
553 result = Vec::new();
555 for shape in &shapes {
556 for contour in shape {
557 if contour.len() >= 3 {
558 result = contour.clone();
559 break;
560 }
561 }
562 if !result.is_empty() {
563 break;
564 }
565 }
566
567 if result.len() < 3 {
568 return Err(Error::invalid_geometry(
569 None,
570 "Geometry too large to fit in boundary".into(),
571 ));
572 }
573 }
574
575 let result_tuples: Vec<(f64, f64)> = result.iter().map(|&[x, y]| (x, y)).collect();
577 Ok(Nfp::from_polygon(result_tuples))
578}
579
580fn shrink_polygon(polygon: &[(f64, f64)], offset: f64) -> Result<Vec<(f64, f64)>> {
586 if polygon.len() < 3 {
587 return Err(Error::invalid_geometry(
588 None,
589 "Polygon must have at least 3 vertices".into(),
590 ));
591 }
592
593 if polygon.len() == 4 {
595 let (min_x, min_y, max_x, max_y) = bounding_box(polygon);
596
597 let is_axis_aligned = polygon.iter().all(|&(x, y)| {
599 ((x - min_x).abs() < 1e-10 || (x - max_x).abs() < 1e-10)
600 && ((y - min_y).abs() < 1e-10 || (y - max_y).abs() < 1e-10)
601 });
602
603 if is_axis_aligned {
604 let new_min_x = min_x + offset;
606 let new_min_y = min_y + offset;
607 let new_max_x = max_x - offset;
608 let new_max_y = max_y - offset;
609
610 if new_min_x >= new_max_x || new_min_y >= new_max_y {
612 return Err(Error::invalid_geometry(
613 None,
614 "Offset polygon collapsed".into(),
615 ));
616 }
617
618 return Ok(vec![
619 (new_min_x, new_min_y),
620 (new_max_x, new_min_y),
621 (new_max_x, new_max_y),
622 (new_min_x, new_max_y),
623 ]);
624 }
625 }
626
627 crate::polygon_ops::offset_polygon(polygon, -offset)
631 .into_iter()
632 .max_by(|p, q| {
633 signed_area(p)
634 .abs()
635 .partial_cmp(&signed_area(q).abs())
636 .unwrap_or(std::cmp::Ordering::Equal)
637 })
638 .filter(|ring| signed_area(ring).abs() > 1e-10)
639 .ok_or_else(|| Error::invalid_geometry(None, "Offset polygon collapsed".into()))
640}
641
642fn bounding_box(polygon: &[(f64, f64)]) -> (f64, f64, f64, f64) {
644 let mut min_x = f64::INFINITY;
645 let mut min_y = f64::INFINITY;
646 let mut max_x = f64::NEG_INFINITY;
647 let mut max_y = f64::NEG_INFINITY;
648
649 for &(x, y) in polygon {
650 min_x = min_x.min(x);
651 min_y = min_y.min(y);
652 max_x = max_x.max(x);
653 max_y = max_y.max(y);
654 }
655
656 (min_x, min_y, max_x, max_y)
657}
658
659fn compute_nfp_convex(stationary: &[(f64, f64)], orbiting: &[(f64, f64)]) -> Result<Nfp> {
664 use u_nesting_core::geom::minkowski::nfp_convex;
665
666 let polygon = nfp_convex(stationary, orbiting);
667 Ok(Nfp::from_polygon(polygon))
668}
669
670fn compute_minkowski_sum_convex(poly_a: &[(f64, f64)], poly_b: &[(f64, f64)]) -> Result<Nfp> {
674 use u_nesting_core::geom::minkowski::minkowski_sum_convex;
675
676 let polygon = minkowski_sum_convex(poly_a, poly_b);
677 Ok(Nfp::from_polygon(polygon))
678}
679
680fn compute_nfp_general(
687 stat_exterior: &[(f64, f64)],
688 rotated_orbiting: &[(f64, f64)],
689) -> Result<Nfp> {
690 let stat_triangles = triangulate_polygon(stat_exterior);
692 let orb_triangles = triangulate_polygon(rotated_orbiting);
693
694 if stat_triangles.is_empty() || orb_triangles.is_empty() {
695 let stat_hull = convex_hull_of_points(stat_exterior);
697 let orb_hull = convex_hull_of_points(rotated_orbiting);
698 let reflected: Vec<(f64, f64)> = orb_hull.iter().map(|&(x, y)| (-x, -y)).collect();
699 return compute_minkowski_sum_convex(&stat_hull, &reflected);
700 }
701
702 let pairs: Vec<_> = stat_triangles
705 .iter()
706 .flat_map(|stat_tri| {
707 orb_triangles
708 .iter()
709 .map(move |orb_tri| (stat_tri.clone(), orb_tri.clone()))
710 })
711 .collect();
712
713 #[cfg(feature = "parallel")]
714 let partial_nfps: Vec<Vec<(f64, f64)>> = pairs
715 .par_iter()
716 .flat_map(|(stat_tri, orb_tri)| {
717 let reflected: Vec<(f64, f64)> = orb_tri.iter().map(|&(x, y)| (-x, -y)).collect();
718 if let Ok(nfp) = compute_minkowski_sum_convex(stat_tri, &reflected) {
719 nfp.polygons
720 .into_iter()
721 .filter(|polygon| polygon.len() >= 3)
722 .collect::<Vec<_>>()
723 } else {
724 Vec::new()
725 }
726 })
727 .collect();
728 #[cfg(not(feature = "parallel"))]
729 let partial_nfps: Vec<Vec<(f64, f64)>> = pairs
730 .iter()
731 .flat_map(|(stat_tri, orb_tri)| {
732 let reflected: Vec<(f64, f64)> = orb_tri.iter().map(|&(x, y)| (-x, -y)).collect();
733 if let Ok(nfp) = compute_minkowski_sum_convex(stat_tri, &reflected) {
734 nfp.polygons
735 .into_iter()
736 .filter(|polygon| polygon.len() >= 3)
737 .collect::<Vec<_>>()
738 } else {
739 Vec::new()
740 }
741 })
742 .collect();
743
744 if partial_nfps.is_empty() {
745 let stat_hull = convex_hull_of_points(stat_exterior);
747 let orb_hull = convex_hull_of_points(rotated_orbiting);
748 let reflected: Vec<(f64, f64)> = orb_hull.iter().map(|&(x, y)| (-x, -y)).collect();
749 return compute_minkowski_sum_convex(&stat_hull, &reflected);
750 }
751
752 union_polygons(&partial_nfps)
754}
755
756fn triangulate_polygon(polygon: &[(f64, f64)]) -> Vec<Vec<(f64, f64)>> {
758 if polygon.len() < 3 {
759 return Vec::new();
760 }
761
762 if is_polygon_convex(polygon) {
764 return vec![polygon.to_vec()];
765 }
766
767 let mut vertices: Vec<(f64, f64)> = ensure_ccw(polygon);
769 let mut triangles = Vec::new();
770
771 while vertices.len() > 3 {
772 let n = vertices.len();
773 let mut ear_found = false;
774
775 for i in 0..n {
776 let prev = (i + n - 1) % n;
777 let next = (i + 1) % n;
778
779 if is_ear(&vertices, prev, i, next) {
781 triangles.push(vec![vertices[prev], vertices[i], vertices[next]]);
782 vertices.remove(i);
783 ear_found = true;
784 break;
785 }
786 }
787
788 if !ear_found {
789 return vec![convex_hull_of_points(polygon)];
792 }
793 }
794
795 if vertices.len() == 3 {
796 triangles.push(vertices);
797 }
798
799 triangles
800}
801
802fn point_in_triangle_robust(p: (f64, f64), a: (f64, f64), b: (f64, f64), c: (f64, f64)) -> bool {
806 let o1 = orient2d_filtered(a, b, p);
807 let o2 = orient2d_filtered(b, c, p);
808 let o3 = orient2d_filtered(c, a, p);
809
810 (o1 == Orientation::CounterClockwise
813 && o2 == Orientation::CounterClockwise
814 && o3 == Orientation::CounterClockwise)
815 || (o1 == Orientation::Clockwise
816 && o2 == Orientation::Clockwise
817 && o3 == Orientation::Clockwise)
818}
819
820fn is_ear(vertices: &[(f64, f64)], prev: usize, curr: usize, next: usize) -> bool {
824 let a = vertices[prev];
825 let b = vertices[curr];
826 let c = vertices[next];
827
828 let orientation = orient2d_filtered(a, b, c);
831 if !orientation.is_ccw() {
832 return false; }
834
835 for (i, &p) in vertices.iter().enumerate() {
837 if i == prev || i == curr || i == next {
838 continue;
839 }
840 if point_in_triangle_robust(p, a, b, c) {
841 return false;
842 }
843 }
844
845 true
846}
847
848fn union_polygons(polygons: &[Vec<(f64, f64)>]) -> Result<Nfp> {
850 if polygons.is_empty() {
851 return Ok(Nfp::new());
852 }
853
854 if polygons.len() == 1 {
855 return Ok(Nfp::from_polygon(polygons[0].clone()));
856 }
857
858 let mut result: Vec<Vec<[f64; 2]>> = vec![polygons[0].iter().map(|&(x, y)| [x, y]).collect()];
860
861 for polygon in &polygons[1..] {
863 let clip: Vec<[f64; 2]> = polygon.iter().map(|&(x, y)| [x, y]).collect();
864
865 let shapes = result.overlay(&[clip], OverlayRule::Union, FillRule::NonZero);
867
868 result = Vec::new();
870 for shape in shapes {
871 for contour in shape {
872 if contour.len() >= 3 {
873 result.push(contour);
874 }
875 }
876 }
877
878 if result.is_empty() {
879 continue;
881 }
882 }
883
884 let nfp_polygons: Vec<Vec<(f64, f64)>> = result
886 .into_iter()
887 .map(|contour| contour.into_iter().map(|[x, y]| (x, y)).collect())
888 .collect();
889
890 if nfp_polygons.is_empty() {
891 return Ok(Nfp::from_polygon(polygons[0].clone()));
893 }
894
895 Ok(Nfp::from_polygons(nfp_polygons))
896}
897
898fn rotate_polygon(polygon: &[(f64, f64)], angle: f64) -> Vec<(f64, f64)> {
904 if angle.abs() < 1e-10 {
905 return polygon.to_vec();
906 }
907
908 let cos_a = angle.cos();
909 let sin_a = angle.sin();
910
911 polygon
912 .iter()
913 .map(|&(x, y)| (x * cos_a - y * sin_a, x * sin_a + y * cos_a))
914 .collect()
915}
916
917fn is_polygon_convex(polygon: &[(f64, f64)]) -> bool {
919 geom_polygon::is_convex(polygon)
920}
921
922fn ensure_ccw(polygon: &[(f64, f64)]) -> Vec<(f64, f64)> {
924 geom_polygon::ensure_ccw(polygon)
925}
926
927fn signed_area(polygon: &[(f64, f64)]) -> f64 {
930 geom_polygon::signed_area(polygon)
931}
932
933fn convex_hull_of_points(points: &[(f64, f64)]) -> Vec<(f64, f64)> {
935 geom_polygon::convex_hull(points)
936}
937
938pub fn point_in_polygon(point: (f64, f64), polygon: &[(f64, f64)]) -> bool {
944 let (px, py) = point;
945 let n = polygon.len();
946 let mut inside = false;
947
948 let mut j = n - 1;
949 for i in 0..n {
950 let (xi, yi) = polygon[i];
951 let (xj, yj) = polygon[j];
952
953 if ((yi > py) != (yj > py)) && (px < (xj - xi) * (py - yi) / (yj - yi) + xi) {
954 inside = !inside;
955 }
956 j = i;
957 }
958
959 inside
960}
961
962pub fn point_outside_all_nfps(point: (f64, f64), nfps: &[&Nfp]) -> bool {
964 for nfp in nfps {
965 for polygon in &nfp.polygons {
966 if point_in_polygon(point, polygon) {
967 return false;
968 }
969 }
970 }
971 true
972}
973
974fn point_on_polygon_boundary(point: (f64, f64), polygon: &[(f64, f64)]) -> bool {
976 let (px, py) = point;
977 let n = polygon.len();
978 const EPS: f64 = 1e-10;
979
980 for i in 0..n {
981 let (x1, y1) = polygon[i];
982 let (x2, y2) = polygon[(i + 1) % n];
983
984 let dx = x2 - x1;
987 let dy = y2 - y1;
988 let len_sq = dx * dx + dy * dy;
989
990 if len_sq < EPS * EPS {
991 if (px - x1).abs() < EPS && (py - y1).abs() < EPS {
993 return true;
994 }
995 continue;
996 }
997
998 let t = ((px - x1) * dx + (py - y1) * dy) / len_sq;
1000
1001 if (-EPS..=1.0 + EPS).contains(&t) {
1003 let proj_x = x1 + t * dx;
1005 let proj_y = y1 + t * dy;
1006 let dist_sq = (px - proj_x).powi(2) + (py - proj_y).powi(2);
1007
1008 if dist_sq < EPS * EPS {
1009 return true;
1010 }
1011 }
1012 }
1013
1014 false
1015}
1016
1017#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1024pub enum PackingAxis {
1025 X,
1027 Y,
1029}
1030
1031impl PackingAxis {
1032 pub fn of(boundary: &crate::boundary::Boundary2D) -> Self {
1034 use u_nesting_core::geometry::Boundary;
1035 if boundary.is_infinite() {
1036 return PackingAxis::Y;
1037 }
1038 let (min, max) = boundary.aabb();
1039 if max[1] - min[1] >= max[0] - min[0] {
1040 PackingAxis::Y
1041 } else {
1042 PackingAxis::X
1043 }
1044 }
1045
1046 pub fn key(self, point: (f64, f64)) -> (f64, f64) {
1049 match self {
1050 PackingAxis::X => point,
1051 PackingAxis::Y => (point.1, point.0),
1052 }
1053 }
1054
1055 pub fn precedes(self, a: (f64, f64), b: (f64, f64), eps: f64) -> bool {
1058 let (a0, a1) = self.key(a);
1059 let (b0, b1) = self.key(b);
1060 a0 < b0 - eps || (a0 < b0 + eps && a1 < b1 - eps)
1061 }
1062}
1063
1064pub fn find_bottom_left_placement(
1086 ifp: &Nfp,
1087 nfps: &[&Nfp],
1088 sample_step: f64,
1089 axis: PackingAxis,
1090) -> Option<(f64, f64)> {
1091 if ifp.is_empty() {
1092 return None;
1093 }
1094
1095 let mut candidates: Vec<(f64, f64)> = Vec::new();
1097
1098 for polygon in &ifp.polygons {
1099 candidates.extend(polygon.iter().copied());
1100 }
1101
1102 for nfp in nfps {
1104 for polygon in &nfp.polygons {
1105 candidates.extend(polygon.iter().copied());
1106 }
1107 }
1108
1109 for nfp in nfps {
1115 for polygon in &nfp.polygons {
1116 for ifp_polygon in &ifp.polygons {
1117 push_edge_crossings(ifp_polygon, polygon, &mut candidates);
1118 }
1119 }
1120 }
1121 let nfp_polygons: Vec<&Vec<(f64, f64)>> =
1122 nfps.iter().flat_map(|nfp| nfp.polygons.iter()).collect();
1123 for (i, a) in nfp_polygons.iter().enumerate() {
1124 for b in &nfp_polygons[i + 1..] {
1125 push_edge_crossings(a, b, &mut candidates);
1126 }
1127 }
1128
1129 let (min_x, min_y, max_x, max_y) = ifp_bounding_box(ifp);
1131
1132 let mut y = min_y;
1134 while y <= max_y {
1135 let mut x = min_x;
1136 while x <= max_x {
1137 candidates.push((x, y));
1138 x += sample_step;
1139 }
1140 y += sample_step;
1141 }
1142
1143 candidates.retain(|p| p.0.is_finite() && p.1.is_finite());
1147 candidates.sort_by(|a, b| {
1148 let (a, b) = (axis.key(*a), axis.key(*b));
1149 a.0.total_cmp(&b.0).then(a.1.total_cmp(&b.1))
1150 });
1151
1152 let boxes: Vec<(f64, f64, f64, f64)> = nfp_polygons
1155 .iter()
1156 .map(|polygon| bounding_box(polygon))
1157 .collect();
1158
1159 candidates.into_iter().find(|&point| {
1160 let in_ifp = ifp
1162 .polygons
1163 .iter()
1164 .any(|p| point_in_polygon(point, p) || point_on_polygon_boundary(point, p));
1165 in_ifp
1167 && nfp_polygons
1168 .iter()
1169 .zip(&boxes)
1170 .all(|(polygon, &(min_x, min_y, max_x, max_y))| {
1171 point.0 <= min_x
1172 || point.0 >= max_x
1173 || point.1 <= min_y
1174 || point.1 >= max_y
1175 || !point_in_polygon(point, polygon)
1176 })
1177 })
1178}
1179
1180fn push_edge_crossings(a: &[(f64, f64)], b: &[(f64, f64)], out: &mut Vec<(f64, f64)>) {
1183 let (a_min_x, a_min_y, a_max_x, a_max_y) = bounding_box(a);
1184 let (b_min_x, b_min_y, b_max_x, b_max_y) = bounding_box(b);
1185 if a_max_x < b_min_x || b_max_x < a_min_x || a_max_y < b_min_y || b_max_y < a_min_y {
1186 return;
1187 }
1188 for i in 0..a.len() {
1189 let (p, p2) = (a[i], a[(i + 1) % a.len()]);
1190 let (rx, ry) = (p2.0 - p.0, p2.1 - p.1);
1191 for j in 0..b.len() {
1192 let (q, q2) = (b[j], b[(j + 1) % b.len()]);
1193 let (sx, sy) = (q2.0 - q.0, q2.1 - q.1);
1194 let denom = rx * sy - ry * sx;
1195 if denom.abs() < 1e-12 {
1196 continue; }
1198 let (qpx, qpy) = (q.0 - p.0, q.1 - p.1);
1199 let t = (qpx * sy - qpy * sx) / denom;
1200 let u = (qpx * ry - qpy * rx) / denom;
1201 if (0.0..=1.0).contains(&t) && (0.0..=1.0).contains(&u) {
1202 out.push((p.0 + t * rx, p.1 + t * ry));
1203 }
1204 }
1205 }
1206}
1207
1208fn ifp_bounding_box(ifp: &Nfp) -> (f64, f64, f64, f64) {
1210 let mut min_x = f64::INFINITY;
1211 let mut min_y = f64::INFINITY;
1212 let mut max_x = f64::NEG_INFINITY;
1213 let mut max_y = f64::NEG_INFINITY;
1214
1215 for polygon in &ifp.polygons {
1216 for &(x, y) in polygon {
1217 min_x = min_x.min(x);
1218 min_y = min_y.min(y);
1219 max_x = max_x.max(x);
1220 max_y = max_y.max(y);
1221 }
1222 }
1223
1224 (min_x, min_y, max_x, max_y)
1225}
1226
1227#[derive(Debug, Clone)]
1229pub struct PlacedGeometry {
1230 pub geometry: Geometry2D,
1232 pub position: (f64, f64),
1234 pub rotation: f64,
1236 pub mirrored: bool,
1238}
1239
1240impl PlacedGeometry {
1241 pub fn new(geometry: Geometry2D, position: (f64, f64), rotation: f64) -> Self {
1244 Self {
1245 geometry,
1246 position,
1247 rotation,
1248 mirrored: false,
1249 }
1250 }
1251
1252 pub fn with_mirrored(mut self, mirrored: bool) -> Self {
1254 self.mirrored = mirrored;
1255 self
1256 }
1257
1258 pub fn translated_exterior(&self) -> Vec<(f64, f64)> {
1260 let base = if self.mirrored {
1261 crate::polygon_ops::mirror_polygon(self.geometry.exterior())
1262 } else {
1263 self.geometry.exterior().to_vec()
1264 };
1265 let rotated = rotate_polygon(&base, self.rotation);
1266 rotated
1267 .into_iter()
1268 .map(|(x, y)| (x + self.position.0, y + self.position.1))
1269 .collect()
1270 }
1271}
1272
1273pub fn verify_no_overlap(
1288 geometry: &Geometry2D,
1289 position: (f64, f64),
1290 rotation: f64,
1291 placed_geometries: &[PlacedGeometry],
1292) -> bool {
1293 verify_no_overlap_mirrored(geometry, position, rotation, false, placed_geometries)
1294}
1295
1296pub fn verify_no_overlap_mirrored(
1300 geometry: &Geometry2D,
1301 position: (f64, f64),
1302 rotation: f64,
1303 mirrored: bool,
1304 placed_geometries: &[PlacedGeometry],
1305) -> bool {
1306 use crate::nfp_sliding::polygons_overlap;
1307
1308 let base = if mirrored {
1310 crate::polygon_ops::mirror_polygon(geometry.exterior())
1311 } else {
1312 geometry.exterior().to_vec()
1313 };
1314 let rotated = rotate_polygon(&base, rotation);
1315 let transformed: Vec<(f64, f64)> = rotated
1316 .into_iter()
1317 .map(|(x, y)| (x + position.0, y + position.1))
1318 .collect();
1319
1320 for placed in placed_geometries {
1322 let placed_polygon = placed.translated_exterior();
1323
1324 if polygons_overlap(&transformed, &placed_polygon) {
1325 return false; }
1327 }
1328
1329 true }
1331
1332#[derive(Debug, Clone, Hash, PartialEq, Eq)]
1338struct NfpCacheKey {
1339 geometry_a: String,
1340 geometry_b: String,
1341 rotation_millideg: i32, mirror_a: bool,
1346 mirror_b: bool,
1347}
1348
1349impl NfpCacheKey {
1350 fn new_mirrored(
1351 id_a: &str,
1352 id_b: &str,
1353 rotation_rad: f64,
1354 mirror_a: bool,
1355 mirror_b: bool,
1356 ) -> Self {
1357 let rotation_millideg = ((rotation_rad * 180.0 / PI) * 1000.0).round() as i32;
1359 Self {
1360 geometry_a: id_a.to_string(),
1361 geometry_b: id_b.to_string(),
1362 rotation_millideg,
1363 mirror_a,
1364 mirror_b,
1365 }
1366 }
1367}
1368
1369#[derive(Debug)]
1371pub struct NfpCache {
1372 cache: RwLock<HashMap<NfpCacheKey, Arc<Nfp>>>,
1373 max_size: usize,
1374}
1375
1376impl NfpCache {
1377 pub fn new() -> Self {
1379 Self::with_capacity(1000)
1380 }
1381
1382 pub fn with_capacity(max_size: usize) -> Self {
1384 Self {
1385 cache: RwLock::new(HashMap::new()),
1386 max_size,
1387 }
1388 }
1389
1390 pub fn get_or_compute<F>(&self, key: (&str, &str, f64), compute: F) -> Result<Arc<Nfp>>
1396 where
1397 F: FnOnce() -> Result<Nfp>,
1398 {
1399 self.get_or_compute_mirrored((key.0, key.1, key.2, false, false), compute)
1400 }
1401
1402 pub fn get_or_compute_mirrored<F>(
1410 &self,
1411 key: (&str, &str, f64, bool, bool),
1412 compute: F,
1413 ) -> Result<Arc<Nfp>>
1414 where
1415 F: FnOnce() -> Result<Nfp>,
1416 {
1417 let cache_key = NfpCacheKey::new_mirrored(key.0, key.1, key.2, key.3, key.4);
1418
1419 {
1421 let cache = self.cache.read().map_err(|e| {
1422 Error::Internal(format!("Failed to acquire cache read lock: {}", e))
1423 })?;
1424 if let Some(nfp) = cache.get(&cache_key) {
1425 return Ok(Arc::clone(nfp));
1426 }
1427 }
1428
1429 let nfp = Arc::new(compute()?);
1431
1432 {
1434 let mut cache = self.cache.write().map_err(|e| {
1435 Error::Internal(format!("Failed to acquire cache write lock: {}", e))
1436 })?;
1437
1438 if cache.len() >= self.max_size {
1440 let keys_to_remove: Vec<_> =
1441 cache.keys().take(self.max_size / 2).cloned().collect();
1442 for key in keys_to_remove {
1443 cache.remove(&key);
1444 }
1445 }
1446
1447 cache.insert(cache_key, Arc::clone(&nfp));
1448 }
1449
1450 Ok(nfp)
1451 }
1452
1453 pub fn len(&self) -> usize {
1455 self.cache.read().map(|c| c.len()).unwrap_or(0)
1456 }
1457
1458 pub fn is_empty(&self) -> bool {
1460 self.len() == 0
1461 }
1462
1463 pub fn clear(&self) {
1465 if let Ok(mut cache) = self.cache.write() {
1466 cache.clear();
1467 }
1468 }
1469}
1470
1471impl Default for NfpCache {
1472 fn default() -> Self {
1473 Self::new()
1474 }
1475}
1476
1477#[cfg(test)]
1478mod tests {
1479 use super::*;
1480 use approx::assert_relative_eq;
1481
1482 fn rect(w: f64, h: f64) -> Vec<(f64, f64)> {
1483 vec![(0.0, 0.0), (w, 0.0), (w, h), (0.0, h)]
1484 }
1485
1486 fn triangle() -> Vec<(f64, f64)> {
1487 vec![(0.0, 0.0), (10.0, 0.0), (5.0, 10.0)]
1488 }
1489
1490 #[test]
1491 fn test_is_polygon_convex() {
1492 assert!(is_polygon_convex(&rect(10.0, 10.0)));
1494
1495 assert!(is_polygon_convex(&triangle()));
1497
1498 let l_shape = vec![
1500 (0.0, 0.0),
1501 (10.0, 0.0),
1502 (10.0, 5.0),
1503 (5.0, 5.0),
1504 (5.0, 10.0),
1505 (0.0, 10.0),
1506 ];
1507 assert!(!is_polygon_convex(&l_shape));
1508 }
1509
1510 #[test]
1511 fn test_signed_area() {
1512 let ccw_square = rect(10.0, 10.0);
1514 assert!(signed_area(&ccw_square) > 0.0);
1515 assert_relative_eq!(signed_area(&ccw_square).abs(), 100.0, epsilon = 1e-10);
1516
1517 let cw_square: Vec<_> = ccw_square.into_iter().rev().collect();
1519 assert!(signed_area(&cw_square) < 0.0);
1520 }
1521
1522 #[test]
1523 fn test_rotate_polygon() {
1524 let square = rect(10.0, 10.0);
1525
1526 let rotated = rotate_polygon(&square, 0.0);
1528 assert_eq!(rotated.len(), square.len());
1529
1530 let rotated = rotate_polygon(&[(1.0, 0.0)], PI / 2.0);
1532 assert_relative_eq!(rotated[0].0, 0.0, epsilon = 1e-10);
1533 assert_relative_eq!(rotated[0].1, 1.0, epsilon = 1e-10);
1534 }
1535
1536 fn chiral_l() -> Geometry2D {
1541 Geometry2D::l_shape("L", 30.0, 20.0, 20.0, 10.0)
1542 }
1543
1544 #[test]
1545 fn test_nfp_mirror_orbiting_changes_result() {
1546 let stationary = Geometry2D::rectangle("S", 100.0, 100.0);
1547 let orbiting = chiral_l();
1548
1549 let unmirrored = compute_nfp_mirrored(&stationary, &orbiting, 0.0, false, false).unwrap();
1550 let mirrored = compute_nfp_mirrored(&stationary, &orbiting, 0.0, false, true).unwrap();
1551
1552 assert!(!unmirrored.is_empty());
1553 assert!(!mirrored.is_empty());
1554 assert_ne!(
1555 unmirrored.polygons, mirrored.polygons,
1556 "mirroring the orbiting polygon must change the NFP for a chiral shape"
1557 );
1558
1559 let unmirrored_area: f64 = unmirrored
1562 .polygons
1563 .iter()
1564 .map(|p| signed_area(p).abs())
1565 .sum();
1566 let mirrored_area: f64 = mirrored.polygons.iter().map(|p| signed_area(p).abs()).sum();
1567 assert_relative_eq!(unmirrored_area, mirrored_area, epsilon = 1e-6);
1568 }
1569
1570 #[test]
1571 fn test_nfp_mirror_stationary_changes_result() {
1572 let stationary = chiral_l();
1573 let orbiting = Geometry2D::rectangle("O", 5.0, 5.0);
1574
1575 let unmirrored = compute_nfp_mirrored(&stationary, &orbiting, 0.0, false, false).unwrap();
1576 let mirrored = compute_nfp_mirrored(&stationary, &orbiting, 0.0, true, false).unwrap();
1577
1578 assert!(!unmirrored.is_empty());
1579 assert!(!mirrored.is_empty());
1580 assert_ne!(
1581 unmirrored.polygons, mirrored.polygons,
1582 "mirroring the stationary polygon must change the NFP for a chiral shape"
1583 );
1584 }
1585
1586 #[test]
1587 fn test_compute_nfp_unchanged_by_new_mirror_plumbing() {
1588 let a = Geometry2D::rectangle("A", 10.0, 10.0);
1592 let b = Geometry2D::rectangle("B", 5.0, 5.0);
1593 let via_plain = compute_nfp(&a, &b, 0.0).unwrap();
1594 let via_mirrored = compute_nfp_mirrored(&a, &b, 0.0, false, false).unwrap();
1595 assert_eq!(via_plain.polygons, via_mirrored.polygons);
1596 }
1597
1598 #[test]
1599 fn test_ifp_mirror_changes_result() {
1600 let boundary = rect(100.0, 100.0);
1601 let geom = chiral_l();
1602
1603 let unmirrored =
1604 compute_ifp_with_margin_and_mirror(&boundary, &geom, 0.0, 0.0, false).unwrap();
1605 let mirrored =
1606 compute_ifp_with_margin_and_mirror(&boundary, &geom, 0.0, 0.0, true).unwrap();
1607
1608 assert!(!unmirrored.is_empty());
1609 assert!(!mirrored.is_empty());
1610 assert_ne!(
1611 unmirrored.polygons, mirrored.polygons,
1612 "mirroring the geometry must change its IFP within the boundary for a chiral shape"
1613 );
1614 }
1615
1616 #[test]
1617 fn test_compute_ifp_with_margin_unchanged_by_new_mirror_plumbing() {
1618 let boundary = rect(100.0, 100.0);
1619 let geom = Geometry2D::rectangle("G", 10.0, 10.0);
1620 let via_plain = compute_ifp_with_margin(&boundary, &geom, 0.0, 5.0).unwrap();
1621 let via_mirrored =
1622 compute_ifp_with_margin_and_mirror(&boundary, &geom, 0.0, 5.0, false).unwrap();
1623 assert_eq!(via_plain.polygons, via_mirrored.polygons);
1624 }
1625
1626 #[test]
1627 fn test_nfp_cache_mirror_flags_distinguish_entries() {
1628 let cache = NfpCache::new();
1629 let mut calls = 0;
1630
1631 let unmirrored = cache
1632 .get_or_compute_mirrored(("A", "B", 0.0, false, false), || {
1633 calls += 1;
1634 Ok(Nfp::from_polygon(rect(1.0, 1.0)))
1635 })
1636 .unwrap();
1637
1638 let mirrored = cache
1642 .get_or_compute_mirrored(("A", "B", 0.0, true, false), || {
1643 calls += 1;
1644 Ok(Nfp::from_polygon(rect(2.0, 2.0)))
1645 })
1646 .unwrap();
1647
1648 assert_eq!(
1649 calls, 2,
1650 "distinct mirror flags must both invoke compute, not share a slot"
1651 );
1652 assert_ne!(unmirrored.polygons, mirrored.polygons);
1653
1654 let unmirrored_again = cache
1656 .get_or_compute_mirrored(("A", "B", 0.0, false, false), || {
1657 calls += 1;
1658 Ok(Nfp::from_polygon(rect(99.0, 99.0)))
1659 })
1660 .unwrap();
1661 assert_eq!(calls, 2, "re-querying an existing key must hit the cache");
1662 assert_eq!(unmirrored.polygons, unmirrored_again.polygons);
1663 }
1664
1665 #[test]
1666 fn test_nfp_two_squares() {
1667 let a = Geometry2D::rectangle("A", 10.0, 10.0);
1668 let b = Geometry2D::rectangle("B", 5.0, 5.0);
1669
1670 let nfp = compute_nfp(&a, &b, 0.0).unwrap();
1671
1672 assert!(!nfp.is_empty());
1673 assert_eq!(nfp.polygons.len(), 1);
1674
1675 let polygon = &nfp.polygons[0];
1678 assert!(polygon.len() >= 4);
1679 }
1680
1681 #[test]
1682 fn test_nfp_with_rotation() {
1683 let a = Geometry2D::rectangle("A", 10.0, 10.0);
1684 let b = Geometry2D::rectangle("B", 5.0, 5.0);
1685
1686 let nfp = compute_nfp(&a, &b, PI / 4.0).unwrap();
1688
1689 assert!(!nfp.is_empty());
1690 }
1692
1693 #[test]
1694 fn test_ifp_square_in_boundary() {
1695 let boundary = rect(100.0, 100.0);
1696 let geom = Geometry2D::rectangle("G", 10.0, 10.0);
1697
1698 let ifp = compute_ifp(&boundary, &geom, 0.0).unwrap();
1699
1700 assert!(!ifp.is_empty());
1701 let polygon = &ifp.polygons[0];
1704 let (min_x, min_y, max_x, max_y) = bounding_box(polygon);
1705 assert_relative_eq!(min_x, 0.0, epsilon = 1e-10);
1706 assert_relative_eq!(min_y, 0.0, epsilon = 1e-10);
1707 assert_relative_eq!(max_x, 90.0, epsilon = 1e-10);
1708 assert_relative_eq!(max_y, 90.0, epsilon = 1e-10);
1709 }
1710
1711 #[test]
1712 fn test_ifp_bounds_correct() {
1713 let boundary = rect(100.0, 50.0);
1715 let geom = Geometry2D::rectangle("R", 25.0, 25.0);
1716
1717 let ifp = compute_ifp(&boundary, &geom, 0.0).unwrap();
1718
1719 assert!(!ifp.is_empty());
1720 let polygon = &ifp.polygons[0];
1722 let (min_x, min_y, max_x, max_y) = bounding_box(polygon);
1723 assert_relative_eq!(min_x, 0.0, epsilon = 1e-10);
1724 assert_relative_eq!(min_y, 0.0, epsilon = 1e-10);
1725 assert_relative_eq!(max_x, 75.0, epsilon = 1e-10);
1726 assert_relative_eq!(max_y, 25.0, epsilon = 1e-10);
1727
1728 assert!(point_in_polygon((0.0, 0.0), polygon) || point_on_boundary((0.0, 0.0), polygon));
1730 assert!(point_in_polygon((25.0, 0.0), polygon) || point_on_boundary((25.0, 0.0), polygon));
1731 assert!(point_in_polygon((50.0, 0.0), polygon) || point_on_boundary((50.0, 0.0), polygon));
1732 assert!(point_in_polygon((75.0, 0.0), polygon) || point_on_boundary((75.0, 0.0), polygon));
1733 }
1734
1735 fn point_on_boundary(point: (f64, f64), polygon: &[(f64, f64)]) -> bool {
1737 let (px, py) = point;
1738 let n = polygon.len();
1739 for i in 0..n {
1740 let (x1, y1) = polygon[i];
1741 let (x2, y2) = polygon[(i + 1) % n];
1742 let d1 = ((px - x1).powi(2) + (py - y1).powi(2)).sqrt();
1744 let d2 = ((px - x2).powi(2) + (py - y2).powi(2)).sqrt();
1745 let d_total = ((x2 - x1).powi(2) + (y2 - y1).powi(2)).sqrt();
1746 if (d1 + d2 - d_total).abs() < 1e-10 {
1747 return true;
1748 }
1749 }
1750 false
1751 }
1752
1753 #[test]
1754 fn test_nfp_same_size_rectangles() {
1755 let a = Geometry2D::rectangle("A", 25.0, 25.0);
1757 let b = Geometry2D::rectangle("B", 25.0, 25.0);
1758
1759 let nfp = compute_nfp(&a, &b, 0.0).unwrap();
1760 assert!(!nfp.is_empty());
1761
1762 let polygon = &nfp.polygons[0];
1763 let (min_x, min_y, max_x, max_y) = bounding_box(polygon);
1764 let width = max_x - min_x;
1767 let height = max_y - min_y;
1768 eprintln!("NFP dimensions: {}x{}", width, height);
1769 eprintln!(
1770 "NFP bounds: ({}, {}) to ({}, {})",
1771 min_x, min_y, max_x, max_y
1772 );
1773 assert_relative_eq!(width, 50.0, epsilon = 1e-6);
1775 assert_relative_eq!(height, 50.0, epsilon = 1e-6);
1776 }
1777
1778 #[test]
1779 fn test_nfp_cache() {
1780 let cache = NfpCache::new();
1781
1782 let compute_count = std::sync::atomic::AtomicUsize::new(0);
1783
1784 let result1 = cache
1785 .get_or_compute(("A", "B", 0.0), || {
1786 compute_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1787 Ok(Nfp::from_polygon(vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)]))
1788 })
1789 .unwrap();
1790
1791 let result2 = cache
1792 .get_or_compute(("A", "B", 0.0), || {
1793 compute_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1794 Ok(Nfp::from_polygon(vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)]))
1795 })
1796 .unwrap();
1797
1798 assert_eq!(compute_count.load(std::sync::atomic::Ordering::SeqCst), 1);
1800 assert_eq!(result1.polygons, result2.polygons);
1801 assert_eq!(cache.len(), 1);
1802 }
1803
1804 #[test]
1805 fn test_nfp_cache_different_rotations() {
1806 let cache = NfpCache::new();
1807
1808 cache
1809 .get_or_compute(("A", "B", 0.0), || {
1810 Ok(Nfp::from_polygon(vec![(0.0, 0.0), (1.0, 0.0)]))
1811 })
1812 .unwrap();
1813
1814 cache
1815 .get_or_compute(("A", "B", PI / 2.0), || {
1816 Ok(Nfp::from_polygon(vec![(0.0, 0.0), (0.0, 1.0)]))
1817 })
1818 .unwrap();
1819
1820 assert_eq!(cache.len(), 2);
1822 }
1823
1824 #[test]
1825 fn test_convex_hull_of_points() {
1826 let points = vec![
1827 (0.0, 0.0),
1828 (10.0, 0.0),
1829 (5.0, 5.0), (10.0, 10.0),
1831 (0.0, 10.0),
1832 ];
1833
1834 let hull = convex_hull_of_points(&points);
1835
1836 assert_eq!(hull.len(), 4);
1838 }
1839
1840 #[test]
1841 fn test_shrink_polygon_square() {
1842 let square = rect(100.0, 100.0);
1843 let shrunk = shrink_polygon(&square, 10.0).unwrap();
1844
1845 assert_eq!(shrunk.len(), 4);
1847
1848 let original_area = signed_area(&square).abs();
1850 let shrunk_area = signed_area(&shrunk).abs();
1851 assert!(
1852 shrunk_area < original_area,
1853 "shrunk_area ({}) should be < original_area ({})",
1854 shrunk_area,
1855 original_area
1856 );
1857
1858 assert_relative_eq!(shrunk_area, 6400.0, epsilon = 1.0);
1861 }
1862
1863 #[test]
1864 fn test_shrink_polygon_collapse() {
1865 let small_square = rect(10.0, 10.0);
1866
1867 let result = shrink_polygon(&small_square, 6.0);
1869 assert!(
1870 result.is_err(),
1871 "Polygon should collapse when offset >= width/2"
1872 );
1873 }
1874
1875 #[test]
1876 fn test_ifp_with_margin() {
1877 let boundary = rect(100.0, 100.0);
1878 let geom = Geometry2D::rectangle("G", 10.0, 10.0);
1879
1880 let ifp_no_margin = compute_ifp(&boundary, &geom, 0.0).unwrap();
1882
1883 let ifp_with_margin = compute_ifp_with_margin(&boundary, &geom, 0.0, 5.0).unwrap();
1885
1886 assert!(!ifp_no_margin.is_empty());
1887 assert!(!ifp_with_margin.is_empty());
1888
1889 let (min_x_no, _min_y_no, max_x_no, _max_y_no) = ifp_bounding_box(&ifp_no_margin);
1891 let (min_x_margin, _min_y_margin, max_x_margin, _max_y_margin) =
1892 ifp_bounding_box(&ifp_with_margin);
1893
1894 let width_no = max_x_no - min_x_no;
1895 let width_margin = max_x_margin - min_x_margin;
1896
1897 assert!(
1901 width_margin < width_no,
1902 "width_margin ({}) should be < width_no ({})",
1903 width_margin,
1904 width_no
1905 );
1906 }
1907
1908 #[test]
1909 fn shrinking_a_non_rectangular_boundary_moves_every_edge_inward_by_the_offset() {
1910 let l_boundary = vec![
1913 (0.0, 0.0),
1914 (100.0, 0.0),
1915 (100.0, 40.0),
1916 (40.0, 40.0),
1917 (40.0, 100.0),
1918 (0.0, 100.0),
1919 ];
1920 let shrunk = shrink_polygon(&l_boundary, 10.0).unwrap();
1921 let n = l_boundary.len();
1922 let dist = |p: (f64, f64)| {
1923 (0..n)
1924 .map(|i| {
1925 let (a, b) = (l_boundary[i], l_boundary[(i + 1) % n]);
1926 let (dx, dy) = (b.0 - a.0, b.1 - a.1);
1927 let t = (((p.0 - a.0) * dx + (p.1 - a.1) * dy) / (dx * dx + dy * dy))
1928 .clamp(0.0, 1.0);
1929 ((p.0 - a.0 - t * dx).powi(2) + (p.1 - a.1 - t * dy).powi(2)).sqrt()
1930 })
1931 .fold(f64::INFINITY, f64::min)
1932 };
1933 for &v in &shrunk {
1934 assert!(
1935 dist(v) >= 10.0 - 1e-6,
1936 "vertex {v:?} is {} from the boundary",
1937 dist(v)
1938 );
1939 }
1940 let min_x = shrunk.iter().map(|p| p.0).fold(f64::MAX, f64::min);
1942 let min_y = shrunk.iter().map(|p| p.1).fold(f64::MAX, f64::min);
1943 assert!((10.0..10.02).contains(&min_x), "left edge at {min_x}");
1944 assert!((10.0..10.02).contains(&min_y), "bottom edge at {min_y}");
1945 }
1946
1947 #[test]
1948 fn test_ifp_margin_boundary_collapse() {
1949 let boundary = rect(20.0, 20.0);
1950
1951 let result = shrink_polygon(&boundary, 12.0);
1953 assert!(
1954 result.is_err(),
1955 "Boundary should collapse with margin >= width/2"
1956 );
1957 }
1958
1959 #[test]
1960 fn test_ifp_margin_large_geometry() {
1961 let boundary = rect(30.0, 30.0);
1962 let geom = Geometry2D::rectangle("G", 20.0, 20.0);
1963
1964 let ifp_no_margin = compute_ifp(&boundary, &geom, 0.0).unwrap();
1966 let (min_x_no, _, max_x_no, _) = ifp_bounding_box(&ifp_no_margin);
1967 let width_no = max_x_no - min_x_no;
1968
1969 let ifp_with_margin = compute_ifp_with_margin(&boundary, &geom, 0.0, 5.0).unwrap();
1971 let (min_x_margin, _, max_x_margin, _) = ifp_bounding_box(&ifp_with_margin);
1972 let width_margin = max_x_margin - min_x_margin;
1973
1974 assert!(
1976 width_margin <= width_no,
1977 "width_margin ({}) should be <= width_no ({})",
1978 width_margin,
1979 width_no
1980 );
1981 }
1982
1983 #[test]
1984 fn test_nfp_non_convex_l_shape() {
1985 let l_shape = Geometry2D::new("L").with_polygon(vec![
1987 (0.0, 0.0),
1988 (20.0, 0.0),
1989 (20.0, 10.0),
1990 (10.0, 10.0),
1991 (10.0, 20.0),
1992 (0.0, 20.0),
1993 ]);
1994
1995 let small_square = Geometry2D::rectangle("S", 5.0, 5.0);
1996
1997 let nfp = compute_nfp(&l_shape, &small_square, 0.0).unwrap();
1999
2000 assert!(!nfp.is_empty());
2001 assert!(nfp.vertex_count() >= 4);
2003 }
2004
2005 #[test]
2006 fn test_triangulate_polygon_convex() {
2007 let square = rect(10.0, 10.0);
2008 let triangles = triangulate_polygon(&square);
2009
2010 assert_eq!(triangles.len(), 1);
2012 assert_eq!(triangles[0].len(), 4);
2013 }
2014
2015 #[test]
2016 fn test_triangulate_polygon_non_convex() {
2017 let l_shape = vec![
2019 (0.0, 0.0),
2020 (20.0, 0.0),
2021 (20.0, 10.0),
2022 (10.0, 10.0),
2023 (10.0, 20.0),
2024 (0.0, 20.0),
2025 ];
2026
2027 let triangles = triangulate_polygon(&l_shape);
2028
2029 assert!(!triangles.is_empty());
2031 }
2032
2033 #[test]
2034 fn test_union_polygons() {
2035 let poly1 = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
2037 let poly2 = vec![(5.0, 5.0), (15.0, 5.0), (15.0, 15.0), (5.0, 15.0)];
2038
2039 let result = union_polygons(&[poly1, poly2]).unwrap();
2040
2041 assert!(!result.is_empty());
2042 assert!(result.vertex_count() >= 6);
2044 }
2045
2046 #[test]
2051 fn test_convex_near_collinear_vertices() {
2052 let near_collinear = vec![
2054 (0.0, 0.0),
2055 (1.0, 1e-15), (2.0, 0.0),
2057 (2.0, 1.0),
2058 (0.0, 1.0),
2059 ];
2060
2061 let result = is_polygon_convex(&near_collinear);
2063 let _ = result; }
2066
2067 #[test]
2068 fn test_triangulation_near_degenerate() {
2069 let near_degenerate_l = vec![
2071 (0.0, 0.0),
2072 (10.0, 0.0),
2073 (10.0, 5.0),
2074 (5.0 + 1e-12, 5.0), (5.0, 10.0),
2076 (0.0, 10.0),
2077 ];
2078
2079 let triangles = triangulate_polygon(&near_degenerate_l);
2081
2082 assert!(!triangles.is_empty());
2084 }
2085
2086 #[test]
2087 fn test_nfp_nearly_touching_rectangles() {
2088 let a = Geometry2D::rectangle("A", 10.0, 10.0);
2090 let b = Geometry2D::rectangle("B", 5.0, 5.0);
2091
2092 let nfp = compute_nfp(&a, &b, 0.0).unwrap();
2094 assert!(!nfp.is_empty());
2095 }
2096
2097 #[test]
2098 fn test_ifp_geometry_nearly_fills_boundary() {
2099 let boundary = rect(100.0, 100.0);
2101 let geom = Geometry2D::rectangle("G", 99.9999, 99.9999);
2102
2103 let result = compute_ifp(&boundary, &geom, 0.0);
2105
2106 match result {
2108 Ok(ifp) => {
2109 let (min_x, min_y, max_x, max_y) = ifp_bounding_box(&ifp);
2111 let width = max_x - min_x;
2112 let height = max_y - min_y;
2113 assert!(width < 0.001 && height < 0.001);
2114 }
2115 Err(_) => {
2116 }
2118 }
2119 }
2120
2121 #[test]
2122 fn test_point_in_polygon_on_boundary() {
2123 let square = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
2125
2126 let on_bottom_edge = (5.0, 0.0);
2128 let on_right_edge = (10.0, 5.0);
2129 let on_top_edge = (5.0, 10.0);
2130 let on_left_edge = (0.0, 5.0);
2131
2132 let _ = point_in_polygon(on_bottom_edge, &square);
2135 let _ = point_in_polygon(on_right_edge, &square);
2136 let _ = point_in_polygon(on_top_edge, &square);
2137 let _ = point_in_polygon(on_left_edge, &square);
2138 }
2139
2140 #[test]
2141 fn test_point_in_triangle_robust_degenerate() {
2142 let a = (0.0, 0.0);
2144 let b = (5.0, 0.0);
2145 let c = (10.0, 0.0);
2146
2147 let p = (3.0, 0.0);
2149
2150 assert!(!point_in_triangle_robust(p, a, b, c));
2152 }
2153
2154 #[test]
2155 fn test_ear_detection_with_collinear_points() {
2156 let with_collinear = vec![
2158 (0.0, 0.0),
2159 (5.0, 0.0),
2160 (10.0, 0.0), (10.0, 10.0),
2162 (0.0, 10.0),
2163 ];
2164
2165 let triangles = triangulate_polygon(&with_collinear);
2167
2168 for triangle in &triangles {
2170 assert!(triangle.len() >= 3);
2171 }
2172 }
2173
2174 #[test]
2175 fn test_nfp_with_very_small_polygon() {
2176 let tiny = Geometry2D::rectangle("tiny", 1e-6, 1e-6);
2178 let normal = Geometry2D::rectangle("normal", 10.0, 10.0);
2179
2180 let nfp = compute_nfp(&normal, &tiny, 0.0).unwrap();
2182 assert!(!nfp.is_empty());
2183 }
2184
2185 #[test]
2186 fn test_nfp_with_very_large_polygon() {
2187 let large = Geometry2D::rectangle("large", 1e6, 1e6);
2189 let normal = Geometry2D::rectangle("normal", 100.0, 100.0);
2190
2191 let nfp = compute_nfp(&large, &normal, 0.0).unwrap();
2193 assert!(!nfp.is_empty());
2194 }
2195
2196 #[test]
2197 fn test_signed_area_with_extreme_coordinates() {
2198 let moderate_coords = vec![
2205 (1e6, 1e6),
2206 (1e6 + 100.0, 1e6),
2207 (1e6 + 100.0, 1e6 + 100.0),
2208 (1e6, 1e6 + 100.0),
2209 ];
2210
2211 let area = signed_area(&moderate_coords);
2212
2213 assert_relative_eq!(area.abs(), 10000.0, epsilon = 1.0);
2215 }
2216
2217 #[test]
2218 fn test_ensure_ccw_with_near_zero_area() {
2219 let tiny_area = vec![(0.0, 0.0), (1e-10, 0.0), (1e-10, 1e-10), (0.0, 1e-10)];
2221
2222 let ccw = ensure_ccw(&tiny_area);
2224 assert_eq!(ccw.len(), tiny_area.len());
2225 }
2226
2227 #[test]
2232 fn test_nfp_method_default() {
2233 let config = NfpConfig::default();
2234 assert_eq!(config.method, NfpMethod::MinkowskiSum);
2235 }
2236
2237 #[test]
2238 fn test_nfp_method_minkowski_sum() {
2239 let a = Geometry2D::rectangle("A", 10.0, 10.0);
2240 let b = Geometry2D::rectangle("B", 5.0, 5.0);
2241
2242 let nfp = compute_nfp_with_method(&a, &b, 0.0, NfpMethod::MinkowskiSum).unwrap();
2243
2244 assert!(!nfp.is_empty());
2245 assert!(nfp.vertex_count() >= 4);
2246 }
2247
2248 #[test]
2249 fn test_nfp_method_sliding() {
2250 let a = Geometry2D::rectangle("A", 10.0, 10.0);
2251 let b = Geometry2D::rectangle("B", 5.0, 5.0);
2252
2253 let result = compute_nfp_with_method(&a, &b, 0.0, NfpMethod::Sliding);
2254
2255 assert!(result.is_ok(), "Sliding method should not error");
2257 let nfp = result.unwrap();
2258 assert!(!nfp.is_empty(), "NFP should not be empty");
2259
2260 }
2264
2265 #[test]
2266 fn test_nfp_method_config_builder() {
2267 let config = NfpConfig::with_method(NfpMethod::Sliding)
2268 .with_tolerance(1e-5)
2269 .with_max_iterations(5000);
2270
2271 assert_eq!(config.method, NfpMethod::Sliding);
2272 assert!((config.contact_tolerance - 1e-5).abs() < 1e-10);
2273 assert_eq!(config.max_iterations, 5000);
2274 }
2275
2276 #[test]
2277 fn test_nfp_methods_both_succeed() {
2278 let a = Geometry2D::rectangle("A", 10.0, 10.0);
2279 let b = Geometry2D::rectangle("B", 5.0, 5.0);
2280
2281 let nfp_mink = compute_nfp_with_method(&a, &b, 0.0, NfpMethod::MinkowskiSum).unwrap();
2282 let nfp_slide = compute_nfp_with_method(&a, &b, 0.0, NfpMethod::Sliding).unwrap();
2283
2284 assert!(!nfp_mink.is_empty());
2286 assert!(!nfp_slide.is_empty());
2287
2288 assert!(nfp_mink.vertex_count() >= 4);
2290
2291 }
2296
2297 #[test]
2298 fn test_nfp_sliding_l_shape() {
2299 let l_shape = Geometry2D::new("L").with_polygon(vec![
2301 (0.0, 0.0),
2302 (20.0, 0.0),
2303 (20.0, 10.0),
2304 (10.0, 10.0),
2305 (10.0, 20.0),
2306 (0.0, 20.0),
2307 ]);
2308
2309 let small_square = Geometry2D::rectangle("S", 5.0, 5.0);
2310
2311 let result = compute_nfp_with_method(&l_shape, &small_square, 0.0, NfpMethod::Sliding);
2313
2314 assert!(result.is_ok(), "Sliding should not error on L-shape");
2316 let nfp = result.unwrap();
2317 assert!(!nfp.is_empty(), "NFP should not be empty for L-shape");
2318 }
2319
2320 #[test]
2321 fn test_nfp_with_config() {
2322 let a = Geometry2D::rectangle("A", 10.0, 10.0);
2323 let b = Geometry2D::rectangle("B", 5.0, 5.0);
2324
2325 let config = NfpConfig {
2326 method: NfpMethod::Sliding,
2327 contact_tolerance: 1e-4,
2328 max_iterations: 2000,
2329 };
2330
2331 let nfp = compute_nfp_with_config(&a, &b, 0.0, &config).unwrap();
2332
2333 assert!(!nfp.is_empty());
2334 }
2335}