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
38use crate::placement_utils::polygon_centroid;
39
40pub fn rotate_nfp(nfp: &Nfp, angle: f64) -> Nfp {
45 if angle.abs() < 1e-10 {
46 return nfp.clone();
47 }
48
49 let cos_a = angle.cos();
50 let sin_a = angle.sin();
51
52 Nfp {
53 polygons: nfp
54 .polygons
55 .iter()
56 .map(|polygon| {
57 polygon
58 .iter()
59 .map(|&(x, y)| (x * cos_a - y * sin_a, x * sin_a + y * cos_a))
60 .collect()
61 })
62 .collect(),
63 }
64}
65
66pub fn translate_nfp(nfp: &Nfp, offset: (f64, f64)) -> Nfp {
68 Nfp {
69 polygons: nfp
70 .polygons
71 .iter()
72 .map(|polygon| {
73 polygon
74 .iter()
75 .map(|(x, y)| (x + offset.0, y + offset.1))
76 .collect()
77 })
78 .collect(),
79 }
80}
81
82#[derive(Debug, Clone)]
84pub struct Nfp {
85 pub polygons: Vec<Vec<(f64, f64)>>,
88}
89
90impl Nfp {
91 pub fn new() -> Self {
93 Self {
94 polygons: Vec::new(),
95 }
96 }
97
98 pub fn from_polygon(polygon: Vec<(f64, f64)>) -> Self {
100 Self {
101 polygons: vec![polygon],
102 }
103 }
104
105 pub fn from_polygons(polygons: Vec<Vec<(f64, f64)>>) -> Self {
107 Self { polygons }
108 }
109
110 pub fn is_empty(&self) -> bool {
112 self.polygons.is_empty()
113 }
114
115 pub fn vertex_count(&self) -> usize {
117 self.polygons.iter().map(|p| p.len()).sum()
118 }
119}
120
121impl Default for Nfp {
122 fn default() -> Self {
123 Self::new()
124 }
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
133pub enum NfpMethod {
134 #[default]
140 MinkowskiSum,
141
142 Sliding,
149}
150
151#[derive(Debug, Clone)]
153pub struct NfpConfig {
154 pub method: NfpMethod,
156 pub contact_tolerance: f64,
158 pub max_iterations: usize,
160}
161
162impl Default for NfpConfig {
163 fn default() -> Self {
164 Self {
165 method: NfpMethod::MinkowskiSum,
166 contact_tolerance: 1e-6,
167 max_iterations: 10000,
168 }
169 }
170}
171
172impl NfpConfig {
173 pub fn with_method(method: NfpMethod) -> Self {
175 Self {
176 method,
177 ..Default::default()
178 }
179 }
180
181 pub fn with_tolerance(mut self, tolerance: f64) -> Self {
183 self.contact_tolerance = tolerance;
184 self
185 }
186
187 pub fn with_max_iterations(mut self, max_iter: usize) -> Self {
189 self.max_iterations = max_iter;
190 self
191 }
192}
193
194pub fn compute_nfp_with_method(
205 stationary: &Geometry2D,
206 orbiting: &Geometry2D,
207 rotation: f64,
208 method: NfpMethod,
209) -> Result<Nfp> {
210 compute_nfp_with_config(
211 stationary,
212 orbiting,
213 rotation,
214 &NfpConfig::with_method(method),
215 )
216}
217
218pub fn compute_nfp_with_config(
229 stationary: &Geometry2D,
230 orbiting: &Geometry2D,
231 rotation: f64,
232 config: &NfpConfig,
233) -> Result<Nfp> {
234 let stat_exterior = stationary.exterior();
235 let orb_exterior = orbiting.exterior();
236
237 if stat_exterior.len() < 3 || orb_exterior.len() < 3 {
238 return Err(Error::InvalidGeometry(
239 "Polygons must have at least 3 vertices".into(),
240 ));
241 }
242
243 let rotated_orbiting = rotate_polygon(orb_exterior, rotation);
245
246 match config.method {
247 NfpMethod::MinkowskiSum => {
248 if stationary.is_convex()
250 && is_polygon_convex(&rotated_orbiting)
251 && stationary.holes().is_empty()
252 {
253 compute_nfp_convex(stat_exterior, &rotated_orbiting)
254 } else {
255 compute_nfp_general(stationary, &rotated_orbiting)
256 }
257 }
258 NfpMethod::Sliding => {
259 let sliding_config = SlidingNfpConfig {
261 contact_tolerance: config.contact_tolerance,
262 max_iterations: config.max_iterations,
263 min_translation: config.contact_tolerance * 0.01,
264 };
265
266 let reflected: Vec<(f64, f64)> =
268 rotated_orbiting.iter().map(|&(x, y)| (-x, -y)).collect();
269
270 compute_nfp_sliding(stat_exterior, &reflected, &sliding_config)
271 }
272 }
273}
274
275pub fn compute_nfp(stationary: &Geometry2D, orbiting: &Geometry2D, rotation: f64) -> Result<Nfp> {
292 let stat_exterior = stationary.exterior();
294 let orb_exterior = orbiting.exterior();
295
296 if stat_exterior.len() < 3 || orb_exterior.len() < 3 {
297 return Err(Error::InvalidGeometry(
298 "Polygons must have at least 3 vertices".into(),
299 ));
300 }
301
302 let rotated_orbiting = rotate_polygon(orb_exterior, rotation);
304
305 if stationary.is_convex()
307 && is_polygon_convex(&rotated_orbiting)
308 && stationary.holes().is_empty()
309 {
310 compute_nfp_convex(stat_exterior, &rotated_orbiting)
312 } else {
313 compute_nfp_general(stationary, &rotated_orbiting)
315 }
316}
317
318pub fn compute_ifp(
331 boundary_polygon: &[(f64, f64)],
332 geometry: &Geometry2D,
333 rotation: f64,
334) -> Result<Nfp> {
335 compute_ifp_with_margin(boundary_polygon, geometry, rotation, 0.0)
336}
337
338pub fn compute_ifp_with_margin(
353 boundary_polygon: &[(f64, f64)],
354 geometry: &Geometry2D,
355 rotation: f64,
356 margin: f64,
357) -> Result<Nfp> {
358 if boundary_polygon.len() < 3 {
359 return Err(Error::InvalidBoundary(
360 "Boundary must have at least 3 vertices".into(),
361 ));
362 }
363
364 let geom_exterior = geometry.exterior();
365 if geom_exterior.len() < 3 {
366 return Err(Error::InvalidGeometry(
367 "Geometry must have at least 3 vertices".into(),
368 ));
369 }
370
371 let rotated_geom = rotate_polygon(geom_exterior, rotation);
373
374 let effective_boundary = if margin > 0.0 {
376 shrink_polygon(boundary_polygon, margin)?
377 } else {
378 boundary_polygon.to_vec()
379 };
380
381 if effective_boundary.len() < 3 {
382 return Err(Error::InvalidBoundary(
383 "Boundary too small after applying margin".into(),
384 ));
385 }
386
387 compute_minkowski_erosion(&effective_boundary, &rotated_geom)
396}
397
398fn compute_minkowski_erosion(boundary: &[(f64, f64)], geometry: &[(f64, f64)]) -> Result<Nfp> {
404 if boundary.len() < 3 || geometry.len() < 3 {
405 return Err(Error::InvalidGeometry(
406 "Both boundary and geometry must have at least 3 vertices".into(),
407 ));
408 }
409
410 let (b_min_x, b_min_y, b_max_x, b_max_y) = bounding_box(boundary);
412 let is_rect = boundary.len() == 4
413 && boundary.iter().all(|&(x, y)| {
414 ((x - b_min_x).abs() < 1e-10 || (x - b_max_x).abs() < 1e-10)
415 && ((y - b_min_y).abs() < 1e-10 || (y - b_max_y).abs() < 1e-10)
416 });
417
418 let (g_min_x, g_min_y, g_max_x, g_max_y) = bounding_box(geometry);
420
421 if is_rect {
422 let ifp_min_x = b_min_x - g_min_x;
430 let ifp_max_x = b_max_x - g_max_x;
431 let ifp_min_y = b_min_y - g_min_y;
432 let ifp_max_y = b_max_y - g_max_y;
433
434 if ifp_min_x > ifp_max_x + 1e-10 || ifp_min_y > ifp_max_y + 1e-10 {
436 return Err(Error::InvalidGeometry(
437 "Geometry too large to fit in boundary".into(),
438 ));
439 }
440
441 let ifp_min_x = ifp_min_x.min(ifp_max_x);
443 let ifp_min_y = ifp_min_y.min(ifp_max_y);
444
445 return Ok(Nfp::from_polygon(vec![
446 (ifp_min_x, ifp_min_y),
447 (ifp_max_x, ifp_min_y),
448 (ifp_max_x, ifp_max_y),
449 (ifp_min_x, ifp_max_y),
450 ]));
451 }
452
453 compute_minkowski_erosion_general(boundary, geometry)
457}
458
459fn compute_minkowski_erosion_general(
461 boundary: &[(f64, f64)],
462 geometry: &[(f64, f64)],
463) -> Result<Nfp> {
464 if geometry.is_empty() {
465 return Ok(Nfp::from_polygon(boundary.to_vec()));
466 }
467
468 let first_g = geometry[0];
470 let mut result: Vec<[f64; 2]> = boundary
471 .iter()
472 .map(|&(x, y)| [x - first_g.0, y - first_g.1])
473 .collect();
474
475 for &(gx, gy) in geometry.iter().skip(1) {
477 let translated: Vec<[f64; 2]> = boundary.iter().map(|&(x, y)| [x - gx, y - gy]).collect();
478
479 let shapes = result.overlay(&[translated], OverlayRule::Intersect, FillRule::NonZero);
481
482 if shapes.is_empty() {
483 return Err(Error::InvalidGeometry(
484 "Geometry too large to fit in boundary".into(),
485 ));
486 }
487
488 result = Vec::new();
490 for shape in &shapes {
491 for contour in shape {
492 if contour.len() >= 3 {
493 result = contour.clone();
494 break;
495 }
496 }
497 if !result.is_empty() {
498 break;
499 }
500 }
501
502 if result.len() < 3 {
503 return Err(Error::InvalidGeometry(
504 "Geometry too large to fit in boundary".into(),
505 ));
506 }
507 }
508
509 let result_tuples: Vec<(f64, f64)> = result.iter().map(|&[x, y]| (x, y)).collect();
511 Ok(Nfp::from_polygon(result_tuples))
512}
513
514fn shrink_polygon(polygon: &[(f64, f64)], offset: f64) -> Result<Vec<(f64, f64)>> {
519 if polygon.len() < 3 {
520 return Err(Error::InvalidGeometry(
521 "Polygon must have at least 3 vertices".into(),
522 ));
523 }
524
525 if polygon.len() == 4 {
527 let (min_x, min_y, max_x, max_y) = bounding_box(polygon);
528
529 let is_axis_aligned = polygon.iter().all(|&(x, y)| {
531 ((x - min_x).abs() < 1e-10 || (x - max_x).abs() < 1e-10)
532 && ((y - min_y).abs() < 1e-10 || (y - max_y).abs() < 1e-10)
533 });
534
535 if is_axis_aligned {
536 let new_min_x = min_x + offset;
538 let new_min_y = min_y + offset;
539 let new_max_x = max_x - offset;
540 let new_max_y = max_y - offset;
541
542 if new_min_x >= new_max_x || new_min_y >= new_max_y {
544 return Err(Error::InvalidGeometry("Offset polygon collapsed".into()));
545 }
546
547 return Ok(vec![
548 (new_min_x, new_min_y),
549 (new_max_x, new_min_y),
550 (new_max_x, new_max_y),
551 (new_min_x, new_max_y),
552 ]);
553 }
554 }
555
556 let (cx, cy) = polygon_centroid(polygon);
558
559 let result: Vec<(f64, f64)> = polygon
560 .iter()
561 .filter_map(|&(x, y)| {
562 let dx = x - cx;
563 let dy = y - cy;
564 let dist = (dx * dx + dy * dy).sqrt();
565
566 if dist < offset + 1e-10 {
567 return None;
569 }
570
571 let factor = (dist - offset) / dist;
573 Some((cx + dx * factor, cy + dy * factor))
574 })
575 .collect();
576
577 if result.len() < 3 {
579 return Err(Error::InvalidGeometry("Offset polygon collapsed".into()));
580 }
581
582 let area = signed_area(&result).abs();
584 if area <= 1e-10 {
585 return Err(Error::InvalidGeometry("Offset polygon collapsed".into()));
586 }
587
588 Ok(result)
589}
590
591fn bounding_box(polygon: &[(f64, f64)]) -> (f64, f64, f64, f64) {
593 let mut min_x = f64::INFINITY;
594 let mut min_y = f64::INFINITY;
595 let mut max_x = f64::NEG_INFINITY;
596 let mut max_y = f64::NEG_INFINITY;
597
598 for &(x, y) in polygon {
599 min_x = min_x.min(x);
600 min_y = min_y.min(y);
601 max_x = max_x.max(x);
602 max_y = max_y.max(y);
603 }
604
605 (min_x, min_y, max_x, max_y)
606}
607
608fn compute_nfp_convex(stationary: &[(f64, f64)], orbiting: &[(f64, f64)]) -> Result<Nfp> {
613 use u_nesting_core::geom::minkowski::nfp_convex;
614
615 let polygon = nfp_convex(stationary, orbiting);
616 Ok(Nfp::from_polygon(polygon))
617}
618
619fn compute_minkowski_sum_convex(poly_a: &[(f64, f64)], poly_b: &[(f64, f64)]) -> Result<Nfp> {
623 use u_nesting_core::geom::minkowski::minkowski_sum_convex;
624
625 let polygon = minkowski_sum_convex(poly_a, poly_b);
626 Ok(Nfp::from_polygon(polygon))
627}
628
629fn compute_nfp_general(stationary: &Geometry2D, rotated_orbiting: &[(f64, f64)]) -> Result<Nfp> {
636 let stat_triangles = triangulate_polygon(stationary.exterior());
638 let orb_triangles = triangulate_polygon(rotated_orbiting);
639
640 if stat_triangles.is_empty() || orb_triangles.is_empty() {
641 let stat_hull = stationary.convex_hull();
643 let orb_hull = convex_hull_of_points(rotated_orbiting);
644 let reflected: Vec<(f64, f64)> = orb_hull.iter().map(|&(x, y)| (-x, -y)).collect();
645 return compute_minkowski_sum_convex(&stat_hull, &reflected);
646 }
647
648 let pairs: Vec<_> = stat_triangles
651 .iter()
652 .flat_map(|stat_tri| {
653 orb_triangles
654 .iter()
655 .map(move |orb_tri| (stat_tri.clone(), orb_tri.clone()))
656 })
657 .collect();
658
659 #[cfg(feature = "parallel")]
660 let partial_nfps: Vec<Vec<(f64, f64)>> = pairs
661 .par_iter()
662 .flat_map(|(stat_tri, orb_tri)| {
663 let reflected: Vec<(f64, f64)> = orb_tri.iter().map(|&(x, y)| (-x, -y)).collect();
664 if let Ok(nfp) = compute_minkowski_sum_convex(stat_tri, &reflected) {
665 nfp.polygons
666 .into_iter()
667 .filter(|polygon| polygon.len() >= 3)
668 .collect::<Vec<_>>()
669 } else {
670 Vec::new()
671 }
672 })
673 .collect();
674 #[cfg(not(feature = "parallel"))]
675 let partial_nfps: Vec<Vec<(f64, f64)>> = pairs
676 .iter()
677 .flat_map(|(stat_tri, orb_tri)| {
678 let reflected: Vec<(f64, f64)> = orb_tri.iter().map(|&(x, y)| (-x, -y)).collect();
679 if let Ok(nfp) = compute_minkowski_sum_convex(stat_tri, &reflected) {
680 nfp.polygons
681 .into_iter()
682 .filter(|polygon| polygon.len() >= 3)
683 .collect::<Vec<_>>()
684 } else {
685 Vec::new()
686 }
687 })
688 .collect();
689
690 if partial_nfps.is_empty() {
691 let stat_hull = stationary.convex_hull();
693 let orb_hull = convex_hull_of_points(rotated_orbiting);
694 let reflected: Vec<(f64, f64)> = orb_hull.iter().map(|&(x, y)| (-x, -y)).collect();
695 return compute_minkowski_sum_convex(&stat_hull, &reflected);
696 }
697
698 union_polygons(&partial_nfps)
700}
701
702fn triangulate_polygon(polygon: &[(f64, f64)]) -> Vec<Vec<(f64, f64)>> {
704 if polygon.len() < 3 {
705 return Vec::new();
706 }
707
708 if is_polygon_convex(polygon) {
710 return vec![polygon.to_vec()];
711 }
712
713 let mut vertices: Vec<(f64, f64)> = ensure_ccw(polygon);
715 let mut triangles = Vec::new();
716
717 while vertices.len() > 3 {
718 let n = vertices.len();
719 let mut ear_found = false;
720
721 for i in 0..n {
722 let prev = (i + n - 1) % n;
723 let next = (i + 1) % n;
724
725 if is_ear(&vertices, prev, i, next) {
727 triangles.push(vec![vertices[prev], vertices[i], vertices[next]]);
728 vertices.remove(i);
729 ear_found = true;
730 break;
731 }
732 }
733
734 if !ear_found {
735 return vec![convex_hull_of_points(polygon)];
738 }
739 }
740
741 if vertices.len() == 3 {
742 triangles.push(vertices);
743 }
744
745 triangles
746}
747
748fn point_in_triangle_robust(p: (f64, f64), a: (f64, f64), b: (f64, f64), c: (f64, f64)) -> bool {
752 let o1 = orient2d_filtered(a, b, p);
753 let o2 = orient2d_filtered(b, c, p);
754 let o3 = orient2d_filtered(c, a, p);
755
756 (o1 == Orientation::CounterClockwise
759 && o2 == Orientation::CounterClockwise
760 && o3 == Orientation::CounterClockwise)
761 || (o1 == Orientation::Clockwise
762 && o2 == Orientation::Clockwise
763 && o3 == Orientation::Clockwise)
764}
765
766fn is_ear(vertices: &[(f64, f64)], prev: usize, curr: usize, next: usize) -> bool {
770 let a = vertices[prev];
771 let b = vertices[curr];
772 let c = vertices[next];
773
774 let orientation = orient2d_filtered(a, b, c);
777 if !orientation.is_ccw() {
778 return false; }
780
781 for (i, &p) in vertices.iter().enumerate() {
783 if i == prev || i == curr || i == next {
784 continue;
785 }
786 if point_in_triangle_robust(p, a, b, c) {
787 return false;
788 }
789 }
790
791 true
792}
793
794fn union_polygons(polygons: &[Vec<(f64, f64)>]) -> Result<Nfp> {
796 if polygons.is_empty() {
797 return Ok(Nfp::new());
798 }
799
800 if polygons.len() == 1 {
801 return Ok(Nfp::from_polygon(polygons[0].clone()));
802 }
803
804 let mut result: Vec<Vec<[f64; 2]>> = vec![polygons[0].iter().map(|&(x, y)| [x, y]).collect()];
806
807 for polygon in &polygons[1..] {
809 let clip: Vec<[f64; 2]> = polygon.iter().map(|&(x, y)| [x, y]).collect();
810
811 let shapes = result.overlay(&[clip], OverlayRule::Union, FillRule::NonZero);
813
814 result = Vec::new();
816 for shape in shapes {
817 for contour in shape {
818 if contour.len() >= 3 {
819 result.push(contour);
820 }
821 }
822 }
823
824 if result.is_empty() {
825 continue;
827 }
828 }
829
830 let nfp_polygons: Vec<Vec<(f64, f64)>> = result
832 .into_iter()
833 .map(|contour| contour.into_iter().map(|[x, y]| (x, y)).collect())
834 .collect();
835
836 if nfp_polygons.is_empty() {
837 return Ok(Nfp::from_polygon(polygons[0].clone()));
839 }
840
841 Ok(Nfp::from_polygons(nfp_polygons))
842}
843
844fn rotate_polygon(polygon: &[(f64, f64)], angle: f64) -> Vec<(f64, f64)> {
850 if angle.abs() < 1e-10 {
851 return polygon.to_vec();
852 }
853
854 let cos_a = angle.cos();
855 let sin_a = angle.sin();
856
857 polygon
858 .iter()
859 .map(|&(x, y)| (x * cos_a - y * sin_a, x * sin_a + y * cos_a))
860 .collect()
861}
862
863fn is_polygon_convex(polygon: &[(f64, f64)]) -> bool {
865 geom_polygon::is_convex(polygon)
866}
867
868fn ensure_ccw(polygon: &[(f64, f64)]) -> Vec<(f64, f64)> {
870 geom_polygon::ensure_ccw(polygon)
871}
872
873fn signed_area(polygon: &[(f64, f64)]) -> f64 {
876 geom_polygon::signed_area(polygon)
877}
878
879fn convex_hull_of_points(points: &[(f64, f64)]) -> Vec<(f64, f64)> {
881 geom_polygon::convex_hull(points)
882}
883
884pub fn point_in_polygon(point: (f64, f64), polygon: &[(f64, f64)]) -> bool {
890 let (px, py) = point;
891 let n = polygon.len();
892 let mut inside = false;
893
894 let mut j = n - 1;
895 for i in 0..n {
896 let (xi, yi) = polygon[i];
897 let (xj, yj) = polygon[j];
898
899 if ((yi > py) != (yj > py)) && (px < (xj - xi) * (py - yi) / (yj - yi) + xi) {
900 inside = !inside;
901 }
902 j = i;
903 }
904
905 inside
906}
907
908pub fn point_outside_all_nfps(point: (f64, f64), nfps: &[&Nfp]) -> bool {
910 for nfp in nfps {
911 for polygon in &nfp.polygons {
912 if point_in_polygon(point, polygon) {
913 return false;
914 }
915 }
916 }
917 true
918}
919
920fn point_outside_all_nfps_strict(point: (f64, f64), nfps: &[&Nfp]) -> bool {
923 for nfp in nfps {
924 for polygon in &nfp.polygons {
925 if point_in_polygon(point, polygon) {
927 return false;
928 }
929 }
930 }
931 true
932}
933
934fn point_on_polygon_boundary(point: (f64, f64), polygon: &[(f64, f64)]) -> bool {
936 let (px, py) = point;
937 let n = polygon.len();
938 const EPS: f64 = 1e-10;
939
940 for i in 0..n {
941 let (x1, y1) = polygon[i];
942 let (x2, y2) = polygon[(i + 1) % n];
943
944 let dx = x2 - x1;
947 let dy = y2 - y1;
948 let len_sq = dx * dx + dy * dy;
949
950 if len_sq < EPS * EPS {
951 if (px - x1).abs() < EPS && (py - y1).abs() < EPS {
953 return true;
954 }
955 continue;
956 }
957
958 let t = ((px - x1) * dx + (py - y1) * dy) / len_sq;
960
961 if (-EPS..=1.0 + EPS).contains(&t) {
963 let proj_x = x1 + t * dx;
965 let proj_y = y1 + t * dy;
966 let dist_sq = (px - proj_x).powi(2) + (py - proj_y).powi(2);
967
968 if dist_sq < EPS * EPS {
969 return true;
970 }
971 }
972 }
973
974 false
975}
976
977pub fn find_bottom_left_placement(
998 ifp: &Nfp,
999 nfps: &[&Nfp],
1000 sample_step: f64,
1001) -> Option<(f64, f64)> {
1002 if ifp.is_empty() {
1003 return None;
1004 }
1005
1006 let mut candidates: Vec<(f64, f64)> = Vec::new();
1008
1009 for polygon in &ifp.polygons {
1010 candidates.extend(polygon.iter().copied());
1011 }
1012
1013 for nfp in nfps {
1015 for polygon in &nfp.polygons {
1016 candidates.extend(polygon.iter().copied());
1017 }
1018 }
1019
1020 let (min_x, min_y, max_x, max_y) = ifp_bounding_box(ifp);
1022
1023 let mut y = min_y;
1025 while y <= max_y {
1026 let mut x = min_x;
1027 while x <= max_x {
1028 candidates.push((x, y));
1029 x += sample_step;
1030 }
1031 y += sample_step;
1032 }
1033
1034 let valid_candidates: Vec<(f64, f64)> = candidates
1036 .into_iter()
1037 .filter(|&point| {
1038 let in_ifp = ifp
1040 .polygons
1041 .iter()
1042 .any(|p| point_in_polygon(point, p) || point_on_polygon_boundary(point, p));
1043 if !in_ifp {
1044 return false;
1045 }
1046 point_outside_all_nfps_strict(point, nfps)
1048 })
1049 .collect();
1050
1051 valid_candidates.into_iter().min_by(|a, b| {
1054 match a.0.partial_cmp(&b.0) {
1056 Some(std::cmp::Ordering::Equal) => {
1057 a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)
1058 }
1059 Some(ord) => ord,
1060 None => std::cmp::Ordering::Equal,
1061 }
1062 })
1063}
1064
1065fn ifp_bounding_box(ifp: &Nfp) -> (f64, f64, f64, f64) {
1067 let mut min_x = f64::INFINITY;
1068 let mut min_y = f64::INFINITY;
1069 let mut max_x = f64::NEG_INFINITY;
1070 let mut max_y = f64::NEG_INFINITY;
1071
1072 for polygon in &ifp.polygons {
1073 for &(x, y) in polygon {
1074 min_x = min_x.min(x);
1075 min_y = min_y.min(y);
1076 max_x = max_x.max(x);
1077 max_y = max_y.max(y);
1078 }
1079 }
1080
1081 (min_x, min_y, max_x, max_y)
1082}
1083
1084#[derive(Debug, Clone)]
1086pub struct PlacedGeometry {
1087 pub geometry: Geometry2D,
1089 pub position: (f64, f64),
1091 pub rotation: f64,
1093}
1094
1095impl PlacedGeometry {
1096 pub fn new(geometry: Geometry2D, position: (f64, f64), rotation: f64) -> Self {
1098 Self {
1099 geometry,
1100 position,
1101 rotation,
1102 }
1103 }
1104
1105 pub fn translated_exterior(&self) -> Vec<(f64, f64)> {
1107 let rotated = rotate_polygon(self.geometry.exterior(), self.rotation);
1108 rotated
1109 .into_iter()
1110 .map(|(x, y)| (x + self.position.0, y + self.position.1))
1111 .collect()
1112 }
1113}
1114
1115pub fn verify_no_overlap(
1130 geometry: &Geometry2D,
1131 position: (f64, f64),
1132 rotation: f64,
1133 placed_geometries: &[PlacedGeometry],
1134) -> bool {
1135 use crate::nfp_sliding::polygons_overlap;
1136
1137 let rotated = rotate_polygon(geometry.exterior(), rotation);
1139 let transformed: Vec<(f64, f64)> = rotated
1140 .into_iter()
1141 .map(|(x, y)| (x + position.0, y + position.1))
1142 .collect();
1143
1144 for placed in placed_geometries {
1146 let placed_polygon = placed.translated_exterior();
1147
1148 if polygons_overlap(&transformed, &placed_polygon) {
1149 return false; }
1151 }
1152
1153 true }
1155
1156#[derive(Debug, Clone, Hash, PartialEq, Eq)]
1162struct NfpCacheKey {
1163 geometry_a: String,
1164 geometry_b: String,
1165 rotation_millideg: i32, }
1167
1168impl NfpCacheKey {
1169 fn new(id_a: &str, id_b: &str, rotation_rad: f64) -> Self {
1170 let rotation_millideg = ((rotation_rad * 180.0 / PI) * 1000.0).round() as i32;
1172 Self {
1173 geometry_a: id_a.to_string(),
1174 geometry_b: id_b.to_string(),
1175 rotation_millideg,
1176 }
1177 }
1178}
1179
1180#[derive(Debug)]
1182pub struct NfpCache {
1183 cache: RwLock<HashMap<NfpCacheKey, Arc<Nfp>>>,
1184 max_size: usize,
1185}
1186
1187impl NfpCache {
1188 pub fn new() -> Self {
1190 Self::with_capacity(1000)
1191 }
1192
1193 pub fn with_capacity(max_size: usize) -> Self {
1195 Self {
1196 cache: RwLock::new(HashMap::new()),
1197 max_size,
1198 }
1199 }
1200
1201 pub fn get_or_compute<F>(&self, key: (&str, &str, f64), compute: F) -> Result<Arc<Nfp>>
1207 where
1208 F: FnOnce() -> Result<Nfp>,
1209 {
1210 let cache_key = NfpCacheKey::new(key.0, key.1, key.2);
1211
1212 {
1214 let cache = self.cache.read().map_err(|e| {
1215 Error::Internal(format!("Failed to acquire cache read lock: {}", e))
1216 })?;
1217 if let Some(nfp) = cache.get(&cache_key) {
1218 return Ok(Arc::clone(nfp));
1219 }
1220 }
1221
1222 let nfp = Arc::new(compute()?);
1224
1225 {
1227 let mut cache = self.cache.write().map_err(|e| {
1228 Error::Internal(format!("Failed to acquire cache write lock: {}", e))
1229 })?;
1230
1231 if cache.len() >= self.max_size {
1233 let keys_to_remove: Vec<_> =
1234 cache.keys().take(self.max_size / 2).cloned().collect();
1235 for key in keys_to_remove {
1236 cache.remove(&key);
1237 }
1238 }
1239
1240 cache.insert(cache_key, Arc::clone(&nfp));
1241 }
1242
1243 Ok(nfp)
1244 }
1245
1246 pub fn len(&self) -> usize {
1248 self.cache.read().map(|c| c.len()).unwrap_or(0)
1249 }
1250
1251 pub fn is_empty(&self) -> bool {
1253 self.len() == 0
1254 }
1255
1256 pub fn clear(&self) {
1258 if let Ok(mut cache) = self.cache.write() {
1259 cache.clear();
1260 }
1261 }
1262}
1263
1264impl Default for NfpCache {
1265 fn default() -> Self {
1266 Self::new()
1267 }
1268}
1269
1270#[cfg(test)]
1271mod tests {
1272 use super::*;
1273 use approx::assert_relative_eq;
1274
1275 fn rect(w: f64, h: f64) -> Vec<(f64, f64)> {
1276 vec![(0.0, 0.0), (w, 0.0), (w, h), (0.0, h)]
1277 }
1278
1279 fn triangle() -> Vec<(f64, f64)> {
1280 vec![(0.0, 0.0), (10.0, 0.0), (5.0, 10.0)]
1281 }
1282
1283 #[test]
1284 fn test_is_polygon_convex() {
1285 assert!(is_polygon_convex(&rect(10.0, 10.0)));
1287
1288 assert!(is_polygon_convex(&triangle()));
1290
1291 let l_shape = vec![
1293 (0.0, 0.0),
1294 (10.0, 0.0),
1295 (10.0, 5.0),
1296 (5.0, 5.0),
1297 (5.0, 10.0),
1298 (0.0, 10.0),
1299 ];
1300 assert!(!is_polygon_convex(&l_shape));
1301 }
1302
1303 #[test]
1304 fn test_signed_area() {
1305 let ccw_square = rect(10.0, 10.0);
1307 assert!(signed_area(&ccw_square) > 0.0);
1308 assert_relative_eq!(signed_area(&ccw_square).abs(), 100.0, epsilon = 1e-10);
1309
1310 let cw_square: Vec<_> = ccw_square.into_iter().rev().collect();
1312 assert!(signed_area(&cw_square) < 0.0);
1313 }
1314
1315 #[test]
1316 fn test_rotate_polygon() {
1317 let square = rect(10.0, 10.0);
1318
1319 let rotated = rotate_polygon(&square, 0.0);
1321 assert_eq!(rotated.len(), square.len());
1322
1323 let rotated = rotate_polygon(&[(1.0, 0.0)], PI / 2.0);
1325 assert_relative_eq!(rotated[0].0, 0.0, epsilon = 1e-10);
1326 assert_relative_eq!(rotated[0].1, 1.0, epsilon = 1e-10);
1327 }
1328
1329 #[test]
1330 fn test_nfp_two_squares() {
1331 let a = Geometry2D::rectangle("A", 10.0, 10.0);
1332 let b = Geometry2D::rectangle("B", 5.0, 5.0);
1333
1334 let nfp = compute_nfp(&a, &b, 0.0).unwrap();
1335
1336 assert!(!nfp.is_empty());
1337 assert_eq!(nfp.polygons.len(), 1);
1338
1339 let polygon = &nfp.polygons[0];
1342 assert!(polygon.len() >= 4);
1343 }
1344
1345 #[test]
1346 fn test_nfp_with_rotation() {
1347 let a = Geometry2D::rectangle("A", 10.0, 10.0);
1348 let b = Geometry2D::rectangle("B", 5.0, 5.0);
1349
1350 let nfp = compute_nfp(&a, &b, PI / 4.0).unwrap();
1352
1353 assert!(!nfp.is_empty());
1354 }
1356
1357 #[test]
1358 fn test_ifp_square_in_boundary() {
1359 let boundary = rect(100.0, 100.0);
1360 let geom = Geometry2D::rectangle("G", 10.0, 10.0);
1361
1362 let ifp = compute_ifp(&boundary, &geom, 0.0).unwrap();
1363
1364 assert!(!ifp.is_empty());
1365 let polygon = &ifp.polygons[0];
1368 let (min_x, min_y, max_x, max_y) = bounding_box(polygon);
1369 assert_relative_eq!(min_x, 0.0, epsilon = 1e-10);
1370 assert_relative_eq!(min_y, 0.0, epsilon = 1e-10);
1371 assert_relative_eq!(max_x, 90.0, epsilon = 1e-10);
1372 assert_relative_eq!(max_y, 90.0, epsilon = 1e-10);
1373 }
1374
1375 #[test]
1376 fn test_ifp_bounds_correct() {
1377 let boundary = rect(100.0, 50.0);
1379 let geom = Geometry2D::rectangle("R", 25.0, 25.0);
1380
1381 let ifp = compute_ifp(&boundary, &geom, 0.0).unwrap();
1382
1383 assert!(!ifp.is_empty());
1384 let polygon = &ifp.polygons[0];
1386 let (min_x, min_y, max_x, max_y) = bounding_box(polygon);
1387 assert_relative_eq!(min_x, 0.0, epsilon = 1e-10);
1388 assert_relative_eq!(min_y, 0.0, epsilon = 1e-10);
1389 assert_relative_eq!(max_x, 75.0, epsilon = 1e-10);
1390 assert_relative_eq!(max_y, 25.0, epsilon = 1e-10);
1391
1392 assert!(point_in_polygon((0.0, 0.0), polygon) || point_on_boundary((0.0, 0.0), polygon));
1394 assert!(point_in_polygon((25.0, 0.0), polygon) || point_on_boundary((25.0, 0.0), polygon));
1395 assert!(point_in_polygon((50.0, 0.0), polygon) || point_on_boundary((50.0, 0.0), polygon));
1396 assert!(point_in_polygon((75.0, 0.0), polygon) || point_on_boundary((75.0, 0.0), polygon));
1397 }
1398
1399 fn point_on_boundary(point: (f64, f64), polygon: &[(f64, f64)]) -> bool {
1401 let (px, py) = point;
1402 let n = polygon.len();
1403 for i in 0..n {
1404 let (x1, y1) = polygon[i];
1405 let (x2, y2) = polygon[(i + 1) % n];
1406 let d1 = ((px - x1).powi(2) + (py - y1).powi(2)).sqrt();
1408 let d2 = ((px - x2).powi(2) + (py - y2).powi(2)).sqrt();
1409 let d_total = ((x2 - x1).powi(2) + (y2 - y1).powi(2)).sqrt();
1410 if (d1 + d2 - d_total).abs() < 1e-10 {
1411 return true;
1412 }
1413 }
1414 false
1415 }
1416
1417 #[test]
1418 fn test_nfp_same_size_rectangles() {
1419 let a = Geometry2D::rectangle("A", 25.0, 25.0);
1421 let b = Geometry2D::rectangle("B", 25.0, 25.0);
1422
1423 let nfp = compute_nfp(&a, &b, 0.0).unwrap();
1424 assert!(!nfp.is_empty());
1425
1426 let polygon = &nfp.polygons[0];
1427 let (min_x, min_y, max_x, max_y) = bounding_box(polygon);
1428 let width = max_x - min_x;
1431 let height = max_y - min_y;
1432 eprintln!("NFP dimensions: {}x{}", width, height);
1433 eprintln!(
1434 "NFP bounds: ({}, {}) to ({}, {})",
1435 min_x, min_y, max_x, max_y
1436 );
1437 assert_relative_eq!(width, 50.0, epsilon = 1e-6);
1439 assert_relative_eq!(height, 50.0, epsilon = 1e-6);
1440 }
1441
1442 #[test]
1443 fn test_nfp_cache() {
1444 let cache = NfpCache::new();
1445
1446 let compute_count = std::sync::atomic::AtomicUsize::new(0);
1447
1448 let result1 = cache
1449 .get_or_compute(("A", "B", 0.0), || {
1450 compute_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1451 Ok(Nfp::from_polygon(vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)]))
1452 })
1453 .unwrap();
1454
1455 let result2 = cache
1456 .get_or_compute(("A", "B", 0.0), || {
1457 compute_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1458 Ok(Nfp::from_polygon(vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)]))
1459 })
1460 .unwrap();
1461
1462 assert_eq!(compute_count.load(std::sync::atomic::Ordering::SeqCst), 1);
1464 assert_eq!(result1.polygons, result2.polygons);
1465 assert_eq!(cache.len(), 1);
1466 }
1467
1468 #[test]
1469 fn test_nfp_cache_different_rotations() {
1470 let cache = NfpCache::new();
1471
1472 cache
1473 .get_or_compute(("A", "B", 0.0), || {
1474 Ok(Nfp::from_polygon(vec![(0.0, 0.0), (1.0, 0.0)]))
1475 })
1476 .unwrap();
1477
1478 cache
1479 .get_or_compute(("A", "B", PI / 2.0), || {
1480 Ok(Nfp::from_polygon(vec![(0.0, 0.0), (0.0, 1.0)]))
1481 })
1482 .unwrap();
1483
1484 assert_eq!(cache.len(), 2);
1486 }
1487
1488 #[test]
1489 fn test_convex_hull_of_points() {
1490 let points = vec![
1491 (0.0, 0.0),
1492 (10.0, 0.0),
1493 (5.0, 5.0), (10.0, 10.0),
1495 (0.0, 10.0),
1496 ];
1497
1498 let hull = convex_hull_of_points(&points);
1499
1500 assert_eq!(hull.len(), 4);
1502 }
1503
1504 #[test]
1505 fn test_shrink_polygon_square() {
1506 let square = rect(100.0, 100.0);
1507 let shrunk = shrink_polygon(&square, 10.0).unwrap();
1508
1509 assert_eq!(shrunk.len(), 4);
1511
1512 let original_area = signed_area(&square).abs();
1514 let shrunk_area = signed_area(&shrunk).abs();
1515 assert!(
1516 shrunk_area < original_area,
1517 "shrunk_area ({}) should be < original_area ({})",
1518 shrunk_area,
1519 original_area
1520 );
1521
1522 assert_relative_eq!(shrunk_area, 6400.0, epsilon = 1.0);
1525 }
1526
1527 #[test]
1528 fn test_shrink_polygon_collapse() {
1529 let small_square = rect(10.0, 10.0);
1530
1531 let result = shrink_polygon(&small_square, 6.0);
1533 assert!(
1534 result.is_err(),
1535 "Polygon should collapse when offset >= width/2"
1536 );
1537 }
1538
1539 #[test]
1540 fn test_ifp_with_margin() {
1541 let boundary = rect(100.0, 100.0);
1542 let geom = Geometry2D::rectangle("G", 10.0, 10.0);
1543
1544 let ifp_no_margin = compute_ifp(&boundary, &geom, 0.0).unwrap();
1546
1547 let ifp_with_margin = compute_ifp_with_margin(&boundary, &geom, 0.0, 5.0).unwrap();
1549
1550 assert!(!ifp_no_margin.is_empty());
1551 assert!(!ifp_with_margin.is_empty());
1552
1553 let (min_x_no, _min_y_no, max_x_no, _max_y_no) = ifp_bounding_box(&ifp_no_margin);
1555 let (min_x_margin, _min_y_margin, max_x_margin, _max_y_margin) =
1556 ifp_bounding_box(&ifp_with_margin);
1557
1558 let width_no = max_x_no - min_x_no;
1559 let width_margin = max_x_margin - min_x_margin;
1560
1561 assert!(
1565 width_margin < width_no,
1566 "width_margin ({}) should be < width_no ({})",
1567 width_margin,
1568 width_no
1569 );
1570 }
1571
1572 #[test]
1573 fn test_ifp_margin_boundary_collapse() {
1574 let boundary = rect(20.0, 20.0);
1575
1576 let result = shrink_polygon(&boundary, 12.0);
1578 assert!(
1579 result.is_err(),
1580 "Boundary should collapse with margin >= width/2"
1581 );
1582 }
1583
1584 #[test]
1585 fn test_ifp_margin_large_geometry() {
1586 let boundary = rect(30.0, 30.0);
1587 let geom = Geometry2D::rectangle("G", 20.0, 20.0);
1588
1589 let ifp_no_margin = compute_ifp(&boundary, &geom, 0.0).unwrap();
1591 let (min_x_no, _, max_x_no, _) = ifp_bounding_box(&ifp_no_margin);
1592 let width_no = max_x_no - min_x_no;
1593
1594 let ifp_with_margin = compute_ifp_with_margin(&boundary, &geom, 0.0, 5.0).unwrap();
1596 let (min_x_margin, _, max_x_margin, _) = ifp_bounding_box(&ifp_with_margin);
1597 let width_margin = max_x_margin - min_x_margin;
1598
1599 assert!(
1601 width_margin <= width_no,
1602 "width_margin ({}) should be <= width_no ({})",
1603 width_margin,
1604 width_no
1605 );
1606 }
1607
1608 #[test]
1609 fn test_nfp_non_convex_l_shape() {
1610 let l_shape = Geometry2D::new("L").with_polygon(vec![
1612 (0.0, 0.0),
1613 (20.0, 0.0),
1614 (20.0, 10.0),
1615 (10.0, 10.0),
1616 (10.0, 20.0),
1617 (0.0, 20.0),
1618 ]);
1619
1620 let small_square = Geometry2D::rectangle("S", 5.0, 5.0);
1621
1622 let nfp = compute_nfp(&l_shape, &small_square, 0.0).unwrap();
1624
1625 assert!(!nfp.is_empty());
1626 assert!(nfp.vertex_count() >= 4);
1628 }
1629
1630 #[test]
1631 fn test_triangulate_polygon_convex() {
1632 let square = rect(10.0, 10.0);
1633 let triangles = triangulate_polygon(&square);
1634
1635 assert_eq!(triangles.len(), 1);
1637 assert_eq!(triangles[0].len(), 4);
1638 }
1639
1640 #[test]
1641 fn test_triangulate_polygon_non_convex() {
1642 let l_shape = vec![
1644 (0.0, 0.0),
1645 (20.0, 0.0),
1646 (20.0, 10.0),
1647 (10.0, 10.0),
1648 (10.0, 20.0),
1649 (0.0, 20.0),
1650 ];
1651
1652 let triangles = triangulate_polygon(&l_shape);
1653
1654 assert!(!triangles.is_empty());
1656 }
1657
1658 #[test]
1659 fn test_union_polygons() {
1660 let poly1 = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
1662 let poly2 = vec![(5.0, 5.0), (15.0, 5.0), (15.0, 15.0), (5.0, 15.0)];
1663
1664 let result = union_polygons(&[poly1, poly2]).unwrap();
1665
1666 assert!(!result.is_empty());
1667 assert!(result.vertex_count() >= 6);
1669 }
1670
1671 #[test]
1676 fn test_convex_near_collinear_vertices() {
1677 let near_collinear = vec![
1679 (0.0, 0.0),
1680 (1.0, 1e-15), (2.0, 0.0),
1682 (2.0, 1.0),
1683 (0.0, 1.0),
1684 ];
1685
1686 let result = is_polygon_convex(&near_collinear);
1688 let _ = result; }
1691
1692 #[test]
1693 fn test_triangulation_near_degenerate() {
1694 let near_degenerate_l = vec![
1696 (0.0, 0.0),
1697 (10.0, 0.0),
1698 (10.0, 5.0),
1699 (5.0 + 1e-12, 5.0), (5.0, 10.0),
1701 (0.0, 10.0),
1702 ];
1703
1704 let triangles = triangulate_polygon(&near_degenerate_l);
1706
1707 assert!(!triangles.is_empty());
1709 }
1710
1711 #[test]
1712 fn test_nfp_nearly_touching_rectangles() {
1713 let a = Geometry2D::rectangle("A", 10.0, 10.0);
1715 let b = Geometry2D::rectangle("B", 5.0, 5.0);
1716
1717 let nfp = compute_nfp(&a, &b, 0.0).unwrap();
1719 assert!(!nfp.is_empty());
1720 }
1721
1722 #[test]
1723 fn test_ifp_geometry_nearly_fills_boundary() {
1724 let boundary = rect(100.0, 100.0);
1726 let geom = Geometry2D::rectangle("G", 99.9999, 99.9999);
1727
1728 let result = compute_ifp(&boundary, &geom, 0.0);
1730
1731 match result {
1733 Ok(ifp) => {
1734 let (min_x, min_y, max_x, max_y) = ifp_bounding_box(&ifp);
1736 let width = max_x - min_x;
1737 let height = max_y - min_y;
1738 assert!(width < 0.001 && height < 0.001);
1739 }
1740 Err(_) => {
1741 }
1743 }
1744 }
1745
1746 #[test]
1747 fn test_point_in_polygon_on_boundary() {
1748 let square = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
1750
1751 let on_bottom_edge = (5.0, 0.0);
1753 let on_right_edge = (10.0, 5.0);
1754 let on_top_edge = (5.0, 10.0);
1755 let on_left_edge = (0.0, 5.0);
1756
1757 let _ = point_in_polygon(on_bottom_edge, &square);
1760 let _ = point_in_polygon(on_right_edge, &square);
1761 let _ = point_in_polygon(on_top_edge, &square);
1762 let _ = point_in_polygon(on_left_edge, &square);
1763 }
1764
1765 #[test]
1766 fn test_point_in_triangle_robust_degenerate() {
1767 let a = (0.0, 0.0);
1769 let b = (5.0, 0.0);
1770 let c = (10.0, 0.0);
1771
1772 let p = (3.0, 0.0);
1774
1775 assert!(!point_in_triangle_robust(p, a, b, c));
1777 }
1778
1779 #[test]
1780 fn test_ear_detection_with_collinear_points() {
1781 let with_collinear = vec![
1783 (0.0, 0.0),
1784 (5.0, 0.0),
1785 (10.0, 0.0), (10.0, 10.0),
1787 (0.0, 10.0),
1788 ];
1789
1790 let triangles = triangulate_polygon(&with_collinear);
1792
1793 for triangle in &triangles {
1795 assert!(triangle.len() >= 3);
1796 }
1797 }
1798
1799 #[test]
1800 fn test_nfp_with_very_small_polygon() {
1801 let tiny = Geometry2D::rectangle("tiny", 1e-6, 1e-6);
1803 let normal = Geometry2D::rectangle("normal", 10.0, 10.0);
1804
1805 let nfp = compute_nfp(&normal, &tiny, 0.0).unwrap();
1807 assert!(!nfp.is_empty());
1808 }
1809
1810 #[test]
1811 fn test_nfp_with_very_large_polygon() {
1812 let large = Geometry2D::rectangle("large", 1e6, 1e6);
1814 let normal = Geometry2D::rectangle("normal", 100.0, 100.0);
1815
1816 let nfp = compute_nfp(&large, &normal, 0.0).unwrap();
1818 assert!(!nfp.is_empty());
1819 }
1820
1821 #[test]
1822 fn test_signed_area_with_extreme_coordinates() {
1823 let moderate_coords = vec![
1830 (1e6, 1e6),
1831 (1e6 + 100.0, 1e6),
1832 (1e6 + 100.0, 1e6 + 100.0),
1833 (1e6, 1e6 + 100.0),
1834 ];
1835
1836 let area = signed_area(&moderate_coords);
1837
1838 assert_relative_eq!(area.abs(), 10000.0, epsilon = 1.0);
1840 }
1841
1842 #[test]
1843 fn test_ensure_ccw_with_near_zero_area() {
1844 let tiny_area = vec![(0.0, 0.0), (1e-10, 0.0), (1e-10, 1e-10), (0.0, 1e-10)];
1846
1847 let ccw = ensure_ccw(&tiny_area);
1849 assert_eq!(ccw.len(), tiny_area.len());
1850 }
1851
1852 #[test]
1857 fn test_nfp_method_default() {
1858 let config = NfpConfig::default();
1859 assert_eq!(config.method, NfpMethod::MinkowskiSum);
1860 }
1861
1862 #[test]
1863 fn test_nfp_method_minkowski_sum() {
1864 let a = Geometry2D::rectangle("A", 10.0, 10.0);
1865 let b = Geometry2D::rectangle("B", 5.0, 5.0);
1866
1867 let nfp = compute_nfp_with_method(&a, &b, 0.0, NfpMethod::MinkowskiSum).unwrap();
1868
1869 assert!(!nfp.is_empty());
1870 assert!(nfp.vertex_count() >= 4);
1871 }
1872
1873 #[test]
1874 fn test_nfp_method_sliding() {
1875 let a = Geometry2D::rectangle("A", 10.0, 10.0);
1876 let b = Geometry2D::rectangle("B", 5.0, 5.0);
1877
1878 let result = compute_nfp_with_method(&a, &b, 0.0, NfpMethod::Sliding);
1879
1880 assert!(result.is_ok(), "Sliding method should not error");
1882 let nfp = result.unwrap();
1883 assert!(!nfp.is_empty(), "NFP should not be empty");
1884
1885 }
1889
1890 #[test]
1891 fn test_nfp_method_config_builder() {
1892 let config = NfpConfig::with_method(NfpMethod::Sliding)
1893 .with_tolerance(1e-5)
1894 .with_max_iterations(5000);
1895
1896 assert_eq!(config.method, NfpMethod::Sliding);
1897 assert!((config.contact_tolerance - 1e-5).abs() < 1e-10);
1898 assert_eq!(config.max_iterations, 5000);
1899 }
1900
1901 #[test]
1902 fn test_nfp_methods_both_succeed() {
1903 let a = Geometry2D::rectangle("A", 10.0, 10.0);
1904 let b = Geometry2D::rectangle("B", 5.0, 5.0);
1905
1906 let nfp_mink = compute_nfp_with_method(&a, &b, 0.0, NfpMethod::MinkowskiSum).unwrap();
1907 let nfp_slide = compute_nfp_with_method(&a, &b, 0.0, NfpMethod::Sliding).unwrap();
1908
1909 assert!(!nfp_mink.is_empty());
1911 assert!(!nfp_slide.is_empty());
1912
1913 assert!(nfp_mink.vertex_count() >= 4);
1915
1916 }
1921
1922 #[test]
1923 fn test_nfp_sliding_l_shape() {
1924 let l_shape = Geometry2D::new("L").with_polygon(vec![
1926 (0.0, 0.0),
1927 (20.0, 0.0),
1928 (20.0, 10.0),
1929 (10.0, 10.0),
1930 (10.0, 20.0),
1931 (0.0, 20.0),
1932 ]);
1933
1934 let small_square = Geometry2D::rectangle("S", 5.0, 5.0);
1935
1936 let result = compute_nfp_with_method(&l_shape, &small_square, 0.0, NfpMethod::Sliding);
1938
1939 assert!(result.is_ok(), "Sliding should not error on L-shape");
1941 let nfp = result.unwrap();
1942 assert!(!nfp.is_empty(), "NFP should not be empty for L-shape");
1943 }
1944
1945 #[test]
1946 fn test_nfp_with_config() {
1947 let a = Geometry2D::rectangle("A", 10.0, 10.0);
1948 let b = Geometry2D::rectangle("B", 5.0, 5.0);
1949
1950 let config = NfpConfig {
1951 method: NfpMethod::Sliding,
1952 contact_tolerance: 1e-4,
1953 max_iterations: 2000,
1954 };
1955
1956 let nfp = compute_nfp_with_config(&a, &b, 0.0, &config).unwrap();
1957
1958 assert!(!nfp.is_empty());
1959 }
1960}