u_nesting_d2/
geometry.rs

1//! 2D geometry types.
2
3use geo::{Area, Centroid, ConvexHull, Coord, LineString, Polygon as GeoPolygon};
4use u_nesting_core::geometry::{Geometry, Geometry2DExt, GeometryId, RotationConstraint};
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 polygon geometry that can be nested.
12#[derive(Debug, Clone)]
13#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
14pub struct Geometry2D {
15    /// Unique identifier.
16    id: GeometryId,
17
18    /// Outer boundary of the polygon.
19    exterior: Vec<(f64, f64)>,
20
21    /// Interior holes (if any).
22    holes: Vec<Vec<(f64, f64)>>,
23
24    /// Number of copies to place.
25    quantity: usize,
26
27    /// Rotation constraint.
28    rotation_constraint: RotationConstraint<f64>,
29
30    /// Whether the geometry can be flipped (mirrored).
31    allow_flip: bool,
32
33    /// Placement priority (higher = placed first).
34    priority: i32,
35
36    /// Cached area.
37    #[cfg_attr(feature = "serde", serde(skip))]
38    cached_area: Option<f64>,
39
40    /// Cached convex hull.
41    #[cfg_attr(feature = "serde", serde(skip))]
42    cached_convex_hull: Option<Vec<(f64, f64)>>,
43
44    /// Cached perimeter.
45    #[cfg_attr(feature = "serde", serde(skip))]
46    cached_perimeter: Option<f64>,
47
48    /// Cached convexity flag.
49    #[cfg_attr(feature = "serde", serde(skip))]
50    cached_is_convex: Option<bool>,
51}
52
53impl Geometry2D {
54    /// Creates a new 2D geometry with the given ID.
55    pub fn new(id: impl Into<GeometryId>) -> Self {
56        Self {
57            id: id.into(),
58            exterior: Vec::new(),
59            holes: Vec::new(),
60            quantity: 1,
61            rotation_constraint: RotationConstraint::None,
62            allow_flip: false,
63            priority: 0,
64            cached_area: None,
65            cached_convex_hull: None,
66            cached_perimeter: None,
67            cached_is_convex: None,
68        }
69    }
70
71    /// Sets the polygon from a list of (x, y) vertices.
72    pub fn with_polygon(mut self, vertices: Vec<(f64, f64)>) -> Self {
73        self.exterior = vertices;
74        self.clear_cache();
75        self
76    }
77
78    /// Adds an interior hole.
79    pub fn with_hole(mut self, vertices: Vec<(f64, f64)>) -> Self {
80        self.holes.push(vertices);
81        self.clear_cache();
82        self
83    }
84
85    /// Sets the quantity to place.
86    pub fn with_quantity(mut self, n: usize) -> Self {
87        self.quantity = n;
88        self
89    }
90
91    /// Sets the allowed rotation angles in degrees.
92    pub fn with_rotations_deg(mut self, angles: Vec<f64>) -> Self {
93        let radians: Vec<f64> = angles.into_iter().map(|a| a.to_radians()).collect();
94        self.rotation_constraint = RotationConstraint::Discrete(radians);
95        self
96    }
97
98    /// Sets the allowed rotation angles in radians.
99    pub fn with_rotations(mut self, angles: Vec<f64>) -> Self {
100        self.rotation_constraint = RotationConstraint::Discrete(angles);
101        self
102    }
103
104    /// Sets the rotation constraint.
105    pub fn with_rotation_constraint(mut self, constraint: RotationConstraint<f64>) -> Self {
106        self.rotation_constraint = constraint;
107        self
108    }
109
110    /// Allows flipping (mirroring) the geometry.
111    pub fn with_flip(mut self, allow: bool) -> Self {
112        self.allow_flip = allow;
113        self
114    }
115
116    /// Sets the placement priority.
117    pub fn with_priority(mut self, priority: i32) -> Self {
118        self.priority = priority;
119        self
120    }
121
122    /// Creates a rectangular geometry.
123    pub fn rectangle(id: impl Into<GeometryId>, width: f64, height: f64) -> Self {
124        Self::new(id).with_polygon(vec![
125            (0.0, 0.0),
126            (width, 0.0),
127            (width, height),
128            (0.0, height),
129        ])
130    }
131
132    /// Creates a circle approximation with n vertices.
133    pub fn circle(id: impl Into<GeometryId>, radius: f64, n: usize) -> Self {
134        let n = n.max(8);
135        let step = std::f64::consts::TAU / n as f64;
136        let vertices: Vec<(f64, f64)> = (0..n)
137            .map(|i| {
138                let angle = i as f64 * step;
139                (radius * angle.cos() + radius, radius * angle.sin() + radius)
140            })
141            .collect();
142        Self::new(id).with_polygon(vertices)
143    }
144
145    /// Creates an L-shaped geometry.
146    pub fn l_shape(
147        id: impl Into<GeometryId>,
148        width: f64,
149        height: f64,
150        notch_width: f64,
151        notch_height: f64,
152    ) -> Self {
153        Self::new(id).with_polygon(vec![
154            (0.0, 0.0),
155            (width, 0.0),
156            (width, notch_height),
157            (notch_width, notch_height),
158            (notch_width, height),
159            (0.0, height),
160        ])
161    }
162
163    /// Returns the exterior vertices.
164    pub fn exterior(&self) -> &[(f64, f64)] {
165        &self.exterior
166    }
167
168    /// Returns the allowed rotation angles (for compatibility).
169    pub fn rotations(&self) -> Vec<f64> {
170        self.rotation_constraint.angles()
171    }
172
173    /// Returns whether flipping is allowed.
174    pub fn allow_flip(&self) -> bool {
175        self.allow_flip
176    }
177
178    /// Converts to a geo crate Polygon.
179    pub fn to_geo_polygon(&self) -> GeoPolygon<f64> {
180        let exterior = LineString::from(
181            self.exterior
182                .iter()
183                .map(|&(x, y)| Coord { x, y })
184                .collect::<Vec<_>>(),
185        );
186
187        let holes: Vec<LineString<f64>> = self
188            .holes
189            .iter()
190            .map(|hole| {
191                LineString::from(
192                    hole.iter()
193                        .map(|&(x, y)| Coord { x, y })
194                        .collect::<Vec<_>>(),
195                )
196            })
197            .collect();
198
199        GeoPolygon::new(exterior, holes)
200    }
201
202    /// Clears all cached values.
203    fn clear_cache(&mut self) {
204        self.cached_area = None;
205        self.cached_convex_hull = None;
206        self.cached_perimeter = None;
207        self.cached_is_convex = None;
208    }
209
210    /// Calculates the area of the polygon.
211    fn calculate_area(&self) -> f64 {
212        self.to_geo_polygon().unsigned_area()
213    }
214
215    /// Calculates the perimeter of the polygon.
216    fn calculate_perimeter(&self) -> f64 {
217        use geo::{Euclidean, Length};
218        let poly = self.to_geo_polygon();
219        let mut perim = poly.exterior().length::<Euclidean>();
220        for hole in poly.interiors() {
221            perim += hole.length::<Euclidean>();
222        }
223        perim
224    }
225
226    /// Calculates the convex hull.
227    fn calculate_convex_hull(&self) -> Vec<(f64, f64)> {
228        let poly = self.to_geo_polygon();
229        let hull = poly.convex_hull();
230        hull.exterior().points().map(|p| (p.x(), p.y())).collect()
231    }
232
233    /// Checks if the polygon is convex.
234    fn calculate_is_convex(&self) -> bool {
235        if self.exterior.len() < 3 || !self.holes.is_empty() {
236            return false;
237        }
238
239        // A polygon is convex if all cross products of consecutive edge pairs
240        // have the same sign
241        let n = self.exterior.len();
242        let mut sign = 0i32;
243
244        for i in 0..n {
245            let (x1, y1) = self.exterior[i];
246            let (x2, y2) = self.exterior[(i + 1) % n];
247            let (x3, y3) = self.exterior[(i + 2) % n];
248
249            let cross = (x2 - x1) * (y3 - y2) - (y2 - y1) * (x3 - x2);
250
251            if cross.abs() > 1e-10 {
252                let current_sign = if cross > 0.0 { 1 } else { -1 };
253                if sign == 0 {
254                    sign = current_sign;
255                } else if sign != current_sign {
256                    return false;
257                }
258            }
259        }
260
261        true
262    }
263}
264
265impl Geometry for Geometry2D {
266    type Scalar = f64;
267
268    fn id(&self) -> &GeometryId {
269        &self.id
270    }
271
272    fn quantity(&self) -> usize {
273        self.quantity
274    }
275
276    fn measure(&self) -> f64 {
277        if let Some(area) = self.cached_area {
278            area
279        } else {
280            self.calculate_area()
281        }
282    }
283
284    fn aabb(&self) -> ([f64; 2], [f64; 2]) {
285        let (min, max) = self.aabb_vec();
286        ([min[0], min[1]], [max[0], max[1]])
287    }
288
289    fn aabb_vec(&self) -> (Vec<f64>, Vec<f64>) {
290        if self.exterior.is_empty() {
291            return (vec![0.0, 0.0], vec![0.0, 0.0]);
292        }
293
294        let mut min_x = f64::MAX;
295        let mut min_y = f64::MAX;
296        let mut max_x = f64::MIN;
297        let mut max_y = f64::MIN;
298
299        for &(x, y) in &self.exterior {
300            min_x = min_x.min(x);
301            min_y = min_y.min(y);
302            max_x = max_x.max(x);
303            max_y = max_y.max(y);
304        }
305
306        (vec![min_x, min_y], vec![max_x, max_y])
307    }
308
309    fn centroid(&self) -> Vec<f64> {
310        if let Some(c) = self.to_geo_polygon().centroid() {
311            vec![c.x(), c.y()]
312        } else {
313            vec![0.0, 0.0]
314        }
315    }
316
317    fn validate(&self) -> Result<()> {
318        if self.exterior.len() < 3 {
319            return Err(Error::InvalidGeometry(format!(
320                "Polygon '{}' must have at least 3 vertices",
321                self.id
322            )));
323        }
324
325        if self.quantity == 0 {
326            return Err(Error::InvalidGeometry(format!(
327                "Quantity for '{}' must be at least 1",
328                self.id
329            )));
330        }
331
332        // Check for self-intersection could be added here
333
334        Ok(())
335    }
336
337    fn rotation_constraint(&self) -> &RotationConstraint<f64> {
338        &self.rotation_constraint
339    }
340
341    fn allow_mirror(&self) -> bool {
342        self.allow_flip
343    }
344
345    fn priority(&self) -> i32 {
346        self.priority
347    }
348}
349
350impl Geometry2DExt for Geometry2D {
351    fn aabb_2d(&self) -> AABB2D<f64> {
352        let (min, max) = self.aabb_vec();
353        AABB2D::new(min[0], min[1], max[0], max[1])
354    }
355
356    fn outer_ring(&self) -> &[(f64, f64)] {
357        &self.exterior
358    }
359
360    fn holes(&self) -> &[Vec<(f64, f64)>] {
361        &self.holes
362    }
363
364    fn is_convex(&self) -> bool {
365        if let Some(is_convex) = self.cached_is_convex {
366            is_convex
367        } else {
368            self.calculate_is_convex()
369        }
370    }
371
372    fn convex_hull(&self) -> Vec<(f64, f64)> {
373        if let Some(ref hull) = self.cached_convex_hull {
374            hull.clone()
375        } else {
376            self.calculate_convex_hull()
377        }
378    }
379
380    fn perimeter(&self) -> f64 {
381        if let Some(perim) = self.cached_perimeter {
382            perim
383        } else {
384            self.calculate_perimeter()
385        }
386    }
387}
388
389impl Geometry2D {
390    /// Computes the AABB of the geometry at a given rotation angle (in radians).
391    ///
392    /// Returns (min, max) as ([min_x, min_y], [max_x, max_y])
393    pub fn aabb_at_rotation(&self, rotation: f64) -> ([f64; 2], [f64; 2]) {
394        if rotation.abs() < 1e-10 {
395            return self.aabb();
396        }
397
398        let cos_r = rotation.cos();
399        let sin_r = rotation.sin();
400
401        let mut min_x = f64::INFINITY;
402        let mut min_y = f64::INFINITY;
403        let mut max_x = f64::NEG_INFINITY;
404        let mut max_y = f64::NEG_INFINITY;
405
406        for &(x, y) in &self.exterior {
407            let rx = x * cos_r - y * sin_r;
408            let ry = x * sin_r + y * cos_r;
409            min_x = min_x.min(rx);
410            min_y = min_y.min(ry);
411            max_x = max_x.max(rx);
412            max_y = max_y.max(ry);
413        }
414
415        ([min_x, min_y], [max_x, max_y])
416    }
417
418    /// Returns the width and height of the AABB at a given rotation.
419    pub fn dimensions_at_rotation(&self, rotation: f64) -> (f64, f64) {
420        let (min, max) = self.aabb_at_rotation(rotation);
421        (max[0] - min[0], max[1] - min[1])
422    }
423}
424
425#[cfg(test)]
426mod tests {
427    use super::*;
428    use approx::assert_relative_eq;
429
430    #[test]
431    fn test_rectangle_area() {
432        let rect = Geometry2D::rectangle("R1", 10.0, 5.0);
433        assert_relative_eq!(rect.measure(), 50.0, epsilon = 0.001);
434    }
435
436    #[test]
437    fn test_polygon_with_hole() {
438        let poly = Geometry2D::new("P1")
439            .with_polygon(vec![(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)])
440            .with_hole(vec![(25.0, 25.0), (75.0, 25.0), (75.0, 75.0), (25.0, 75.0)]);
441
442        // Area = 100*100 - 50*50 = 10000 - 2500 = 7500
443        assert_relative_eq!(poly.measure(), 7500.0, epsilon = 0.001);
444    }
445
446    #[test]
447    fn test_aabb() {
448        let poly = Geometry2D::new("P1").with_polygon(vec![
449            (10.0, 20.0),
450            (50.0, 20.0),
451            (50.0, 80.0),
452            (10.0, 80.0),
453        ]);
454
455        let aabb = poly.aabb_2d();
456        assert_relative_eq!(aabb.min_x, 10.0);
457        assert_relative_eq!(aabb.min_y, 20.0);
458        assert_relative_eq!(aabb.max_x, 50.0);
459        assert_relative_eq!(aabb.max_y, 80.0);
460    }
461
462    #[test]
463    fn test_rectangle_is_convex() {
464        let rect = Geometry2D::rectangle("R1", 10.0, 10.0);
465        assert!(rect.is_convex());
466    }
467
468    #[test]
469    fn test_l_shape_is_not_convex() {
470        let l = Geometry2D::l_shape("L1", 20.0, 20.0, 10.0, 10.0);
471        assert!(!l.is_convex());
472    }
473
474    #[test]
475    fn test_convex_hull() {
476        let l = Geometry2D::l_shape("L1", 20.0, 20.0, 10.0, 10.0);
477        let hull = l.convex_hull();
478        // Convex hull of L-shape should be a quadrilateral
479        assert!(hull.len() >= 4);
480    }
481
482    #[test]
483    fn test_centroid() {
484        let rect = Geometry2D::rectangle("R1", 10.0, 10.0);
485        let centroid = rect.centroid();
486        assert_relative_eq!(centroid[0], 5.0, epsilon = 0.001);
487        assert_relative_eq!(centroid[1], 5.0, epsilon = 0.001);
488    }
489
490    #[test]
491    fn test_perimeter() {
492        let rect = Geometry2D::rectangle("R1", 10.0, 5.0);
493        assert_relative_eq!(rect.perimeter(), 30.0, epsilon = 0.001);
494    }
495
496    #[test]
497    fn test_validation() {
498        let valid = Geometry2D::rectangle("R1", 10.0, 10.0);
499        assert!(valid.validate().is_ok());
500
501        let invalid = Geometry2D::new("P1").with_polygon(vec![(0.0, 0.0), (1.0, 0.0)]);
502        assert!(invalid.validate().is_err());
503    }
504
505    #[test]
506    fn test_circle() {
507        let circle = Geometry2D::circle("C1", 10.0, 32);
508        let area = circle.measure();
509        let expected = std::f64::consts::PI * 10.0 * 10.0;
510        // Circle approximation should be close to actual area
511        assert_relative_eq!(area, expected, epsilon = 5.0);
512    }
513}