use u_nesting_core::error::{check_above, check_at_least};
use u_nesting_core::geom::nalgebra_types::NaVector3 as Vector3;
use u_nesting_core::geometry::{Geometry, GeometryId, RotationConstraint};
use u_nesting_core::{Error, Result};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum OrientationConstraint {
#[default]
Any,
Upright,
Fixed,
}
impl OrientationConstraint {
pub const NAMES: [&'static str; 3] = ["any", "upright", "fixed"];
pub fn parse(name: &str) -> Option<Self> {
match name.trim().to_lowercase().as_str() {
"any" => Some(Self::Any),
"upright" => Some(Self::Upright),
"fixed" => Some(Self::Fixed),
_ => None,
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Geometry3D {
id: GeometryId,
dimensions: Vector3<f64>,
quantity: usize,
mass: Option<f64>,
orientation: OrientationConstraint,
rotation_constraint: RotationConstraint<f64>,
stackable: bool,
max_stack_weight: Option<f64>,
}
impl Geometry3D {
pub fn new(id: impl Into<GeometryId>, width: f64, depth: f64, height: f64) -> Self {
Self {
id: id.into(),
dimensions: Vector3::new(width, depth, height),
quantity: 1,
mass: None,
orientation: OrientationConstraint::default(),
rotation_constraint: RotationConstraint::None,
stackable: true,
max_stack_weight: None,
}
}
pub fn box_shape(id: impl Into<GeometryId>, width: f64, depth: f64, height: f64) -> Self {
Self::new(id, width, depth, height)
}
pub fn with_quantity(mut self, n: usize) -> Self {
self.quantity = n;
self
}
pub fn with_mass(mut self, mass: f64) -> Self {
self.mass = Some(mass);
self
}
pub fn with_orientation(mut self, constraint: OrientationConstraint) -> Self {
self.orientation = constraint;
self
}
pub fn with_stackable(mut self, stackable: bool) -> Self {
self.stackable = stackable;
self
}
pub fn with_max_stack_weight(mut self, weight: f64) -> Self {
self.max_stack_weight = Some(weight);
self
}
pub fn dimensions(&self) -> &Vector3<f64> {
&self.dimensions
}
pub fn width(&self) -> f64 {
self.dimensions.x
}
pub fn depth(&self) -> f64 {
self.dimensions.y
}
pub fn height(&self) -> f64 {
self.dimensions.z
}
pub fn mass(&self) -> Option<f64> {
self.mass
}
pub fn orientation_constraint(&self) -> OrientationConstraint {
self.orientation
}
pub fn is_stackable(&self) -> bool {
self.stackable
}
pub fn allowed_orientations(&self) -> Vec<(usize, usize, usize)> {
match self.orientation {
OrientationConstraint::Fixed => vec![(0, 1, 2)],
OrientationConstraint::Upright => vec![(0, 1, 2), (1, 0, 2)],
OrientationConstraint::Any => vec![
(0, 1, 2), (0, 2, 1), (1, 0, 2), (1, 2, 0), (2, 0, 1), (2, 1, 0), ],
}
}
pub fn orientation_label(&self, orientation: usize) -> String {
const AXES: [char; 3] = ['x', 'y', 'z'];
let orientations = self.allowed_orientations();
let (a, b, c) = orientations.get(orientation).copied().unwrap_or((0, 1, 2));
[AXES[a], AXES[b], AXES[c]].iter().collect()
}
pub fn dimensions_for_orientation(&self, orientation: usize) -> Vector3<f64> {
let orientations = self.allowed_orientations();
if orientation >= orientations.len() {
return self.dimensions;
}
let (x_idx, y_idx, z_idx) = orientations[orientation];
Vector3::new(
self.dimensions[x_idx],
self.dimensions[y_idx],
self.dimensions[z_idx],
)
}
}
#[cfg(feature = "serde")]
impl Geometry3D {
pub fn from_request(
index: usize,
request: u_nesting_core::api_types::Geometry3DRequest,
) -> Result<Self> {
let quantity = u_nesting_core::check_quantity(
&format!("geometries[{index}].quantity"),
&request.id,
request.quantity,
)?;
let [width, depth, height] = request.dimensions;
let mut geom = Self::new(request.id, width, depth, height).with_quantity(quantity);
if let Some(mass) = request.mass {
geom = geom.with_mass(mass);
}
if let Some(name) = request.orientation {
let constraint =
OrientationConstraint::parse(&name).ok_or_else(|| Error::UnknownOption {
parameter: format!("geometries[{index}].orientation"),
got: name.clone(),
expected: OrientationConstraint::NAMES.map(String::from).to_vec(),
})?;
geom = geom.with_orientation(constraint);
}
geom.check_measures(&format!("geometries[{index}]."))?;
Ok(geom)
}
pub fn from_requests(
requests: Vec<u_nesting_core::api_types::Geometry3DRequest>,
) -> Result<Vec<Self>> {
requests
.into_iter()
.enumerate()
.map(|(index, request)| Self::from_request(index, request))
.collect()
}
}
impl Geometry for Geometry3D {
type Scalar = f64;
fn id(&self) -> &GeometryId {
&self.id
}
fn quantity(&self) -> usize {
self.quantity
}
fn measure(&self) -> f64 {
self.dimensions.x * self.dimensions.y * self.dimensions.z
}
fn aabb_vec(&self) -> (Vec<f64>, Vec<f64>) {
(
vec![0.0, 0.0, 0.0],
vec![self.dimensions.x, self.dimensions.y, self.dimensions.z],
)
}
fn centroid(&self) -> Vec<f64> {
vec![
self.dimensions.x / 2.0,
self.dimensions.y / 2.0,
self.dimensions.z / 2.0,
]
}
fn rotation_constraint(&self) -> &RotationConstraint<f64> {
&self.rotation_constraint
}
fn validate(&self) -> Result<()> {
self.check_measures("")?;
u_nesting_core::check_quantity("quantity", &self.id, self.quantity as i128)?;
Ok(())
}
}
impl Geometry3D {
fn check_measures(&self, at: &str) -> Result<()> {
let of_this = |mut e: Error| {
if let Error::OutOfRange { id, .. } = &mut e {
*id = Some(self.id.clone());
}
e
};
for (axis, length) in self.dimensions.iter().enumerate() {
check_above(&format!("{at}dimensions[{axis}]"), *length, 0.0).map_err(of_this)?;
}
if let Some(mass) = self.mass {
check_at_least(&format!("{at}mass"), mass, 0.0).map_err(of_this)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_box_volume() {
let box3d = Geometry3D::new("B1", 10.0, 20.0, 30.0);
assert_relative_eq!(box3d.measure(), 6000.0, epsilon = 0.001);
}
#[test]
fn test_orientations() {
let box3d = Geometry3D::new("B1", 10.0, 20.0, 30.0);
assert_eq!(box3d.allowed_orientations().len(), 6);
let upright = box3d
.clone()
.with_orientation(OrientationConstraint::Upright);
assert_eq!(upright.allowed_orientations().len(), 2);
let fixed = box3d.clone().with_orientation(OrientationConstraint::Fixed);
assert_eq!(fixed.allowed_orientations().len(), 1);
}
#[test]
fn test_orientation_label() {
let geom = Geometry3D::new("B1", 10.0, 20.0, 30.0);
assert_eq!(geom.orientation_label(0), "xyz");
assert_eq!(geom.orientation_label(1), "xzy");
assert_eq!(geom.orientation_label(2), "yxz");
assert_eq!(geom.orientation_label(3), "yzx");
assert_eq!(geom.orientation_label(4), "zxy");
assert_eq!(geom.orientation_label(5), "zyx");
assert_eq!(geom.orientation_label(99), "xyz");
let fixed = geom.clone().with_orientation(OrientationConstraint::Fixed);
assert_eq!(fixed.orientation_label(0), "xyz");
}
#[test]
fn test_aabb() {
use u_nesting_core::geometry::Geometry;
let box3d = Geometry3D::new("B1", 10.0, 20.0, 30.0);
let (min, max) = box3d.aabb_vec();
assert_eq!(min, vec![0.0, 0.0, 0.0]);
assert_eq!(max, vec![10.0, 20.0, 30.0]);
}
#[test]
fn test_validation() {
let valid = Geometry3D::new("B1", 10.0, 20.0, 30.0);
assert!(valid.validate().is_ok());
let invalid = Geometry3D::new("B2", -10.0, 20.0, 30.0);
assert!(invalid.validate().is_err());
let zero_qty = Geometry3D::new("B3", 10.0, 20.0, 30.0).with_quantity(0);
assert!(zero_qty.validate().is_err());
}
#[test]
fn a_dimension_or_mass_out_of_range_names_the_field() {
let refused = |geometry: Geometry3D| match geometry.validate() {
Err(Error::OutOfRange {
parameter,
id,
min,
exclusive_min,
got,
..
}) => (parameter, id, min, exclusive_min, got),
other => panic!("{other:?}"),
};
assert_eq!(
refused(Geometry3D::new("crate", 10.0, 0.0, 30.0)),
(
"dimensions[1]".to_string(),
Some("crate".to_string()),
None,
Some(0.0),
0.0
)
);
assert_eq!(
refused(Geometry3D::new("crate", 10.0, 20.0, 30.0).with_mass(-1.0)),
(
"mass".to_string(),
Some("crate".to_string()),
Some(0.0),
None,
-1.0
)
);
assert!(Geometry3D::new("crate", 1.0, 1.0, 1.0)
.with_mass(0.0)
.validate()
.is_ok());
}
}