use crate::geometry::Geometry2D;
use crate::nfp_sliding::{compute_nfp_sliding, SlidingNfpConfig};
use i_overlay::core::fill_rule::FillRule;
use i_overlay::core::overlay_rule::OverlayRule;
use i_overlay::float::single::SingleFloatOverlay;
#[cfg(feature = "parallel")]
use rayon::prelude::*;
use std::collections::HashMap;
use std::f64::consts::PI;
use std::sync::{Arc, RwLock};
use u_nesting_core::geom::polygon as geom_polygon;
use u_nesting_core::geometry::Geometry2DExt;
use u_nesting_core::robust::{orient2d_filtered, Orientation};
use u_nesting_core::{Error, Result};
use crate::placement_utils::polygon_centroid;
pub fn rotate_nfp(nfp: &Nfp, angle: f64) -> Nfp {
if angle.abs() < 1e-10 {
return nfp.clone();
}
let cos_a = angle.cos();
let sin_a = angle.sin();
Nfp {
polygons: nfp
.polygons
.iter()
.map(|polygon| {
polygon
.iter()
.map(|&(x, y)| (x * cos_a - y * sin_a, x * sin_a + y * cos_a))
.collect()
})
.collect(),
}
}
pub fn translate_nfp(nfp: &Nfp, offset: (f64, f64)) -> Nfp {
Nfp {
polygons: nfp
.polygons
.iter()
.map(|polygon| {
polygon
.iter()
.map(|(x, y)| (x + offset.0, y + offset.1))
.collect()
})
.collect(),
}
}
#[derive(Debug, Clone)]
pub struct Nfp {
pub polygons: Vec<Vec<(f64, f64)>>,
}
impl Nfp {
pub fn new() -> Self {
Self {
polygons: Vec::new(),
}
}
pub fn from_polygon(polygon: Vec<(f64, f64)>) -> Self {
Self {
polygons: vec![polygon],
}
}
pub fn from_polygons(polygons: Vec<Vec<(f64, f64)>>) -> Self {
Self { polygons }
}
pub fn is_empty(&self) -> bool {
self.polygons.is_empty()
}
pub fn vertex_count(&self) -> usize {
self.polygons.iter().map(|p| p.len()).sum()
}
}
impl Default for Nfp {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum NfpMethod {
#[default]
MinkowskiSum,
Sliding,
}
#[derive(Debug, Clone)]
pub struct NfpConfig {
pub method: NfpMethod,
pub contact_tolerance: f64,
pub max_iterations: usize,
}
impl Default for NfpConfig {
fn default() -> Self {
Self {
method: NfpMethod::MinkowskiSum,
contact_tolerance: 1e-6,
max_iterations: 10000,
}
}
}
impl NfpConfig {
pub fn with_method(method: NfpMethod) -> Self {
Self {
method,
..Default::default()
}
}
pub fn with_tolerance(mut self, tolerance: f64) -> Self {
self.contact_tolerance = tolerance;
self
}
pub fn with_max_iterations(mut self, max_iter: usize) -> Self {
self.max_iterations = max_iter;
self
}
}
pub fn compute_nfp_with_method(
stationary: &Geometry2D,
orbiting: &Geometry2D,
rotation: f64,
method: NfpMethod,
) -> Result<Nfp> {
compute_nfp_with_config(
stationary,
orbiting,
rotation,
&NfpConfig::with_method(method),
)
}
pub fn compute_nfp_with_config(
stationary: &Geometry2D,
orbiting: &Geometry2D,
rotation: f64,
config: &NfpConfig,
) -> Result<Nfp> {
let stat_exterior = stationary.exterior();
let orb_exterior = orbiting.exterior();
if stat_exterior.len() < 3 || orb_exterior.len() < 3 {
return Err(Error::InvalidGeometry(
"Polygons must have at least 3 vertices".into(),
));
}
let rotated_orbiting = rotate_polygon(orb_exterior, rotation);
match config.method {
NfpMethod::MinkowskiSum => {
if stationary.is_convex()
&& is_polygon_convex(&rotated_orbiting)
&& stationary.holes().is_empty()
{
compute_nfp_convex(stat_exterior, &rotated_orbiting)
} else {
compute_nfp_general(stationary, &rotated_orbiting)
}
}
NfpMethod::Sliding => {
let sliding_config = SlidingNfpConfig {
contact_tolerance: config.contact_tolerance,
max_iterations: config.max_iterations,
min_translation: config.contact_tolerance * 0.01,
};
let reflected: Vec<(f64, f64)> =
rotated_orbiting.iter().map(|&(x, y)| (-x, -y)).collect();
compute_nfp_sliding(stat_exterior, &reflected, &sliding_config)
}
}
}
pub fn compute_nfp(stationary: &Geometry2D, orbiting: &Geometry2D, rotation: f64) -> Result<Nfp> {
let stat_exterior = stationary.exterior();
let orb_exterior = orbiting.exterior();
if stat_exterior.len() < 3 || orb_exterior.len() < 3 {
return Err(Error::InvalidGeometry(
"Polygons must have at least 3 vertices".into(),
));
}
let rotated_orbiting = rotate_polygon(orb_exterior, rotation);
if stationary.is_convex()
&& is_polygon_convex(&rotated_orbiting)
&& stationary.holes().is_empty()
{
compute_nfp_convex(stat_exterior, &rotated_orbiting)
} else {
compute_nfp_general(stationary, &rotated_orbiting)
}
}
pub fn compute_ifp(
boundary_polygon: &[(f64, f64)],
geometry: &Geometry2D,
rotation: f64,
) -> Result<Nfp> {
compute_ifp_with_margin(boundary_polygon, geometry, rotation, 0.0)
}
pub fn compute_ifp_with_margin(
boundary_polygon: &[(f64, f64)],
geometry: &Geometry2D,
rotation: f64,
margin: f64,
) -> Result<Nfp> {
if boundary_polygon.len() < 3 {
return Err(Error::InvalidBoundary(
"Boundary must have at least 3 vertices".into(),
));
}
let geom_exterior = geometry.exterior();
if geom_exterior.len() < 3 {
return Err(Error::InvalidGeometry(
"Geometry must have at least 3 vertices".into(),
));
}
let rotated_geom = rotate_polygon(geom_exterior, rotation);
let effective_boundary = if margin > 0.0 {
shrink_polygon(boundary_polygon, margin)?
} else {
boundary_polygon.to_vec()
};
if effective_boundary.len() < 3 {
return Err(Error::InvalidBoundary(
"Boundary too small after applying margin".into(),
));
}
compute_minkowski_erosion(&effective_boundary, &rotated_geom)
}
fn compute_minkowski_erosion(boundary: &[(f64, f64)], geometry: &[(f64, f64)]) -> Result<Nfp> {
if boundary.len() < 3 || geometry.len() < 3 {
return Err(Error::InvalidGeometry(
"Both boundary and geometry must have at least 3 vertices".into(),
));
}
let (b_min_x, b_min_y, b_max_x, b_max_y) = bounding_box(boundary);
let is_rect = boundary.len() == 4
&& boundary.iter().all(|&(x, y)| {
((x - b_min_x).abs() < 1e-10 || (x - b_max_x).abs() < 1e-10)
&& ((y - b_min_y).abs() < 1e-10 || (y - b_max_y).abs() < 1e-10)
});
let (g_min_x, g_min_y, g_max_x, g_max_y) = bounding_box(geometry);
if is_rect {
let ifp_min_x = b_min_x - g_min_x;
let ifp_max_x = b_max_x - g_max_x;
let ifp_min_y = b_min_y - g_min_y;
let ifp_max_y = b_max_y - g_max_y;
if ifp_min_x > ifp_max_x + 1e-10 || ifp_min_y > ifp_max_y + 1e-10 {
return Err(Error::InvalidGeometry(
"Geometry too large to fit in boundary".into(),
));
}
let ifp_min_x = ifp_min_x.min(ifp_max_x);
let ifp_min_y = ifp_min_y.min(ifp_max_y);
return Ok(Nfp::from_polygon(vec![
(ifp_min_x, ifp_min_y),
(ifp_max_x, ifp_min_y),
(ifp_max_x, ifp_max_y),
(ifp_min_x, ifp_max_y),
]));
}
compute_minkowski_erosion_general(boundary, geometry)
}
fn compute_minkowski_erosion_general(
boundary: &[(f64, f64)],
geometry: &[(f64, f64)],
) -> Result<Nfp> {
if geometry.is_empty() {
return Ok(Nfp::from_polygon(boundary.to_vec()));
}
let first_g = geometry[0];
let mut result: Vec<[f64; 2]> = boundary
.iter()
.map(|&(x, y)| [x - first_g.0, y - first_g.1])
.collect();
for &(gx, gy) in geometry.iter().skip(1) {
let translated: Vec<[f64; 2]> = boundary.iter().map(|&(x, y)| [x - gx, y - gy]).collect();
let shapes = result.overlay(&[translated], OverlayRule::Intersect, FillRule::NonZero);
if shapes.is_empty() {
return Err(Error::InvalidGeometry(
"Geometry too large to fit in boundary".into(),
));
}
result = Vec::new();
for shape in &shapes {
for contour in shape {
if contour.len() >= 3 {
result = contour.clone();
break;
}
}
if !result.is_empty() {
break;
}
}
if result.len() < 3 {
return Err(Error::InvalidGeometry(
"Geometry too large to fit in boundary".into(),
));
}
}
let result_tuples: Vec<(f64, f64)> = result.iter().map(|&[x, y]| (x, y)).collect();
Ok(Nfp::from_polygon(result_tuples))
}
fn shrink_polygon(polygon: &[(f64, f64)], offset: f64) -> Result<Vec<(f64, f64)>> {
if polygon.len() < 3 {
return Err(Error::InvalidGeometry(
"Polygon must have at least 3 vertices".into(),
));
}
if polygon.len() == 4 {
let (min_x, min_y, max_x, max_y) = bounding_box(polygon);
let is_axis_aligned = polygon.iter().all(|&(x, y)| {
((x - min_x).abs() < 1e-10 || (x - max_x).abs() < 1e-10)
&& ((y - min_y).abs() < 1e-10 || (y - max_y).abs() < 1e-10)
});
if is_axis_aligned {
let new_min_x = min_x + offset;
let new_min_y = min_y + offset;
let new_max_x = max_x - offset;
let new_max_y = max_y - offset;
if new_min_x >= new_max_x || new_min_y >= new_max_y {
return Err(Error::InvalidGeometry("Offset polygon collapsed".into()));
}
return Ok(vec![
(new_min_x, new_min_y),
(new_max_x, new_min_y),
(new_max_x, new_max_y),
(new_min_x, new_max_y),
]);
}
}
let (cx, cy) = polygon_centroid(polygon);
let result: Vec<(f64, f64)> = polygon
.iter()
.filter_map(|&(x, y)| {
let dx = x - cx;
let dy = y - cy;
let dist = (dx * dx + dy * dy).sqrt();
if dist < offset + 1e-10 {
return None;
}
let factor = (dist - offset) / dist;
Some((cx + dx * factor, cy + dy * factor))
})
.collect();
if result.len() < 3 {
return Err(Error::InvalidGeometry("Offset polygon collapsed".into()));
}
let area = signed_area(&result).abs();
if area <= 1e-10 {
return Err(Error::InvalidGeometry("Offset polygon collapsed".into()));
}
Ok(result)
}
fn bounding_box(polygon: &[(f64, f64)]) -> (f64, f64, f64, f64) {
let mut min_x = f64::INFINITY;
let mut min_y = f64::INFINITY;
let mut max_x = f64::NEG_INFINITY;
let mut max_y = f64::NEG_INFINITY;
for &(x, y) in polygon {
min_x = min_x.min(x);
min_y = min_y.min(y);
max_x = max_x.max(x);
max_y = max_y.max(y);
}
(min_x, min_y, max_x, max_y)
}
fn compute_nfp_convex(stationary: &[(f64, f64)], orbiting: &[(f64, f64)]) -> Result<Nfp> {
use u_nesting_core::geom::minkowski::nfp_convex;
let polygon = nfp_convex(stationary, orbiting);
Ok(Nfp::from_polygon(polygon))
}
fn compute_minkowski_sum_convex(poly_a: &[(f64, f64)], poly_b: &[(f64, f64)]) -> Result<Nfp> {
use u_nesting_core::geom::minkowski::minkowski_sum_convex;
let polygon = minkowski_sum_convex(poly_a, poly_b);
Ok(Nfp::from_polygon(polygon))
}
fn compute_nfp_general(stationary: &Geometry2D, rotated_orbiting: &[(f64, f64)]) -> Result<Nfp> {
let stat_triangles = triangulate_polygon(stationary.exterior());
let orb_triangles = triangulate_polygon(rotated_orbiting);
if stat_triangles.is_empty() || orb_triangles.is_empty() {
let stat_hull = stationary.convex_hull();
let orb_hull = convex_hull_of_points(rotated_orbiting);
let reflected: Vec<(f64, f64)> = orb_hull.iter().map(|&(x, y)| (-x, -y)).collect();
return compute_minkowski_sum_convex(&stat_hull, &reflected);
}
let pairs: Vec<_> = stat_triangles
.iter()
.flat_map(|stat_tri| {
orb_triangles
.iter()
.map(move |orb_tri| (stat_tri.clone(), orb_tri.clone()))
})
.collect();
#[cfg(feature = "parallel")]
let partial_nfps: Vec<Vec<(f64, f64)>> = pairs
.par_iter()
.flat_map(|(stat_tri, orb_tri)| {
let reflected: Vec<(f64, f64)> = orb_tri.iter().map(|&(x, y)| (-x, -y)).collect();
if let Ok(nfp) = compute_minkowski_sum_convex(stat_tri, &reflected) {
nfp.polygons
.into_iter()
.filter(|polygon| polygon.len() >= 3)
.collect::<Vec<_>>()
} else {
Vec::new()
}
})
.collect();
#[cfg(not(feature = "parallel"))]
let partial_nfps: Vec<Vec<(f64, f64)>> = pairs
.iter()
.flat_map(|(stat_tri, orb_tri)| {
let reflected: Vec<(f64, f64)> = orb_tri.iter().map(|&(x, y)| (-x, -y)).collect();
if let Ok(nfp) = compute_minkowski_sum_convex(stat_tri, &reflected) {
nfp.polygons
.into_iter()
.filter(|polygon| polygon.len() >= 3)
.collect::<Vec<_>>()
} else {
Vec::new()
}
})
.collect();
if partial_nfps.is_empty() {
let stat_hull = stationary.convex_hull();
let orb_hull = convex_hull_of_points(rotated_orbiting);
let reflected: Vec<(f64, f64)> = orb_hull.iter().map(|&(x, y)| (-x, -y)).collect();
return compute_minkowski_sum_convex(&stat_hull, &reflected);
}
union_polygons(&partial_nfps)
}
fn triangulate_polygon(polygon: &[(f64, f64)]) -> Vec<Vec<(f64, f64)>> {
if polygon.len() < 3 {
return Vec::new();
}
if is_polygon_convex(polygon) {
return vec![polygon.to_vec()];
}
let mut vertices: Vec<(f64, f64)> = ensure_ccw(polygon);
let mut triangles = Vec::new();
while vertices.len() > 3 {
let n = vertices.len();
let mut ear_found = false;
for i in 0..n {
let prev = (i + n - 1) % n;
let next = (i + 1) % n;
if is_ear(&vertices, prev, i, next) {
triangles.push(vec![vertices[prev], vertices[i], vertices[next]]);
vertices.remove(i);
ear_found = true;
break;
}
}
if !ear_found {
return vec![convex_hull_of_points(polygon)];
}
}
if vertices.len() == 3 {
triangles.push(vertices);
}
triangles
}
fn point_in_triangle_robust(p: (f64, f64), a: (f64, f64), b: (f64, f64), c: (f64, f64)) -> bool {
let o1 = orient2d_filtered(a, b, p);
let o2 = orient2d_filtered(b, c, p);
let o3 = orient2d_filtered(c, a, p);
(o1 == Orientation::CounterClockwise
&& o2 == Orientation::CounterClockwise
&& o3 == Orientation::CounterClockwise)
|| (o1 == Orientation::Clockwise
&& o2 == Orientation::Clockwise
&& o3 == Orientation::Clockwise)
}
fn is_ear(vertices: &[(f64, f64)], prev: usize, curr: usize, next: usize) -> bool {
let a = vertices[prev];
let b = vertices[curr];
let c = vertices[next];
let orientation = orient2d_filtered(a, b, c);
if !orientation.is_ccw() {
return false; }
for (i, &p) in vertices.iter().enumerate() {
if i == prev || i == curr || i == next {
continue;
}
if point_in_triangle_robust(p, a, b, c) {
return false;
}
}
true
}
fn union_polygons(polygons: &[Vec<(f64, f64)>]) -> Result<Nfp> {
if polygons.is_empty() {
return Ok(Nfp::new());
}
if polygons.len() == 1 {
return Ok(Nfp::from_polygon(polygons[0].clone()));
}
let mut result: Vec<Vec<[f64; 2]>> = vec![polygons[0].iter().map(|&(x, y)| [x, y]).collect()];
for polygon in &polygons[1..] {
let clip: Vec<[f64; 2]> = polygon.iter().map(|&(x, y)| [x, y]).collect();
let shapes = result.overlay(&[clip], OverlayRule::Union, FillRule::NonZero);
result = Vec::new();
for shape in shapes {
for contour in shape {
if contour.len() >= 3 {
result.push(contour);
}
}
}
if result.is_empty() {
continue;
}
}
let nfp_polygons: Vec<Vec<(f64, f64)>> = result
.into_iter()
.map(|contour| contour.into_iter().map(|[x, y]| (x, y)).collect())
.collect();
if nfp_polygons.is_empty() {
return Ok(Nfp::from_polygon(polygons[0].clone()));
}
Ok(Nfp::from_polygons(nfp_polygons))
}
fn rotate_polygon(polygon: &[(f64, f64)], angle: f64) -> Vec<(f64, f64)> {
if angle.abs() < 1e-10 {
return polygon.to_vec();
}
let cos_a = angle.cos();
let sin_a = angle.sin();
polygon
.iter()
.map(|&(x, y)| (x * cos_a - y * sin_a, x * sin_a + y * cos_a))
.collect()
}
fn is_polygon_convex(polygon: &[(f64, f64)]) -> bool {
geom_polygon::is_convex(polygon)
}
fn ensure_ccw(polygon: &[(f64, f64)]) -> Vec<(f64, f64)> {
geom_polygon::ensure_ccw(polygon)
}
fn signed_area(polygon: &[(f64, f64)]) -> f64 {
geom_polygon::signed_area(polygon)
}
fn convex_hull_of_points(points: &[(f64, f64)]) -> Vec<(f64, f64)> {
geom_polygon::convex_hull(points)
}
pub fn point_in_polygon(point: (f64, f64), polygon: &[(f64, f64)]) -> bool {
let (px, py) = point;
let n = polygon.len();
let mut inside = false;
let mut j = n - 1;
for i in 0..n {
let (xi, yi) = polygon[i];
let (xj, yj) = polygon[j];
if ((yi > py) != (yj > py)) && (px < (xj - xi) * (py - yi) / (yj - yi) + xi) {
inside = !inside;
}
j = i;
}
inside
}
pub fn point_outside_all_nfps(point: (f64, f64), nfps: &[&Nfp]) -> bool {
for nfp in nfps {
for polygon in &nfp.polygons {
if point_in_polygon(point, polygon) {
return false;
}
}
}
true
}
fn point_outside_all_nfps_strict(point: (f64, f64), nfps: &[&Nfp]) -> bool {
for nfp in nfps {
for polygon in &nfp.polygons {
if point_in_polygon(point, polygon) {
return false;
}
}
}
true
}
fn point_on_polygon_boundary(point: (f64, f64), polygon: &[(f64, f64)]) -> bool {
let (px, py) = point;
let n = polygon.len();
const EPS: f64 = 1e-10;
for i in 0..n {
let (x1, y1) = polygon[i];
let (x2, y2) = polygon[(i + 1) % n];
let dx = x2 - x1;
let dy = y2 - y1;
let len_sq = dx * dx + dy * dy;
if len_sq < EPS * EPS {
if (px - x1).abs() < EPS && (py - y1).abs() < EPS {
return true;
}
continue;
}
let t = ((px - x1) * dx + (py - y1) * dy) / len_sq;
if (-EPS..=1.0 + EPS).contains(&t) {
let proj_x = x1 + t * dx;
let proj_y = y1 + t * dy;
let dist_sq = (px - proj_x).powi(2) + (py - proj_y).powi(2);
if dist_sq < EPS * EPS {
return true;
}
}
}
false
}
pub fn find_bottom_left_placement(
ifp: &Nfp,
nfps: &[&Nfp],
sample_step: f64,
) -> Option<(f64, f64)> {
if ifp.is_empty() {
return None;
}
let mut candidates: Vec<(f64, f64)> = Vec::new();
for polygon in &ifp.polygons {
candidates.extend(polygon.iter().copied());
}
for nfp in nfps {
for polygon in &nfp.polygons {
candidates.extend(polygon.iter().copied());
}
}
let (min_x, min_y, max_x, max_y) = ifp_bounding_box(ifp);
let mut y = min_y;
while y <= max_y {
let mut x = min_x;
while x <= max_x {
candidates.push((x, y));
x += sample_step;
}
y += sample_step;
}
let valid_candidates: Vec<(f64, f64)> = candidates
.into_iter()
.filter(|&point| {
let in_ifp = ifp
.polygons
.iter()
.any(|p| point_in_polygon(point, p) || point_on_polygon_boundary(point, p));
if !in_ifp {
return false;
}
point_outside_all_nfps_strict(point, nfps)
})
.collect();
valid_candidates.into_iter().min_by(|a, b| {
match a.0.partial_cmp(&b.0) {
Some(std::cmp::Ordering::Equal) => {
a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)
}
Some(ord) => ord,
None => std::cmp::Ordering::Equal,
}
})
}
fn ifp_bounding_box(ifp: &Nfp) -> (f64, f64, f64, f64) {
let mut min_x = f64::INFINITY;
let mut min_y = f64::INFINITY;
let mut max_x = f64::NEG_INFINITY;
let mut max_y = f64::NEG_INFINITY;
for polygon in &ifp.polygons {
for &(x, y) in polygon {
min_x = min_x.min(x);
min_y = min_y.min(y);
max_x = max_x.max(x);
max_y = max_y.max(y);
}
}
(min_x, min_y, max_x, max_y)
}
#[derive(Debug, Clone)]
pub struct PlacedGeometry {
pub geometry: Geometry2D,
pub position: (f64, f64),
pub rotation: f64,
}
impl PlacedGeometry {
pub fn new(geometry: Geometry2D, position: (f64, f64), rotation: f64) -> Self {
Self {
geometry,
position,
rotation,
}
}
pub fn translated_exterior(&self) -> Vec<(f64, f64)> {
let rotated = rotate_polygon(self.geometry.exterior(), self.rotation);
rotated
.into_iter()
.map(|(x, y)| (x + self.position.0, y + self.position.1))
.collect()
}
}
pub fn verify_no_overlap(
geometry: &Geometry2D,
position: (f64, f64),
rotation: f64,
placed_geometries: &[PlacedGeometry],
) -> bool {
use crate::nfp_sliding::polygons_overlap;
let rotated = rotate_polygon(geometry.exterior(), rotation);
let transformed: Vec<(f64, f64)> = rotated
.into_iter()
.map(|(x, y)| (x + position.0, y + position.1))
.collect();
for placed in placed_geometries {
let placed_polygon = placed.translated_exterior();
if polygons_overlap(&transformed, &placed_polygon) {
return false; }
}
true }
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
struct NfpCacheKey {
geometry_a: String,
geometry_b: String,
rotation_millideg: i32, }
impl NfpCacheKey {
fn new(id_a: &str, id_b: &str, rotation_rad: f64) -> Self {
let rotation_millideg = ((rotation_rad * 180.0 / PI) * 1000.0).round() as i32;
Self {
geometry_a: id_a.to_string(),
geometry_b: id_b.to_string(),
rotation_millideg,
}
}
}
#[derive(Debug)]
pub struct NfpCache {
cache: RwLock<HashMap<NfpCacheKey, Arc<Nfp>>>,
max_size: usize,
}
impl NfpCache {
pub fn new() -> Self {
Self::with_capacity(1000)
}
pub fn with_capacity(max_size: usize) -> Self {
Self {
cache: RwLock::new(HashMap::new()),
max_size,
}
}
pub fn get_or_compute<F>(&self, key: (&str, &str, f64), compute: F) -> Result<Arc<Nfp>>
where
F: FnOnce() -> Result<Nfp>,
{
let cache_key = NfpCacheKey::new(key.0, key.1, key.2);
{
let cache = self.cache.read().map_err(|e| {
Error::Internal(format!("Failed to acquire cache read lock: {}", e))
})?;
if let Some(nfp) = cache.get(&cache_key) {
return Ok(Arc::clone(nfp));
}
}
let nfp = Arc::new(compute()?);
{
let mut cache = self.cache.write().map_err(|e| {
Error::Internal(format!("Failed to acquire cache write lock: {}", e))
})?;
if cache.len() >= self.max_size {
let keys_to_remove: Vec<_> =
cache.keys().take(self.max_size / 2).cloned().collect();
for key in keys_to_remove {
cache.remove(&key);
}
}
cache.insert(cache_key, Arc::clone(&nfp));
}
Ok(nfp)
}
pub fn len(&self) -> usize {
self.cache.read().map(|c| c.len()).unwrap_or(0)
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn clear(&self) {
if let Ok(mut cache) = self.cache.write() {
cache.clear();
}
}
}
impl Default for NfpCache {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
fn rect(w: f64, h: f64) -> Vec<(f64, f64)> {
vec![(0.0, 0.0), (w, 0.0), (w, h), (0.0, h)]
}
fn triangle() -> Vec<(f64, f64)> {
vec![(0.0, 0.0), (10.0, 0.0), (5.0, 10.0)]
}
#[test]
fn test_is_polygon_convex() {
assert!(is_polygon_convex(&rect(10.0, 10.0)));
assert!(is_polygon_convex(&triangle()));
let l_shape = vec![
(0.0, 0.0),
(10.0, 0.0),
(10.0, 5.0),
(5.0, 5.0),
(5.0, 10.0),
(0.0, 10.0),
];
assert!(!is_polygon_convex(&l_shape));
}
#[test]
fn test_signed_area() {
let ccw_square = rect(10.0, 10.0);
assert!(signed_area(&ccw_square) > 0.0);
assert_relative_eq!(signed_area(&ccw_square).abs(), 100.0, epsilon = 1e-10);
let cw_square: Vec<_> = ccw_square.into_iter().rev().collect();
assert!(signed_area(&cw_square) < 0.0);
}
#[test]
fn test_rotate_polygon() {
let square = rect(10.0, 10.0);
let rotated = rotate_polygon(&square, 0.0);
assert_eq!(rotated.len(), square.len());
let rotated = rotate_polygon(&[(1.0, 0.0)], PI / 2.0);
assert_relative_eq!(rotated[0].0, 0.0, epsilon = 1e-10);
assert_relative_eq!(rotated[0].1, 1.0, epsilon = 1e-10);
}
#[test]
fn test_nfp_two_squares() {
let a = Geometry2D::rectangle("A", 10.0, 10.0);
let b = Geometry2D::rectangle("B", 5.0, 5.0);
let nfp = compute_nfp(&a, &b, 0.0).unwrap();
assert!(!nfp.is_empty());
assert_eq!(nfp.polygons.len(), 1);
let polygon = &nfp.polygons[0];
assert!(polygon.len() >= 4);
}
#[test]
fn test_nfp_with_rotation() {
let a = Geometry2D::rectangle("A", 10.0, 10.0);
let b = Geometry2D::rectangle("B", 5.0, 5.0);
let nfp = compute_nfp(&a, &b, PI / 4.0).unwrap();
assert!(!nfp.is_empty());
}
#[test]
fn test_ifp_square_in_boundary() {
let boundary = rect(100.0, 100.0);
let geom = Geometry2D::rectangle("G", 10.0, 10.0);
let ifp = compute_ifp(&boundary, &geom, 0.0).unwrap();
assert!(!ifp.is_empty());
let polygon = &ifp.polygons[0];
let (min_x, min_y, max_x, max_y) = bounding_box(polygon);
assert_relative_eq!(min_x, 0.0, epsilon = 1e-10);
assert_relative_eq!(min_y, 0.0, epsilon = 1e-10);
assert_relative_eq!(max_x, 90.0, epsilon = 1e-10);
assert_relative_eq!(max_y, 90.0, epsilon = 1e-10);
}
#[test]
fn test_ifp_bounds_correct() {
let boundary = rect(100.0, 50.0);
let geom = Geometry2D::rectangle("R", 25.0, 25.0);
let ifp = compute_ifp(&boundary, &geom, 0.0).unwrap();
assert!(!ifp.is_empty());
let polygon = &ifp.polygons[0];
let (min_x, min_y, max_x, max_y) = bounding_box(polygon);
assert_relative_eq!(min_x, 0.0, epsilon = 1e-10);
assert_relative_eq!(min_y, 0.0, epsilon = 1e-10);
assert_relative_eq!(max_x, 75.0, epsilon = 1e-10);
assert_relative_eq!(max_y, 25.0, epsilon = 1e-10);
assert!(point_in_polygon((0.0, 0.0), polygon) || point_on_boundary((0.0, 0.0), polygon));
assert!(point_in_polygon((25.0, 0.0), polygon) || point_on_boundary((25.0, 0.0), polygon));
assert!(point_in_polygon((50.0, 0.0), polygon) || point_on_boundary((50.0, 0.0), polygon));
assert!(point_in_polygon((75.0, 0.0), polygon) || point_on_boundary((75.0, 0.0), polygon));
}
fn point_on_boundary(point: (f64, f64), polygon: &[(f64, f64)]) -> bool {
let (px, py) = point;
let n = polygon.len();
for i in 0..n {
let (x1, y1) = polygon[i];
let (x2, y2) = polygon[(i + 1) % n];
let d1 = ((px - x1).powi(2) + (py - y1).powi(2)).sqrt();
let d2 = ((px - x2).powi(2) + (py - y2).powi(2)).sqrt();
let d_total = ((x2 - x1).powi(2) + (y2 - y1).powi(2)).sqrt();
if (d1 + d2 - d_total).abs() < 1e-10 {
return true;
}
}
false
}
#[test]
fn test_nfp_same_size_rectangles() {
let a = Geometry2D::rectangle("A", 25.0, 25.0);
let b = Geometry2D::rectangle("B", 25.0, 25.0);
let nfp = compute_nfp(&a, &b, 0.0).unwrap();
assert!(!nfp.is_empty());
let polygon = &nfp.polygons[0];
let (min_x, min_y, max_x, max_y) = bounding_box(polygon);
let width = max_x - min_x;
let height = max_y - min_y;
eprintln!("NFP dimensions: {}x{}", width, height);
eprintln!(
"NFP bounds: ({}, {}) to ({}, {})",
min_x, min_y, max_x, max_y
);
assert_relative_eq!(width, 50.0, epsilon = 1e-6);
assert_relative_eq!(height, 50.0, epsilon = 1e-6);
}
#[test]
fn test_nfp_cache() {
let cache = NfpCache::new();
let compute_count = std::sync::atomic::AtomicUsize::new(0);
let result1 = cache
.get_or_compute(("A", "B", 0.0), || {
compute_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Ok(Nfp::from_polygon(vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)]))
})
.unwrap();
let result2 = cache
.get_or_compute(("A", "B", 0.0), || {
compute_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
Ok(Nfp::from_polygon(vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)]))
})
.unwrap();
assert_eq!(compute_count.load(std::sync::atomic::Ordering::SeqCst), 1);
assert_eq!(result1.polygons, result2.polygons);
assert_eq!(cache.len(), 1);
}
#[test]
fn test_nfp_cache_different_rotations() {
let cache = NfpCache::new();
cache
.get_or_compute(("A", "B", 0.0), || {
Ok(Nfp::from_polygon(vec![(0.0, 0.0), (1.0, 0.0)]))
})
.unwrap();
cache
.get_or_compute(("A", "B", PI / 2.0), || {
Ok(Nfp::from_polygon(vec![(0.0, 0.0), (0.0, 1.0)]))
})
.unwrap();
assert_eq!(cache.len(), 2);
}
#[test]
fn test_convex_hull_of_points() {
let points = vec![
(0.0, 0.0),
(10.0, 0.0),
(5.0, 5.0), (10.0, 10.0),
(0.0, 10.0),
];
let hull = convex_hull_of_points(&points);
assert_eq!(hull.len(), 4);
}
#[test]
fn test_shrink_polygon_square() {
let square = rect(100.0, 100.0);
let shrunk = shrink_polygon(&square, 10.0).unwrap();
assert_eq!(shrunk.len(), 4);
let original_area = signed_area(&square).abs();
let shrunk_area = signed_area(&shrunk).abs();
assert!(
shrunk_area < original_area,
"shrunk_area ({}) should be < original_area ({})",
shrunk_area,
original_area
);
assert_relative_eq!(shrunk_area, 6400.0, epsilon = 1.0);
}
#[test]
fn test_shrink_polygon_collapse() {
let small_square = rect(10.0, 10.0);
let result = shrink_polygon(&small_square, 6.0);
assert!(
result.is_err(),
"Polygon should collapse when offset >= width/2"
);
}
#[test]
fn test_ifp_with_margin() {
let boundary = rect(100.0, 100.0);
let geom = Geometry2D::rectangle("G", 10.0, 10.0);
let ifp_no_margin = compute_ifp(&boundary, &geom, 0.0).unwrap();
let ifp_with_margin = compute_ifp_with_margin(&boundary, &geom, 0.0, 5.0).unwrap();
assert!(!ifp_no_margin.is_empty());
assert!(!ifp_with_margin.is_empty());
let (min_x_no, _min_y_no, max_x_no, _max_y_no) = ifp_bounding_box(&ifp_no_margin);
let (min_x_margin, _min_y_margin, max_x_margin, _max_y_margin) =
ifp_bounding_box(&ifp_with_margin);
let width_no = max_x_no - min_x_no;
let width_margin = max_x_margin - min_x_margin;
assert!(
width_margin < width_no,
"width_margin ({}) should be < width_no ({})",
width_margin,
width_no
);
}
#[test]
fn test_ifp_margin_boundary_collapse() {
let boundary = rect(20.0, 20.0);
let result = shrink_polygon(&boundary, 12.0);
assert!(
result.is_err(),
"Boundary should collapse with margin >= width/2"
);
}
#[test]
fn test_ifp_margin_large_geometry() {
let boundary = rect(30.0, 30.0);
let geom = Geometry2D::rectangle("G", 20.0, 20.0);
let ifp_no_margin = compute_ifp(&boundary, &geom, 0.0).unwrap();
let (min_x_no, _, max_x_no, _) = ifp_bounding_box(&ifp_no_margin);
let width_no = max_x_no - min_x_no;
let ifp_with_margin = compute_ifp_with_margin(&boundary, &geom, 0.0, 5.0).unwrap();
let (min_x_margin, _, max_x_margin, _) = ifp_bounding_box(&ifp_with_margin);
let width_margin = max_x_margin - min_x_margin;
assert!(
width_margin <= width_no,
"width_margin ({}) should be <= width_no ({})",
width_margin,
width_no
);
}
#[test]
fn test_nfp_non_convex_l_shape() {
let l_shape = Geometry2D::new("L").with_polygon(vec![
(0.0, 0.0),
(20.0, 0.0),
(20.0, 10.0),
(10.0, 10.0),
(10.0, 20.0),
(0.0, 20.0),
]);
let small_square = Geometry2D::rectangle("S", 5.0, 5.0);
let nfp = compute_nfp(&l_shape, &small_square, 0.0).unwrap();
assert!(!nfp.is_empty());
assert!(nfp.vertex_count() >= 4);
}
#[test]
fn test_triangulate_polygon_convex() {
let square = rect(10.0, 10.0);
let triangles = triangulate_polygon(&square);
assert_eq!(triangles.len(), 1);
assert_eq!(triangles[0].len(), 4);
}
#[test]
fn test_triangulate_polygon_non_convex() {
let l_shape = vec![
(0.0, 0.0),
(20.0, 0.0),
(20.0, 10.0),
(10.0, 10.0),
(10.0, 20.0),
(0.0, 20.0),
];
let triangles = triangulate_polygon(&l_shape);
assert!(!triangles.is_empty());
}
#[test]
fn test_union_polygons() {
let poly1 = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
let poly2 = vec![(5.0, 5.0), (15.0, 5.0), (15.0, 15.0), (5.0, 15.0)];
let result = union_polygons(&[poly1, poly2]).unwrap();
assert!(!result.is_empty());
assert!(result.vertex_count() >= 6);
}
#[test]
fn test_convex_near_collinear_vertices() {
let near_collinear = vec![
(0.0, 0.0),
(1.0, 1e-15), (2.0, 0.0),
(2.0, 1.0),
(0.0, 1.0),
];
let result = is_polygon_convex(&near_collinear);
let _ = result; }
#[test]
fn test_triangulation_near_degenerate() {
let near_degenerate_l = vec![
(0.0, 0.0),
(10.0, 0.0),
(10.0, 5.0),
(5.0 + 1e-12, 5.0), (5.0, 10.0),
(0.0, 10.0),
];
let triangles = triangulate_polygon(&near_degenerate_l);
assert!(!triangles.is_empty());
}
#[test]
fn test_nfp_nearly_touching_rectangles() {
let a = Geometry2D::rectangle("A", 10.0, 10.0);
let b = Geometry2D::rectangle("B", 5.0, 5.0);
let nfp = compute_nfp(&a, &b, 0.0).unwrap();
assert!(!nfp.is_empty());
}
#[test]
fn test_ifp_geometry_nearly_fills_boundary() {
let boundary = rect(100.0, 100.0);
let geom = Geometry2D::rectangle("G", 99.9999, 99.9999);
let result = compute_ifp(&boundary, &geom, 0.0);
match result {
Ok(ifp) => {
let (min_x, min_y, max_x, max_y) = ifp_bounding_box(&ifp);
let width = max_x - min_x;
let height = max_y - min_y;
assert!(width < 0.001 && height < 0.001);
}
Err(_) => {
}
}
}
#[test]
fn test_point_in_polygon_on_boundary() {
let square = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
let on_bottom_edge = (5.0, 0.0);
let on_right_edge = (10.0, 5.0);
let on_top_edge = (5.0, 10.0);
let on_left_edge = (0.0, 5.0);
let _ = point_in_polygon(on_bottom_edge, &square);
let _ = point_in_polygon(on_right_edge, &square);
let _ = point_in_polygon(on_top_edge, &square);
let _ = point_in_polygon(on_left_edge, &square);
}
#[test]
fn test_point_in_triangle_robust_degenerate() {
let a = (0.0, 0.0);
let b = (5.0, 0.0);
let c = (10.0, 0.0);
let p = (3.0, 0.0);
assert!(!point_in_triangle_robust(p, a, b, c));
}
#[test]
fn test_ear_detection_with_collinear_points() {
let with_collinear = vec![
(0.0, 0.0),
(5.0, 0.0),
(10.0, 0.0), (10.0, 10.0),
(0.0, 10.0),
];
let triangles = triangulate_polygon(&with_collinear);
for triangle in &triangles {
assert!(triangle.len() >= 3);
}
}
#[test]
fn test_nfp_with_very_small_polygon() {
let tiny = Geometry2D::rectangle("tiny", 1e-6, 1e-6);
let normal = Geometry2D::rectangle("normal", 10.0, 10.0);
let nfp = compute_nfp(&normal, &tiny, 0.0).unwrap();
assert!(!nfp.is_empty());
}
#[test]
fn test_nfp_with_very_large_polygon() {
let large = Geometry2D::rectangle("large", 1e6, 1e6);
let normal = Geometry2D::rectangle("normal", 100.0, 100.0);
let nfp = compute_nfp(&large, &normal, 0.0).unwrap();
assert!(!nfp.is_empty());
}
#[test]
fn test_signed_area_with_extreme_coordinates() {
let moderate_coords = vec![
(1e6, 1e6),
(1e6 + 100.0, 1e6),
(1e6 + 100.0, 1e6 + 100.0),
(1e6, 1e6 + 100.0),
];
let area = signed_area(&moderate_coords);
assert_relative_eq!(area.abs(), 10000.0, epsilon = 1.0);
}
#[test]
fn test_ensure_ccw_with_near_zero_area() {
let tiny_area = vec![(0.0, 0.0), (1e-10, 0.0), (1e-10, 1e-10), (0.0, 1e-10)];
let ccw = ensure_ccw(&tiny_area);
assert_eq!(ccw.len(), tiny_area.len());
}
#[test]
fn test_nfp_method_default() {
let config = NfpConfig::default();
assert_eq!(config.method, NfpMethod::MinkowskiSum);
}
#[test]
fn test_nfp_method_minkowski_sum() {
let a = Geometry2D::rectangle("A", 10.0, 10.0);
let b = Geometry2D::rectangle("B", 5.0, 5.0);
let nfp = compute_nfp_with_method(&a, &b, 0.0, NfpMethod::MinkowskiSum).unwrap();
assert!(!nfp.is_empty());
assert!(nfp.vertex_count() >= 4);
}
#[test]
fn test_nfp_method_sliding() {
let a = Geometry2D::rectangle("A", 10.0, 10.0);
let b = Geometry2D::rectangle("B", 5.0, 5.0);
let result = compute_nfp_with_method(&a, &b, 0.0, NfpMethod::Sliding);
assert!(result.is_ok(), "Sliding method should not error");
let nfp = result.unwrap();
assert!(!nfp.is_empty(), "NFP should not be empty");
}
#[test]
fn test_nfp_method_config_builder() {
let config = NfpConfig::with_method(NfpMethod::Sliding)
.with_tolerance(1e-5)
.with_max_iterations(5000);
assert_eq!(config.method, NfpMethod::Sliding);
assert!((config.contact_tolerance - 1e-5).abs() < 1e-10);
assert_eq!(config.max_iterations, 5000);
}
#[test]
fn test_nfp_methods_both_succeed() {
let a = Geometry2D::rectangle("A", 10.0, 10.0);
let b = Geometry2D::rectangle("B", 5.0, 5.0);
let nfp_mink = compute_nfp_with_method(&a, &b, 0.0, NfpMethod::MinkowskiSum).unwrap();
let nfp_slide = compute_nfp_with_method(&a, &b, 0.0, NfpMethod::Sliding).unwrap();
assert!(!nfp_mink.is_empty());
assert!(!nfp_slide.is_empty());
assert!(nfp_mink.vertex_count() >= 4);
}
#[test]
fn test_nfp_sliding_l_shape() {
let l_shape = Geometry2D::new("L").with_polygon(vec![
(0.0, 0.0),
(20.0, 0.0),
(20.0, 10.0),
(10.0, 10.0),
(10.0, 20.0),
(0.0, 20.0),
]);
let small_square = Geometry2D::rectangle("S", 5.0, 5.0);
let result = compute_nfp_with_method(&l_shape, &small_square, 0.0, NfpMethod::Sliding);
assert!(result.is_ok(), "Sliding should not error on L-shape");
let nfp = result.unwrap();
assert!(!nfp.is_empty(), "NFP should not be empty for L-shape");
}
#[test]
fn test_nfp_with_config() {
let a = Geometry2D::rectangle("A", 10.0, 10.0);
let b = Geometry2D::rectangle("B", 5.0, 5.0);
let config = NfpConfig {
method: NfpMethod::Sliding,
contact_tolerance: 1e-4,
max_iterations: 2000,
};
let nfp = compute_nfp_with_config(&a, &b, 0.0, &config).unwrap();
assert!(!nfp.is_empty());
}
}