u_nesting_d2/
boundary.rs

1//! 2D boundary types.
2
3use geo::{Contains, Coord, LineString, Point, Polygon as GeoPolygon};
4use u_nesting_core::geometry::{Boundary, Boundary2DExt};
5use u_nesting_core::transform::AABB2D;
6use u_nesting_core::{Error, Result};
7
8#[cfg(feature = "serde")]
9use serde::{Deserialize, Serialize};
10
11/// A 2D boundary (container) for nesting.
12#[derive(Debug, Clone)]
13#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
14pub struct Boundary2D {
15    /// Boundary shape as polygon vertices.
16    exterior: Vec<(f64, f64)>,
17
18    /// Interior obstacles/holes (regions where placement is forbidden).
19    holes: Vec<Vec<(f64, f64)>>,
20
21    /// Width (for rectangular boundaries).
22    width: Option<f64>,
23
24    /// Height (for rectangular boundaries).
25    height: Option<f64>,
26
27    /// Whether the boundary length can extend infinitely (strip packing).
28    infinite_length: bool,
29
30    /// Cached area.
31    #[cfg_attr(feature = "serde", serde(skip))]
32    cached_area: Option<f64>,
33}
34
35impl Boundary2D {
36    /// Creates a new boundary from polygon vertices.
37    pub fn new(vertices: Vec<(f64, f64)>) -> Self {
38        Self {
39            exterior: vertices,
40            holes: Vec::new(),
41            width: None,
42            height: None,
43            infinite_length: false,
44            cached_area: None,
45        }
46    }
47
48    /// Creates a rectangular boundary.
49    pub fn rectangle(width: f64, height: f64) -> Self {
50        Self {
51            exterior: vec![(0.0, 0.0), (width, 0.0), (width, height), (0.0, height)],
52            holes: Vec::new(),
53            width: Some(width),
54            height: Some(height),
55            infinite_length: false,
56            cached_area: Some(width * height),
57        }
58    }
59
60    /// Creates an infinite strip (for strip packing problems).
61    pub fn strip(width: f64) -> Self {
62        Self {
63            exterior: vec![(0.0, 0.0), (width, 0.0), (width, f64::MAX), (0.0, f64::MAX)],
64            holes: Vec::new(),
65            width: Some(width),
66            height: None,
67            infinite_length: true,
68            cached_area: None,
69        }
70    }
71
72    /// Adds an interior obstacle/hole.
73    pub fn with_hole(mut self, vertices: Vec<(f64, f64)>) -> Self {
74        self.holes.push(vertices);
75        self.cached_area = None;
76        self
77    }
78
79    /// Returns the width (if rectangular).
80    pub fn width(&self) -> Option<f64> {
81        self.width
82    }
83
84    /// Returns the height (if rectangular and not infinite).
85    pub fn height(&self) -> Option<f64> {
86        self.height
87    }
88
89    /// Returns whether this is an infinite strip.
90    pub fn is_infinite(&self) -> bool {
91        self.infinite_length
92    }
93
94    /// Returns the exterior vertices.
95    pub fn exterior(&self) -> &[(f64, f64)] {
96        &self.exterior
97    }
98
99    /// Returns the interior holes.
100    pub fn holes(&self) -> &[Vec<(f64, f64)>] {
101        &self.holes
102    }
103
104    /// Converts to a geo crate Polygon.
105    pub fn to_geo_polygon(&self) -> GeoPolygon<f64> {
106        let exterior = LineString::from(
107            self.exterior
108                .iter()
109                .map(|&(x, y)| Coord { x, y })
110                .collect::<Vec<_>>(),
111        );
112
113        let holes: Vec<LineString<f64>> = self
114            .holes
115            .iter()
116            .map(|hole| {
117                LineString::from(
118                    hole.iter()
119                        .map(|&(x, y)| Coord { x, y })
120                        .collect::<Vec<_>>(),
121                )
122            })
123            .collect();
124
125        GeoPolygon::new(exterior, holes)
126    }
127
128    /// Calculates the area of the boundary.
129    fn calculate_area(&self) -> f64 {
130        if self.infinite_length {
131            return f64::INFINITY;
132        }
133        use geo::Area;
134        self.to_geo_polygon().unsigned_area()
135    }
136}
137
138impl Boundary for Boundary2D {
139    type Scalar = f64;
140
141    fn measure(&self) -> f64 {
142        if let Some(area) = self.cached_area {
143            area
144        } else {
145            self.calculate_area()
146        }
147    }
148
149    fn aabb(&self) -> ([f64; 2], [f64; 2]) {
150        let (min, max) = self.aabb_vec();
151        ([min[0], min[1]], [max[0], max[1]])
152    }
153
154    fn aabb_vec(&self) -> (Vec<f64>, Vec<f64>) {
155        if self.exterior.is_empty() {
156            return (vec![0.0, 0.0], vec![0.0, 0.0]);
157        }
158
159        let mut min_x = f64::MAX;
160        let mut min_y = f64::MAX;
161        let mut max_x = f64::MIN;
162        let mut max_y = f64::MIN;
163
164        for &(x, y) in &self.exterior {
165            if y < f64::MAX / 2.0 {
166                // Ignore infinite values
167                min_x = min_x.min(x);
168                min_y = min_y.min(y);
169                max_x = max_x.max(x);
170                max_y = max_y.max(y);
171            }
172        }
173
174        (vec![min_x, min_y], vec![max_x, max_y])
175    }
176
177    fn validate(&self) -> Result<()> {
178        if self.exterior.len() < 3 {
179            return Err(Error::InvalidBoundary(
180                "Boundary must have at least 3 vertices".into(),
181            ));
182        }
183
184        if let (Some(w), Some(h)) = (self.width, self.height) {
185            if w <= 0.0 || h <= 0.0 {
186                return Err(Error::InvalidBoundary(
187                    "Width and height must be positive".into(),
188                ));
189            }
190        }
191
192        Ok(())
193    }
194
195    fn contains_point(&self, point: &[f64]) -> bool {
196        if point.len() < 2 {
197            return false;
198        }
199        let p = Point::new(point[0], point[1]);
200        let poly = self.to_geo_polygon();
201        poly.contains(&p)
202    }
203}
204
205impl Boundary2DExt for Boundary2D {
206    fn aabb_2d(&self) -> AABB2D<f64> {
207        let (min, max) = self.aabb_vec();
208        AABB2D::new(min[0], min[1], max[0], max[1])
209    }
210
211    fn vertices(&self) -> &[(f64, f64)] {
212        &self.exterior
213    }
214
215    fn contains_polygon(&self, polygon: &[(f64, f64)]) -> bool {
216        let boundary_poly = self.to_geo_polygon();
217
218        // Check if all vertices are inside
219        for &(x, y) in polygon {
220            let p = Point::new(x, y);
221            if !boundary_poly.contains(&p) {
222                return false;
223            }
224        }
225
226        // For complete correctness, should also check edge intersections
227        // but for performance, vertex containment is often sufficient
228        true
229    }
230
231    fn effective_area(&self, margin: f64) -> f64 {
232        if self.infinite_length {
233            return f64::INFINITY;
234        }
235
236        if let (Some(w), Some(h)) = (self.width, self.height) {
237            let eff_w = (w - 2.0 * margin).max(0.0);
238            let eff_h = (h - 2.0 * margin).max(0.0);
239            eff_w * eff_h
240        } else {
241            // For non-rectangular boundaries, use buffer operation
242            // For now, approximate by subtracting perimeter * margin
243            use geo::{Euclidean, Length};
244            let perim = self.to_geo_polygon().exterior().length::<Euclidean>();
245            (self.calculate_area() - perim * margin).max(0.0)
246        }
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253    use approx::assert_relative_eq;
254
255    #[test]
256    fn test_rectangle_boundary() {
257        let boundary = Boundary2D::rectangle(100.0, 50.0);
258        assert_relative_eq!(boundary.measure(), 5000.0, epsilon = 0.001);
259        assert_eq!(boundary.width(), Some(100.0));
260        assert_eq!(boundary.height(), Some(50.0));
261    }
262
263    #[test]
264    fn test_strip_boundary() {
265        let boundary = Boundary2D::strip(100.0);
266        assert!(boundary.is_infinite());
267        assert_eq!(boundary.width(), Some(100.0));
268        assert_eq!(boundary.height(), None);
269    }
270
271    #[test]
272    fn test_contains_point() {
273        let boundary = Boundary2D::rectangle(100.0, 100.0);
274        assert!(boundary.contains_point(&[50.0, 50.0]));
275        assert!(!boundary.contains_point(&[150.0, 50.0]));
276        assert!(!boundary.contains_point(&[-10.0, 50.0]));
277    }
278
279    #[test]
280    fn test_effective_area() {
281        let boundary = Boundary2D::rectangle(100.0, 100.0);
282        let eff = boundary.effective_area(10.0);
283        // 80 * 80 = 6400
284        assert_relative_eq!(eff, 6400.0, epsilon = 0.001);
285    }
286
287    #[test]
288    fn test_aabb_2d() {
289        let boundary = Boundary2D::rectangle(100.0, 50.0);
290        let aabb = boundary.aabb_2d();
291        assert_relative_eq!(aabb.min_x, 0.0);
292        assert_relative_eq!(aabb.min_y, 0.0);
293        assert_relative_eq!(aabb.max_x, 100.0);
294        assert_relative_eq!(aabb.max_y, 50.0);
295    }
296
297    #[test]
298    fn test_validation() {
299        let valid = Boundary2D::rectangle(100.0, 50.0);
300        assert!(valid.validate().is_ok());
301
302        let invalid = Boundary2D::new(vec![(0.0, 0.0), (1.0, 0.0)]);
303        assert!(invalid.validate().is_err());
304    }
305}