u_nesting_core/
geometry.rs

1//! Core geometry traits and types.
2
3use crate::transform::{AABB2D, AABB3D};
4use crate::Result;
5use nalgebra::RealField;
6
7#[cfg(feature = "serde")]
8use serde::{Deserialize, Serialize};
9
10/// Unique identifier for a geometry.
11pub type GeometryId = String;
12
13/// Allowed rotation angles for a geometry.
14#[derive(Debug, Clone, PartialEq)]
15#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
16#[derive(Default)]
17pub enum RotationConstraint<S> {
18    /// No rotation allowed (fixed orientation).
19    #[default]
20    None,
21    /// Free rotation (any angle).
22    Free,
23    /// Discrete rotation steps (e.g., 0, 90, 180, 270 degrees).
24    Discrete(Vec<S>),
25}
26
27impl<S: RealField + Copy> RotationConstraint<S> {
28    /// Creates a constraint for axis-aligned rotations only (0, 90, 180, 270 degrees).
29    pub fn axis_aligned() -> Self {
30        let pi = S::pi();
31        let half_pi = pi / (S::one() + S::one());
32        Self::Discrete(vec![S::zero(), half_pi, pi, pi + half_pi])
33    }
34
35    /// Creates a constraint for n evenly-spaced rotations.
36    ///
37    /// # Panics
38    /// Panics if `n` exceeds the precision of the scalar type (unlikely in practice:
39    /// n > 2^24 for f32, n > 2^53 for f64).
40    pub fn steps(n: usize) -> Self {
41        if n == 0 {
42            return Self::None;
43        }
44        let two_pi = S::two_pi();
45        let step =
46            two_pi / S::from_usize(n).expect("n exceeds scalar precision (use n < 2^24 for f32)");
47        let angles: Vec<S> = (0..n)
48            .map(|i| step * S::from_usize(i).expect("index exceeds scalar precision"))
49            .collect();
50        Self::Discrete(angles)
51    }
52
53    /// Returns true if no rotation is allowed.
54    pub fn is_fixed(&self) -> bool {
55        matches!(self, Self::None)
56    }
57
58    /// Returns the list of allowed angles.
59    pub fn angles(&self) -> Vec<S> {
60        match self {
61            Self::None => vec![S::zero()],
62            Self::Free => vec![], // Empty means any angle
63            Self::Discrete(angles) => angles.clone(),
64        }
65    }
66}
67
68/// Trait for geometric shapes that can be nested or packed.
69pub trait Geometry: Clone + Send + Sync {
70    /// The coordinate type (f32 or f64).
71    type Scalar: RealField + Copy;
72
73    /// Returns the unique identifier for this geometry.
74    fn id(&self) -> &GeometryId;
75
76    /// Returns the quantity of this geometry to place.
77    fn quantity(&self) -> usize;
78
79    /// Returns the area (2D) or volume (3D) of this geometry.
80    fn measure(&self) -> Self::Scalar;
81
82    /// Returns the axis-aligned bounding box as (min, max) corners.
83    fn aabb(&self) -> ([Self::Scalar; 2], [Self::Scalar; 2]) {
84        // Default implementation for 2D, override for 3D
85        let (min, max) = self.aabb_vec();
86        ([min[0], min[1]], [max[0], max[1]])
87    }
88
89    /// Returns the axis-aligned bounding box as Vec (for generic dimension support).
90    fn aabb_vec(&self) -> (Vec<Self::Scalar>, Vec<Self::Scalar>);
91
92    /// Returns the centroid (center of mass) of this geometry.
93    fn centroid(&self) -> Vec<Self::Scalar>;
94
95    /// Validates the geometry and returns an error if invalid.
96    fn validate(&self) -> Result<()>;
97
98    /// Returns the allowed rotations for this geometry.
99    fn rotation_constraint(&self) -> &RotationConstraint<Self::Scalar>;
100
101    /// Returns whether mirroring/flipping is allowed.
102    fn allow_mirror(&self) -> bool {
103        false
104    }
105
106    /// Returns optional priority for placement order (higher = placed first).
107    fn priority(&self) -> i32 {
108        0
109    }
110}
111
112/// Extended trait for 2D geometries.
113pub trait Geometry2DExt: Geometry {
114    /// Returns the 2D AABB.
115    fn aabb_2d(&self) -> AABB2D<Self::Scalar>;
116
117    /// Returns the outer boundary as a sequence of points (polygon vertices).
118    fn outer_ring(&self) -> &[(Self::Scalar, Self::Scalar)];
119
120    /// Returns any holes in the geometry.
121    fn holes(&self) -> &[Vec<(Self::Scalar, Self::Scalar)>];
122
123    /// Returns true if this geometry has holes.
124    fn has_holes(&self) -> bool {
125        !self.holes().is_empty()
126    }
127
128    /// Returns true if this geometry is convex.
129    fn is_convex(&self) -> bool;
130
131    /// Returns the convex hull of this geometry.
132    fn convex_hull(&self) -> Vec<(Self::Scalar, Self::Scalar)>;
133
134    /// Returns the perimeter of this geometry.
135    fn perimeter(&self) -> Self::Scalar;
136}
137
138/// Extended trait for 3D geometries.
139pub trait Geometry3DExt: Geometry {
140    /// Returns the 3D AABB.
141    fn aabb_3d(&self) -> AABB3D<Self::Scalar>;
142
143    /// Returns the surface area of this geometry.
144    fn surface_area(&self) -> Self::Scalar;
145
146    /// Returns the mass of this geometry, if defined.
147    fn mass(&self) -> Option<Self::Scalar>;
148
149    /// Returns the center of mass of this geometry.
150    fn center_of_mass(&self) -> (Self::Scalar, Self::Scalar, Self::Scalar);
151
152    /// Returns whether this geometry can be stacked upon.
153    fn stackable(&self) -> bool {
154        true
155    }
156
157    /// Returns the maximum stacking load this geometry can support.
158    fn max_stack_load(&self) -> Option<Self::Scalar> {
159        None
160    }
161}
162
163/// Trait for boundaries/containers that hold geometries.
164pub trait Boundary: Clone + Send + Sync {
165    /// The coordinate type (f32 or f64).
166    type Scalar: RealField + Copy;
167
168    /// Returns the area (2D) or volume (3D) of this boundary.
169    fn measure(&self) -> Self::Scalar;
170
171    /// Returns the axis-aligned bounding box as (min, max) corners.
172    fn aabb(&self) -> ([Self::Scalar; 2], [Self::Scalar; 2]) {
173        let (min, max) = self.aabb_vec();
174        ([min[0], min[1]], [max[0], max[1]])
175    }
176
177    /// Returns the axis-aligned bounding box as Vec.
178    fn aabb_vec(&self) -> (Vec<Self::Scalar>, Vec<Self::Scalar>);
179
180    /// Validates the boundary and returns an error if invalid.
181    fn validate(&self) -> Result<()>;
182
183    /// Checks if a point is inside the boundary.
184    fn contains_point(&self, point: &[Self::Scalar]) -> bool;
185}
186
187/// Extended trait for 2D boundaries.
188pub trait Boundary2DExt: Boundary {
189    /// Returns the 2D AABB.
190    fn aabb_2d(&self) -> AABB2D<Self::Scalar>;
191
192    /// Returns the boundary polygon vertices.
193    fn vertices(&self) -> &[(Self::Scalar, Self::Scalar)];
194
195    /// Checks if a polygon is fully contained within this boundary.
196    fn contains_polygon(&self, polygon: &[(Self::Scalar, Self::Scalar)]) -> bool;
197
198    /// Returns the effective usable area after applying margin.
199    fn effective_area(&self, margin: Self::Scalar) -> Self::Scalar;
200}
201
202/// Extended trait for 3D boundaries.
203pub trait Boundary3DExt: Boundary {
204    /// Returns the 3D AABB.
205    fn aabb_3d(&self) -> AABB3D<Self::Scalar>;
206
207    /// Returns the maximum weight/mass capacity.
208    fn max_mass(&self) -> Option<Self::Scalar>;
209
210    /// Checks if a box is fully contained within this boundary.
211    fn contains_box(&self, min: &[Self::Scalar; 3], max: &[Self::Scalar; 3]) -> bool;
212
213    /// Returns the effective usable volume after applying margin.
214    fn effective_volume(&self, margin: Self::Scalar) -> Self::Scalar;
215}
216
217/// Orientation constraints for 3D packing.
218#[derive(Debug, Clone, Copy, PartialEq, Eq)]
219#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
220#[derive(Default)]
221pub enum Orientation3D {
222    /// Original orientation only (no rotation).
223    Fixed,
224    /// Any of the 6 axis-aligned orientations.
225    #[default]
226    AxisAligned,
227    /// Any of the 24 orthogonal orientations.
228    Orthogonal,
229    /// Free rotation (any orientation).
230    Free,
231}
232
233impl Orientation3D {
234    /// Returns the number of discrete orientations.
235    pub fn count(&self) -> usize {
236        match self {
237            Self::Fixed => 1,
238            Self::AxisAligned => 6,
239            Self::Orthogonal => 24,
240            Self::Free => usize::MAX, // Continuous
241        }
242    }
243
244    /// Returns true if rotation is completely fixed.
245    pub fn is_fixed(&self) -> bool {
246        matches!(self, Self::Fixed)
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253
254    #[test]
255    fn test_rotation_constraint_axis_aligned() {
256        let constraint: RotationConstraint<f64> = RotationConstraint::axis_aligned();
257        let angles = constraint.angles();
258        assert_eq!(angles.len(), 4);
259    }
260
261    #[test]
262    fn test_rotation_constraint_steps() {
263        let constraint: RotationConstraint<f64> = RotationConstraint::steps(8);
264        let angles = constraint.angles();
265        assert_eq!(angles.len(), 8);
266    }
267
268    #[test]
269    fn test_orientation_3d_count() {
270        assert_eq!(Orientation3D::Fixed.count(), 1);
271        assert_eq!(Orientation3D::AxisAligned.count(), 6);
272        assert_eq!(Orientation3D::Orthogonal.count(), 24);
273    }
274}