use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum Shape {
Cuboid {
#[serde(default = "default_one")]
x: f32,
#[serde(default = "default_one")]
y: f32,
#[serde(default = "default_one")]
z: f32,
},
Sphere {
#[serde(default = "default_half")]
radius: f32,
},
Cylinder {
#[serde(default = "default_half")]
radius: f32,
#[serde(default = "default_one")]
height: f32,
},
Cone {
#[serde(default = "default_half")]
radius: f32,
#[serde(default = "default_one")]
height: f32,
},
Capsule {
#[serde(default = "default_half")]
radius: f32,
#[serde(default = "default_half")]
half_length: f32,
},
Torus {
#[serde(default = "default_one")]
major_radius: f32,
#[serde(default = "default_quarter")]
minor_radius: f32,
},
Plane {
#[serde(default = "default_ten")]
x: f32,
#[serde(default = "default_ten")]
z: f32,
},
Pyramid {
#[serde(default = "default_one")]
base_x: f32,
#[serde(default = "default_one")]
base_z: f32,
#[serde(default = "default_one")]
height: f32,
},
Tetrahedron {
#[serde(default = "default_one")]
radius: f32,
},
Icosahedron {
#[serde(default = "default_one")]
radius: f32,
},
Wedge {
#[serde(default = "default_one")]
x: f32,
#[serde(default = "default_one")]
y: f32,
#[serde(default = "default_one")]
z: f32,
},
Prism {
#[serde(default = "default_six")]
#[cfg_attr(feature = "schema", schemars(range(min = 3, max = 8)))]
sides: u8,
#[serde(default = "default_half")]
radius: f32,
#[serde(default = "default_one")]
height: f32,
},
}
fn default_one() -> f32 {
1.0
}
fn default_six() -> u8 {
6
}
fn default_half() -> f32 {
0.5
}
fn default_quarter() -> f32 {
0.25
}
fn default_ten() -> f32 {
10.0
}
impl Shape {
pub fn estimate_triangles(&self) -> usize {
match self {
Shape::Cuboid { .. } => 12,
Shape::Sphere { .. } => 760, Shape::Cylinder { .. } => 124, Shape::Cone { .. } => 64, Shape::Capsule { .. } => 1520, Shape::Torus { .. } => 2048, Shape::Plane { .. } => 2,
Shape::Pyramid { .. } => 6, Shape::Tetrahedron { .. } => 4, Shape::Icosahedron { .. } => 20, Shape::Wedge { .. } => 8, Shape::Prism { sides, .. } => 4 * usize::from(*sides).max(3) - 4,
}
}
pub fn local_aabb_half(&self) -> [f32; 3] {
match self {
Shape::Cuboid { x, y, z } => [x / 2.0, y / 2.0, z / 2.0],
Shape::Sphere { radius } => [*radius, *radius, *radius],
Shape::Cylinder { radius, height } => [*radius, height / 2.0, *radius],
Shape::Cone { radius, height } => [*radius, height / 2.0, *radius],
Shape::Capsule {
radius,
half_length,
} => [*radius, half_length + radius, *radius],
Shape::Torus {
major_radius,
minor_radius,
} => [
major_radius + minor_radius,
*minor_radius,
major_radius + minor_radius,
],
Shape::Plane { x, z } => [x / 2.0, 0.0, z / 2.0],
Shape::Pyramid {
base_x,
base_z,
height,
} => [base_x / 2.0, height / 2.0, base_z / 2.0],
Shape::Tetrahedron { radius } => [*radius, *radius, *radius],
Shape::Icosahedron { radius } => [*radius, *radius, *radius],
Shape::Wedge { x, y, z } => [x / 2.0, y / 2.0, z / 2.0],
Shape::Prism { radius, height, .. } => [*radius, height / 2.0, *radius],
}
}
pub fn kind(&self) -> &'static str {
match self {
Shape::Cuboid { .. } => "cuboid",
Shape::Sphere { .. } => "sphere",
Shape::Cylinder { .. } => "cylinder",
Shape::Cone { .. } => "cone",
Shape::Capsule { .. } => "capsule",
Shape::Torus { .. } => "torus",
Shape::Plane { .. } => "plane",
Shape::Pyramid { .. } => "pyramid",
Shape::Tetrahedron { .. } => "tetrahedron",
Shape::Icosahedron { .. } => "icosahedron",
Shape::Wedge { .. } => "wedge",
Shape::Prism { .. } => "prism",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum PrimitiveShapeKind {
Cuboid,
Sphere,
Cylinder,
Cone,
Capsule,
Torus,
Plane,
Pyramid,
Tetrahedron,
Icosahedron,
Wedge,
Prism,
}
impl Shape {
pub fn primitive_kind(&self) -> PrimitiveShapeKind {
match self {
Shape::Cuboid { .. } => PrimitiveShapeKind::Cuboid,
Shape::Sphere { .. } => PrimitiveShapeKind::Sphere,
Shape::Cylinder { .. } => PrimitiveShapeKind::Cylinder,
Shape::Cone { .. } => PrimitiveShapeKind::Cone,
Shape::Capsule { .. } => PrimitiveShapeKind::Capsule,
Shape::Torus { .. } => PrimitiveShapeKind::Torus,
Shape::Plane { .. } => PrimitiveShapeKind::Plane,
Shape::Pyramid { .. } => PrimitiveShapeKind::Pyramid,
Shape::Tetrahedron { .. } => PrimitiveShapeKind::Tetrahedron,
Shape::Icosahedron { .. } => PrimitiveShapeKind::Icosahedron,
Shape::Wedge { .. } => PrimitiveShapeKind::Wedge,
Shape::Prism { .. } => PrimitiveShapeKind::Prism,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shape_roundtrip_json() {
let shapes = vec![
Shape::Cuboid {
x: 4.0,
y: 3.0,
z: 5.0,
},
Shape::Sphere { radius: 2.0 },
Shape::Torus {
major_radius: 3.0,
minor_radius: 0.5,
},
Shape::Plane { x: 20.0, z: 20.0 },
Shape::Pyramid {
base_x: 4.0,
base_z: 4.0,
height: 3.0,
},
Shape::Tetrahedron { radius: 1.5 },
Shape::Icosahedron { radius: 2.0 },
Shape::Wedge {
x: 2.0,
y: 1.0,
z: 3.0,
},
Shape::Prism {
sides: 6,
radius: 0.6,
height: 1.5,
},
];
for shape in &shapes {
let json = serde_json::to_string(shape).unwrap();
let back: Shape = serde_json::from_str(&json).unwrap();
assert_eq!(*shape, back, "roundtrip failed for {}", shape.kind());
}
}
#[test]
fn prism_defaults_box_and_budget() {
let prism: Shape = serde_json::from_str(r#"{"Prism": {}}"#).unwrap();
assert_eq!(
prism,
Shape::Prism {
sides: 6,
radius: 0.5,
height: 1.0
}
);
assert_eq!(prism.kind(), "prism");
assert_eq!(prism.primitive_kind(), PrimitiveShapeKind::Prism);
assert_eq!(prism.local_aabb_half(), [0.5, 0.5, 0.5]);
assert_eq!(prism.estimate_triangles(), 20); let triangle: Shape =
serde_json::from_str(r#"{"Prism": {"sides": 3, "radius": 1.0, "height": 2.0}}"#)
.unwrap();
assert_eq!(triangle.estimate_triangles(), 8); assert_eq!(triangle.local_aabb_half(), [1.0, 1.0, 1.0]);
assert_eq!(
serde_json::to_string(&triangle).unwrap(),
r#"{"Prism":{"sides":3,"radius":1.0,"height":2.0}}"#
);
}
#[test]
fn cuboid_aabb() {
let s = Shape::Cuboid {
x: 4.0,
y: 6.0,
z: 2.0,
};
assert_eq!(s.local_aabb_half(), [2.0, 3.0, 1.0]);
}
#[test]
fn triangle_estimates_are_nonzero() {
let shapes = [
Shape::Cuboid {
x: 1.0,
y: 1.0,
z: 1.0,
},
Shape::Sphere { radius: 1.0 },
Shape::Cylinder {
radius: 1.0,
height: 1.0,
},
Shape::Cone {
radius: 1.0,
height: 1.0,
},
Shape::Capsule {
radius: 1.0,
half_length: 1.0,
},
Shape::Torus {
major_radius: 1.0,
minor_radius: 0.25,
},
Shape::Plane { x: 1.0, z: 1.0 },
Shape::Pyramid {
base_x: 4.0,
base_z: 4.0,
height: 3.0,
},
Shape::Tetrahedron { radius: 1.0 },
Shape::Icosahedron { radius: 1.0 },
Shape::Wedge {
x: 1.0,
y: 1.0,
z: 1.0,
},
Shape::Prism {
sides: 8,
radius: 1.0,
height: 1.0,
},
];
for s in &shapes {
assert!(s.estimate_triangles() > 0, "{} has 0 triangles", s.kind());
}
}
}