Skip to main content

u_nesting_d2/
geometry.rs

1//! 2D geometry types.
2
3use u_nesting_core::geom::polygon as geom_polygon;
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    /// Clears all cached values.
179    fn clear_cache(&mut self) {
180        self.cached_area = None;
181        self.cached_convex_hull = None;
182        self.cached_perimeter = None;
183        self.cached_is_convex = None;
184    }
185
186    /// Calculates the area of the polygon (exterior minus holes).
187    fn calculate_area(&self) -> f64 {
188        let hole_refs: Vec<&[(f64, f64)]> = self.holes.iter().map(|h| h.as_slice()).collect();
189        geom_polygon::area_with_holes(&self.exterior, &hole_refs)
190    }
191
192    /// Calculates the perimeter of the polygon.
193    fn calculate_perimeter(&self) -> f64 {
194        let mut perim = geom_polygon::perimeter(&self.exterior);
195        for hole in &self.holes {
196            perim += geom_polygon::perimeter(hole);
197        }
198        perim
199    }
200
201    /// Calculates the convex hull.
202    fn calculate_convex_hull(&self) -> Vec<(f64, f64)> {
203        geom_polygon::convex_hull(&self.exterior)
204    }
205
206    /// Checks if the polygon is convex.
207    fn calculate_is_convex(&self) -> bool {
208        if self.exterior.len() < 3 || !self.holes.is_empty() {
209            return false;
210        }
211
212        // A polygon is convex if all cross products of consecutive edge pairs
213        // have the same sign
214        let n = self.exterior.len();
215        let mut sign = 0i32;
216
217        for i in 0..n {
218            let (x1, y1) = self.exterior[i];
219            let (x2, y2) = self.exterior[(i + 1) % n];
220            let (x3, y3) = self.exterior[(i + 2) % n];
221
222            let cross = (x2 - x1) * (y3 - y2) - (y2 - y1) * (x3 - x2);
223
224            if cross.abs() > 1e-10 {
225                let current_sign = if cross > 0.0 { 1 } else { -1 };
226                if sign == 0 {
227                    sign = current_sign;
228                } else if sign != current_sign {
229                    return false;
230                }
231            }
232        }
233
234        true
235    }
236}
237
238impl Geometry for Geometry2D {
239    type Scalar = f64;
240
241    fn id(&self) -> &GeometryId {
242        &self.id
243    }
244
245    fn quantity(&self) -> usize {
246        self.quantity
247    }
248
249    fn measure(&self) -> f64 {
250        if let Some(area) = self.cached_area {
251            area
252        } else {
253            self.calculate_area()
254        }
255    }
256
257    fn aabb(&self) -> ([f64; 2], [f64; 2]) {
258        let (min, max) = self.aabb_vec();
259        ([min[0], min[1]], [max[0], max[1]])
260    }
261
262    fn aabb_vec(&self) -> (Vec<f64>, Vec<f64>) {
263        if self.exterior.is_empty() {
264            return (vec![0.0, 0.0], vec![0.0, 0.0]);
265        }
266
267        let mut min_x = f64::MAX;
268        let mut min_y = f64::MAX;
269        let mut max_x = f64::MIN;
270        let mut max_y = f64::MIN;
271
272        for &(x, y) in &self.exterior {
273            min_x = min_x.min(x);
274            min_y = min_y.min(y);
275            max_x = max_x.max(x);
276            max_y = max_y.max(y);
277        }
278
279        (vec![min_x, min_y], vec![max_x, max_y])
280    }
281
282    fn centroid(&self) -> Vec<f64> {
283        let hole_refs: Vec<&[(f64, f64)]> = self.holes.iter().map(|h| h.as_slice()).collect();
284        if let Some((cx, cy)) = geom_polygon::centroid_with_holes(&self.exterior, &hole_refs) {
285            vec![cx, cy]
286        } else {
287            vec![0.0, 0.0]
288        }
289    }
290
291    fn validate(&self) -> Result<()> {
292        if self.exterior.len() < 3 {
293            return Err(Error::InvalidGeometry(format!(
294                "Polygon '{}' must have at least 3 vertices",
295                self.id
296            )));
297        }
298
299        if self.quantity == 0 {
300            return Err(Error::InvalidGeometry(format!(
301                "Quantity for '{}' must be at least 1",
302                self.id
303            )));
304        }
305
306        // Check for self-intersection could be added here
307
308        Ok(())
309    }
310
311    fn rotation_constraint(&self) -> &RotationConstraint<f64> {
312        &self.rotation_constraint
313    }
314
315    fn allow_mirror(&self) -> bool {
316        self.allow_flip
317    }
318
319    fn priority(&self) -> i32 {
320        self.priority
321    }
322}
323
324impl Geometry2DExt for Geometry2D {
325    fn aabb_2d(&self) -> AABB2D<f64> {
326        let (min, max) = self.aabb_vec();
327        AABB2D::new(min[0], min[1], max[0], max[1])
328    }
329
330    fn outer_ring(&self) -> &[(f64, f64)] {
331        &self.exterior
332    }
333
334    fn holes(&self) -> &[Vec<(f64, f64)>] {
335        &self.holes
336    }
337
338    fn is_convex(&self) -> bool {
339        if let Some(is_convex) = self.cached_is_convex {
340            is_convex
341        } else {
342            self.calculate_is_convex()
343        }
344    }
345
346    fn convex_hull(&self) -> Vec<(f64, f64)> {
347        if let Some(ref hull) = self.cached_convex_hull {
348            hull.clone()
349        } else {
350            self.calculate_convex_hull()
351        }
352    }
353
354    fn perimeter(&self) -> f64 {
355        if let Some(perim) = self.cached_perimeter {
356            perim
357        } else {
358            self.calculate_perimeter()
359        }
360    }
361}
362
363impl Geometry2D {
364    /// Computes the AABB of the geometry at a given rotation angle (in radians).
365    ///
366    /// Returns (min, max) as ([min_x, min_y], [max_x, max_y])
367    pub fn aabb_at_rotation(&self, rotation: f64) -> ([f64; 2], [f64; 2]) {
368        if rotation.abs() < 1e-10 {
369            return self.aabb();
370        }
371
372        let cos_r = rotation.cos();
373        let sin_r = rotation.sin();
374
375        let mut min_x = f64::INFINITY;
376        let mut min_y = f64::INFINITY;
377        let mut max_x = f64::NEG_INFINITY;
378        let mut max_y = f64::NEG_INFINITY;
379
380        for &(x, y) in &self.exterior {
381            let rx = x * cos_r - y * sin_r;
382            let ry = x * sin_r + y * cos_r;
383            min_x = min_x.min(rx);
384            min_y = min_y.min(ry);
385            max_x = max_x.max(rx);
386            max_y = max_y.max(ry);
387        }
388
389        ([min_x, min_y], [max_x, max_y])
390    }
391
392    /// Returns the width and height of the AABB at a given rotation.
393    pub fn dimensions_at_rotation(&self, rotation: f64) -> (f64, f64) {
394        let (min, max) = self.aabb_at_rotation(rotation);
395        (max[0] - min[0], max[1] - min[1])
396    }
397}
398
399#[cfg(test)]
400mod tests {
401    use super::*;
402    use approx::assert_relative_eq;
403
404    #[test]
405    fn test_rectangle_area() {
406        let rect = Geometry2D::rectangle("R1", 10.0, 5.0);
407        assert_relative_eq!(rect.measure(), 50.0, epsilon = 0.001);
408    }
409
410    #[test]
411    fn test_polygon_with_hole() {
412        let poly = Geometry2D::new("P1")
413            .with_polygon(vec![(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)])
414            .with_hole(vec![(25.0, 25.0), (75.0, 25.0), (75.0, 75.0), (25.0, 75.0)]);
415
416        // Area = 100*100 - 50*50 = 10000 - 2500 = 7500
417        assert_relative_eq!(poly.measure(), 7500.0, epsilon = 0.001);
418    }
419
420    #[test]
421    fn test_aabb() {
422        let poly = Geometry2D::new("P1").with_polygon(vec![
423            (10.0, 20.0),
424            (50.0, 20.0),
425            (50.0, 80.0),
426            (10.0, 80.0),
427        ]);
428
429        let aabb = poly.aabb_2d();
430        assert_relative_eq!(aabb.min_x, 10.0);
431        assert_relative_eq!(aabb.min_y, 20.0);
432        assert_relative_eq!(aabb.max_x, 50.0);
433        assert_relative_eq!(aabb.max_y, 80.0);
434    }
435
436    #[test]
437    fn test_rectangle_is_convex() {
438        let rect = Geometry2D::rectangle("R1", 10.0, 10.0);
439        assert!(rect.is_convex());
440    }
441
442    #[test]
443    fn test_l_shape_is_not_convex() {
444        let l = Geometry2D::l_shape("L1", 20.0, 20.0, 10.0, 10.0);
445        assert!(!l.is_convex());
446    }
447
448    #[test]
449    fn test_convex_hull() {
450        let l = Geometry2D::l_shape("L1", 20.0, 20.0, 10.0, 10.0);
451        let hull = l.convex_hull();
452        // Convex hull of L-shape should be a quadrilateral
453        assert!(hull.len() >= 4);
454    }
455
456    #[test]
457    fn test_centroid() {
458        let rect = Geometry2D::rectangle("R1", 10.0, 10.0);
459        let centroid = rect.centroid();
460        assert_relative_eq!(centroid[0], 5.0, epsilon = 0.001);
461        assert_relative_eq!(centroid[1], 5.0, epsilon = 0.001);
462    }
463
464    #[test]
465    fn test_perimeter() {
466        let rect = Geometry2D::rectangle("R1", 10.0, 5.0);
467        assert_relative_eq!(rect.perimeter(), 30.0, epsilon = 0.001);
468    }
469
470    #[test]
471    fn test_validation() {
472        let valid = Geometry2D::rectangle("R1", 10.0, 10.0);
473        assert!(valid.validate().is_ok());
474
475        let invalid = Geometry2D::new("P1").with_polygon(vec![(0.0, 0.0), (1.0, 0.0)]);
476        assert!(invalid.validate().is_err());
477    }
478
479    #[test]
480    fn test_circle() {
481        let circle = Geometry2D::circle("C1", 10.0, 32);
482        let area = circle.measure();
483        let expected = std::f64::consts::PI * 10.0 * 10.0;
484        // Circle approximation should be close to actual area
485        assert_relative_eq!(area, expected, epsilon = 5.0);
486    }
487}