1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
12pub enum Shape {
13 Cuboid {
14 #[serde(default = "default_one")]
15 x: f32,
16 #[serde(default = "default_one")]
17 y: f32,
18 #[serde(default = "default_one")]
19 z: f32,
20 },
21 Sphere {
22 #[serde(default = "default_half")]
23 radius: f32,
24 },
25 Cylinder {
26 #[serde(default = "default_half")]
27 radius: f32,
28 #[serde(default = "default_one")]
29 height: f32,
30 },
31 Cone {
32 #[serde(default = "default_half")]
33 radius: f32,
34 #[serde(default = "default_one")]
35 height: f32,
36 },
37 Capsule {
38 #[serde(default = "default_half")]
39 radius: f32,
40 #[serde(default = "default_half")]
41 half_length: f32,
42 },
43 Torus {
44 #[serde(default = "default_one")]
45 major_radius: f32,
46 #[serde(default = "default_quarter")]
47 minor_radius: f32,
48 },
49 Plane {
50 #[serde(default = "default_ten")]
51 x: f32,
52 #[serde(default = "default_ten")]
53 z: f32,
54 },
55 Pyramid {
56 #[serde(default = "default_one")]
57 base_x: f32,
58 #[serde(default = "default_one")]
59 base_z: f32,
60 #[serde(default = "default_one")]
61 height: f32,
62 },
63 Tetrahedron {
64 #[serde(default = "default_one")]
65 radius: f32,
66 },
67 Icosahedron {
68 #[serde(default = "default_one")]
69 radius: f32,
70 },
71 Wedge {
72 #[serde(default = "default_one")]
73 x: f32,
74 #[serde(default = "default_one")]
75 y: f32,
76 #[serde(default = "default_one")]
77 z: f32,
78 },
79 Prism {
81 #[serde(default = "default_six")]
83 #[cfg_attr(feature = "schema", schemars(range(min = 3, max = 8)))]
84 sides: u8,
85 #[serde(default = "default_half")]
87 radius: f32,
88 #[serde(default = "default_one")]
90 height: f32,
91 },
92}
93
94fn default_one() -> f32 {
95 1.0
96}
97fn default_six() -> u8 {
98 6
99}
100fn default_half() -> f32 {
101 0.5
102}
103fn default_quarter() -> f32 {
104 0.25
105}
106fn default_ten() -> f32 {
107 10.0
108}
109
110impl Shape {
111 pub fn estimate_triangles(&self) -> usize {
113 match self {
114 Shape::Cuboid { .. } => 12,
115 Shape::Sphere { .. } => 760, Shape::Cylinder { .. } => 124, Shape::Cone { .. } => 64, Shape::Capsule { .. } => 1520, Shape::Torus { .. } => 2048, Shape::Plane { .. } => 2,
121 Shape::Pyramid { .. } => 6, Shape::Tetrahedron { .. } => 4, Shape::Icosahedron { .. } => 20, Shape::Wedge { .. } => 8, Shape::Prism { sides, .. } => 4 * usize::from(*sides).max(3) - 4,
127 }
128 }
129
130 pub fn local_aabb_half(&self) -> [f32; 3] {
132 match self {
133 Shape::Cuboid { x, y, z } => [x / 2.0, y / 2.0, z / 2.0],
134 Shape::Sphere { radius } => [*radius, *radius, *radius],
135 Shape::Cylinder { radius, height } => [*radius, height / 2.0, *radius],
136 Shape::Cone { radius, height } => [*radius, height / 2.0, *radius],
137 Shape::Capsule {
138 radius,
139 half_length,
140 } => [*radius, half_length + radius, *radius],
141 Shape::Torus {
142 major_radius,
143 minor_radius,
144 } => [
145 major_radius + minor_radius,
146 *minor_radius,
147 major_radius + minor_radius,
148 ],
149 Shape::Plane { x, z } => [x / 2.0, 0.0, z / 2.0],
150 Shape::Pyramid {
151 base_x,
152 base_z,
153 height,
154 } => [base_x / 2.0, height / 2.0, base_z / 2.0],
155 Shape::Tetrahedron { radius } => [*radius, *radius, *radius],
156 Shape::Icosahedron { radius } => [*radius, *radius, *radius],
157 Shape::Wedge { x, y, z } => [x / 2.0, y / 2.0, z / 2.0],
158 Shape::Prism { radius, height, .. } => [*radius, height / 2.0, *radius],
161 }
162 }
163
164 pub fn kind(&self) -> &'static str {
166 match self {
167 Shape::Cuboid { .. } => "cuboid",
168 Shape::Sphere { .. } => "sphere",
169 Shape::Cylinder { .. } => "cylinder",
170 Shape::Cone { .. } => "cone",
171 Shape::Capsule { .. } => "capsule",
172 Shape::Torus { .. } => "torus",
173 Shape::Plane { .. } => "plane",
174 Shape::Pyramid { .. } => "pyramid",
175 Shape::Tetrahedron { .. } => "tetrahedron",
176 Shape::Icosahedron { .. } => "icosahedron",
177 Shape::Wedge { .. } => "wedge",
178 Shape::Prism { .. } => "prism",
179 }
180 }
181}
182
183#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
185#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
186pub enum PrimitiveShapeKind {
187 Cuboid,
188 Sphere,
189 Cylinder,
190 Cone,
191 Capsule,
192 Torus,
193 Plane,
194 Pyramid,
195 Tetrahedron,
196 Icosahedron,
197 Wedge,
198 Prism,
199}
200
201impl Shape {
202 pub fn primitive_kind(&self) -> PrimitiveShapeKind {
204 match self {
205 Shape::Cuboid { .. } => PrimitiveShapeKind::Cuboid,
206 Shape::Sphere { .. } => PrimitiveShapeKind::Sphere,
207 Shape::Cylinder { .. } => PrimitiveShapeKind::Cylinder,
208 Shape::Cone { .. } => PrimitiveShapeKind::Cone,
209 Shape::Capsule { .. } => PrimitiveShapeKind::Capsule,
210 Shape::Torus { .. } => PrimitiveShapeKind::Torus,
211 Shape::Plane { .. } => PrimitiveShapeKind::Plane,
212 Shape::Pyramid { .. } => PrimitiveShapeKind::Pyramid,
213 Shape::Tetrahedron { .. } => PrimitiveShapeKind::Tetrahedron,
214 Shape::Icosahedron { .. } => PrimitiveShapeKind::Icosahedron,
215 Shape::Wedge { .. } => PrimitiveShapeKind::Wedge,
216 Shape::Prism { .. } => PrimitiveShapeKind::Prism,
217 }
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 #[test]
226 fn shape_roundtrip_json() {
227 let shapes = vec![
228 Shape::Cuboid {
229 x: 4.0,
230 y: 3.0,
231 z: 5.0,
232 },
233 Shape::Sphere { radius: 2.0 },
234 Shape::Torus {
235 major_radius: 3.0,
236 minor_radius: 0.5,
237 },
238 Shape::Plane { x: 20.0, z: 20.0 },
239 Shape::Pyramid {
240 base_x: 4.0,
241 base_z: 4.0,
242 height: 3.0,
243 },
244 Shape::Tetrahedron { radius: 1.5 },
245 Shape::Icosahedron { radius: 2.0 },
246 Shape::Wedge {
247 x: 2.0,
248 y: 1.0,
249 z: 3.0,
250 },
251 Shape::Prism {
252 sides: 6,
253 radius: 0.6,
254 height: 1.5,
255 },
256 ];
257
258 for shape in &shapes {
259 let json = serde_json::to_string(shape).unwrap();
260 let back: Shape = serde_json::from_str(&json).unwrap();
261 assert_eq!(*shape, back, "roundtrip failed for {}", shape.kind());
262 }
263 }
264
265 #[test]
270 fn prism_defaults_box_and_budget() {
271 let prism: Shape = serde_json::from_str(r#"{"Prism": {}}"#).unwrap();
272 assert_eq!(
273 prism,
274 Shape::Prism {
275 sides: 6,
276 radius: 0.5,
277 height: 1.0
278 }
279 );
280 assert_eq!(prism.kind(), "prism");
281 assert_eq!(prism.primitive_kind(), PrimitiveShapeKind::Prism);
282 assert_eq!(prism.local_aabb_half(), [0.5, 0.5, 0.5]);
283 assert_eq!(prism.estimate_triangles(), 20); let triangle: Shape =
285 serde_json::from_str(r#"{"Prism": {"sides": 3, "radius": 1.0, "height": 2.0}}"#)
286 .unwrap();
287 assert_eq!(triangle.estimate_triangles(), 8); assert_eq!(triangle.local_aabb_half(), [1.0, 1.0, 1.0]);
289 assert_eq!(
291 serde_json::to_string(&triangle).unwrap(),
292 r#"{"Prism":{"sides":3,"radius":1.0,"height":2.0}}"#
293 );
294 }
295
296 #[test]
297 fn cuboid_aabb() {
298 let s = Shape::Cuboid {
299 x: 4.0,
300 y: 6.0,
301 z: 2.0,
302 };
303 assert_eq!(s.local_aabb_half(), [2.0, 3.0, 1.0]);
304 }
305
306 #[test]
307 fn triangle_estimates_are_nonzero() {
308 let shapes = [
309 Shape::Cuboid {
310 x: 1.0,
311 y: 1.0,
312 z: 1.0,
313 },
314 Shape::Sphere { radius: 1.0 },
315 Shape::Cylinder {
316 radius: 1.0,
317 height: 1.0,
318 },
319 Shape::Cone {
320 radius: 1.0,
321 height: 1.0,
322 },
323 Shape::Capsule {
324 radius: 1.0,
325 half_length: 1.0,
326 },
327 Shape::Torus {
328 major_radius: 1.0,
329 minor_radius: 0.25,
330 },
331 Shape::Plane { x: 1.0, z: 1.0 },
332 Shape::Pyramid {
333 base_x: 4.0,
334 base_z: 4.0,
335 height: 3.0,
336 },
337 Shape::Tetrahedron { radius: 1.0 },
338 Shape::Icosahedron { radius: 1.0 },
339 Shape::Wedge {
340 x: 1.0,
341 y: 1.0,
342 z: 1.0,
343 },
344 Shape::Prism {
345 sides: 8,
346 radius: 1.0,
347 height: 1.0,
348 },
349 ];
350 for s in &shapes {
351 assert!(s.estimate_triangles() > 0, "{} has 0 triangles", s.kind());
352 }
353 }
354}