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(stat_exterior, &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 compute_nfp_mirrored(stationary, orbiting, rotation, false, false)
293}
294
295pub fn compute_nfp_mirrored(
311 stationary: &Geometry2D,
312 orbiting: &Geometry2D,
313 rotation: f64,
314 mirror_stationary: bool,
315 mirror_orbiting: bool,
316) -> Result<Nfp> {
317 let stat_exterior = stationary.exterior();
319 let orb_exterior = orbiting.exterior();
320
321 if stat_exterior.len() < 3 || orb_exterior.len() < 3 {
322 return Err(Error::InvalidGeometry(
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::InvalidBoundary(
414 "Boundary must have at least 3 vertices".into(),
415 ));
416 }
417
418 let geom_exterior = geometry.exterior();
419 if geom_exterior.len() < 3 {
420 return Err(Error::InvalidGeometry(
421 "Geometry must have at least 3 vertices".into(),
422 ));
423 }
424
425 let base_geom = if mirror {
427 crate::polygon_ops::mirror_polygon(geom_exterior)
428 } else {
429 geom_exterior.to_vec()
430 };
431 let rotated_geom = rotate_polygon(&base_geom, rotation);
432
433 let effective_boundary = if margin > 0.0 {
435 shrink_polygon(boundary_polygon, margin)?
436 } else {
437 boundary_polygon.to_vec()
438 };
439
440 if effective_boundary.len() < 3 {
441 return Err(Error::InvalidBoundary(
442 "Boundary too small after applying margin".into(),
443 ));
444 }
445
446 compute_minkowski_erosion(&effective_boundary, &rotated_geom)
455}
456
457fn compute_minkowski_erosion(boundary: &[(f64, f64)], geometry: &[(f64, f64)]) -> Result<Nfp> {
463 if boundary.len() < 3 || geometry.len() < 3 {
464 return Err(Error::InvalidGeometry(
465 "Both boundary and geometry must have at least 3 vertices".into(),
466 ));
467 }
468
469 let (b_min_x, b_min_y, b_max_x, b_max_y) = bounding_box(boundary);
471 let is_rect = boundary.len() == 4
472 && boundary.iter().all(|&(x, y)| {
473 ((x - b_min_x).abs() < 1e-10 || (x - b_max_x).abs() < 1e-10)
474 && ((y - b_min_y).abs() < 1e-10 || (y - b_max_y).abs() < 1e-10)
475 });
476
477 let (g_min_x, g_min_y, g_max_x, g_max_y) = bounding_box(geometry);
479
480 if is_rect {
481 let ifp_min_x = b_min_x - g_min_x;
489 let ifp_max_x = b_max_x - g_max_x;
490 let ifp_min_y = b_min_y - g_min_y;
491 let ifp_max_y = b_max_y - g_max_y;
492
493 if ifp_min_x > ifp_max_x + 1e-10 || ifp_min_y > ifp_max_y + 1e-10 {
495 return Err(Error::InvalidGeometry(
496 "Geometry too large to fit in boundary".into(),
497 ));
498 }
499
500 let ifp_min_x = ifp_min_x.min(ifp_max_x);
502 let ifp_min_y = ifp_min_y.min(ifp_max_y);
503
504 return Ok(Nfp::from_polygon(vec![
505 (ifp_min_x, ifp_min_y),
506 (ifp_max_x, ifp_min_y),
507 (ifp_max_x, ifp_max_y),
508 (ifp_min_x, ifp_max_y),
509 ]));
510 }
511
512 compute_minkowski_erosion_general(boundary, geometry)
516}
517
518fn compute_minkowski_erosion_general(
520 boundary: &[(f64, f64)],
521 geometry: &[(f64, f64)],
522) -> Result<Nfp> {
523 if geometry.is_empty() {
524 return Ok(Nfp::from_polygon(boundary.to_vec()));
525 }
526
527 let first_g = geometry[0];
529 let mut result: Vec<[f64; 2]> = boundary
530 .iter()
531 .map(|&(x, y)| [x - first_g.0, y - first_g.1])
532 .collect();
533
534 for &(gx, gy) in geometry.iter().skip(1) {
536 let translated: Vec<[f64; 2]> = boundary.iter().map(|&(x, y)| [x - gx, y - gy]).collect();
537
538 let shapes = result.overlay(&[translated], OverlayRule::Intersect, FillRule::NonZero);
540
541 if shapes.is_empty() {
542 return Err(Error::InvalidGeometry(
543 "Geometry too large to fit in boundary".into(),
544 ));
545 }
546
547 result = Vec::new();
549 for shape in &shapes {
550 for contour in shape {
551 if contour.len() >= 3 {
552 result = contour.clone();
553 break;
554 }
555 }
556 if !result.is_empty() {
557 break;
558 }
559 }
560
561 if result.len() < 3 {
562 return Err(Error::InvalidGeometry(
563 "Geometry too large to fit in boundary".into(),
564 ));
565 }
566 }
567
568 let result_tuples: Vec<(f64, f64)> = result.iter().map(|&[x, y]| (x, y)).collect();
570 Ok(Nfp::from_polygon(result_tuples))
571}
572
573fn shrink_polygon(polygon: &[(f64, f64)], offset: f64) -> Result<Vec<(f64, f64)>> {
578 if polygon.len() < 3 {
579 return Err(Error::InvalidGeometry(
580 "Polygon must have at least 3 vertices".into(),
581 ));
582 }
583
584 if polygon.len() == 4 {
586 let (min_x, min_y, max_x, max_y) = bounding_box(polygon);
587
588 let is_axis_aligned = polygon.iter().all(|&(x, y)| {
590 ((x - min_x).abs() < 1e-10 || (x - max_x).abs() < 1e-10)
591 && ((y - min_y).abs() < 1e-10 || (y - max_y).abs() < 1e-10)
592 });
593
594 if is_axis_aligned {
595 let new_min_x = min_x + offset;
597 let new_min_y = min_y + offset;
598 let new_max_x = max_x - offset;
599 let new_max_y = max_y - offset;
600
601 if new_min_x >= new_max_x || new_min_y >= new_max_y {
603 return Err(Error::InvalidGeometry("Offset polygon collapsed".into()));
604 }
605
606 return Ok(vec![
607 (new_min_x, new_min_y),
608 (new_max_x, new_min_y),
609 (new_max_x, new_max_y),
610 (new_min_x, new_max_y),
611 ]);
612 }
613 }
614
615 let (cx, cy) = polygon_centroid(polygon);
617
618 let result: Vec<(f64, f64)> = polygon
619 .iter()
620 .filter_map(|&(x, y)| {
621 let dx = x - cx;
622 let dy = y - cy;
623 let dist = (dx * dx + dy * dy).sqrt();
624
625 if dist < offset + 1e-10 {
626 return None;
628 }
629
630 let factor = (dist - offset) / dist;
632 Some((cx + dx * factor, cy + dy * factor))
633 })
634 .collect();
635
636 if result.len() < 3 {
638 return Err(Error::InvalidGeometry("Offset polygon collapsed".into()));
639 }
640
641 let area = signed_area(&result).abs();
643 if area <= 1e-10 {
644 return Err(Error::InvalidGeometry("Offset polygon collapsed".into()));
645 }
646
647 Ok(result)
648}
649
650fn bounding_box(polygon: &[(f64, f64)]) -> (f64, f64, f64, f64) {
652 let mut min_x = f64::INFINITY;
653 let mut min_y = f64::INFINITY;
654 let mut max_x = f64::NEG_INFINITY;
655 let mut max_y = f64::NEG_INFINITY;
656
657 for &(x, y) in polygon {
658 min_x = min_x.min(x);
659 min_y = min_y.min(y);
660 max_x = max_x.max(x);
661 max_y = max_y.max(y);
662 }
663
664 (min_x, min_y, max_x, max_y)
665}
666
667fn compute_nfp_convex(stationary: &[(f64, f64)], orbiting: &[(f64, f64)]) -> Result<Nfp> {
672 use u_nesting_core::geom::minkowski::nfp_convex;
673
674 let polygon = nfp_convex(stationary, orbiting);
675 Ok(Nfp::from_polygon(polygon))
676}
677
678fn compute_minkowski_sum_convex(poly_a: &[(f64, f64)], poly_b: &[(f64, f64)]) -> Result<Nfp> {
682 use u_nesting_core::geom::minkowski::minkowski_sum_convex;
683
684 let polygon = minkowski_sum_convex(poly_a, poly_b);
685 Ok(Nfp::from_polygon(polygon))
686}
687
688fn compute_nfp_general(
695 stat_exterior: &[(f64, f64)],
696 rotated_orbiting: &[(f64, f64)],
697) -> Result<Nfp> {
698 let stat_triangles = triangulate_polygon(stat_exterior);
700 let orb_triangles = triangulate_polygon(rotated_orbiting);
701
702 if stat_triangles.is_empty() || orb_triangles.is_empty() {
703 let stat_hull = convex_hull_of_points(stat_exterior);
705 let orb_hull = convex_hull_of_points(rotated_orbiting);
706 let reflected: Vec<(f64, f64)> = orb_hull.iter().map(|&(x, y)| (-x, -y)).collect();
707 return compute_minkowski_sum_convex(&stat_hull, &reflected);
708 }
709
710 let pairs: Vec<_> = stat_triangles
713 .iter()
714 .flat_map(|stat_tri| {
715 orb_triangles
716 .iter()
717 .map(move |orb_tri| (stat_tri.clone(), orb_tri.clone()))
718 })
719 .collect();
720
721 #[cfg(feature = "parallel")]
722 let partial_nfps: Vec<Vec<(f64, f64)>> = pairs
723 .par_iter()
724 .flat_map(|(stat_tri, orb_tri)| {
725 let reflected: Vec<(f64, f64)> = orb_tri.iter().map(|&(x, y)| (-x, -y)).collect();
726 if let Ok(nfp) = compute_minkowski_sum_convex(stat_tri, &reflected) {
727 nfp.polygons
728 .into_iter()
729 .filter(|polygon| polygon.len() >= 3)
730 .collect::<Vec<_>>()
731 } else {
732 Vec::new()
733 }
734 })
735 .collect();
736 #[cfg(not(feature = "parallel"))]
737 let partial_nfps: Vec<Vec<(f64, f64)>> = pairs
738 .iter()
739 .flat_map(|(stat_tri, orb_tri)| {
740 let reflected: Vec<(f64, f64)> = orb_tri.iter().map(|&(x, y)| (-x, -y)).collect();
741 if let Ok(nfp) = compute_minkowski_sum_convex(stat_tri, &reflected) {
742 nfp.polygons
743 .into_iter()
744 .filter(|polygon| polygon.len() >= 3)
745 .collect::<Vec<_>>()
746 } else {
747 Vec::new()
748 }
749 })
750 .collect();
751
752 if partial_nfps.is_empty() {
753 let stat_hull = convex_hull_of_points(stat_exterior);
755 let orb_hull = convex_hull_of_points(rotated_orbiting);
756 let reflected: Vec<(f64, f64)> = orb_hull.iter().map(|&(x, y)| (-x, -y)).collect();
757 return compute_minkowski_sum_convex(&stat_hull, &reflected);
758 }
759
760 union_polygons(&partial_nfps)
762}
763
764fn triangulate_polygon(polygon: &[(f64, f64)]) -> Vec<Vec<(f64, f64)>> {
766 if polygon.len() < 3 {
767 return Vec::new();
768 }
769
770 if is_polygon_convex(polygon) {
772 return vec![polygon.to_vec()];
773 }
774
775 let mut vertices: Vec<(f64, f64)> = ensure_ccw(polygon);
777 let mut triangles = Vec::new();
778
779 while vertices.len() > 3 {
780 let n = vertices.len();
781 let mut ear_found = false;
782
783 for i in 0..n {
784 let prev = (i + n - 1) % n;
785 let next = (i + 1) % n;
786
787 if is_ear(&vertices, prev, i, next) {
789 triangles.push(vec![vertices[prev], vertices[i], vertices[next]]);
790 vertices.remove(i);
791 ear_found = true;
792 break;
793 }
794 }
795
796 if !ear_found {
797 return vec![convex_hull_of_points(polygon)];
800 }
801 }
802
803 if vertices.len() == 3 {
804 triangles.push(vertices);
805 }
806
807 triangles
808}
809
810fn point_in_triangle_robust(p: (f64, f64), a: (f64, f64), b: (f64, f64), c: (f64, f64)) -> bool {
814 let o1 = orient2d_filtered(a, b, p);
815 let o2 = orient2d_filtered(b, c, p);
816 let o3 = orient2d_filtered(c, a, p);
817
818 (o1 == Orientation::CounterClockwise
821 && o2 == Orientation::CounterClockwise
822 && o3 == Orientation::CounterClockwise)
823 || (o1 == Orientation::Clockwise
824 && o2 == Orientation::Clockwise
825 && o3 == Orientation::Clockwise)
826}
827
828fn is_ear(vertices: &[(f64, f64)], prev: usize, curr: usize, next: usize) -> bool {
832 let a = vertices[prev];
833 let b = vertices[curr];
834 let c = vertices[next];
835
836 let orientation = orient2d_filtered(a, b, c);
839 if !orientation.is_ccw() {
840 return false; }
842
843 for (i, &p) in vertices.iter().enumerate() {
845 if i == prev || i == curr || i == next {
846 continue;
847 }
848 if point_in_triangle_robust(p, a, b, c) {
849 return false;
850 }
851 }
852
853 true
854}
855
856fn union_polygons(polygons: &[Vec<(f64, f64)>]) -> Result<Nfp> {
858 if polygons.is_empty() {
859 return Ok(Nfp::new());
860 }
861
862 if polygons.len() == 1 {
863 return Ok(Nfp::from_polygon(polygons[0].clone()));
864 }
865
866 let mut result: Vec<Vec<[f64; 2]>> = vec![polygons[0].iter().map(|&(x, y)| [x, y]).collect()];
868
869 for polygon in &polygons[1..] {
871 let clip: Vec<[f64; 2]> = polygon.iter().map(|&(x, y)| [x, y]).collect();
872
873 let shapes = result.overlay(&[clip], OverlayRule::Union, FillRule::NonZero);
875
876 result = Vec::new();
878 for shape in shapes {
879 for contour in shape {
880 if contour.len() >= 3 {
881 result.push(contour);
882 }
883 }
884 }
885
886 if result.is_empty() {
887 continue;
889 }
890 }
891
892 let nfp_polygons: Vec<Vec<(f64, f64)>> = result
894 .into_iter()
895 .map(|contour| contour.into_iter().map(|[x, y]| (x, y)).collect())
896 .collect();
897
898 if nfp_polygons.is_empty() {
899 return Ok(Nfp::from_polygon(polygons[0].clone()));
901 }
902
903 Ok(Nfp::from_polygons(nfp_polygons))
904}
905
906fn rotate_polygon(polygon: &[(f64, f64)], angle: f64) -> Vec<(f64, f64)> {
912 if angle.abs() < 1e-10 {
913 return polygon.to_vec();
914 }
915
916 let cos_a = angle.cos();
917 let sin_a = angle.sin();
918
919 polygon
920 .iter()
921 .map(|&(x, y)| (x * cos_a - y * sin_a, x * sin_a + y * cos_a))
922 .collect()
923}
924
925fn is_polygon_convex(polygon: &[(f64, f64)]) -> bool {
927 geom_polygon::is_convex(polygon)
928}
929
930fn ensure_ccw(polygon: &[(f64, f64)]) -> Vec<(f64, f64)> {
932 geom_polygon::ensure_ccw(polygon)
933}
934
935fn signed_area(polygon: &[(f64, f64)]) -> f64 {
938 geom_polygon::signed_area(polygon)
939}
940
941fn convex_hull_of_points(points: &[(f64, f64)]) -> Vec<(f64, f64)> {
943 geom_polygon::convex_hull(points)
944}
945
946pub fn point_in_polygon(point: (f64, f64), polygon: &[(f64, f64)]) -> bool {
952 let (px, py) = point;
953 let n = polygon.len();
954 let mut inside = false;
955
956 let mut j = n - 1;
957 for i in 0..n {
958 let (xi, yi) = polygon[i];
959 let (xj, yj) = polygon[j];
960
961 if ((yi > py) != (yj > py)) && (px < (xj - xi) * (py - yi) / (yj - yi) + xi) {
962 inside = !inside;
963 }
964 j = i;
965 }
966
967 inside
968}
969
970pub fn point_outside_all_nfps(point: (f64, f64), nfps: &[&Nfp]) -> bool {
972 for nfp in nfps {
973 for polygon in &nfp.polygons {
974 if point_in_polygon(point, polygon) {
975 return false;
976 }
977 }
978 }
979 true
980}
981
982fn point_outside_all_nfps_strict(point: (f64, f64), nfps: &[&Nfp]) -> bool {
985 for nfp in nfps {
986 for polygon in &nfp.polygons {
987 if point_in_polygon(point, polygon) {
989 return false;
990 }
991 }
992 }
993 true
994}
995
996fn point_on_polygon_boundary(point: (f64, f64), polygon: &[(f64, f64)]) -> bool {
998 let (px, py) = point;
999 let n = polygon.len();
1000 const EPS: f64 = 1e-10;
1001
1002 for i in 0..n {
1003 let (x1, y1) = polygon[i];
1004 let (x2, y2) = polygon[(i + 1) % n];
1005
1006 let dx = x2 - x1;
1009 let dy = y2 - y1;
1010 let len_sq = dx * dx + dy * dy;
1011
1012 if len_sq < EPS * EPS {
1013 if (px - x1).abs() < EPS && (py - y1).abs() < EPS {
1015 return true;
1016 }
1017 continue;
1018 }
1019
1020 let t = ((px - x1) * dx + (py - y1) * dy) / len_sq;
1022
1023 if (-EPS..=1.0 + EPS).contains(&t) {
1025 let proj_x = x1 + t * dx;
1027 let proj_y = y1 + t * dy;
1028 let dist_sq = (px - proj_x).powi(2) + (py - proj_y).powi(2);
1029
1030 if dist_sq < EPS * EPS {
1031 return true;
1032 }
1033 }
1034 }
1035
1036 false
1037}
1038
1039pub fn find_bottom_left_placement(
1060 ifp: &Nfp,
1061 nfps: &[&Nfp],
1062 sample_step: f64,
1063) -> Option<(f64, f64)> {
1064 if ifp.is_empty() {
1065 return None;
1066 }
1067
1068 let mut candidates: Vec<(f64, f64)> = Vec::new();
1070
1071 for polygon in &ifp.polygons {
1072 candidates.extend(polygon.iter().copied());
1073 }
1074
1075 for nfp in nfps {
1077 for polygon in &nfp.polygons {
1078 candidates.extend(polygon.iter().copied());
1079 }
1080 }
1081
1082 let (min_x, min_y, max_x, max_y) = ifp_bounding_box(ifp);
1084
1085 let mut y = min_y;
1087 while y <= max_y {
1088 let mut x = min_x;
1089 while x <= max_x {
1090 candidates.push((x, y));
1091 x += sample_step;
1092 }
1093 y += sample_step;
1094 }
1095
1096 let valid_candidates: Vec<(f64, f64)> = candidates
1098 .into_iter()
1099 .filter(|&point| {
1100 let in_ifp = ifp
1102 .polygons
1103 .iter()
1104 .any(|p| point_in_polygon(point, p) || point_on_polygon_boundary(point, p));
1105 if !in_ifp {
1106 return false;
1107 }
1108 point_outside_all_nfps_strict(point, nfps)
1110 })
1111 .collect();
1112
1113 valid_candidates.into_iter().min_by(|a, b| {
1116 match a.0.partial_cmp(&b.0) {
1118 Some(std::cmp::Ordering::Equal) => {
1119 a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)
1120 }
1121 Some(ord) => ord,
1122 None => std::cmp::Ordering::Equal,
1123 }
1124 })
1125}
1126
1127fn ifp_bounding_box(ifp: &Nfp) -> (f64, f64, f64, f64) {
1129 let mut min_x = f64::INFINITY;
1130 let mut min_y = f64::INFINITY;
1131 let mut max_x = f64::NEG_INFINITY;
1132 let mut max_y = f64::NEG_INFINITY;
1133
1134 for polygon in &ifp.polygons {
1135 for &(x, y) in polygon {
1136 min_x = min_x.min(x);
1137 min_y = min_y.min(y);
1138 max_x = max_x.max(x);
1139 max_y = max_y.max(y);
1140 }
1141 }
1142
1143 (min_x, min_y, max_x, max_y)
1144}
1145
1146#[derive(Debug, Clone)]
1148pub struct PlacedGeometry {
1149 pub geometry: Geometry2D,
1151 pub position: (f64, f64),
1153 pub rotation: f64,
1155 pub mirrored: bool,
1157}
1158
1159impl PlacedGeometry {
1160 pub fn new(geometry: Geometry2D, position: (f64, f64), rotation: f64) -> Self {
1163 Self {
1164 geometry,
1165 position,
1166 rotation,
1167 mirrored: false,
1168 }
1169 }
1170
1171 pub fn with_mirrored(mut self, mirrored: bool) -> Self {
1173 self.mirrored = mirrored;
1174 self
1175 }
1176
1177 pub fn translated_exterior(&self) -> Vec<(f64, f64)> {
1179 let base = if self.mirrored {
1180 crate::polygon_ops::mirror_polygon(self.geometry.exterior())
1181 } else {
1182 self.geometry.exterior().to_vec()
1183 };
1184 let rotated = rotate_polygon(&base, self.rotation);
1185 rotated
1186 .into_iter()
1187 .map(|(x, y)| (x + self.position.0, y + self.position.1))
1188 .collect()
1189 }
1190}
1191
1192pub fn verify_no_overlap(
1207 geometry: &Geometry2D,
1208 position: (f64, f64),
1209 rotation: f64,
1210 placed_geometries: &[PlacedGeometry],
1211) -> bool {
1212 verify_no_overlap_mirrored(geometry, position, rotation, false, placed_geometries)
1213}
1214
1215pub fn verify_no_overlap_mirrored(
1219 geometry: &Geometry2D,
1220 position: (f64, f64),
1221 rotation: f64,
1222 mirrored: bool,
1223 placed_geometries: &[PlacedGeometry],
1224) -> bool {
1225 use crate::nfp_sliding::polygons_overlap;
1226
1227 let base = if mirrored {
1229 crate::polygon_ops::mirror_polygon(geometry.exterior())
1230 } else {
1231 geometry.exterior().to_vec()
1232 };
1233 let rotated = rotate_polygon(&base, rotation);
1234 let transformed: Vec<(f64, f64)> = rotated
1235 .into_iter()
1236 .map(|(x, y)| (x + position.0, y + position.1))
1237 .collect();
1238
1239 for placed in placed_geometries {
1241 let placed_polygon = placed.translated_exterior();
1242
1243 if polygons_overlap(&transformed, &placed_polygon) {
1244 return false; }
1246 }
1247
1248 true }
1250
1251#[derive(Debug, Clone, Hash, PartialEq, Eq)]
1257struct NfpCacheKey {
1258 geometry_a: String,
1259 geometry_b: String,
1260 rotation_millideg: i32, mirror_a: bool,
1265 mirror_b: bool,
1266}
1267
1268impl NfpCacheKey {
1269 fn new_mirrored(
1270 id_a: &str,
1271 id_b: &str,
1272 rotation_rad: f64,
1273 mirror_a: bool,
1274 mirror_b: bool,
1275 ) -> Self {
1276 let rotation_millideg = ((rotation_rad * 180.0 / PI) * 1000.0).round() as i32;
1278 Self {
1279 geometry_a: id_a.to_string(),
1280 geometry_b: id_b.to_string(),
1281 rotation_millideg,
1282 mirror_a,
1283 mirror_b,
1284 }
1285 }
1286}
1287
1288#[derive(Debug)]
1290pub struct NfpCache {
1291 cache: RwLock<HashMap<NfpCacheKey, Arc<Nfp>>>,
1292 max_size: usize,
1293}
1294
1295impl NfpCache {
1296 pub fn new() -> Self {
1298 Self::with_capacity(1000)
1299 }
1300
1301 pub fn with_capacity(max_size: usize) -> Self {
1303 Self {
1304 cache: RwLock::new(HashMap::new()),
1305 max_size,
1306 }
1307 }
1308
1309 pub fn get_or_compute<F>(&self, key: (&str, &str, f64), compute: F) -> Result<Arc<Nfp>>
1315 where
1316 F: FnOnce() -> Result<Nfp>,
1317 {
1318 self.get_or_compute_mirrored((key.0, key.1, key.2, false, false), compute)
1319 }
1320
1321 pub fn get_or_compute_mirrored<F>(
1329 &self,
1330 key: (&str, &str, f64, bool, bool),
1331 compute: F,
1332 ) -> Result<Arc<Nfp>>
1333 where
1334 F: FnOnce() -> Result<Nfp>,
1335 {
1336 let cache_key = NfpCacheKey::new_mirrored(key.0, key.1, key.2, key.3, key.4);
1337
1338 {
1340 let cache = self.cache.read().map_err(|e| {
1341 Error::Internal(format!("Failed to acquire cache read lock: {}", e))
1342 })?;
1343 if let Some(nfp) = cache.get(&cache_key) {
1344 return Ok(Arc::clone(nfp));
1345 }
1346 }
1347
1348 let nfp = Arc::new(compute()?);
1350
1351 {
1353 let mut cache = self.cache.write().map_err(|e| {
1354 Error::Internal(format!("Failed to acquire cache write lock: {}", e))
1355 })?;
1356
1357 if cache.len() >= self.max_size {
1359 let keys_to_remove: Vec<_> =
1360 cache.keys().take(self.max_size / 2).cloned().collect();
1361 for key in keys_to_remove {
1362 cache.remove(&key);
1363 }
1364 }
1365
1366 cache.insert(cache_key, Arc::clone(&nfp));
1367 }
1368
1369 Ok(nfp)
1370 }
1371
1372 pub fn len(&self) -> usize {
1374 self.cache.read().map(|c| c.len()).unwrap_or(0)
1375 }
1376
1377 pub fn is_empty(&self) -> bool {
1379 self.len() == 0
1380 }
1381
1382 pub fn clear(&self) {
1384 if let Ok(mut cache) = self.cache.write() {
1385 cache.clear();
1386 }
1387 }
1388}
1389
1390impl Default for NfpCache {
1391 fn default() -> Self {
1392 Self::new()
1393 }
1394}
1395
1396#[cfg(test)]
1397mod tests {
1398 use super::*;
1399 use approx::assert_relative_eq;
1400
1401 fn rect(w: f64, h: f64) -> Vec<(f64, f64)> {
1402 vec![(0.0, 0.0), (w, 0.0), (w, h), (0.0, h)]
1403 }
1404
1405 fn triangle() -> Vec<(f64, f64)> {
1406 vec![(0.0, 0.0), (10.0, 0.0), (5.0, 10.0)]
1407 }
1408
1409 #[test]
1410 fn test_is_polygon_convex() {
1411 assert!(is_polygon_convex(&rect(10.0, 10.0)));
1413
1414 assert!(is_polygon_convex(&triangle()));
1416
1417 let l_shape = vec![
1419 (0.0, 0.0),
1420 (10.0, 0.0),
1421 (10.0, 5.0),
1422 (5.0, 5.0),
1423 (5.0, 10.0),
1424 (0.0, 10.0),
1425 ];
1426 assert!(!is_polygon_convex(&l_shape));
1427 }
1428
1429 #[test]
1430 fn test_signed_area() {
1431 let ccw_square = rect(10.0, 10.0);
1433 assert!(signed_area(&ccw_square) > 0.0);
1434 assert_relative_eq!(signed_area(&ccw_square).abs(), 100.0, epsilon = 1e-10);
1435
1436 let cw_square: Vec<_> = ccw_square.into_iter().rev().collect();
1438 assert!(signed_area(&cw_square) < 0.0);
1439 }
1440
1441 #[test]
1442 fn test_rotate_polygon() {
1443 let square = rect(10.0, 10.0);
1444
1445 let rotated = rotate_polygon(&square, 0.0);
1447 assert_eq!(rotated.len(), square.len());
1448
1449 let rotated = rotate_polygon(&[(1.0, 0.0)], PI / 2.0);
1451 assert_relative_eq!(rotated[0].0, 0.0, epsilon = 1e-10);
1452 assert_relative_eq!(rotated[0].1, 1.0, epsilon = 1e-10);
1453 }
1454
1455 fn chiral_l() -> Geometry2D {
1460 Geometry2D::l_shape("L", 30.0, 20.0, 20.0, 10.0)
1461 }
1462
1463 #[test]
1464 fn test_nfp_mirror_orbiting_changes_result() {
1465 let stationary = Geometry2D::rectangle("S", 100.0, 100.0);
1466 let orbiting = chiral_l();
1467
1468 let unmirrored = compute_nfp_mirrored(&stationary, &orbiting, 0.0, false, false).unwrap();
1469 let mirrored = compute_nfp_mirrored(&stationary, &orbiting, 0.0, false, true).unwrap();
1470
1471 assert!(!unmirrored.is_empty());
1472 assert!(!mirrored.is_empty());
1473 assert_ne!(
1474 unmirrored.polygons, mirrored.polygons,
1475 "mirroring the orbiting polygon must change the NFP for a chiral shape"
1476 );
1477
1478 let unmirrored_area: f64 = unmirrored
1481 .polygons
1482 .iter()
1483 .map(|p| signed_area(p).abs())
1484 .sum();
1485 let mirrored_area: f64 = mirrored.polygons.iter().map(|p| signed_area(p).abs()).sum();
1486 assert_relative_eq!(unmirrored_area, mirrored_area, epsilon = 1e-6);
1487 }
1488
1489 #[test]
1490 fn test_nfp_mirror_stationary_changes_result() {
1491 let stationary = chiral_l();
1492 let orbiting = Geometry2D::rectangle("O", 5.0, 5.0);
1493
1494 let unmirrored = compute_nfp_mirrored(&stationary, &orbiting, 0.0, false, false).unwrap();
1495 let mirrored = compute_nfp_mirrored(&stationary, &orbiting, 0.0, true, false).unwrap();
1496
1497 assert!(!unmirrored.is_empty());
1498 assert!(!mirrored.is_empty());
1499 assert_ne!(
1500 unmirrored.polygons, mirrored.polygons,
1501 "mirroring the stationary polygon must change the NFP for a chiral shape"
1502 );
1503 }
1504
1505 #[test]
1506 fn test_compute_nfp_unchanged_by_new_mirror_plumbing() {
1507 let a = Geometry2D::rectangle("A", 10.0, 10.0);
1511 let b = Geometry2D::rectangle("B", 5.0, 5.0);
1512 let via_plain = compute_nfp(&a, &b, 0.0).unwrap();
1513 let via_mirrored = compute_nfp_mirrored(&a, &b, 0.0, false, false).unwrap();
1514 assert_eq!(via_plain.polygons, via_mirrored.polygons);
1515 }
1516
1517 #[test]
1518 fn test_ifp_mirror_changes_result() {
1519 let boundary = rect(100.0, 100.0);
1520 let geom = chiral_l();
1521
1522 let unmirrored =
1523 compute_ifp_with_margin_and_mirror(&boundary, &geom, 0.0, 0.0, false).unwrap();
1524 let mirrored =
1525 compute_ifp_with_margin_and_mirror(&boundary, &geom, 0.0, 0.0, true).unwrap();
1526
1527 assert!(!unmirrored.is_empty());
1528 assert!(!mirrored.is_empty());
1529 assert_ne!(
1530 unmirrored.polygons, mirrored.polygons,
1531 "mirroring the geometry must change its IFP within the boundary for a chiral shape"
1532 );
1533 }
1534
1535 #[test]
1536 fn test_compute_ifp_with_margin_unchanged_by_new_mirror_plumbing() {
1537 let boundary = rect(100.0, 100.0);
1538 let geom = Geometry2D::rectangle("G", 10.0, 10.0);
1539 let via_plain = compute_ifp_with_margin(&boundary, &geom, 0.0, 5.0).unwrap();
1540 let via_mirrored =
1541 compute_ifp_with_margin_and_mirror(&boundary, &geom, 0.0, 5.0, false).unwrap();
1542 assert_eq!(via_plain.polygons, via_mirrored.polygons);
1543 }
1544
1545 #[test]
1546 fn test_nfp_cache_mirror_flags_distinguish_entries() {
1547 let cache = NfpCache::new();
1548 let mut calls = 0;
1549
1550 let unmirrored = cache
1551 .get_or_compute_mirrored(("A", "B", 0.0, false, false), || {
1552 calls += 1;
1553 Ok(Nfp::from_polygon(rect(1.0, 1.0)))
1554 })
1555 .unwrap();
1556
1557 let mirrored = cache
1561 .get_or_compute_mirrored(("A", "B", 0.0, true, false), || {
1562 calls += 1;
1563 Ok(Nfp::from_polygon(rect(2.0, 2.0)))
1564 })
1565 .unwrap();
1566
1567 assert_eq!(
1568 calls, 2,
1569 "distinct mirror flags must both invoke compute, not share a slot"
1570 );
1571 assert_ne!(unmirrored.polygons, mirrored.polygons);
1572
1573 let unmirrored_again = cache
1575 .get_or_compute_mirrored(("A", "B", 0.0, false, false), || {
1576 calls += 1;
1577 Ok(Nfp::from_polygon(rect(99.0, 99.0)))
1578 })
1579 .unwrap();
1580 assert_eq!(calls, 2, "re-querying an existing key must hit the cache");
1581 assert_eq!(unmirrored.polygons, unmirrored_again.polygons);
1582 }
1583
1584 #[test]
1585 fn test_nfp_two_squares() {
1586 let a = Geometry2D::rectangle("A", 10.0, 10.0);
1587 let b = Geometry2D::rectangle("B", 5.0, 5.0);
1588
1589 let nfp = compute_nfp(&a, &b, 0.0).unwrap();
1590
1591 assert!(!nfp.is_empty());
1592 assert_eq!(nfp.polygons.len(), 1);
1593
1594 let polygon = &nfp.polygons[0];
1597 assert!(polygon.len() >= 4);
1598 }
1599
1600 #[test]
1601 fn test_nfp_with_rotation() {
1602 let a = Geometry2D::rectangle("A", 10.0, 10.0);
1603 let b = Geometry2D::rectangle("B", 5.0, 5.0);
1604
1605 let nfp = compute_nfp(&a, &b, PI / 4.0).unwrap();
1607
1608 assert!(!nfp.is_empty());
1609 }
1611
1612 #[test]
1613 fn test_ifp_square_in_boundary() {
1614 let boundary = rect(100.0, 100.0);
1615 let geom = Geometry2D::rectangle("G", 10.0, 10.0);
1616
1617 let ifp = compute_ifp(&boundary, &geom, 0.0).unwrap();
1618
1619 assert!(!ifp.is_empty());
1620 let polygon = &ifp.polygons[0];
1623 let (min_x, min_y, max_x, max_y) = bounding_box(polygon);
1624 assert_relative_eq!(min_x, 0.0, epsilon = 1e-10);
1625 assert_relative_eq!(min_y, 0.0, epsilon = 1e-10);
1626 assert_relative_eq!(max_x, 90.0, epsilon = 1e-10);
1627 assert_relative_eq!(max_y, 90.0, epsilon = 1e-10);
1628 }
1629
1630 #[test]
1631 fn test_ifp_bounds_correct() {
1632 let boundary = rect(100.0, 50.0);
1634 let geom = Geometry2D::rectangle("R", 25.0, 25.0);
1635
1636 let ifp = compute_ifp(&boundary, &geom, 0.0).unwrap();
1637
1638 assert!(!ifp.is_empty());
1639 let polygon = &ifp.polygons[0];
1641 let (min_x, min_y, max_x, max_y) = bounding_box(polygon);
1642 assert_relative_eq!(min_x, 0.0, epsilon = 1e-10);
1643 assert_relative_eq!(min_y, 0.0, epsilon = 1e-10);
1644 assert_relative_eq!(max_x, 75.0, epsilon = 1e-10);
1645 assert_relative_eq!(max_y, 25.0, epsilon = 1e-10);
1646
1647 assert!(point_in_polygon((0.0, 0.0), polygon) || point_on_boundary((0.0, 0.0), polygon));
1649 assert!(point_in_polygon((25.0, 0.0), polygon) || point_on_boundary((25.0, 0.0), polygon));
1650 assert!(point_in_polygon((50.0, 0.0), polygon) || point_on_boundary((50.0, 0.0), polygon));
1651 assert!(point_in_polygon((75.0, 0.0), polygon) || point_on_boundary((75.0, 0.0), polygon));
1652 }
1653
1654 fn point_on_boundary(point: (f64, f64), polygon: &[(f64, f64)]) -> bool {
1656 let (px, py) = point;
1657 let n = polygon.len();
1658 for i in 0..n {
1659 let (x1, y1) = polygon[i];
1660 let (x2, y2) = polygon[(i + 1) % n];
1661 let d1 = ((px - x1).powi(2) + (py - y1).powi(2)).sqrt();
1663 let d2 = ((px - x2).powi(2) + (py - y2).powi(2)).sqrt();
1664 let d_total = ((x2 - x1).powi(2) + (y2 - y1).powi(2)).sqrt();
1665 if (d1 + d2 - d_total).abs() < 1e-10 {
1666 return true;
1667 }
1668 }
1669 false
1670 }
1671
1672 #[test]
1673 fn test_nfp_same_size_rectangles() {
1674 let a = Geometry2D::rectangle("A", 25.0, 25.0);
1676 let b = Geometry2D::rectangle("B", 25.0, 25.0);
1677
1678 let nfp = compute_nfp(&a, &b, 0.0).unwrap();
1679 assert!(!nfp.is_empty());
1680
1681 let polygon = &nfp.polygons[0];
1682 let (min_x, min_y, max_x, max_y) = bounding_box(polygon);
1683 let width = max_x - min_x;
1686 let height = max_y - min_y;
1687 eprintln!("NFP dimensions: {}x{}", width, height);
1688 eprintln!(
1689 "NFP bounds: ({}, {}) to ({}, {})",
1690 min_x, min_y, max_x, max_y
1691 );
1692 assert_relative_eq!(width, 50.0, epsilon = 1e-6);
1694 assert_relative_eq!(height, 50.0, epsilon = 1e-6);
1695 }
1696
1697 #[test]
1698 fn test_nfp_cache() {
1699 let cache = NfpCache::new();
1700
1701 let compute_count = std::sync::atomic::AtomicUsize::new(0);
1702
1703 let result1 = cache
1704 .get_or_compute(("A", "B", 0.0), || {
1705 compute_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1706 Ok(Nfp::from_polygon(vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)]))
1707 })
1708 .unwrap();
1709
1710 let result2 = cache
1711 .get_or_compute(("A", "B", 0.0), || {
1712 compute_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1713 Ok(Nfp::from_polygon(vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)]))
1714 })
1715 .unwrap();
1716
1717 assert_eq!(compute_count.load(std::sync::atomic::Ordering::SeqCst), 1);
1719 assert_eq!(result1.polygons, result2.polygons);
1720 assert_eq!(cache.len(), 1);
1721 }
1722
1723 #[test]
1724 fn test_nfp_cache_different_rotations() {
1725 let cache = NfpCache::new();
1726
1727 cache
1728 .get_or_compute(("A", "B", 0.0), || {
1729 Ok(Nfp::from_polygon(vec![(0.0, 0.0), (1.0, 0.0)]))
1730 })
1731 .unwrap();
1732
1733 cache
1734 .get_or_compute(("A", "B", PI / 2.0), || {
1735 Ok(Nfp::from_polygon(vec![(0.0, 0.0), (0.0, 1.0)]))
1736 })
1737 .unwrap();
1738
1739 assert_eq!(cache.len(), 2);
1741 }
1742
1743 #[test]
1744 fn test_convex_hull_of_points() {
1745 let points = vec![
1746 (0.0, 0.0),
1747 (10.0, 0.0),
1748 (5.0, 5.0), (10.0, 10.0),
1750 (0.0, 10.0),
1751 ];
1752
1753 let hull = convex_hull_of_points(&points);
1754
1755 assert_eq!(hull.len(), 4);
1757 }
1758
1759 #[test]
1760 fn test_shrink_polygon_square() {
1761 let square = rect(100.0, 100.0);
1762 let shrunk = shrink_polygon(&square, 10.0).unwrap();
1763
1764 assert_eq!(shrunk.len(), 4);
1766
1767 let original_area = signed_area(&square).abs();
1769 let shrunk_area = signed_area(&shrunk).abs();
1770 assert!(
1771 shrunk_area < original_area,
1772 "shrunk_area ({}) should be < original_area ({})",
1773 shrunk_area,
1774 original_area
1775 );
1776
1777 assert_relative_eq!(shrunk_area, 6400.0, epsilon = 1.0);
1780 }
1781
1782 #[test]
1783 fn test_shrink_polygon_collapse() {
1784 let small_square = rect(10.0, 10.0);
1785
1786 let result = shrink_polygon(&small_square, 6.0);
1788 assert!(
1789 result.is_err(),
1790 "Polygon should collapse when offset >= width/2"
1791 );
1792 }
1793
1794 #[test]
1795 fn test_ifp_with_margin() {
1796 let boundary = rect(100.0, 100.0);
1797 let geom = Geometry2D::rectangle("G", 10.0, 10.0);
1798
1799 let ifp_no_margin = compute_ifp(&boundary, &geom, 0.0).unwrap();
1801
1802 let ifp_with_margin = compute_ifp_with_margin(&boundary, &geom, 0.0, 5.0).unwrap();
1804
1805 assert!(!ifp_no_margin.is_empty());
1806 assert!(!ifp_with_margin.is_empty());
1807
1808 let (min_x_no, _min_y_no, max_x_no, _max_y_no) = ifp_bounding_box(&ifp_no_margin);
1810 let (min_x_margin, _min_y_margin, max_x_margin, _max_y_margin) =
1811 ifp_bounding_box(&ifp_with_margin);
1812
1813 let width_no = max_x_no - min_x_no;
1814 let width_margin = max_x_margin - min_x_margin;
1815
1816 assert!(
1820 width_margin < width_no,
1821 "width_margin ({}) should be < width_no ({})",
1822 width_margin,
1823 width_no
1824 );
1825 }
1826
1827 #[test]
1828 fn test_ifp_margin_boundary_collapse() {
1829 let boundary = rect(20.0, 20.0);
1830
1831 let result = shrink_polygon(&boundary, 12.0);
1833 assert!(
1834 result.is_err(),
1835 "Boundary should collapse with margin >= width/2"
1836 );
1837 }
1838
1839 #[test]
1840 fn test_ifp_margin_large_geometry() {
1841 let boundary = rect(30.0, 30.0);
1842 let geom = Geometry2D::rectangle("G", 20.0, 20.0);
1843
1844 let ifp_no_margin = compute_ifp(&boundary, &geom, 0.0).unwrap();
1846 let (min_x_no, _, max_x_no, _) = ifp_bounding_box(&ifp_no_margin);
1847 let width_no = max_x_no - min_x_no;
1848
1849 let ifp_with_margin = compute_ifp_with_margin(&boundary, &geom, 0.0, 5.0).unwrap();
1851 let (min_x_margin, _, max_x_margin, _) = ifp_bounding_box(&ifp_with_margin);
1852 let width_margin = max_x_margin - min_x_margin;
1853
1854 assert!(
1856 width_margin <= width_no,
1857 "width_margin ({}) should be <= width_no ({})",
1858 width_margin,
1859 width_no
1860 );
1861 }
1862
1863 #[test]
1864 fn test_nfp_non_convex_l_shape() {
1865 let l_shape = Geometry2D::new("L").with_polygon(vec![
1867 (0.0, 0.0),
1868 (20.0, 0.0),
1869 (20.0, 10.0),
1870 (10.0, 10.0),
1871 (10.0, 20.0),
1872 (0.0, 20.0),
1873 ]);
1874
1875 let small_square = Geometry2D::rectangle("S", 5.0, 5.0);
1876
1877 let nfp = compute_nfp(&l_shape, &small_square, 0.0).unwrap();
1879
1880 assert!(!nfp.is_empty());
1881 assert!(nfp.vertex_count() >= 4);
1883 }
1884
1885 #[test]
1886 fn test_triangulate_polygon_convex() {
1887 let square = rect(10.0, 10.0);
1888 let triangles = triangulate_polygon(&square);
1889
1890 assert_eq!(triangles.len(), 1);
1892 assert_eq!(triangles[0].len(), 4);
1893 }
1894
1895 #[test]
1896 fn test_triangulate_polygon_non_convex() {
1897 let l_shape = vec![
1899 (0.0, 0.0),
1900 (20.0, 0.0),
1901 (20.0, 10.0),
1902 (10.0, 10.0),
1903 (10.0, 20.0),
1904 (0.0, 20.0),
1905 ];
1906
1907 let triangles = triangulate_polygon(&l_shape);
1908
1909 assert!(!triangles.is_empty());
1911 }
1912
1913 #[test]
1914 fn test_union_polygons() {
1915 let poly1 = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
1917 let poly2 = vec![(5.0, 5.0), (15.0, 5.0), (15.0, 15.0), (5.0, 15.0)];
1918
1919 let result = union_polygons(&[poly1, poly2]).unwrap();
1920
1921 assert!(!result.is_empty());
1922 assert!(result.vertex_count() >= 6);
1924 }
1925
1926 #[test]
1931 fn test_convex_near_collinear_vertices() {
1932 let near_collinear = vec![
1934 (0.0, 0.0),
1935 (1.0, 1e-15), (2.0, 0.0),
1937 (2.0, 1.0),
1938 (0.0, 1.0),
1939 ];
1940
1941 let result = is_polygon_convex(&near_collinear);
1943 let _ = result; }
1946
1947 #[test]
1948 fn test_triangulation_near_degenerate() {
1949 let near_degenerate_l = vec![
1951 (0.0, 0.0),
1952 (10.0, 0.0),
1953 (10.0, 5.0),
1954 (5.0 + 1e-12, 5.0), (5.0, 10.0),
1956 (0.0, 10.0),
1957 ];
1958
1959 let triangles = triangulate_polygon(&near_degenerate_l);
1961
1962 assert!(!triangles.is_empty());
1964 }
1965
1966 #[test]
1967 fn test_nfp_nearly_touching_rectangles() {
1968 let a = Geometry2D::rectangle("A", 10.0, 10.0);
1970 let b = Geometry2D::rectangle("B", 5.0, 5.0);
1971
1972 let nfp = compute_nfp(&a, &b, 0.0).unwrap();
1974 assert!(!nfp.is_empty());
1975 }
1976
1977 #[test]
1978 fn test_ifp_geometry_nearly_fills_boundary() {
1979 let boundary = rect(100.0, 100.0);
1981 let geom = Geometry2D::rectangle("G", 99.9999, 99.9999);
1982
1983 let result = compute_ifp(&boundary, &geom, 0.0);
1985
1986 match result {
1988 Ok(ifp) => {
1989 let (min_x, min_y, max_x, max_y) = ifp_bounding_box(&ifp);
1991 let width = max_x - min_x;
1992 let height = max_y - min_y;
1993 assert!(width < 0.001 && height < 0.001);
1994 }
1995 Err(_) => {
1996 }
1998 }
1999 }
2000
2001 #[test]
2002 fn test_point_in_polygon_on_boundary() {
2003 let square = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
2005
2006 let on_bottom_edge = (5.0, 0.0);
2008 let on_right_edge = (10.0, 5.0);
2009 let on_top_edge = (5.0, 10.0);
2010 let on_left_edge = (0.0, 5.0);
2011
2012 let _ = point_in_polygon(on_bottom_edge, &square);
2015 let _ = point_in_polygon(on_right_edge, &square);
2016 let _ = point_in_polygon(on_top_edge, &square);
2017 let _ = point_in_polygon(on_left_edge, &square);
2018 }
2019
2020 #[test]
2021 fn test_point_in_triangle_robust_degenerate() {
2022 let a = (0.0, 0.0);
2024 let b = (5.0, 0.0);
2025 let c = (10.0, 0.0);
2026
2027 let p = (3.0, 0.0);
2029
2030 assert!(!point_in_triangle_robust(p, a, b, c));
2032 }
2033
2034 #[test]
2035 fn test_ear_detection_with_collinear_points() {
2036 let with_collinear = vec![
2038 (0.0, 0.0),
2039 (5.0, 0.0),
2040 (10.0, 0.0), (10.0, 10.0),
2042 (0.0, 10.0),
2043 ];
2044
2045 let triangles = triangulate_polygon(&with_collinear);
2047
2048 for triangle in &triangles {
2050 assert!(triangle.len() >= 3);
2051 }
2052 }
2053
2054 #[test]
2055 fn test_nfp_with_very_small_polygon() {
2056 let tiny = Geometry2D::rectangle("tiny", 1e-6, 1e-6);
2058 let normal = Geometry2D::rectangle("normal", 10.0, 10.0);
2059
2060 let nfp = compute_nfp(&normal, &tiny, 0.0).unwrap();
2062 assert!(!nfp.is_empty());
2063 }
2064
2065 #[test]
2066 fn test_nfp_with_very_large_polygon() {
2067 let large = Geometry2D::rectangle("large", 1e6, 1e6);
2069 let normal = Geometry2D::rectangle("normal", 100.0, 100.0);
2070
2071 let nfp = compute_nfp(&large, &normal, 0.0).unwrap();
2073 assert!(!nfp.is_empty());
2074 }
2075
2076 #[test]
2077 fn test_signed_area_with_extreme_coordinates() {
2078 let moderate_coords = vec![
2085 (1e6, 1e6),
2086 (1e6 + 100.0, 1e6),
2087 (1e6 + 100.0, 1e6 + 100.0),
2088 (1e6, 1e6 + 100.0),
2089 ];
2090
2091 let area = signed_area(&moderate_coords);
2092
2093 assert_relative_eq!(area.abs(), 10000.0, epsilon = 1.0);
2095 }
2096
2097 #[test]
2098 fn test_ensure_ccw_with_near_zero_area() {
2099 let tiny_area = vec![(0.0, 0.0), (1e-10, 0.0), (1e-10, 1e-10), (0.0, 1e-10)];
2101
2102 let ccw = ensure_ccw(&tiny_area);
2104 assert_eq!(ccw.len(), tiny_area.len());
2105 }
2106
2107 #[test]
2112 fn test_nfp_method_default() {
2113 let config = NfpConfig::default();
2114 assert_eq!(config.method, NfpMethod::MinkowskiSum);
2115 }
2116
2117 #[test]
2118 fn test_nfp_method_minkowski_sum() {
2119 let a = Geometry2D::rectangle("A", 10.0, 10.0);
2120 let b = Geometry2D::rectangle("B", 5.0, 5.0);
2121
2122 let nfp = compute_nfp_with_method(&a, &b, 0.0, NfpMethod::MinkowskiSum).unwrap();
2123
2124 assert!(!nfp.is_empty());
2125 assert!(nfp.vertex_count() >= 4);
2126 }
2127
2128 #[test]
2129 fn test_nfp_method_sliding() {
2130 let a = Geometry2D::rectangle("A", 10.0, 10.0);
2131 let b = Geometry2D::rectangle("B", 5.0, 5.0);
2132
2133 let result = compute_nfp_with_method(&a, &b, 0.0, NfpMethod::Sliding);
2134
2135 assert!(result.is_ok(), "Sliding method should not error");
2137 let nfp = result.unwrap();
2138 assert!(!nfp.is_empty(), "NFP should not be empty");
2139
2140 }
2144
2145 #[test]
2146 fn test_nfp_method_config_builder() {
2147 let config = NfpConfig::with_method(NfpMethod::Sliding)
2148 .with_tolerance(1e-5)
2149 .with_max_iterations(5000);
2150
2151 assert_eq!(config.method, NfpMethod::Sliding);
2152 assert!((config.contact_tolerance - 1e-5).abs() < 1e-10);
2153 assert_eq!(config.max_iterations, 5000);
2154 }
2155
2156 #[test]
2157 fn test_nfp_methods_both_succeed() {
2158 let a = Geometry2D::rectangle("A", 10.0, 10.0);
2159 let b = Geometry2D::rectangle("B", 5.0, 5.0);
2160
2161 let nfp_mink = compute_nfp_with_method(&a, &b, 0.0, NfpMethod::MinkowskiSum).unwrap();
2162 let nfp_slide = compute_nfp_with_method(&a, &b, 0.0, NfpMethod::Sliding).unwrap();
2163
2164 assert!(!nfp_mink.is_empty());
2166 assert!(!nfp_slide.is_empty());
2167
2168 assert!(nfp_mink.vertex_count() >= 4);
2170
2171 }
2176
2177 #[test]
2178 fn test_nfp_sliding_l_shape() {
2179 let l_shape = Geometry2D::new("L").with_polygon(vec![
2181 (0.0, 0.0),
2182 (20.0, 0.0),
2183 (20.0, 10.0),
2184 (10.0, 10.0),
2185 (10.0, 20.0),
2186 (0.0, 20.0),
2187 ]);
2188
2189 let small_square = Geometry2D::rectangle("S", 5.0, 5.0);
2190
2191 let result = compute_nfp_with_method(&l_shape, &small_square, 0.0, NfpMethod::Sliding);
2193
2194 assert!(result.is_ok(), "Sliding should not error on L-shape");
2196 let nfp = result.unwrap();
2197 assert!(!nfp.is_empty(), "NFP should not be empty for L-shape");
2198 }
2199
2200 #[test]
2201 fn test_nfp_with_config() {
2202 let a = Geometry2D::rectangle("A", 10.0, 10.0);
2203 let b = Geometry2D::rectangle("B", 5.0, 5.0);
2204
2205 let config = NfpConfig {
2206 method: NfpMethod::Sliding,
2207 contact_tolerance: 1e-4,
2208 max_iterations: 2000,
2209 };
2210
2211 let nfp = compute_nfp_with_config(&a, &b, 0.0, &config).unwrap();
2212
2213 assert!(!nfp.is_empty());
2214 }
2215}