Skip to main content

u_nesting_core/
geometry.rs

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