Skip to main content

u_nesting_cutting/
contour.rs

1//! Contour extraction from nesting results.
2
3#[cfg(feature = "serde")]
4use serde::{Deserialize, Serialize};
5
6use u_nesting_core::geometry::{Geometry, Geometry2DExt};
7use u_nesting_core::SolveResult;
8
9/// Unique identifier for a cut contour.
10pub type ContourId = usize;
11
12/// Type of contour in the cutting context.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
15pub enum ContourType {
16    /// External boundary of a part.
17    Exterior,
18    /// Internal hole within a part.
19    Interior,
20}
21
22/// A single contour to be cut, extracted from nesting results.
23#[derive(Debug, Clone)]
24#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
25pub struct CutContour {
26    /// Unique ID for this contour.
27    pub id: ContourId,
28    /// ID of the source geometry.
29    pub geometry_id: String,
30    /// Instance index of the placed geometry (0-based).
31    pub instance: usize,
32    /// Whether this is an exterior or interior contour.
33    pub contour_type: ContourType,
34    /// Vertices in world coordinates (after placement transform).
35    pub vertices: Vec<(f64, f64)>,
36    /// Perimeter of this contour.
37    pub perimeter: f64,
38    /// Centroid of this contour.
39    pub centroid: (f64, f64),
40}
41
42/// Extracts cut contours from a nesting solve result.
43///
44/// For each placed geometry instance, extracts the exterior contour and
45/// any interior hole contours. All vertices are transformed to world
46/// coordinates using the placement's position and rotation.
47///
48/// # Arguments
49///
50/// * `solve_result` - The nesting solve result with placements
51/// * `geometries` - The original geometry definitions (must match geometry_ids in placements)
52///
53/// # Returns
54///
55/// A vector of `CutContour` with unique IDs, in order:
56/// exterior and holes for each placed instance.
57pub fn extract_contours<G: Geometry2DExt<Scalar = f64> + Geometry<Scalar = f64>>(
58    solve_result: &SolveResult<f64>,
59    geometries: &[G],
60) -> Vec<CutContour> {
61    let mut contours = Vec::new();
62    let mut next_id: ContourId = 0;
63
64    for placement in &solve_result.placements {
65        // Find matching geometry
66        let geom = match geometries.iter().find(|g| *g.id() == placement.geometry_id) {
67            Some(g) => g,
68            None => continue,
69        };
70
71        // Build transform from placement
72        let transform = placement.to_transform_2d();
73
74        // Extract exterior contour
75        let exterior_world = transform.transform_points(geom.outer_ring());
76        let exterior_perim = u_nesting_core::geom::polygon::perimeter(&exterior_world);
77        let exterior_centroid =
78            u_nesting_core::geom::polygon::centroid(&exterior_world).unwrap_or((0.0, 0.0));
79
80        contours.push(CutContour {
81            id: next_id,
82            geometry_id: placement.geometry_id.clone(),
83            instance: placement.instance,
84            contour_type: ContourType::Exterior,
85            vertices: exterior_world,
86            perimeter: exterior_perim,
87            centroid: exterior_centroid,
88        });
89        next_id += 1;
90
91        // Extract interior hole contours
92        for hole in geom.holes() {
93            let hole_world = transform.transform_points(hole);
94            let hole_perim = u_nesting_core::geom::polygon::perimeter(&hole_world);
95            let hole_centroid =
96                u_nesting_core::geom::polygon::centroid(&hole_world).unwrap_or((0.0, 0.0));
97
98            contours.push(CutContour {
99                id: next_id,
100                geometry_id: placement.geometry_id.clone(),
101                instance: placement.instance,
102                contour_type: ContourType::Interior,
103                vertices: hole_world,
104                perimeter: hole_perim,
105                centroid: hole_centroid,
106            });
107            next_id += 1;
108        }
109    }
110
111    contours
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn test_contour_type_equality() {
120        assert_eq!(ContourType::Exterior, ContourType::Exterior);
121        assert_ne!(ContourType::Exterior, ContourType::Interior);
122    }
123
124    #[test]
125    fn test_cut_contour_creation() {
126        let contour = CutContour {
127            id: 0,
128            geometry_id: "rect1".to_string(),
129            instance: 0,
130            contour_type: ContourType::Exterior,
131            vertices: vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)],
132            perimeter: 40.0,
133            centroid: (5.0, 5.0),
134        };
135        assert_eq!(contour.id, 0);
136        assert_eq!(contour.contour_type, ContourType::Exterior);
137        assert_eq!(contour.vertices.len(), 4);
138    }
139}