1use nalgebra::Vector3;
4use u_nesting_core::geometry::{Geometry, GeometryId, RotationConstraint};
5use u_nesting_core::{Error, Result};
6
7#[cfg(feature = "serde")]
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
12#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
13pub enum OrientationConstraint {
14 #[default]
16 Any,
17 Upright,
19 Fixed,
21}
22
23#[derive(Debug, Clone)]
25#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
26pub struct Geometry3D {
27 id: GeometryId,
29
30 dimensions: Vector3<f64>,
32
33 quantity: usize,
35
36 mass: Option<f64>,
38
39 orientation: OrientationConstraint,
41
42 rotation_constraint: RotationConstraint<f64>,
44
45 stackable: bool,
47
48 max_stack_weight: Option<f64>,
50}
51
52impl Geometry3D {
53 pub fn new(id: impl Into<GeometryId>, width: f64, depth: f64, height: f64) -> Self {
55 Self {
56 id: id.into(),
57 dimensions: Vector3::new(width, depth, height),
58 quantity: 1,
59 mass: None,
60 orientation: OrientationConstraint::default(),
61 rotation_constraint: RotationConstraint::None,
62 stackable: true,
63 max_stack_weight: None,
64 }
65 }
66
67 pub fn box_shape(id: impl Into<GeometryId>, width: f64, depth: f64, height: f64) -> Self {
69 Self::new(id, width, depth, height)
70 }
71
72 pub fn with_quantity(mut self, n: usize) -> Self {
74 self.quantity = n;
75 self
76 }
77
78 pub fn with_mass(mut self, mass: f64) -> Self {
80 self.mass = Some(mass);
81 self
82 }
83
84 pub fn with_orientation(mut self, constraint: OrientationConstraint) -> Self {
86 self.orientation = constraint;
87 self
88 }
89
90 pub fn with_stackable(mut self, stackable: bool) -> Self {
92 self.stackable = stackable;
93 self
94 }
95
96 pub fn with_max_stack_weight(mut self, weight: f64) -> Self {
98 self.max_stack_weight = Some(weight);
99 self
100 }
101
102 pub fn dimensions(&self) -> &Vector3<f64> {
104 &self.dimensions
105 }
106
107 pub fn width(&self) -> f64 {
109 self.dimensions.x
110 }
111
112 pub fn depth(&self) -> f64 {
114 self.dimensions.y
115 }
116
117 pub fn height(&self) -> f64 {
119 self.dimensions.z
120 }
121
122 pub fn mass(&self) -> Option<f64> {
124 self.mass
125 }
126
127 pub fn orientation_constraint(&self) -> OrientationConstraint {
129 self.orientation
130 }
131
132 pub fn is_stackable(&self) -> bool {
134 self.stackable
135 }
136
137 pub fn allowed_orientations(&self) -> Vec<(usize, usize, usize)> {
140 match self.orientation {
141 OrientationConstraint::Fixed => vec![(0, 1, 2)],
142 OrientationConstraint::Upright => vec![(0, 1, 2), (1, 0, 2)],
143 OrientationConstraint::Any => vec![
144 (0, 1, 2), (0, 2, 1), (1, 0, 2), (1, 2, 0), (2, 0, 1), (2, 1, 0), ],
151 }
152 }
153
154 pub fn dimensions_for_orientation(&self, orientation: usize) -> Vector3<f64> {
156 let orientations = self.allowed_orientations();
157 if orientation >= orientations.len() {
158 return self.dimensions;
159 }
160
161 let (x_idx, y_idx, z_idx) = orientations[orientation];
162 Vector3::new(
163 self.dimensions[x_idx],
164 self.dimensions[y_idx],
165 self.dimensions[z_idx],
166 )
167 }
168}
169
170impl Geometry for Geometry3D {
171 type Scalar = f64;
172
173 fn id(&self) -> &GeometryId {
174 &self.id
175 }
176
177 fn quantity(&self) -> usize {
178 self.quantity
179 }
180
181 fn measure(&self) -> f64 {
182 self.dimensions.x * self.dimensions.y * self.dimensions.z
183 }
184
185 fn aabb_vec(&self) -> (Vec<f64>, Vec<f64>) {
186 (
187 vec![0.0, 0.0, 0.0],
188 vec![self.dimensions.x, self.dimensions.y, self.dimensions.z],
189 )
190 }
191
192 fn centroid(&self) -> Vec<f64> {
193 vec![
194 self.dimensions.x / 2.0,
195 self.dimensions.y / 2.0,
196 self.dimensions.z / 2.0,
197 ]
198 }
199
200 fn rotation_constraint(&self) -> &RotationConstraint<f64> {
201 &self.rotation_constraint
202 }
203
204 fn validate(&self) -> Result<()> {
205 if self.dimensions.x <= 0.0 || self.dimensions.y <= 0.0 || self.dimensions.z <= 0.0 {
206 return Err(Error::InvalidGeometry(format!(
207 "All dimensions for '{}' must be positive",
208 self.id
209 )));
210 }
211
212 if self.quantity == 0 {
213 return Err(Error::InvalidGeometry(format!(
214 "Quantity for '{}' must be at least 1",
215 self.id
216 )));
217 }
218
219 if let Some(mass) = self.mass {
220 if mass < 0.0 {
221 return Err(Error::InvalidGeometry(format!(
222 "Mass for '{}' cannot be negative",
223 self.id
224 )));
225 }
226 }
227
228 Ok(())
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235 use approx::assert_relative_eq;
236
237 #[test]
238 fn test_box_volume() {
239 let box3d = Geometry3D::new("B1", 10.0, 20.0, 30.0);
240 assert_relative_eq!(box3d.measure(), 6000.0, epsilon = 0.001);
241 }
242
243 #[test]
244 fn test_orientations() {
245 let box3d = Geometry3D::new("B1", 10.0, 20.0, 30.0);
246
247 assert_eq!(box3d.allowed_orientations().len(), 6);
249
250 let upright = box3d
252 .clone()
253 .with_orientation(OrientationConstraint::Upright);
254 assert_eq!(upright.allowed_orientations().len(), 2);
255
256 let fixed = box3d.clone().with_orientation(OrientationConstraint::Fixed);
258 assert_eq!(fixed.allowed_orientations().len(), 1);
259 }
260
261 #[test]
262 fn test_aabb() {
263 use u_nesting_core::geometry::Geometry;
264 let box3d = Geometry3D::new("B1", 10.0, 20.0, 30.0);
265 let (min, max) = box3d.aabb_vec();
266
267 assert_eq!(min, vec![0.0, 0.0, 0.0]);
268 assert_eq!(max, vec![10.0, 20.0, 30.0]);
269 }
270
271 #[test]
272 fn test_validation() {
273 let valid = Geometry3D::new("B1", 10.0, 20.0, 30.0);
274 assert!(valid.validate().is_ok());
275
276 let invalid = Geometry3D::new("B2", -10.0, 20.0, 30.0);
277 assert!(invalid.validate().is_err());
278
279 let zero_qty = Geometry3D::new("B3", 10.0, 20.0, 30.0).with_quantity(0);
280 assert!(zero_qty.validate().is_err());
281 }
282}