u_nesting_d2/
nfp.rs

1//! No-Fit Polygon (NFP) computation.
2//!
3//! The NFP of two polygons A and B represents all positions where the reference
4//! point of B can be placed such that B touches or overlaps A.
5//!
6//! This module implements:
7//! - **Convex case**: Minkowski sum algorithm (O(n+m) for convex polygons)
8//! - **Non-convex case**: Convex decomposition + union approach using `i_overlay`
9//! - **Sliding algorithm**: Burke et al. (2007) orbiting/sliding approach for robust NFP
10//!
11//! ## Algorithm Selection
12//!
13//! Use [`NfpMethod`] to choose the algorithm:
14//! - `MinkowskiSum`: Fast for convex polygons, uses decomposition for non-convex
15//! - `Sliding`: More robust for complex shapes, follows polygon boundary
16//!
17//! ```rust,ignore
18//! use u_nesting_d2::nfp::{compute_nfp_with_method, NfpMethod};
19//!
20//! let nfp = compute_nfp_with_method(&stationary, &orbiting, 0.0, NfpMethod::Sliding)?;
21//! ```
22
23use 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
37/// Rotates an NFP around the origin by the given angle (in radians).
38///
39/// This is used when computing NFP with relative rotation and then
40/// transforming it to the actual placed geometry's rotation.
41pub 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
63/// Translates an NFP by the given offset.
64pub 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/// NFP computation result.
80#[derive(Debug, Clone)]
81pub struct Nfp {
82    /// The computed NFP polygon(s).
83    /// Multiple polygons can result from non-convex shapes.
84    pub polygons: Vec<Vec<(f64, f64)>>,
85}
86
87impl Nfp {
88    /// Creates a new empty NFP.
89    pub fn new() -> Self {
90        Self {
91            polygons: Vec::new(),
92        }
93    }
94
95    /// Creates an NFP with a single polygon.
96    pub fn from_polygon(polygon: Vec<(f64, f64)>) -> Self {
97        Self {
98            polygons: vec![polygon],
99        }
100    }
101
102    /// Creates an NFP with multiple polygons.
103    pub fn from_polygons(polygons: Vec<Vec<(f64, f64)>>) -> Self {
104        Self { polygons }
105    }
106
107    /// Returns true if the NFP is empty.
108    pub fn is_empty(&self) -> bool {
109        self.polygons.is_empty()
110    }
111
112    /// Returns the total vertex count across all polygons.
113    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// ============================================================================
125// NFP Method Selection
126// ============================================================================
127
128/// Method for computing No-Fit Polygons.
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
130pub enum NfpMethod {
131    /// Minkowski sum algorithm.
132    ///
133    /// - **Convex polygons**: O(n+m) time complexity
134    /// - **Non-convex polygons**: Uses convex decomposition + union
135    /// - Best for: Simple shapes, fast computation
136    #[default]
137    MinkowskiSum,
138
139    /// Sliding/orbiting algorithm (Burke et al. 2007).
140    ///
141    /// - Traces the NFP boundary by sliding one polygon around another
142    /// - More robust for complex interlocking shapes
143    /// - Better handles edge cases like perfect fits
144    /// - Best for: Complex non-convex shapes, high accuracy requirements
145    Sliding,
146}
147
148/// Configuration for NFP computation.
149#[derive(Debug, Clone)]
150pub struct NfpConfig {
151    /// The method to use for NFP computation.
152    pub method: NfpMethod,
153    /// Tolerance for contact detection (Sliding method).
154    pub contact_tolerance: f64,
155    /// Maximum iterations for sliding algorithm.
156    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    /// Creates a new config with the specified method.
171    pub fn with_method(method: NfpMethod) -> Self {
172        Self {
173            method,
174            ..Default::default()
175        }
176    }
177
178    /// Sets the contact tolerance (for Sliding method).
179    pub fn with_tolerance(mut self, tolerance: f64) -> Self {
180        self.contact_tolerance = tolerance;
181        self
182    }
183
184    /// Sets the maximum iterations (for Sliding method).
185    pub fn with_max_iterations(mut self, max_iter: usize) -> Self {
186        self.max_iterations = max_iter;
187        self
188    }
189}
190
191/// Computes the No-Fit Polygon between two geometries using the specified method.
192///
193/// # Arguments
194/// * `stationary` - The fixed polygon
195/// * `orbiting` - The polygon to be placed
196/// * `rotation` - Rotation angle of the orbiting polygon in radians
197/// * `method` - The algorithm to use
198///
199/// # Returns
200/// The computed NFP, or an error if computation fails.
201pub 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
215/// Computes the No-Fit Polygon between two geometries with full configuration.
216///
217/// # Arguments
218/// * `stationary` - The fixed polygon
219/// * `orbiting` - The polygon to be placed
220/// * `rotation` - Rotation angle of the orbiting polygon in radians
221/// * `config` - Configuration including method and parameters
222///
223/// # Returns
224/// The computed NFP, or an error if computation fails.
225pub 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    // Apply rotation to orbiting polygon
241    let rotated_orbiting = rotate_polygon(orb_exterior, rotation);
242
243    match config.method {
244        NfpMethod::MinkowskiSum => {
245            // Use existing Minkowski sum implementation
246            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            // Use sliding algorithm
257            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            // Reflect the orbiting polygon (NFP requires -B)
264            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
272/// Computes the No-Fit Polygon between two geometries.
273///
274/// The NFP represents all positions where the orbiting polygon would
275/// overlap with the stationary polygon.
276///
277/// # Algorithm Selection
278/// - If both polygons are convex: uses fast Minkowski sum (O(n+m))
279/// - Otherwise: uses convex decomposition + union approach
280///
281/// # Arguments
282/// * `stationary` - The fixed polygon
283/// * `orbiting` - The polygon to be placed
284/// * `rotation` - Rotation angle of the orbiting polygon in radians
285///
286/// # Returns
287/// The computed NFP, or an error if computation fails.
288pub fn compute_nfp(stationary: &Geometry2D, orbiting: &Geometry2D, rotation: f64) -> Result<Nfp> {
289    // Get the polygons
290    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    // Apply rotation to orbiting polygon
300    let rotated_orbiting = rotate_polygon(orb_exterior, rotation);
301
302    // Check if both are convex for fast path
303    if stationary.is_convex()
304        && is_polygon_convex(&rotated_orbiting)
305        && stationary.holes().is_empty()
306    {
307        // Fast path: Minkowski sum for convex polygons
308        compute_nfp_convex(stat_exterior, &rotated_orbiting)
309    } else {
310        // General case: decomposition + union
311        compute_nfp_general(stationary, &rotated_orbiting)
312    }
313}
314
315/// Computes the Inner-Fit Polygon (IFP) of a geometry within a boundary.
316///
317/// The IFP represents all valid positions where the reference point of
318/// a geometry can be placed within the boundary.
319///
320/// # Arguments
321/// * `boundary_polygon` - The boundary polygon vertices (counter-clockwise)
322/// * `geometry` - The geometry to fit inside
323/// * `rotation` - Rotation angle of the geometry in radians
324///
325/// # Returns
326/// The computed IFP, or an error if computation fails.
327pub 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
335/// Computes the Inner-Fit Polygon (IFP) of a geometry within a boundary with margin.
336///
337/// The IFP represents all valid positions where the reference point of
338/// a geometry can be placed within the boundary, accounting for a margin
339/// (offset) from the boundary edges.
340///
341/// # Arguments
342/// * `boundary_polygon` - The boundary polygon vertices (counter-clockwise)
343/// * `geometry` - The geometry to fit inside
344/// * `rotation` - Rotation angle of the geometry in radians
345/// * `margin` - Distance to maintain from boundary edges (applied to both boundary and geometry)
346///
347/// # Returns
348/// The computed IFP, or an error if computation fails.
349pub 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    // Apply rotation to geometry
369    let rotated_geom = rotate_polygon(geom_exterior, rotation);
370
371    // Apply margin by shrinking the boundary inward
372    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    // The IFP (Inner-Fit Polygon) is computed using Minkowski EROSION:
385    // IFP = boundary ⊖ geometry = ∩_{g ∈ geometry} (boundary - g)
386    //
387    // This is the set of all positions p where placing the geometry at p
388    // keeps ALL vertices inside the boundary.
389    //
390    // NOTE: This is different from Minkowski SUM (⊕) which gives UNION not intersection!
391    // Previous implementation incorrectly used Minkowski sum.
392    compute_minkowski_erosion(&effective_boundary, &rotated_geom)
393}
394
395/// Computes Minkowski erosion of boundary by geometry: B ⊖ G = ∩_{g ∈ G} (B - g)
396///
397/// This gives all positions where placing geometry keeps it entirely inside boundary.
398/// For a rectangular boundary and convex geometry, this shrinks the boundary
399/// by the geometry's extent in each direction.
400fn 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    // Fast path for rectangular boundary (common case)
408    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    // Get geometry bounding box (AABB of the geometry in its current orientation)
416    let (g_min_x, g_min_y, g_max_x, g_max_y) = bounding_box(geometry);
417
418    if is_rect {
419        // For rectangular boundary: shrink by geometry extents
420        // If geometry reference is at origin and vertices span [g_min, g_max],
421        // then placement p is valid iff:
422        //   p + g_min_x >= b_min_x  =>  p_x >= b_min_x - g_min_x
423        //   p + g_max_x <= b_max_x  =>  p_x <= b_max_x - g_max_x
424        //   p + g_min_y >= b_min_y  =>  p_y >= b_min_y - g_min_y
425        //   p + g_max_y <= b_max_y  =>  p_y <= b_max_y - g_max_y
426        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        // Check if IFP is valid (non-empty)
432        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        // Clamp to ensure valid rectangle
439        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    // General case: intersect translated boundaries
451    // IFP = ∩_{g ∈ G} (B - g)
452    // For each geometry vertex g, translate boundary by -g, then intersect all
453    compute_minkowski_erosion_general(boundary, geometry)
454}
455
456/// General Minkowski erosion using polygon intersection via i_overlay
457fn 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    // Start with boundary translated by first geometry vertex
466    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    // Intersect with boundary translated by each remaining vertex
473    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        // Intersect current result with translated boundary using i_overlay
477        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        // Take the first (largest) resulting polygon
486        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    // Convert back to (f64, f64) format
507    let result_tuples: Vec<(f64, f64)> = result.iter().map(|&[x, y]| (x, y)).collect();
508    Ok(Nfp::from_polygon(result_tuples))
509}
510
511/// Shrinks a polygon by moving all edges inward by the given offset.
512///
513/// For axis-aligned rectangles (the common case for boundaries), this shrinks
514/// each edge inward. For general polygons, it uses a vertex-based approach.
515fn 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    // Check if this is an axis-aligned rectangle (common case for boundaries)
523    if polygon.len() == 4 {
524        let (min_x, min_y, max_x, max_y) = bounding_box(polygon);
525
526        // Check if all vertices are on the bounding box edges (axis-aligned)
527        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            // Simple shrink for axis-aligned rectangle
534            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            // Check if still valid
540            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    // General polygon shrink using centroid-based approach
554    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                // Vertex too close to centroid
565                return None;
566            }
567
568            // Move vertex toward centroid by offset
569            let factor = (dist - offset) / dist;
570            Some((cx + dx * factor, cy + dy * factor))
571        })
572        .collect();
573
574    // Validate result polygon has reasonable size
575    if result.len() < 3 {
576        return Err(Error::InvalidGeometry("Offset polygon collapsed".into()));
577    }
578
579    // Check if the polygon has positive area (not self-intersecting)
580    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
588/// Computes bounding box of a polygon.
589fn 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
605/// Computes polygon centroid.
606fn 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
613/// Computes NFP for two convex polygons using Minkowski sum.
614///
615/// For the NFP, we compute: A ⊕ (-B) where ⊕ is Minkowski sum
616/// This gives all positions where B's reference point would cause overlap with A.
617fn compute_nfp_convex(stationary: &[(f64, f64)], orbiting: &[(f64, f64)]) -> Result<Nfp> {
618    // Reflect the orbiting polygon about origin (negate coordinates)
619    let reflected: Vec<(f64, f64)> = orbiting.iter().map(|&(x, y)| (-x, -y)).collect();
620
621    compute_minkowski_sum_convex(stationary, &reflected)
622}
623
624/// Computes Minkowski sum of two convex polygons.
625///
626/// Algorithm: Merge sorted edge vectors from both polygons.
627/// Time complexity: O(n + m) where n, m are vertex counts.
628fn compute_minkowski_sum_convex(poly_a: &[(f64, f64)], poly_b: &[(f64, f64)]) -> Result<Nfp> {
629    // Ensure polygons are in counter-clockwise order
630    let a = ensure_ccw(poly_a);
631    let b = ensure_ccw(poly_b);
632
633    // Get edge vectors for both polygons
634    let edges_a = get_edge_vectors(&a);
635    let edges_b = get_edge_vectors(&b);
636
637    // Find starting vertices (bottom-most, then left-most)
638    let start_a = find_bottom_left_vertex(&a);
639    let start_b = find_bottom_left_vertex(&b);
640
641    // Starting point of Minkowski sum
642    let start_point = (a[start_a].0 + b[start_b].0, a[start_a].1 + b[start_b].1);
643
644    // Merge edge vectors by angle
645    let merged_edges = merge_edge_vectors(&edges_a, start_a, &edges_b, start_b);
646
647    // Build the result polygon by following merged edges
648    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    // Remove the last point if it's the same as the first (closed polygon)
658    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
669/// Computes NFP for non-convex polygons using convex decomposition + union.
670///
671/// The algorithm:
672/// 1. Decompose both polygons into convex parts (using triangulation)
673/// 2. Compute pairwise Minkowski sums of convex parts
674/// 3. Union all partial results using `i_overlay`
675fn compute_nfp_general(stationary: &Geometry2D, rotated_orbiting: &[(f64, f64)]) -> Result<Nfp> {
676    // Triangulate both polygons into convex parts
677    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        // Fall back to convex hull approximation
682        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    // Compute pairwise Minkowski sums in parallel
689    // Create all pairs for parallel processing
690    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            // Reflect orbiting triangle
703            let reflected: Vec<(f64, f64)> = orb_tri.iter().map(|&(x, y)| (-x, -y)).collect();
704
705            // Compute Minkowski sum of two convex polygons
706            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        // Fall back to convex hull
719        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 all partial NFPs using i_overlay
726    union_polygons(&partial_nfps)
727}
728
729/// Triangulates a polygon into convex parts (ear clipping algorithm).
730fn triangulate_polygon(polygon: &[(f64, f64)]) -> Vec<Vec<(f64, f64)>> {
731    if polygon.len() < 3 {
732        return Vec::new();
733    }
734
735    // For convex polygons, just return the polygon itself
736    if is_polygon_convex(polygon) {
737        return vec![polygon.to_vec()];
738    }
739
740    // Simple ear-clipping triangulation
741    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            // Check if this is an ear (convex vertex with no other vertices inside)
753            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            // No ear found, polygon might be degenerate
763            // Fall back to returning the convex hull
764            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
775/// Checks if a point is strictly inside a triangle using robust predicates.
776///
777/// Uses robust orientation tests to correctly handle near-degenerate cases.
778fn 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    // Point is strictly inside if all orientations are the same
784    // (all CCW or all CW) and none are collinear
785    (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
793/// Checks if vertex i forms an ear in the polygon.
794///
795/// Uses robust geometric predicates for numerical stability.
796fn 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    // Check if the vertex is convex (turn left in CCW polygon)
802    // Using robust orientation test instead of cross product
803    let orientation = orient2d_filtered(a, b, c);
804    if !orientation.is_ccw() {
805        return false; // Reflex or collinear vertex, not an ear
806    }
807
808    // Check if any other vertex is inside this triangle
809    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
821/// Unions multiple polygons using i_overlay.
822fn 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    // Start with the first polygon
832    let mut result: Vec<Vec<[f64; 2]>> = vec![polygons[0].iter().map(|&(x, y)| [x, y]).collect()];
833
834    // Union with each subsequent polygon
835    for polygon in &polygons[1..] {
836        let clip: Vec<[f64; 2]> = polygon.iter().map(|&(x, y)| [x, y]).collect();
837
838        // Perform union using i_overlay
839        let shapes = result.overlay(&[clip], OverlayRule::Union, FillRule::NonZero);
840
841        // Convert shapes back to our format
842        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            // Union failed, continue with remaining polygons
853            continue;
854        }
855    }
856
857    // Convert back to our Nfp format
858    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        // Fall back to returning the first polygon
865        return Ok(Nfp::from_polygon(polygons[0].clone()));
866    }
867
868    Ok(Nfp::from_polygons(nfp_polygons))
869}
870
871// ============================================================================
872// Helper functions
873// ============================================================================
874
875/// Rotates a polygon around the origin by the given angle (in radians).
876fn 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
890/// Checks if a polygon is convex using robust orientation tests.
891///
892/// Uses robust geometric predicates for numerical stability.
893fn 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        // Skip collinear edges
909        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
923/// Ensures polygon vertices are in counter-clockwise order.
924fn 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
932/// Computes the signed area of a polygon.
933/// Positive for counter-clockwise, negative for clockwise.
934fn 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
947/// Gets edge vectors from a polygon.
948fn 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
958/// Finds the index of the bottom-most (then left-most) vertex.
959fn 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
972/// Computes the angle of an edge vector (in radians, 0 to 2π).
973fn 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
982/// Merges edge vectors from two polygons by angle for Minkowski sum.
983fn 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            // Only edges from B remaining
1000            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            // Only edges from A remaining
1005            let idx = (start_a + i_a) % n_a;
1006            result.push(edges_a[idx]);
1007            i_a += 1;
1008        } else {
1009            // Compare angles
1010            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
1030/// Computes convex hull of a set of points.
1031fn 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)) // Remove duplicate closing point
1047        .collect()
1048}
1049
1050// ============================================================================
1051// NFP-guided placement helpers
1052// ============================================================================
1053
1054/// Checks if a point is inside a polygon (using ray casting algorithm).
1055pub 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
1074/// Checks if a point is outside all given NFP polygons (not overlapping any placed piece).
1075pub 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
1086/// Checks if a point is strictly outside all NFPs (boundary points are considered outside).
1087/// This allows pieces to touch but not overlap.
1088fn point_outside_all_nfps_strict(point: (f64, f64), nfps: &[&Nfp]) -> bool {
1089    for nfp in nfps {
1090        for polygon in &nfp.polygons {
1091            // Point must be strictly outside (interior = overlapping)
1092            if point_in_polygon(point, polygon) {
1093                return false;
1094            }
1095        }
1096    }
1097    true
1098}
1099
1100/// Checks if a point is on the boundary of a polygon (not strictly inside or outside).
1101fn 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        // Check if point is on this edge segment
1111        // Using parametric form: P = P1 + t*(P2-P1), 0 <= t <= 1
1112        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            // Degenerate edge - check if point is at vertex
1118            if (px - x1).abs() < EPS && (py - y1).abs() < EPS {
1119                return true;
1120            }
1121            continue;
1122        }
1123
1124        // Project point onto line
1125        let t = ((px - x1) * dx + (py - y1) * dy) / len_sq;
1126
1127        // Check if projection is within segment
1128        if (-EPS..=1.0 + EPS).contains(&t) {
1129            // Check distance from line
1130            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
1143/// Finds the optimal placement point that minimizes strip length.
1144///
1145/// The valid region is defined as points that are:
1146/// 1. Inside the IFP (Inner-Fit Polygon) - the boundary constraint
1147/// 2. Outside all NFPs (No-Fit Polygons) - not overlapping placed pieces
1148///
1149/// The optimization strategy prioritizes:
1150/// 1. Minimize X coordinate first (to minimize strip length in strip packing)
1151/// 2. Then minimize Y coordinate (pack tightly bottom-to-top)
1152///
1153/// This approach produces shorter strip lengths than the traditional
1154/// "bottom-left" approach which prioritizes Y over X.
1155///
1156/// # Arguments
1157/// * `ifp` - The inner-fit polygon (valid positions within boundary)
1158/// * `nfps` - List of NFPs with already placed pieces
1159/// * `sample_step` - Grid sampling step size (smaller = more accurate but slower)
1160///
1161/// # Returns
1162/// The optimal valid point, or None if no valid position exists.
1163pub 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    // First, try the vertices of the IFP (often optimal positions)
1173    let mut candidates: Vec<(f64, f64)> = Vec::new();
1174
1175    for polygon in &ifp.polygons {
1176        candidates.extend(polygon.iter().copied());
1177    }
1178
1179    // Also collect NFP vertices as potential optimal positions
1180    for nfp in nfps {
1181        for polygon in &nfp.polygons {
1182            candidates.extend(polygon.iter().copied());
1183        }
1184    }
1185
1186    // Find the bounding box of the IFP for grid sampling
1187    let (min_x, min_y, max_x, max_y) = ifp_bounding_box(ifp);
1188
1189    // Add grid sample points
1190    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    // Filter candidates to those inside IFP (including boundary) and outside all NFPs
1201    let valid_candidates: Vec<(f64, f64)> = candidates
1202        .into_iter()
1203        .filter(|&point| {
1204            // Must be inside IFP (including boundary points)
1205            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            // Must be outside all NFPs (boundary points OK - touching but not overlapping)
1213            point_outside_all_nfps_strict(point, nfps)
1214        })
1215        .collect();
1216
1217    // Find optimal point: minimize X first (strip length), then Y (pack tightly)
1218    // This produces shorter strip lengths than the traditional "bottom-left" approach
1219    valid_candidates.into_iter().min_by(|a, b| {
1220        // Compare X first (left = shorter strip), then Y (bottom)
1221        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
1231/// Computes the bounding box of an NFP.
1232fn 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/// Represents a placed geometry for NFP computation.
1251#[derive(Debug, Clone)]
1252pub struct PlacedGeometry {
1253    /// The original geometry.
1254    pub geometry: Geometry2D,
1255    /// The placement position (x, y).
1256    pub position: (f64, f64),
1257    /// The rotation angle in radians.
1258    pub rotation: f64,
1259}
1260
1261impl PlacedGeometry {
1262    /// Creates a new placed geometry.
1263    pub fn new(geometry: Geometry2D, position: (f64, f64), rotation: f64) -> Self {
1264        Self {
1265            geometry,
1266            position,
1267            rotation,
1268        }
1269    }
1270
1271    /// Returns the translated polygon vertices.
1272    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// ============================================================================
1282// NFP Cache
1283// ============================================================================
1284
1285/// Cache key for NFP lookups.
1286#[derive(Debug, Clone, Hash, PartialEq, Eq)]
1287struct NfpCacheKey {
1288    geometry_a: String,
1289    geometry_b: String,
1290    rotation_millideg: i32, // Rotation in millidegrees for integer key
1291}
1292
1293impl NfpCacheKey {
1294    fn new(id_a: &str, id_b: &str, rotation_rad: f64) -> Self {
1295        // Convert radians to millidegrees for integer key
1296        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/// Thread-safe NFP cache for storing precomputed NFPs.
1306#[derive(Debug)]
1307pub struct NfpCache {
1308    cache: RwLock<HashMap<NfpCacheKey, Arc<Nfp>>>,
1309    max_size: usize,
1310}
1311
1312impl NfpCache {
1313    /// Creates a new NFP cache with default capacity (1000 entries).
1314    pub fn new() -> Self {
1315        Self::with_capacity(1000)
1316    }
1317
1318    /// Creates a new NFP cache with specified capacity.
1319    pub fn with_capacity(max_size: usize) -> Self {
1320        Self {
1321            cache: RwLock::new(HashMap::new()),
1322            max_size,
1323        }
1324    }
1325
1326    /// Gets a cached NFP or computes and caches it.
1327    ///
1328    /// # Arguments
1329    /// * `key` - Tuple of (geometry_id_a, geometry_id_b, rotation_in_radians)
1330    /// * `compute` - Function to compute the NFP if not cached
1331    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        // Try to get from cache first (read lock)
1338        {
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        // Compute the NFP
1348        let nfp = Arc::new(compute()?);
1349
1350        // Store in cache (write lock)
1351        {
1352            let mut cache = self.cache.write().map_err(|e| {
1353                Error::Internal(format!("Failed to acquire cache write lock: {}", e))
1354            })?;
1355
1356            // Simple eviction: if at capacity, clear half the cache
1357            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    /// Returns the number of cached entries.
1372    pub fn len(&self) -> usize {
1373        self.cache.read().map(|c| c.len()).unwrap_or(0)
1374    }
1375
1376    /// Returns true if the cache is empty.
1377    pub fn is_empty(&self) -> bool {
1378        self.len() == 0
1379    }
1380
1381    /// Clears the cache.
1382    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        // Square is convex
1411        assert!(is_polygon_convex(&rect(10.0, 10.0)));
1412
1413        // Triangle is convex
1414        assert!(is_polygon_convex(&triangle()));
1415
1416        // L-shape is not convex
1417        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        // CCW square has positive area
1431        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        // CW square has negative area
1436        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        // No rotation
1445        let rotated = rotate_polygon(&square, 0.0);
1446        assert_eq!(rotated.len(), square.len());
1447
1448        // 90 degree rotation
1449        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        // NFP of two axis-aligned rectangles should have 4 vertices
1465        // NFP dimensions should be (10+5) x (10+5) = 15 x 15
1466        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        // Compute with 45 degree rotation
1476        let nfp = compute_nfp(&a, &b, PI / 4.0).unwrap();
1477
1478        assert!(!nfp.is_empty());
1479        // Rotated NFP should have more vertices due to the octagonal shape
1480    }
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        // IFP should be a rectangle of size (100-10) x (100-10) = 90 x 90
1491        // Valid placements: X in [0, 90], Y in [0, 90]
1492        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        // Test case from failing test: 25x25 rectangle in 100x50 boundary
1503        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        // IFP should be [0, 75] x [0, 25]
1510        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        // Positions at (0,0), (25,0), (50,0), (75,0) should all be valid
1518        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    /// Helper to check if point is on polygon boundary
1525    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            // Check if point is on line segment
1532            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        // Two same-size rectangles: NFP should be twice the size
1545        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        // NFP should span from -25 to +25 in each dimension = 50x50
1554        // Actually depends on reference point. Let's check actual dimensions.
1555        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        // NFP of two identical rectangles should be 50x50
1563        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        // Should only compute once
1588        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        // Different rotations should be cached separately
1610        assert_eq!(cache.len(), 2);
1611    }
1612
1613    #[test]
1614    fn test_edge_angle() {
1615        // Right (0 degrees)
1616        assert_relative_eq!(edge_angle(1.0, 0.0), 0.0, epsilon = 1e-10);
1617
1618        // Up (90 degrees)
1619        assert_relative_eq!(edge_angle(0.0, 1.0), PI / 2.0, epsilon = 1e-10);
1620
1621        // Left (180 degrees)
1622        assert_relative_eq!(edge_angle(-1.0, 0.0), PI, epsilon = 1e-10);
1623
1624        // Down (270 degrees)
1625        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), // Interior point
1634            (10.0, 10.0),
1635            (0.0, 10.0),
1636        ];
1637
1638        let hull = convex_hull_of_points(&points);
1639
1640        // Hull should have 4 vertices (square without interior point)
1641        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        // Should still have 4 vertices
1650        assert_eq!(shrunk.len(), 4);
1651
1652        // The shrunk polygon should be smaller
1653        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        // Expected area: (100-20)*(100-20) = 6400
1663        // (10.0 offset on each side)
1664        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        // Shrinking by 6 should collapse the 10x10 polygon (becomes 0 or negative)
1672        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        // Without margin
1685        let ifp_no_margin = compute_ifp(&boundary, &geom, 0.0).unwrap();
1686
1687        // With margin
1688        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        // IFP with margin should be smaller
1694        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        // Width should be smaller with margin applied
1702        // Without margin: IFP width = 100 - 10 = 90
1703        // With margin 5: effective boundary is 90x90, IFP width = 90 - 10 = 80
1704        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        // Margin of 12 would make the effective boundary negative (collapse)
1717        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        // Without margin: IFP width = 30 - 20 = 10
1730        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        // With margin 5: effective boundary is 20x20, IFP width = 20 - 20 = 0
1735        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        // IFP should be smaller (possibly degenerate) with margin
1740        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        // L-shape is not convex
1751        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        // Should compute NFP for non-convex polygon
1763        let nfp = compute_nfp(&l_shape, &small_square, 0.0).unwrap();
1764
1765        assert!(!nfp.is_empty());
1766        // NFP should have multiple vertices due to non-convex shape
1767        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        // Convex polygon should return itself
1776        assert_eq!(triangles.len(), 1);
1777        assert_eq!(triangles[0].len(), 4);
1778    }
1779
1780    #[test]
1781    fn test_triangulate_polygon_non_convex() {
1782        // L-shape
1783        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        // Should triangulate into multiple triangles
1795        assert!(triangles.len() >= 1);
1796    }
1797
1798    #[test]
1799    fn test_union_polygons() {
1800        // Two overlapping squares
1801        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        // Union of two overlapping squares should have more than 4 vertices
1808        assert!(result.vertex_count() >= 6);
1809    }
1810
1811    // ========================================================================
1812    // Near-Degenerate Case Tests (Numerical Robustness)
1813    // ========================================================================
1814
1815    #[test]
1816    fn test_convex_near_collinear_vertices() {
1817        // Polygon with nearly collinear vertices that could fail with naive arithmetic
1818        let near_collinear = vec![
1819            (0.0, 0.0),
1820            (1.0, 1e-15), // Nearly on line y=0
1821            (2.0, 0.0),
1822            (2.0, 1.0),
1823            (0.0, 1.0),
1824        ];
1825
1826        // Should handle without crashing
1827        let result = is_polygon_convex(&near_collinear);
1828        // The result depends on numerical precision, but it shouldn't panic
1829        assert!(result == true || result == false);
1830    }
1831
1832    #[test]
1833    fn test_triangulation_near_degenerate() {
1834        // L-shape with vertices very close together
1835        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), // Very close to (5, 5)
1840            (5.0, 10.0),
1841            (0.0, 10.0),
1842        ];
1843
1844        // Should triangulate without crashing
1845        let triangles = triangulate_polygon(&near_degenerate_l);
1846
1847        // Should produce at least one triangle
1848        assert!(!triangles.is_empty());
1849    }
1850
1851    #[test]
1852    fn test_nfp_nearly_touching_rectangles() {
1853        // Two rectangles that are nearly touching (gap of 1e-10)
1854        let a = Geometry2D::rectangle("A", 10.0, 10.0);
1855        let b = Geometry2D::rectangle("B", 5.0, 5.0);
1856
1857        // Should compute NFP correctly even with near-degenerate cases
1858        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        // Geometry that nearly fills the boundary (leaves very small margin)
1865        let boundary = rect(100.0, 100.0);
1866        let geom = Geometry2D::rectangle("G", 99.9999, 99.9999);
1867
1868        // Should handle without error
1869        let result = compute_ifp(&boundary, &geom, 0.0);
1870
1871        // Either succeeds with a tiny IFP or fails gracefully
1872        match result {
1873            Ok(ifp) => {
1874                // IFP should be very small or a single point
1875                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                // Also acceptable - geometry too large to fit meaningfully
1882            }
1883        }
1884    }
1885
1886    #[test]
1887    fn test_point_in_polygon_on_boundary() {
1888        // Test point exactly on polygon edge
1889        let square = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
1890
1891        // Points on edges
1892        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        // Ray casting algorithm behavior on boundaries is implementation-defined,
1898        // but it should not crash
1899        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        // Degenerate triangle (all points collinear)
1908        let a = (0.0, 0.0);
1909        let b = (5.0, 0.0);
1910        let c = (10.0, 0.0);
1911
1912        // Point on the line
1913        let p = (3.0, 0.0);
1914
1915        // Should return false (not inside a degenerate triangle)
1916        assert!(!point_in_triangle_robust(p, a, b, c));
1917    }
1918
1919    #[test]
1920    fn test_ear_detection_with_collinear_points() {
1921        // Polygon with collinear consecutive vertices
1922        let with_collinear = vec![
1923            (0.0, 0.0),
1924            (5.0, 0.0),
1925            (10.0, 0.0), // Collinear with previous two
1926            (10.0, 10.0),
1927            (0.0, 10.0),
1928        ];
1929
1930        // Should handle triangulation without crashing
1931        let triangles = triangulate_polygon(&with_collinear);
1932
1933        // Should produce valid triangles
1934        for triangle in &triangles {
1935            assert!(triangle.len() >= 3);
1936        }
1937    }
1938
1939    #[test]
1940    fn test_nfp_with_very_small_polygon() {
1941        // Very small polygon (micrometer scale)
1942        let tiny = Geometry2D::rectangle("tiny", 1e-6, 1e-6);
1943        let normal = Geometry2D::rectangle("normal", 10.0, 10.0);
1944
1945        // Should compute NFP correctly
1946        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        // Very large polygon (kilometer scale)
1953        let large = Geometry2D::rectangle("large", 1e6, 1e6);
1954        let normal = Geometry2D::rectangle("normal", 100.0, 100.0);
1955
1956        // Should compute NFP correctly
1957        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        // Polygon with very large coordinates
1964        // Note: Standard floating-point arithmetic loses precision at extreme magnitudes.
1965        // This test documents the limitation - for better precision at extreme scales,
1966        // use the robust::signed_area_robust from u_nesting_core.
1967
1968        // Moderate scale - should be accurate
1969        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        // Area should be 10000 (100 * 100)
1979        assert_relative_eq!(area.abs(), 10000.0, epsilon = 1.0);
1980    }
1981
1982    #[test]
1983    fn test_ensure_ccw_with_near_zero_area() {
1984        // Polygon with very small area
1985        let tiny_area = vec![(0.0, 0.0), (1e-10, 0.0), (1e-10, 1e-10), (0.0, 1e-10)];
1986
1987        // Should handle without crashing
1988        let ccw = ensure_ccw(&tiny_area);
1989        assert_eq!(ccw.len(), tiny_area.len());
1990    }
1991
1992    // ========================================================================
1993    // NfpMethod Tests
1994    // ========================================================================
1995
1996    #[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        // Sliding algorithm should return a valid result
2021        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        // Note: Sliding algorithm is still being improved.
2026        // For simple convex cases, MinkowskiSum is more reliable.
2027        // Sliding is intended for complex non-convex cases with interlocking shapes.
2028    }
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        // Both methods should produce non-empty results
2050        assert!(!nfp_mink.is_empty());
2051        assert!(!nfp_slide.is_empty());
2052
2053        // Minkowski sum for convex shapes is well-tested
2054        assert!(nfp_mink.vertex_count() >= 4);
2055
2056        // Sliding algorithm produces valid (though possibly different) NFP
2057        // Note: The sliding algorithm implementation is still being refined.
2058        // For simple convex cases, both methods should produce geometrically
2059        // similar results, but vertex count may differ due to different algorithms.
2060    }
2061
2062    #[test]
2063    fn test_nfp_sliding_l_shape() {
2064        // L-shape (non-convex)
2065        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        // Sliding should handle non-convex shapes without crashing
2077        let result = compute_nfp_with_method(&l_shape, &small_square, 0.0, NfpMethod::Sliding);
2078
2079        // Sliding algorithm should at least produce some result for L-shapes
2080        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}