1use u_nesting_core::geom::nalgebra_types::NaVector3 as 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 orientation_label(&self, orientation: usize) -> String {
163 const AXES: [char; 3] = ['x', 'y', 'z'];
164 let orientations = self.allowed_orientations();
165 let (a, b, c) = orientations.get(orientation).copied().unwrap_or((0, 1, 2));
166 [AXES[a], AXES[b], AXES[c]].iter().collect()
167 }
168
169 pub fn dimensions_for_orientation(&self, orientation: usize) -> Vector3<f64> {
171 let orientations = self.allowed_orientations();
172 if orientation >= orientations.len() {
173 return self.dimensions;
174 }
175
176 let (x_idx, y_idx, z_idx) = orientations[orientation];
177 Vector3::new(
178 self.dimensions[x_idx],
179 self.dimensions[y_idx],
180 self.dimensions[z_idx],
181 )
182 }
183}
184
185impl Geometry for Geometry3D {
186 type Scalar = f64;
187
188 fn id(&self) -> &GeometryId {
189 &self.id
190 }
191
192 fn quantity(&self) -> usize {
193 self.quantity
194 }
195
196 fn measure(&self) -> f64 {
197 self.dimensions.x * self.dimensions.y * self.dimensions.z
198 }
199
200 fn aabb_vec(&self) -> (Vec<f64>, Vec<f64>) {
201 (
202 vec![0.0, 0.0, 0.0],
203 vec![self.dimensions.x, self.dimensions.y, self.dimensions.z],
204 )
205 }
206
207 fn centroid(&self) -> Vec<f64> {
208 vec![
209 self.dimensions.x / 2.0,
210 self.dimensions.y / 2.0,
211 self.dimensions.z / 2.0,
212 ]
213 }
214
215 fn rotation_constraint(&self) -> &RotationConstraint<f64> {
216 &self.rotation_constraint
217 }
218
219 fn validate(&self) -> Result<()> {
220 if self.dimensions.x <= 0.0 || self.dimensions.y <= 0.0 || self.dimensions.z <= 0.0 {
221 return Err(Error::InvalidGeometry(format!(
222 "All dimensions for '{}' must be positive",
223 self.id
224 )));
225 }
226
227 if self.quantity == 0 {
228 return Err(Error::InvalidGeometry(format!(
229 "Quantity for '{}' must be at least 1",
230 self.id
231 )));
232 }
233
234 if let Some(mass) = self.mass {
235 if mass < 0.0 {
236 return Err(Error::InvalidGeometry(format!(
237 "Mass for '{}' cannot be negative",
238 self.id
239 )));
240 }
241 }
242
243 Ok(())
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250 use approx::assert_relative_eq;
251
252 #[test]
253 fn test_box_volume() {
254 let box3d = Geometry3D::new("B1", 10.0, 20.0, 30.0);
255 assert_relative_eq!(box3d.measure(), 6000.0, epsilon = 0.001);
256 }
257
258 #[test]
259 fn test_orientations() {
260 let box3d = Geometry3D::new("B1", 10.0, 20.0, 30.0);
261
262 assert_eq!(box3d.allowed_orientations().len(), 6);
264
265 let upright = box3d
267 .clone()
268 .with_orientation(OrientationConstraint::Upright);
269 assert_eq!(upright.allowed_orientations().len(), 2);
270
271 let fixed = box3d.clone().with_orientation(OrientationConstraint::Fixed);
273 assert_eq!(fixed.allowed_orientations().len(), 1);
274 }
275
276 #[test]
277 fn test_orientation_label() {
278 let geom = Geometry3D::new("B1", 10.0, 20.0, 30.0);
280 assert_eq!(geom.orientation_label(0), "xyz");
281 assert_eq!(geom.orientation_label(1), "xzy");
282 assert_eq!(geom.orientation_label(2), "yxz");
283 assert_eq!(geom.orientation_label(3), "yzx");
284 assert_eq!(geom.orientation_label(4), "zxy");
285 assert_eq!(geom.orientation_label(5), "zyx");
286 assert_eq!(geom.orientation_label(99), "xyz");
288
289 let fixed = geom.clone().with_orientation(OrientationConstraint::Fixed);
291 assert_eq!(fixed.orientation_label(0), "xyz");
292 }
293
294 #[test]
295 fn test_aabb() {
296 use u_nesting_core::geometry::Geometry;
297 let box3d = Geometry3D::new("B1", 10.0, 20.0, 30.0);
298 let (min, max) = box3d.aabb_vec();
299
300 assert_eq!(min, vec![0.0, 0.0, 0.0]);
301 assert_eq!(max, vec![10.0, 20.0, 30.0]);
302 }
303
304 #[test]
305 fn test_validation() {
306 let valid = Geometry3D::new("B1", 10.0, 20.0, 30.0);
307 assert!(valid.validate().is_ok());
308
309 let invalid = Geometry3D::new("B2", -10.0, 20.0, 30.0);
310 assert!(invalid.validate().is_err());
311
312 let zero_qty = Geometry3D::new("B3", 10.0, 20.0, 30.0).with_quantity(0);
313 assert!(zero_qty.validate().is_err());
314 }
315}