1use serde::{Deserialize, Serialize};
15
16#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
19#[serde(untagged)]
20pub enum Shape {
21 Prim(Prim),
23 Group(Group),
25 Lathe(Lathe),
27}
28
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
32pub struct Prim {
33 pub prim: String,
34 #[serde(default)]
36 pub at: [f32; 3],
37 #[serde(default)]
39 pub rot: f32,
40 #[serde(default = "half")]
42 pub r: f32,
43 #[serde(default)]
45 pub size: Option<[f32; 3]>,
46 #[serde(default = "one")]
48 pub h: f32,
49 #[serde(default)]
51 pub r2: f32,
52 #[serde(default)]
54 pub rounded: f32,
55 #[serde(default = "axis_y", skip_serializing_if = "is_axis_y")]
57 pub axis: String,
58}
59
60fn axis_y() -> String {
61 "y".into()
62}
63fn is_axis_y(a: &String) -> bool {
64 a == "y"
65}
66
67#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
70pub struct Group {
71 pub op: String,
72 #[serde(default = "quarter")]
74 pub k: f32,
75 #[serde(default)]
76 pub at: [f32; 3],
77 #[serde(default)]
78 pub rot: f32,
79 pub parts: Vec<Shape>,
80}
81
82#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
85pub struct Lathe {
86 pub lathe: Vec<[f32; 2]>,
88 #[serde(default)]
89 pub at: [f32; 3],
90}
91
92fn half() -> f32 {
93 0.5
94}
95fn one() -> f32 {
96 1.0
97}
98fn quarter() -> f32 {
99 0.25
100}
101
102fn swizzle_extent(e: [f32; 3], axis: &str) -> [f32; 3] {
104 match axis {
105 "x" => [e[1], e[0], e[2]],
106 "z" => [e[0], e[2], e[1]],
107 _ => e,
108 }
109}
110
111pub const PRIMS: &[&str] = &["sphere", "box", "cylinder", "capsule", "cone", "torus"];
113pub const OPS: &[&str] = &["union", "blend", "cut", "intersect"];
115
116impl Shape {
117 pub fn validate(&self) -> Result<(), String> {
119 self.validate_at(0)
120 }
121
122 fn validate_at(&self, depth: usize) -> Result<(), String> {
123 if depth > 24 {
124 return Err("shape tree deeper than 24".into());
125 }
126 match self {
127 Shape::Prim(p) => {
128 if !PRIMS.contains(&p.prim.as_str()) {
129 return Err(format!("unknown prim '{}' — one of {PRIMS:?}", p.prim));
130 }
131 if p.prim == "box" && p.size.is_none() {
132 return Err("box needs a `size`".into());
133 }
134 Ok(())
135 }
136 Shape::Group(g) => {
137 if !OPS.contains(&g.op.as_str()) {
138 return Err(format!("unknown op '{}' — one of {OPS:?}", g.op));
139 }
140 if g.parts.is_empty() {
141 return Err(format!("'{}' needs at least one part", g.op));
142 }
143 for part in &g.parts {
144 part.validate_at(depth + 1)?;
145 }
146 Ok(())
147 }
148 Shape::Lathe(l) => {
149 if l.lathe.len() < 3 {
150 return Err("lathe needs at least 3 profile points".into());
151 }
152 Ok(())
153 }
154 }
155 }
156
157 pub fn bounds(&self) -> ([f32; 3], [f32; 3]) {
160 match self {
161 Shape::Prim(p) => {
162 let e = match p.prim.as_str() {
163 "sphere" => [p.r; 3],
164 "box" => {
165 let s = p.size.unwrap_or([1.0; 3]);
166 [
167 s[0] / 2.0 + p.rounded,
168 s[1] / 2.0 + p.rounded,
169 s[2] / 2.0 + p.rounded,
170 ]
171 }
172 "cylinder" | "cone" => {
173 let r = p.r.max(p.r2);
174 swizzle_extent([r, p.h / 2.0, r], &p.axis)
175 }
176 "capsule" => swizzle_extent([p.r, p.h / 2.0 + p.r, p.r], &p.axis),
177 "torus" => swizzle_extent([p.r + p.r2, p.r2, p.r + p.r2], &p.axis),
178 _ => [p.r; 3],
179 };
180 let d = (e[0] * e[0] + e[2] * e[2]).sqrt();
182 let e = if p.rot != 0.0 { [d, e[1], d] } else { e };
183 (
184 [p.at[0] - e[0], p.at[1] - e[1], p.at[2] - e[2]],
185 [p.at[0] + e[0], p.at[1] + e[1], p.at[2] + e[2]],
186 )
187 }
188 Shape::Group(g) => {
189 let mut it = g.parts.iter().map(|s| s.bounds());
190 let (mut min, mut max) = it.next().unwrap_or(([0.0; 3], [0.0; 3]));
191 match g.op.as_str() {
192 "cut" => {} "intersect" => {
194 for (bmin, bmax) in it {
195 for i in 0..3 {
196 min[i] = min[i].max(bmin[i]);
197 max[i] = max[i].min(bmax[i]);
198 }
199 }
200 }
201 _ => {
202 for (bmin, bmax) in it {
203 for i in 0..3 {
204 min[i] = min[i].min(bmin[i]);
205 max[i] = max[i].max(bmax[i]);
206 }
207 }
208 if g.op == "blend" {
209 for i in 0..3 {
210 min[i] -= g.k;
211 max[i] += g.k;
212 }
213 }
214 }
215 }
216 let d = ((max[0] - min[0]).max(max[2] - min[2])) / 2.0;
217 let (cx, cz) = ((min[0] + max[0]) / 2.0, (min[2] + max[2]) / 2.0);
218 let (mut min, mut max) = if g.rot != 0.0 {
219 ([cx - d, min[1], cz - d], [cx + d, max[1], cz + d])
221 } else {
222 (min, max)
223 };
224 for i in 0..3 {
225 min[i] += g.at[i];
226 max[i] += g.at[i];
227 }
228 (min, max)
229 }
230 Shape::Lathe(l) => {
231 let rmax = l.lathe.iter().map(|p| p[0].abs()).fold(0.0f32, f32::max);
232 let (ymin, ymax) = l
233 .lathe
234 .iter()
235 .fold((f32::INFINITY, f32::NEG_INFINITY), |(a, b), p| {
236 (a.min(p[1]), b.max(p[1]))
237 });
238 (
239 [l.at[0] - rmax, l.at[1] + ymin, l.at[2] - rmax],
240 [l.at[0] + rmax, l.at[1] + ymax, l.at[2] + rmax],
241 )
242 }
243 }
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 #[test]
252 fn parses_the_minimal_forms_and_validates() {
253 let s: Shape = serde_json::from_str(
254 r#"{ "op": "blend", "k": 0.3, "parts": [
255 { "prim": "sphere", "r": 1.0, "at": [0, 0.6, 0] },
256 { "prim": "box", "size": [2, 0.4, 2] },
257 { "lathe": [[0,0],[0.5,0],[0.6,0.8],[0.2,1.2]] }
258 ] }"#,
259 )
260 .unwrap();
261 s.validate().unwrap();
262 let (min, max) = s.bounds();
263 assert!(
264 min[0] < -1.0 && max[1] > 1.0,
265 "bounds cover the parts: {min:?} {max:?}"
266 );
267 let json = serde_json::to_string(&s).unwrap();
269 let back: Shape = serde_json::from_str(&json).unwrap();
270 assert_eq!(back, s);
271 }
272
273 #[test]
274 fn bad_trees_fail_loudly() {
275 let bad: Shape = serde_json::from_str(r#"{ "prim": "blob", "r": 1.0 }"#).unwrap();
276 assert!(bad.validate().is_err());
277 let bad: Shape =
278 serde_json::from_str(r#"{ "op": "melt", "parts": [{ "prim": "sphere" }] }"#).unwrap();
279 assert!(bad.validate().is_err());
280 let bad: Shape = serde_json::from_str(r#"{ "prim": "box" }"#).unwrap();
281 assert!(bad.validate().is_err(), "box without size");
282 }
283}