1use crate::geometry::Geometry2D;
24use crate::nfp_sliding::{compute_nfp_sliding, SlidingNfpConfig};
25use geo::{ConvexHull, Coord, LineString};
26use i_overlay::core::fill_rule::FillRule;
27use i_overlay::core::overlay_rule::OverlayRule;
28use i_overlay::float::single::SingleFloatOverlay;
29use rayon::prelude::*;
30use std::collections::HashMap;
31use std::f64::consts::PI;
32use std::sync::{Arc, RwLock};
33use u_nesting_core::geometry::Geometry2DExt;
34use u_nesting_core::robust::{orient2d_filtered, Orientation};
35use u_nesting_core::{Error, Result};
36
37pub fn rotate_nfp(nfp: &Nfp, angle: f64) -> Nfp {
42 if angle.abs() < 1e-10 {
43 return nfp.clone();
44 }
45
46 let cos_a = angle.cos();
47 let sin_a = angle.sin();
48
49 Nfp {
50 polygons: nfp
51 .polygons
52 .iter()
53 .map(|polygon| {
54 polygon
55 .iter()
56 .map(|&(x, y)| (x * cos_a - y * sin_a, x * sin_a + y * cos_a))
57 .collect()
58 })
59 .collect(),
60 }
61}
62
63pub fn translate_nfp(nfp: &Nfp, offset: (f64, f64)) -> Nfp {
65 Nfp {
66 polygons: nfp
67 .polygons
68 .iter()
69 .map(|polygon| {
70 polygon
71 .iter()
72 .map(|(x, y)| (x + offset.0, y + offset.1))
73 .collect()
74 })
75 .collect(),
76 }
77}
78
79#[derive(Debug, Clone)]
81pub struct Nfp {
82 pub polygons: Vec<Vec<(f64, f64)>>,
85}
86
87impl Nfp {
88 pub fn new() -> Self {
90 Self {
91 polygons: Vec::new(),
92 }
93 }
94
95 pub fn from_polygon(polygon: Vec<(f64, f64)>) -> Self {
97 Self {
98 polygons: vec![polygon],
99 }
100 }
101
102 pub fn from_polygons(polygons: Vec<Vec<(f64, f64)>>) -> Self {
104 Self { polygons }
105 }
106
107 pub fn is_empty(&self) -> bool {
109 self.polygons.is_empty()
110 }
111
112 pub fn vertex_count(&self) -> usize {
114 self.polygons.iter().map(|p| p.len()).sum()
115 }
116}
117
118impl Default for Nfp {
119 fn default() -> Self {
120 Self::new()
121 }
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
130pub enum NfpMethod {
131 #[default]
137 MinkowskiSum,
138
139 Sliding,
146}
147
148#[derive(Debug, Clone)]
150pub struct NfpConfig {
151 pub method: NfpMethod,
153 pub contact_tolerance: f64,
155 pub max_iterations: usize,
157}
158
159impl Default for NfpConfig {
160 fn default() -> Self {
161 Self {
162 method: NfpMethod::MinkowskiSum,
163 contact_tolerance: 1e-6,
164 max_iterations: 10000,
165 }
166 }
167}
168
169impl NfpConfig {
170 pub fn with_method(method: NfpMethod) -> Self {
172 Self {
173 method,
174 ..Default::default()
175 }
176 }
177
178 pub fn with_tolerance(mut self, tolerance: f64) -> Self {
180 self.contact_tolerance = tolerance;
181 self
182 }
183
184 pub fn with_max_iterations(mut self, max_iter: usize) -> Self {
186 self.max_iterations = max_iter;
187 self
188 }
189}
190
191pub fn compute_nfp_with_method(
202 stationary: &Geometry2D,
203 orbiting: &Geometry2D,
204 rotation: f64,
205 method: NfpMethod,
206) -> Result<Nfp> {
207 compute_nfp_with_config(
208 stationary,
209 orbiting,
210 rotation,
211 &NfpConfig::with_method(method),
212 )
213}
214
215pub fn compute_nfp_with_config(
226 stationary: &Geometry2D,
227 orbiting: &Geometry2D,
228 rotation: f64,
229 config: &NfpConfig,
230) -> Result<Nfp> {
231 let stat_exterior = stationary.exterior();
232 let orb_exterior = orbiting.exterior();
233
234 if stat_exterior.len() < 3 || orb_exterior.len() < 3 {
235 return Err(Error::InvalidGeometry(
236 "Polygons must have at least 3 vertices".into(),
237 ));
238 }
239
240 let rotated_orbiting = rotate_polygon(orb_exterior, rotation);
242
243 match config.method {
244 NfpMethod::MinkowskiSum => {
245 if stationary.is_convex()
247 && is_polygon_convex(&rotated_orbiting)
248 && stationary.holes().is_empty()
249 {
250 compute_nfp_convex(stat_exterior, &rotated_orbiting)
251 } else {
252 compute_nfp_general(stationary, &rotated_orbiting)
253 }
254 }
255 NfpMethod::Sliding => {
256 let sliding_config = SlidingNfpConfig {
258 contact_tolerance: config.contact_tolerance,
259 max_iterations: config.max_iterations,
260 min_translation: config.contact_tolerance * 0.01,
261 };
262
263 let reflected: Vec<(f64, f64)> =
265 rotated_orbiting.iter().map(|&(x, y)| (-x, -y)).collect();
266
267 compute_nfp_sliding(stat_exterior, &reflected, &sliding_config)
268 }
269 }
270}
271
272pub fn compute_nfp(stationary: &Geometry2D, orbiting: &Geometry2D, rotation: f64) -> Result<Nfp> {
289 let stat_exterior = stationary.exterior();
291 let orb_exterior = orbiting.exterior();
292
293 if stat_exterior.len() < 3 || orb_exterior.len() < 3 {
294 return Err(Error::InvalidGeometry(
295 "Polygons must have at least 3 vertices".into(),
296 ));
297 }
298
299 let rotated_orbiting = rotate_polygon(orb_exterior, rotation);
301
302 if stationary.is_convex()
304 && is_polygon_convex(&rotated_orbiting)
305 && stationary.holes().is_empty()
306 {
307 compute_nfp_convex(stat_exterior, &rotated_orbiting)
309 } else {
310 compute_nfp_general(stationary, &rotated_orbiting)
312 }
313}
314
315pub fn compute_ifp(
328 boundary_polygon: &[(f64, f64)],
329 geometry: &Geometry2D,
330 rotation: f64,
331) -> Result<Nfp> {
332 compute_ifp_with_margin(boundary_polygon, geometry, rotation, 0.0)
333}
334
335pub fn compute_ifp_with_margin(
350 boundary_polygon: &[(f64, f64)],
351 geometry: &Geometry2D,
352 rotation: f64,
353 margin: f64,
354) -> Result<Nfp> {
355 if boundary_polygon.len() < 3 {
356 return Err(Error::InvalidBoundary(
357 "Boundary must have at least 3 vertices".into(),
358 ));
359 }
360
361 let geom_exterior = geometry.exterior();
362 if geom_exterior.len() < 3 {
363 return Err(Error::InvalidGeometry(
364 "Geometry must have at least 3 vertices".into(),
365 ));
366 }
367
368 let rotated_geom = rotate_polygon(geom_exterior, rotation);
370
371 let effective_boundary = if margin > 0.0 {
373 shrink_polygon(boundary_polygon, margin)?
374 } else {
375 boundary_polygon.to_vec()
376 };
377
378 if effective_boundary.len() < 3 {
379 return Err(Error::InvalidBoundary(
380 "Boundary too small after applying margin".into(),
381 ));
382 }
383
384 compute_minkowski_erosion(&effective_boundary, &rotated_geom)
393}
394
395fn compute_minkowski_erosion(boundary: &[(f64, f64)], geometry: &[(f64, f64)]) -> Result<Nfp> {
401 if boundary.len() < 3 || geometry.len() < 3 {
402 return Err(Error::InvalidGeometry(
403 "Both boundary and geometry must have at least 3 vertices".into(),
404 ));
405 }
406
407 let (b_min_x, b_min_y, b_max_x, b_max_y) = bounding_box(boundary);
409 let is_rect = boundary.len() == 4
410 && boundary.iter().all(|&(x, y)| {
411 ((x - b_min_x).abs() < 1e-10 || (x - b_max_x).abs() < 1e-10)
412 && ((y - b_min_y).abs() < 1e-10 || (y - b_max_y).abs() < 1e-10)
413 });
414
415 let (g_min_x, g_min_y, g_max_x, g_max_y) = bounding_box(geometry);
417
418 if is_rect {
419 let ifp_min_x = b_min_x - g_min_x;
427 let ifp_max_x = b_max_x - g_max_x;
428 let ifp_min_y = b_min_y - g_min_y;
429 let ifp_max_y = b_max_y - g_max_y;
430
431 if ifp_min_x > ifp_max_x + 1e-10 || ifp_min_y > ifp_max_y + 1e-10 {
433 return Err(Error::InvalidGeometry(
434 "Geometry too large to fit in boundary".into(),
435 ));
436 }
437
438 let ifp_min_x = ifp_min_x.min(ifp_max_x);
440 let ifp_min_y = ifp_min_y.min(ifp_max_y);
441
442 return Ok(Nfp::from_polygon(vec![
443 (ifp_min_x, ifp_min_y),
444 (ifp_max_x, ifp_min_y),
445 (ifp_max_x, ifp_max_y),
446 (ifp_min_x, ifp_max_y),
447 ]));
448 }
449
450 compute_minkowski_erosion_general(boundary, geometry)
454}
455
456fn compute_minkowski_erosion_general(
458 boundary: &[(f64, f64)],
459 geometry: &[(f64, f64)],
460) -> Result<Nfp> {
461 if geometry.is_empty() {
462 return Ok(Nfp::from_polygon(boundary.to_vec()));
463 }
464
465 let first_g = geometry[0];
467 let mut result: Vec<[f64; 2]> = boundary
468 .iter()
469 .map(|&(x, y)| [x - first_g.0, y - first_g.1])
470 .collect();
471
472 for &(gx, gy) in geometry.iter().skip(1) {
474 let translated: Vec<[f64; 2]> = boundary.iter().map(|&(x, y)| [x - gx, y - gy]).collect();
475
476 let shapes = result.overlay(&[translated], OverlayRule::Intersect, FillRule::NonZero);
478
479 if shapes.is_empty() {
480 return Err(Error::InvalidGeometry(
481 "Geometry too large to fit in boundary".into(),
482 ));
483 }
484
485 result = Vec::new();
487 for shape in &shapes {
488 for contour in shape {
489 if contour.len() >= 3 {
490 result = contour.clone();
491 break;
492 }
493 }
494 if !result.is_empty() {
495 break;
496 }
497 }
498
499 if result.len() < 3 {
500 return Err(Error::InvalidGeometry(
501 "Geometry too large to fit in boundary".into(),
502 ));
503 }
504 }
505
506 let result_tuples: Vec<(f64, f64)> = result.iter().map(|&[x, y]| (x, y)).collect();
508 Ok(Nfp::from_polygon(result_tuples))
509}
510
511fn shrink_polygon(polygon: &[(f64, f64)], offset: f64) -> Result<Vec<(f64, f64)>> {
516 if polygon.len() < 3 {
517 return Err(Error::InvalidGeometry(
518 "Polygon must have at least 3 vertices".into(),
519 ));
520 }
521
522 if polygon.len() == 4 {
524 let (min_x, min_y, max_x, max_y) = bounding_box(polygon);
525
526 let is_axis_aligned = polygon.iter().all(|&(x, y)| {
528 ((x - min_x).abs() < 1e-10 || (x - max_x).abs() < 1e-10)
529 && ((y - min_y).abs() < 1e-10 || (y - max_y).abs() < 1e-10)
530 });
531
532 if is_axis_aligned {
533 let new_min_x = min_x + offset;
535 let new_min_y = min_y + offset;
536 let new_max_x = max_x - offset;
537 let new_max_y = max_y - offset;
538
539 if new_min_x >= new_max_x || new_min_y >= new_max_y {
541 return Err(Error::InvalidGeometry("Offset polygon collapsed".into()));
542 }
543
544 return Ok(vec![
545 (new_min_x, new_min_y),
546 (new_max_x, new_min_y),
547 (new_max_x, new_max_y),
548 (new_min_x, new_max_y),
549 ]);
550 }
551 }
552
553 let (cx, cy) = polygon_centroid(polygon);
555
556 let result: Vec<(f64, f64)> = polygon
557 .iter()
558 .filter_map(|&(x, y)| {
559 let dx = x - cx;
560 let dy = y - cy;
561 let dist = (dx * dx + dy * dy).sqrt();
562
563 if dist < offset + 1e-10 {
564 return None;
566 }
567
568 let factor = (dist - offset) / dist;
570 Some((cx + dx * factor, cy + dy * factor))
571 })
572 .collect();
573
574 if result.len() < 3 {
576 return Err(Error::InvalidGeometry("Offset polygon collapsed".into()));
577 }
578
579 let area = signed_area(&result).abs();
581 if area <= 1e-10 {
582 return Err(Error::InvalidGeometry("Offset polygon collapsed".into()));
583 }
584
585 Ok(result)
586}
587
588fn bounding_box(polygon: &[(f64, f64)]) -> (f64, f64, f64, f64) {
590 let mut min_x = f64::INFINITY;
591 let mut min_y = f64::INFINITY;
592 let mut max_x = f64::NEG_INFINITY;
593 let mut max_y = f64::NEG_INFINITY;
594
595 for &(x, y) in polygon {
596 min_x = min_x.min(x);
597 min_y = min_y.min(y);
598 max_x = max_x.max(x);
599 max_y = max_y.max(y);
600 }
601
602 (min_x, min_y, max_x, max_y)
603}
604
605fn polygon_centroid(polygon: &[(f64, f64)]) -> (f64, f64) {
607 let n = polygon.len() as f64;
608 let sum_x: f64 = polygon.iter().map(|p| p.0).sum();
609 let sum_y: f64 = polygon.iter().map(|p| p.1).sum();
610 (sum_x / n, sum_y / n)
611}
612
613fn compute_nfp_convex(stationary: &[(f64, f64)], orbiting: &[(f64, f64)]) -> Result<Nfp> {
618 let reflected: Vec<(f64, f64)> = orbiting.iter().map(|&(x, y)| (-x, -y)).collect();
620
621 compute_minkowski_sum_convex(stationary, &reflected)
622}
623
624fn compute_minkowski_sum_convex(poly_a: &[(f64, f64)], poly_b: &[(f64, f64)]) -> Result<Nfp> {
629 let a = ensure_ccw(poly_a);
631 let b = ensure_ccw(poly_b);
632
633 let edges_a = get_edge_vectors(&a);
635 let edges_b = get_edge_vectors(&b);
636
637 let start_a = find_bottom_left_vertex(&a);
639 let start_b = find_bottom_left_vertex(&b);
640
641 let start_point = (a[start_a].0 + b[start_b].0, a[start_a].1 + b[start_b].1);
643
644 let merged_edges = merge_edge_vectors(&edges_a, start_a, &edges_b, start_b);
646
647 let mut result = Vec::with_capacity(merged_edges.len() + 1);
649 let mut current = start_point;
650 result.push(current);
651
652 for (dx, dy) in merged_edges.iter() {
653 current = (current.0 + dx, current.1 + dy);
654 result.push(current);
655 }
656
657 if result.len() > 1 {
659 let first = result[0];
660 let last = result[result.len() - 1];
661 if (first.0 - last.0).abs() < 1e-10 && (first.1 - last.1).abs() < 1e-10 {
662 result.pop();
663 }
664 }
665
666 Ok(Nfp::from_polygon(result))
667}
668
669fn compute_nfp_general(stationary: &Geometry2D, rotated_orbiting: &[(f64, f64)]) -> Result<Nfp> {
676 let stat_triangles = triangulate_polygon(stationary.exterior());
678 let orb_triangles = triangulate_polygon(rotated_orbiting);
679
680 if stat_triangles.is_empty() || orb_triangles.is_empty() {
681 let stat_hull = stationary.convex_hull();
683 let orb_hull = convex_hull_of_points(rotated_orbiting);
684 let reflected: Vec<(f64, f64)> = orb_hull.iter().map(|&(x, y)| (-x, -y)).collect();
685 return compute_minkowski_sum_convex(&stat_hull, &reflected);
686 }
687
688 let pairs: Vec<_> = stat_triangles
691 .iter()
692 .flat_map(|stat_tri| {
693 orb_triangles
694 .iter()
695 .map(move |orb_tri| (stat_tri.clone(), orb_tri.clone()))
696 })
697 .collect();
698
699 let partial_nfps: Vec<Vec<(f64, f64)>> = pairs
700 .par_iter()
701 .flat_map(|(stat_tri, orb_tri)| {
702 let reflected: Vec<(f64, f64)> = orb_tri.iter().map(|&(x, y)| (-x, -y)).collect();
704
705 if let Ok(nfp) = compute_minkowski_sum_convex(stat_tri, &reflected) {
707 nfp.polygons
708 .into_iter()
709 .filter(|polygon| polygon.len() >= 3)
710 .collect::<Vec<_>>()
711 } else {
712 Vec::new()
713 }
714 })
715 .collect();
716
717 if partial_nfps.is_empty() {
718 let stat_hull = stationary.convex_hull();
720 let orb_hull = convex_hull_of_points(rotated_orbiting);
721 let reflected: Vec<(f64, f64)> = orb_hull.iter().map(|&(x, y)| (-x, -y)).collect();
722 return compute_minkowski_sum_convex(&stat_hull, &reflected);
723 }
724
725 union_polygons(&partial_nfps)
727}
728
729fn triangulate_polygon(polygon: &[(f64, f64)]) -> Vec<Vec<(f64, f64)>> {
731 if polygon.len() < 3 {
732 return Vec::new();
733 }
734
735 if is_polygon_convex(polygon) {
737 return vec![polygon.to_vec()];
738 }
739
740 let mut vertices: Vec<(f64, f64)> = ensure_ccw(polygon);
742 let mut triangles = Vec::new();
743
744 while vertices.len() > 3 {
745 let n = vertices.len();
746 let mut ear_found = false;
747
748 for i in 0..n {
749 let prev = (i + n - 1) % n;
750 let next = (i + 1) % n;
751
752 if is_ear(&vertices, prev, i, next) {
754 triangles.push(vec![vertices[prev], vertices[i], vertices[next]]);
755 vertices.remove(i);
756 ear_found = true;
757 break;
758 }
759 }
760
761 if !ear_found {
762 return vec![convex_hull_of_points(polygon)];
765 }
766 }
767
768 if vertices.len() == 3 {
769 triangles.push(vertices);
770 }
771
772 triangles
773}
774
775fn point_in_triangle_robust(p: (f64, f64), a: (f64, f64), b: (f64, f64), c: (f64, f64)) -> bool {
779 let o1 = orient2d_filtered(a, b, p);
780 let o2 = orient2d_filtered(b, c, p);
781 let o3 = orient2d_filtered(c, a, p);
782
783 (o1 == Orientation::CounterClockwise
786 && o2 == Orientation::CounterClockwise
787 && o3 == Orientation::CounterClockwise)
788 || (o1 == Orientation::Clockwise
789 && o2 == Orientation::Clockwise
790 && o3 == Orientation::Clockwise)
791}
792
793fn is_ear(vertices: &[(f64, f64)], prev: usize, curr: usize, next: usize) -> bool {
797 let a = vertices[prev];
798 let b = vertices[curr];
799 let c = vertices[next];
800
801 let orientation = orient2d_filtered(a, b, c);
804 if !orientation.is_ccw() {
805 return false; }
807
808 for (i, &p) in vertices.iter().enumerate() {
810 if i == prev || i == curr || i == next {
811 continue;
812 }
813 if point_in_triangle_robust(p, a, b, c) {
814 return false;
815 }
816 }
817
818 true
819}
820
821fn union_polygons(polygons: &[Vec<(f64, f64)>]) -> Result<Nfp> {
823 if polygons.is_empty() {
824 return Ok(Nfp::new());
825 }
826
827 if polygons.len() == 1 {
828 return Ok(Nfp::from_polygon(polygons[0].clone()));
829 }
830
831 let mut result: Vec<Vec<[f64; 2]>> = vec![polygons[0].iter().map(|&(x, y)| [x, y]).collect()];
833
834 for polygon in &polygons[1..] {
836 let clip: Vec<[f64; 2]> = polygon.iter().map(|&(x, y)| [x, y]).collect();
837
838 let shapes = result.overlay(&[clip], OverlayRule::Union, FillRule::NonZero);
840
841 result = Vec::new();
843 for shape in shapes {
844 for contour in shape {
845 if contour.len() >= 3 {
846 result.push(contour);
847 }
848 }
849 }
850
851 if result.is_empty() {
852 continue;
854 }
855 }
856
857 let nfp_polygons: Vec<Vec<(f64, f64)>> = result
859 .into_iter()
860 .map(|contour| contour.into_iter().map(|[x, y]| (x, y)).collect())
861 .collect();
862
863 if nfp_polygons.is_empty() {
864 return Ok(Nfp::from_polygon(polygons[0].clone()));
866 }
867
868 Ok(Nfp::from_polygons(nfp_polygons))
869}
870
871fn rotate_polygon(polygon: &[(f64, f64)], angle: f64) -> Vec<(f64, f64)> {
877 if angle.abs() < 1e-10 {
878 return polygon.to_vec();
879 }
880
881 let cos_a = angle.cos();
882 let sin_a = angle.sin();
883
884 polygon
885 .iter()
886 .map(|&(x, y)| (x * cos_a - y * sin_a, x * sin_a + y * cos_a))
887 .collect()
888}
889
890fn is_polygon_convex(polygon: &[(f64, f64)]) -> bool {
894 if polygon.len() < 3 {
895 return false;
896 }
897
898 let n = polygon.len();
899 let mut expected_orientation: Option<Orientation> = None;
900
901 for i in 0..n {
902 let p0 = polygon[i];
903 let p1 = polygon[(i + 1) % n];
904 let p2 = polygon[(i + 2) % n];
905
906 let o = orient2d_filtered(p0, p1, p2);
907
908 if o.is_collinear() {
910 continue;
911 }
912
913 match expected_orientation {
914 None => expected_orientation = Some(o),
915 Some(expected) if expected != o => return false,
916 _ => {}
917 }
918 }
919
920 true
921}
922
923fn ensure_ccw(polygon: &[(f64, f64)]) -> Vec<(f64, f64)> {
925 if signed_area(polygon) < 0.0 {
926 polygon.iter().rev().cloned().collect()
927 } else {
928 polygon.to_vec()
929 }
930}
931
932fn signed_area(polygon: &[(f64, f64)]) -> f64 {
935 let n = polygon.len();
936 let mut area = 0.0;
937
938 for i in 0..n {
939 let j = (i + 1) % n;
940 area += polygon[i].0 * polygon[j].1;
941 area -= polygon[j].0 * polygon[i].1;
942 }
943
944 area / 2.0
945}
946
947fn get_edge_vectors(polygon: &[(f64, f64)]) -> Vec<(f64, f64)> {
949 let n = polygon.len();
950 (0..n)
951 .map(|i| {
952 let j = (i + 1) % n;
953 (polygon[j].0 - polygon[i].0, polygon[j].1 - polygon[i].1)
954 })
955 .collect()
956}
957
958fn find_bottom_left_vertex(polygon: &[(f64, f64)]) -> usize {
960 let mut min_idx = 0;
961
962 for (i, &(x, y)) in polygon.iter().enumerate() {
963 let (min_x, min_y) = polygon[min_idx];
964 if y < min_y || (y == min_y && x < min_x) {
965 min_idx = i;
966 }
967 }
968
969 min_idx
970}
971
972fn edge_angle(dx: f64, dy: f64) -> f64 {
974 let angle = dy.atan2(dx);
975 if angle < 0.0 {
976 angle + 2.0 * PI
977 } else {
978 angle
979 }
980}
981
982fn merge_edge_vectors(
984 edges_a: &[(f64, f64)],
985 start_a: usize,
986 edges_b: &[(f64, f64)],
987 start_b: usize,
988) -> Vec<(f64, f64)> {
989 let n_a = edges_a.len();
990 let n_b = edges_b.len();
991 let total = n_a + n_b;
992
993 let mut result = Vec::with_capacity(total);
994 let mut i_a = 0;
995 let mut i_b = 0;
996
997 while i_a < n_a || i_b < n_b {
998 if i_a >= n_a {
999 let idx = (start_b + i_b) % n_b;
1001 result.push(edges_b[idx]);
1002 i_b += 1;
1003 } else if i_b >= n_b {
1004 let idx = (start_a + i_a) % n_a;
1006 result.push(edges_a[idx]);
1007 i_a += 1;
1008 } else {
1009 let idx_a = (start_a + i_a) % n_a;
1011 let idx_b = (start_b + i_b) % n_b;
1012
1013 let angle_a = edge_angle(edges_a[idx_a].0, edges_a[idx_a].1);
1014 let angle_b = edge_angle(edges_b[idx_b].0, edges_b[idx_b].1);
1015
1016 if angle_a <= angle_b + 1e-10 {
1017 result.push(edges_a[idx_a]);
1018 i_a += 1;
1019 }
1020 if angle_b <= angle_a + 1e-10 {
1021 result.push(edges_b[idx_b]);
1022 i_b += 1;
1023 }
1024 }
1025 }
1026
1027 result
1028}
1029
1030fn convex_hull_of_points(points: &[(f64, f64)]) -> Vec<(f64, f64)> {
1032 if points.len() < 3 {
1033 return points.to_vec();
1034 }
1035
1036 let coords: Vec<Coord<f64>> = points.iter().map(|&(x, y)| Coord { x, y }).collect();
1037
1038 let line_string = LineString::from(coords);
1039 let hull = line_string.convex_hull();
1040
1041 hull.exterior()
1042 .coords()
1043 .map(|c| (c.x, c.y))
1044 .collect::<Vec<_>>()
1045 .into_iter()
1046 .take(hull.exterior().coords().count().saturating_sub(1)) .collect()
1048}
1049
1050pub fn point_in_polygon(point: (f64, f64), polygon: &[(f64, f64)]) -> bool {
1056 let (px, py) = point;
1057 let n = polygon.len();
1058 let mut inside = false;
1059
1060 let mut j = n - 1;
1061 for i in 0..n {
1062 let (xi, yi) = polygon[i];
1063 let (xj, yj) = polygon[j];
1064
1065 if ((yi > py) != (yj > py)) && (px < (xj - xi) * (py - yi) / (yj - yi) + xi) {
1066 inside = !inside;
1067 }
1068 j = i;
1069 }
1070
1071 inside
1072}
1073
1074pub fn point_outside_all_nfps(point: (f64, f64), nfps: &[&Nfp]) -> bool {
1076 for nfp in nfps {
1077 for polygon in &nfp.polygons {
1078 if point_in_polygon(point, polygon) {
1079 return false;
1080 }
1081 }
1082 }
1083 true
1084}
1085
1086fn point_outside_all_nfps_strict(point: (f64, f64), nfps: &[&Nfp]) -> bool {
1089 for nfp in nfps {
1090 for polygon in &nfp.polygons {
1091 if point_in_polygon(point, polygon) {
1093 return false;
1094 }
1095 }
1096 }
1097 true
1098}
1099
1100fn point_on_polygon_boundary(point: (f64, f64), polygon: &[(f64, f64)]) -> bool {
1102 let (px, py) = point;
1103 let n = polygon.len();
1104 const EPS: f64 = 1e-10;
1105
1106 for i in 0..n {
1107 let (x1, y1) = polygon[i];
1108 let (x2, y2) = polygon[(i + 1) % n];
1109
1110 let dx = x2 - x1;
1113 let dy = y2 - y1;
1114 let len_sq = dx * dx + dy * dy;
1115
1116 if len_sq < EPS * EPS {
1117 if (px - x1).abs() < EPS && (py - y1).abs() < EPS {
1119 return true;
1120 }
1121 continue;
1122 }
1123
1124 let t = ((px - x1) * dx + (py - y1) * dy) / len_sq;
1126
1127 if (-EPS..=1.0 + EPS).contains(&t) {
1129 let proj_x = x1 + t * dx;
1131 let proj_y = y1 + t * dy;
1132 let dist_sq = (px - proj_x).powi(2) + (py - proj_y).powi(2);
1133
1134 if dist_sq < EPS * EPS {
1135 return true;
1136 }
1137 }
1138 }
1139
1140 false
1141}
1142
1143pub fn find_bottom_left_placement(
1164 ifp: &Nfp,
1165 nfps: &[&Nfp],
1166 sample_step: f64,
1167) -> Option<(f64, f64)> {
1168 if ifp.is_empty() {
1169 return None;
1170 }
1171
1172 let mut candidates: Vec<(f64, f64)> = Vec::new();
1174
1175 for polygon in &ifp.polygons {
1176 candidates.extend(polygon.iter().copied());
1177 }
1178
1179 for nfp in nfps {
1181 for polygon in &nfp.polygons {
1182 candidates.extend(polygon.iter().copied());
1183 }
1184 }
1185
1186 let (min_x, min_y, max_x, max_y) = ifp_bounding_box(ifp);
1188
1189 let mut y = min_y;
1191 while y <= max_y {
1192 let mut x = min_x;
1193 while x <= max_x {
1194 candidates.push((x, y));
1195 x += sample_step;
1196 }
1197 y += sample_step;
1198 }
1199
1200 let valid_candidates: Vec<(f64, f64)> = candidates
1202 .into_iter()
1203 .filter(|&point| {
1204 let in_ifp = ifp
1206 .polygons
1207 .iter()
1208 .any(|p| point_in_polygon(point, p) || point_on_polygon_boundary(point, p));
1209 if !in_ifp {
1210 return false;
1211 }
1212 point_outside_all_nfps_strict(point, nfps)
1214 })
1215 .collect();
1216
1217 valid_candidates.into_iter().min_by(|a, b| {
1220 match a.0.partial_cmp(&b.0) {
1222 Some(std::cmp::Ordering::Equal) => {
1223 a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)
1224 }
1225 Some(ord) => ord,
1226 None => std::cmp::Ordering::Equal,
1227 }
1228 })
1229}
1230
1231fn ifp_bounding_box(ifp: &Nfp) -> (f64, f64, f64, f64) {
1233 let mut min_x = f64::INFINITY;
1234 let mut min_y = f64::INFINITY;
1235 let mut max_x = f64::NEG_INFINITY;
1236 let mut max_y = f64::NEG_INFINITY;
1237
1238 for polygon in &ifp.polygons {
1239 for &(x, y) in polygon {
1240 min_x = min_x.min(x);
1241 min_y = min_y.min(y);
1242 max_x = max_x.max(x);
1243 max_y = max_y.max(y);
1244 }
1245 }
1246
1247 (min_x, min_y, max_x, max_y)
1248}
1249
1250#[derive(Debug, Clone)]
1252pub struct PlacedGeometry {
1253 pub geometry: Geometry2D,
1255 pub position: (f64, f64),
1257 pub rotation: f64,
1259}
1260
1261impl PlacedGeometry {
1262 pub fn new(geometry: Geometry2D, position: (f64, f64), rotation: f64) -> Self {
1264 Self {
1265 geometry,
1266 position,
1267 rotation,
1268 }
1269 }
1270
1271 pub fn translated_exterior(&self) -> Vec<(f64, f64)> {
1273 let rotated = rotate_polygon(self.geometry.exterior(), self.rotation);
1274 rotated
1275 .into_iter()
1276 .map(|(x, y)| (x + self.position.0, y + self.position.1))
1277 .collect()
1278 }
1279}
1280
1281#[derive(Debug, Clone, Hash, PartialEq, Eq)]
1287struct NfpCacheKey {
1288 geometry_a: String,
1289 geometry_b: String,
1290 rotation_millideg: i32, }
1292
1293impl NfpCacheKey {
1294 fn new(id_a: &str, id_b: &str, rotation_rad: f64) -> Self {
1295 let rotation_millideg = ((rotation_rad * 180.0 / PI) * 1000.0).round() as i32;
1297 Self {
1298 geometry_a: id_a.to_string(),
1299 geometry_b: id_b.to_string(),
1300 rotation_millideg,
1301 }
1302 }
1303}
1304
1305#[derive(Debug)]
1307pub struct NfpCache {
1308 cache: RwLock<HashMap<NfpCacheKey, Arc<Nfp>>>,
1309 max_size: usize,
1310}
1311
1312impl NfpCache {
1313 pub fn new() -> Self {
1315 Self::with_capacity(1000)
1316 }
1317
1318 pub fn with_capacity(max_size: usize) -> Self {
1320 Self {
1321 cache: RwLock::new(HashMap::new()),
1322 max_size,
1323 }
1324 }
1325
1326 pub fn get_or_compute<F>(&self, key: (&str, &str, f64), compute: F) -> Result<Arc<Nfp>>
1332 where
1333 F: FnOnce() -> Result<Nfp>,
1334 {
1335 let cache_key = NfpCacheKey::new(key.0, key.1, key.2);
1336
1337 {
1339 let cache = self.cache.read().map_err(|e| {
1340 Error::Internal(format!("Failed to acquire cache read lock: {}", e))
1341 })?;
1342 if let Some(nfp) = cache.get(&cache_key) {
1343 return Ok(Arc::clone(nfp));
1344 }
1345 }
1346
1347 let nfp = Arc::new(compute()?);
1349
1350 {
1352 let mut cache = self.cache.write().map_err(|e| {
1353 Error::Internal(format!("Failed to acquire cache write lock: {}", e))
1354 })?;
1355
1356 if cache.len() >= self.max_size {
1358 let keys_to_remove: Vec<_> =
1359 cache.keys().take(self.max_size / 2).cloned().collect();
1360 for key in keys_to_remove {
1361 cache.remove(&key);
1362 }
1363 }
1364
1365 cache.insert(cache_key, Arc::clone(&nfp));
1366 }
1367
1368 Ok(nfp)
1369 }
1370
1371 pub fn len(&self) -> usize {
1373 self.cache.read().map(|c| c.len()).unwrap_or(0)
1374 }
1375
1376 pub fn is_empty(&self) -> bool {
1378 self.len() == 0
1379 }
1380
1381 pub fn clear(&self) {
1383 if let Ok(mut cache) = self.cache.write() {
1384 cache.clear();
1385 }
1386 }
1387}
1388
1389impl Default for NfpCache {
1390 fn default() -> Self {
1391 Self::new()
1392 }
1393}
1394
1395#[cfg(test)]
1396mod tests {
1397 use super::*;
1398 use approx::assert_relative_eq;
1399
1400 fn rect(w: f64, h: f64) -> Vec<(f64, f64)> {
1401 vec![(0.0, 0.0), (w, 0.0), (w, h), (0.0, h)]
1402 }
1403
1404 fn triangle() -> Vec<(f64, f64)> {
1405 vec![(0.0, 0.0), (10.0, 0.0), (5.0, 10.0)]
1406 }
1407
1408 #[test]
1409 fn test_is_polygon_convex() {
1410 assert!(is_polygon_convex(&rect(10.0, 10.0)));
1412
1413 assert!(is_polygon_convex(&triangle()));
1415
1416 let l_shape = vec![
1418 (0.0, 0.0),
1419 (10.0, 0.0),
1420 (10.0, 5.0),
1421 (5.0, 5.0),
1422 (5.0, 10.0),
1423 (0.0, 10.0),
1424 ];
1425 assert!(!is_polygon_convex(&l_shape));
1426 }
1427
1428 #[test]
1429 fn test_signed_area() {
1430 let ccw_square = rect(10.0, 10.0);
1432 assert!(signed_area(&ccw_square) > 0.0);
1433 assert_relative_eq!(signed_area(&ccw_square).abs(), 100.0, epsilon = 1e-10);
1434
1435 let cw_square: Vec<_> = ccw_square.into_iter().rev().collect();
1437 assert!(signed_area(&cw_square) < 0.0);
1438 }
1439
1440 #[test]
1441 fn test_rotate_polygon() {
1442 let square = rect(10.0, 10.0);
1443
1444 let rotated = rotate_polygon(&square, 0.0);
1446 assert_eq!(rotated.len(), square.len());
1447
1448 let rotated = rotate_polygon(&[(1.0, 0.0)], PI / 2.0);
1450 assert_relative_eq!(rotated[0].0, 0.0, epsilon = 1e-10);
1451 assert_relative_eq!(rotated[0].1, 1.0, epsilon = 1e-10);
1452 }
1453
1454 #[test]
1455 fn test_nfp_two_squares() {
1456 let a = Geometry2D::rectangle("A", 10.0, 10.0);
1457 let b = Geometry2D::rectangle("B", 5.0, 5.0);
1458
1459 let nfp = compute_nfp(&a, &b, 0.0).unwrap();
1460
1461 assert!(!nfp.is_empty());
1462 assert_eq!(nfp.polygons.len(), 1);
1463
1464 let polygon = &nfp.polygons[0];
1467 assert!(polygon.len() >= 4);
1468 }
1469
1470 #[test]
1471 fn test_nfp_with_rotation() {
1472 let a = Geometry2D::rectangle("A", 10.0, 10.0);
1473 let b = Geometry2D::rectangle("B", 5.0, 5.0);
1474
1475 let nfp = compute_nfp(&a, &b, PI / 4.0).unwrap();
1477
1478 assert!(!nfp.is_empty());
1479 }
1481
1482 #[test]
1483 fn test_ifp_square_in_boundary() {
1484 let boundary = rect(100.0, 100.0);
1485 let geom = Geometry2D::rectangle("G", 10.0, 10.0);
1486
1487 let ifp = compute_ifp(&boundary, &geom, 0.0).unwrap();
1488
1489 assert!(!ifp.is_empty());
1490 let polygon = &ifp.polygons[0];
1493 let (min_x, min_y, max_x, max_y) = bounding_box(polygon);
1494 assert_relative_eq!(min_x, 0.0, epsilon = 1e-10);
1495 assert_relative_eq!(min_y, 0.0, epsilon = 1e-10);
1496 assert_relative_eq!(max_x, 90.0, epsilon = 1e-10);
1497 assert_relative_eq!(max_y, 90.0, epsilon = 1e-10);
1498 }
1499
1500 #[test]
1501 fn test_ifp_bounds_correct() {
1502 let boundary = rect(100.0, 50.0);
1504 let geom = Geometry2D::rectangle("R", 25.0, 25.0);
1505
1506 let ifp = compute_ifp(&boundary, &geom, 0.0).unwrap();
1507
1508 assert!(!ifp.is_empty());
1509 let polygon = &ifp.polygons[0];
1511 let (min_x, min_y, max_x, max_y) = bounding_box(polygon);
1512 assert_relative_eq!(min_x, 0.0, epsilon = 1e-10);
1513 assert_relative_eq!(min_y, 0.0, epsilon = 1e-10);
1514 assert_relative_eq!(max_x, 75.0, epsilon = 1e-10);
1515 assert_relative_eq!(max_y, 25.0, epsilon = 1e-10);
1516
1517 assert!(point_in_polygon((0.0, 0.0), polygon) || point_on_boundary((0.0, 0.0), polygon));
1519 assert!(point_in_polygon((25.0, 0.0), polygon) || point_on_boundary((25.0, 0.0), polygon));
1520 assert!(point_in_polygon((50.0, 0.0), polygon) || point_on_boundary((50.0, 0.0), polygon));
1521 assert!(point_in_polygon((75.0, 0.0), polygon) || point_on_boundary((75.0, 0.0), polygon));
1522 }
1523
1524 fn point_on_boundary(point: (f64, f64), polygon: &[(f64, f64)]) -> bool {
1526 let (px, py) = point;
1527 let n = polygon.len();
1528 for i in 0..n {
1529 let (x1, y1) = polygon[i];
1530 let (x2, y2) = polygon[(i + 1) % n];
1531 let d1 = ((px - x1).powi(2) + (py - y1).powi(2)).sqrt();
1533 let d2 = ((px - x2).powi(2) + (py - y2).powi(2)).sqrt();
1534 let d_total = ((x2 - x1).powi(2) + (y2 - y1).powi(2)).sqrt();
1535 if (d1 + d2 - d_total).abs() < 1e-10 {
1536 return true;
1537 }
1538 }
1539 false
1540 }
1541
1542 #[test]
1543 fn test_nfp_same_size_rectangles() {
1544 let a = Geometry2D::rectangle("A", 25.0, 25.0);
1546 let b = Geometry2D::rectangle("B", 25.0, 25.0);
1547
1548 let nfp = compute_nfp(&a, &b, 0.0).unwrap();
1549 assert!(!nfp.is_empty());
1550
1551 let polygon = &nfp.polygons[0];
1552 let (min_x, min_y, max_x, max_y) = bounding_box(polygon);
1553 let width = max_x - min_x;
1556 let height = max_y - min_y;
1557 eprintln!("NFP dimensions: {}x{}", width, height);
1558 eprintln!(
1559 "NFP bounds: ({}, {}) to ({}, {})",
1560 min_x, min_y, max_x, max_y
1561 );
1562 assert_relative_eq!(width, 50.0, epsilon = 1e-6);
1564 assert_relative_eq!(height, 50.0, epsilon = 1e-6);
1565 }
1566
1567 #[test]
1568 fn test_nfp_cache() {
1569 let cache = NfpCache::new();
1570
1571 let compute_count = std::sync::atomic::AtomicUsize::new(0);
1572
1573 let result1 = cache
1574 .get_or_compute(("A", "B", 0.0), || {
1575 compute_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1576 Ok(Nfp::from_polygon(vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)]))
1577 })
1578 .unwrap();
1579
1580 let result2 = cache
1581 .get_or_compute(("A", "B", 0.0), || {
1582 compute_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1583 Ok(Nfp::from_polygon(vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)]))
1584 })
1585 .unwrap();
1586
1587 assert_eq!(compute_count.load(std::sync::atomic::Ordering::SeqCst), 1);
1589 assert_eq!(result1.polygons, result2.polygons);
1590 assert_eq!(cache.len(), 1);
1591 }
1592
1593 #[test]
1594 fn test_nfp_cache_different_rotations() {
1595 let cache = NfpCache::new();
1596
1597 cache
1598 .get_or_compute(("A", "B", 0.0), || {
1599 Ok(Nfp::from_polygon(vec![(0.0, 0.0), (1.0, 0.0)]))
1600 })
1601 .unwrap();
1602
1603 cache
1604 .get_or_compute(("A", "B", PI / 2.0), || {
1605 Ok(Nfp::from_polygon(vec![(0.0, 0.0), (0.0, 1.0)]))
1606 })
1607 .unwrap();
1608
1609 assert_eq!(cache.len(), 2);
1611 }
1612
1613 #[test]
1614 fn test_edge_angle() {
1615 assert_relative_eq!(edge_angle(1.0, 0.0), 0.0, epsilon = 1e-10);
1617
1618 assert_relative_eq!(edge_angle(0.0, 1.0), PI / 2.0, epsilon = 1e-10);
1620
1621 assert_relative_eq!(edge_angle(-1.0, 0.0), PI, epsilon = 1e-10);
1623
1624 assert_relative_eq!(edge_angle(0.0, -1.0), 3.0 * PI / 2.0, epsilon = 1e-10);
1626 }
1627
1628 #[test]
1629 fn test_convex_hull_of_points() {
1630 let points = vec![
1631 (0.0, 0.0),
1632 (10.0, 0.0),
1633 (5.0, 5.0), (10.0, 10.0),
1635 (0.0, 10.0),
1636 ];
1637
1638 let hull = convex_hull_of_points(&points);
1639
1640 assert_eq!(hull.len(), 4);
1642 }
1643
1644 #[test]
1645 fn test_shrink_polygon_square() {
1646 let square = rect(100.0, 100.0);
1647 let shrunk = shrink_polygon(&square, 10.0).unwrap();
1648
1649 assert_eq!(shrunk.len(), 4);
1651
1652 let original_area = signed_area(&square).abs();
1654 let shrunk_area = signed_area(&shrunk).abs();
1655 assert!(
1656 shrunk_area < original_area,
1657 "shrunk_area ({}) should be < original_area ({})",
1658 shrunk_area,
1659 original_area
1660 );
1661
1662 assert_relative_eq!(shrunk_area, 6400.0, epsilon = 1.0);
1665 }
1666
1667 #[test]
1668 fn test_shrink_polygon_collapse() {
1669 let small_square = rect(10.0, 10.0);
1670
1671 let result = shrink_polygon(&small_square, 6.0);
1673 assert!(
1674 result.is_err(),
1675 "Polygon should collapse when offset >= width/2"
1676 );
1677 }
1678
1679 #[test]
1680 fn test_ifp_with_margin() {
1681 let boundary = rect(100.0, 100.0);
1682 let geom = Geometry2D::rectangle("G", 10.0, 10.0);
1683
1684 let ifp_no_margin = compute_ifp(&boundary, &geom, 0.0).unwrap();
1686
1687 let ifp_with_margin = compute_ifp_with_margin(&boundary, &geom, 0.0, 5.0).unwrap();
1689
1690 assert!(!ifp_no_margin.is_empty());
1691 assert!(!ifp_with_margin.is_empty());
1692
1693 let (min_x_no, _min_y_no, max_x_no, _max_y_no) = ifp_bounding_box(&ifp_no_margin);
1695 let (min_x_margin, _min_y_margin, max_x_margin, _max_y_margin) =
1696 ifp_bounding_box(&ifp_with_margin);
1697
1698 let width_no = max_x_no - min_x_no;
1699 let width_margin = max_x_margin - min_x_margin;
1700
1701 assert!(
1705 width_margin < width_no,
1706 "width_margin ({}) should be < width_no ({})",
1707 width_margin,
1708 width_no
1709 );
1710 }
1711
1712 #[test]
1713 fn test_ifp_margin_boundary_collapse() {
1714 let boundary = rect(20.0, 20.0);
1715
1716 let result = shrink_polygon(&boundary, 12.0);
1718 assert!(
1719 result.is_err(),
1720 "Boundary should collapse with margin >= width/2"
1721 );
1722 }
1723
1724 #[test]
1725 fn test_ifp_margin_large_geometry() {
1726 let boundary = rect(30.0, 30.0);
1727 let geom = Geometry2D::rectangle("G", 20.0, 20.0);
1728
1729 let ifp_no_margin = compute_ifp(&boundary, &geom, 0.0).unwrap();
1731 let (min_x_no, _, max_x_no, _) = ifp_bounding_box(&ifp_no_margin);
1732 let width_no = max_x_no - min_x_no;
1733
1734 let ifp_with_margin = compute_ifp_with_margin(&boundary, &geom, 0.0, 5.0).unwrap();
1736 let (min_x_margin, _, max_x_margin, _) = ifp_bounding_box(&ifp_with_margin);
1737 let width_margin = max_x_margin - min_x_margin;
1738
1739 assert!(
1741 width_margin <= width_no,
1742 "width_margin ({}) should be <= width_no ({})",
1743 width_margin,
1744 width_no
1745 );
1746 }
1747
1748 #[test]
1749 fn test_nfp_non_convex_l_shape() {
1750 let l_shape = Geometry2D::new("L").with_polygon(vec![
1752 (0.0, 0.0),
1753 (20.0, 0.0),
1754 (20.0, 10.0),
1755 (10.0, 10.0),
1756 (10.0, 20.0),
1757 (0.0, 20.0),
1758 ]);
1759
1760 let small_square = Geometry2D::rectangle("S", 5.0, 5.0);
1761
1762 let nfp = compute_nfp(&l_shape, &small_square, 0.0).unwrap();
1764
1765 assert!(!nfp.is_empty());
1766 assert!(nfp.vertex_count() >= 4);
1768 }
1769
1770 #[test]
1771 fn test_triangulate_polygon_convex() {
1772 let square = rect(10.0, 10.0);
1773 let triangles = triangulate_polygon(&square);
1774
1775 assert_eq!(triangles.len(), 1);
1777 assert_eq!(triangles[0].len(), 4);
1778 }
1779
1780 #[test]
1781 fn test_triangulate_polygon_non_convex() {
1782 let l_shape = vec![
1784 (0.0, 0.0),
1785 (20.0, 0.0),
1786 (20.0, 10.0),
1787 (10.0, 10.0),
1788 (10.0, 20.0),
1789 (0.0, 20.0),
1790 ];
1791
1792 let triangles = triangulate_polygon(&l_shape);
1793
1794 assert!(triangles.len() >= 1);
1796 }
1797
1798 #[test]
1799 fn test_union_polygons() {
1800 let poly1 = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
1802 let poly2 = vec![(5.0, 5.0), (15.0, 5.0), (15.0, 15.0), (5.0, 15.0)];
1803
1804 let result = union_polygons(&[poly1, poly2]).unwrap();
1805
1806 assert!(!result.is_empty());
1807 assert!(result.vertex_count() >= 6);
1809 }
1810
1811 #[test]
1816 fn test_convex_near_collinear_vertices() {
1817 let near_collinear = vec![
1819 (0.0, 0.0),
1820 (1.0, 1e-15), (2.0, 0.0),
1822 (2.0, 1.0),
1823 (0.0, 1.0),
1824 ];
1825
1826 let result = is_polygon_convex(&near_collinear);
1828 assert!(result == true || result == false);
1830 }
1831
1832 #[test]
1833 fn test_triangulation_near_degenerate() {
1834 let near_degenerate_l = vec![
1836 (0.0, 0.0),
1837 (10.0, 0.0),
1838 (10.0, 5.0),
1839 (5.0 + 1e-12, 5.0), (5.0, 10.0),
1841 (0.0, 10.0),
1842 ];
1843
1844 let triangles = triangulate_polygon(&near_degenerate_l);
1846
1847 assert!(!triangles.is_empty());
1849 }
1850
1851 #[test]
1852 fn test_nfp_nearly_touching_rectangles() {
1853 let a = Geometry2D::rectangle("A", 10.0, 10.0);
1855 let b = Geometry2D::rectangle("B", 5.0, 5.0);
1856
1857 let nfp = compute_nfp(&a, &b, 0.0).unwrap();
1859 assert!(!nfp.is_empty());
1860 }
1861
1862 #[test]
1863 fn test_ifp_geometry_nearly_fills_boundary() {
1864 let boundary = rect(100.0, 100.0);
1866 let geom = Geometry2D::rectangle("G", 99.9999, 99.9999);
1867
1868 let result = compute_ifp(&boundary, &geom, 0.0);
1870
1871 match result {
1873 Ok(ifp) => {
1874 let (min_x, min_y, max_x, max_y) = ifp_bounding_box(&ifp);
1876 let width = max_x - min_x;
1877 let height = max_y - min_y;
1878 assert!(width < 0.001 && height < 0.001);
1879 }
1880 Err(_) => {
1881 }
1883 }
1884 }
1885
1886 #[test]
1887 fn test_point_in_polygon_on_boundary() {
1888 let square = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
1890
1891 let on_bottom_edge = (5.0, 0.0);
1893 let on_right_edge = (10.0, 5.0);
1894 let on_top_edge = (5.0, 10.0);
1895 let on_left_edge = (0.0, 5.0);
1896
1897 let _ = point_in_polygon(on_bottom_edge, &square);
1900 let _ = point_in_polygon(on_right_edge, &square);
1901 let _ = point_in_polygon(on_top_edge, &square);
1902 let _ = point_in_polygon(on_left_edge, &square);
1903 }
1904
1905 #[test]
1906 fn test_point_in_triangle_robust_degenerate() {
1907 let a = (0.0, 0.0);
1909 let b = (5.0, 0.0);
1910 let c = (10.0, 0.0);
1911
1912 let p = (3.0, 0.0);
1914
1915 assert!(!point_in_triangle_robust(p, a, b, c));
1917 }
1918
1919 #[test]
1920 fn test_ear_detection_with_collinear_points() {
1921 let with_collinear = vec![
1923 (0.0, 0.0),
1924 (5.0, 0.0),
1925 (10.0, 0.0), (10.0, 10.0),
1927 (0.0, 10.0),
1928 ];
1929
1930 let triangles = triangulate_polygon(&with_collinear);
1932
1933 for triangle in &triangles {
1935 assert!(triangle.len() >= 3);
1936 }
1937 }
1938
1939 #[test]
1940 fn test_nfp_with_very_small_polygon() {
1941 let tiny = Geometry2D::rectangle("tiny", 1e-6, 1e-6);
1943 let normal = Geometry2D::rectangle("normal", 10.0, 10.0);
1944
1945 let nfp = compute_nfp(&normal, &tiny, 0.0).unwrap();
1947 assert!(!nfp.is_empty());
1948 }
1949
1950 #[test]
1951 fn test_nfp_with_very_large_polygon() {
1952 let large = Geometry2D::rectangle("large", 1e6, 1e6);
1954 let normal = Geometry2D::rectangle("normal", 100.0, 100.0);
1955
1956 let nfp = compute_nfp(&large, &normal, 0.0).unwrap();
1958 assert!(!nfp.is_empty());
1959 }
1960
1961 #[test]
1962 fn test_signed_area_with_extreme_coordinates() {
1963 let moderate_coords = vec![
1970 (1e6, 1e6),
1971 (1e6 + 100.0, 1e6),
1972 (1e6 + 100.0, 1e6 + 100.0),
1973 (1e6, 1e6 + 100.0),
1974 ];
1975
1976 let area = signed_area(&moderate_coords);
1977
1978 assert_relative_eq!(area.abs(), 10000.0, epsilon = 1.0);
1980 }
1981
1982 #[test]
1983 fn test_ensure_ccw_with_near_zero_area() {
1984 let tiny_area = vec![(0.0, 0.0), (1e-10, 0.0), (1e-10, 1e-10), (0.0, 1e-10)];
1986
1987 let ccw = ensure_ccw(&tiny_area);
1989 assert_eq!(ccw.len(), tiny_area.len());
1990 }
1991
1992 #[test]
1997 fn test_nfp_method_default() {
1998 let config = NfpConfig::default();
1999 assert_eq!(config.method, NfpMethod::MinkowskiSum);
2000 }
2001
2002 #[test]
2003 fn test_nfp_method_minkowski_sum() {
2004 let a = Geometry2D::rectangle("A", 10.0, 10.0);
2005 let b = Geometry2D::rectangle("B", 5.0, 5.0);
2006
2007 let nfp = compute_nfp_with_method(&a, &b, 0.0, NfpMethod::MinkowskiSum).unwrap();
2008
2009 assert!(!nfp.is_empty());
2010 assert!(nfp.vertex_count() >= 4);
2011 }
2012
2013 #[test]
2014 fn test_nfp_method_sliding() {
2015 let a = Geometry2D::rectangle("A", 10.0, 10.0);
2016 let b = Geometry2D::rectangle("B", 5.0, 5.0);
2017
2018 let result = compute_nfp_with_method(&a, &b, 0.0, NfpMethod::Sliding);
2019
2020 assert!(result.is_ok(), "Sliding method should not error");
2022 let nfp = result.unwrap();
2023 assert!(!nfp.is_empty(), "NFP should not be empty");
2024
2025 }
2029
2030 #[test]
2031 fn test_nfp_method_config_builder() {
2032 let config = NfpConfig::with_method(NfpMethod::Sliding)
2033 .with_tolerance(1e-5)
2034 .with_max_iterations(5000);
2035
2036 assert_eq!(config.method, NfpMethod::Sliding);
2037 assert!((config.contact_tolerance - 1e-5).abs() < 1e-10);
2038 assert_eq!(config.max_iterations, 5000);
2039 }
2040
2041 #[test]
2042 fn test_nfp_methods_both_succeed() {
2043 let a = Geometry2D::rectangle("A", 10.0, 10.0);
2044 let b = Geometry2D::rectangle("B", 5.0, 5.0);
2045
2046 let nfp_mink = compute_nfp_with_method(&a, &b, 0.0, NfpMethod::MinkowskiSum).unwrap();
2047 let nfp_slide = compute_nfp_with_method(&a, &b, 0.0, NfpMethod::Sliding).unwrap();
2048
2049 assert!(!nfp_mink.is_empty());
2051 assert!(!nfp_slide.is_empty());
2052
2053 assert!(nfp_mink.vertex_count() >= 4);
2055
2056 }
2061
2062 #[test]
2063 fn test_nfp_sliding_l_shape() {
2064 let l_shape = Geometry2D::new("L").with_polygon(vec![
2066 (0.0, 0.0),
2067 (20.0, 0.0),
2068 (20.0, 10.0),
2069 (10.0, 10.0),
2070 (10.0, 20.0),
2071 (0.0, 20.0),
2072 ]);
2073
2074 let small_square = Geometry2D::rectangle("S", 5.0, 5.0);
2075
2076 let result = compute_nfp_with_method(&l_shape, &small_square, 0.0, NfpMethod::Sliding);
2078
2079 assert!(result.is_ok(), "Sliding should not error on L-shape");
2081 let nfp = result.unwrap();
2082 assert!(!nfp.is_empty(), "NFP should not be empty for L-shape");
2083 }
2084
2085 #[test]
2086 fn test_nfp_with_config() {
2087 let a = Geometry2D::rectangle("A", 10.0, 10.0);
2088 let b = Geometry2D::rectangle("B", 5.0, 5.0);
2089
2090 let config = NfpConfig {
2091 method: NfpMethod::Sliding,
2092 contact_tolerance: 1e-4,
2093 max_iterations: 2000,
2094 };
2095
2096 let nfp = compute_nfp_with_config(&a, &b, 0.0, &config).unwrap();
2097
2098 assert!(!nfp.is_empty());
2099 }
2100}