use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Shape {
Prim(Prim),
Group(Group),
Lathe(Lathe),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Prim {
pub prim: String,
#[serde(default)]
pub at: [f32; 3],
#[serde(default)]
pub rot: f32,
#[serde(default = "half")]
pub r: f32,
#[serde(default)]
pub size: Option<[f32; 3]>,
#[serde(default = "one")]
pub h: f32,
#[serde(default)]
pub r2: f32,
#[serde(default)]
pub rounded: f32,
#[serde(default = "axis_y", skip_serializing_if = "is_axis_y")]
pub axis: String,
}
fn axis_y() -> String {
"y".into()
}
fn is_axis_y(a: &String) -> bool {
a == "y"
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Group {
pub op: String,
#[serde(default = "quarter")]
pub k: f32,
#[serde(default)]
pub at: [f32; 3],
#[serde(default)]
pub rot: f32,
pub parts: Vec<Shape>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Lathe {
pub lathe: Vec<[f32; 2]>,
#[serde(default)]
pub at: [f32; 3],
}
fn half() -> f32 {
0.5
}
fn one() -> f32 {
1.0
}
fn quarter() -> f32 {
0.25
}
fn swizzle_extent(e: [f32; 3], axis: &str) -> [f32; 3] {
match axis {
"x" => [e[1], e[0], e[2]],
"z" => [e[0], e[2], e[1]],
_ => e,
}
}
pub const PRIMS: &[&str] = &["sphere", "box", "cylinder", "capsule", "cone", "torus"];
pub const OPS: &[&str] = &["union", "blend", "cut", "intersect"];
impl Shape {
pub fn validate(&self) -> Result<(), String> {
self.validate_at(0)
}
fn validate_at(&self, depth: usize) -> Result<(), String> {
if depth > 24 {
return Err("shape tree deeper than 24".into());
}
match self {
Shape::Prim(p) => {
if !PRIMS.contains(&p.prim.as_str()) {
return Err(format!("unknown prim '{}' — one of {PRIMS:?}", p.prim));
}
if p.prim == "box" && p.size.is_none() {
return Err("box needs a `size`".into());
}
Ok(())
}
Shape::Group(g) => {
if !OPS.contains(&g.op.as_str()) {
return Err(format!("unknown op '{}' — one of {OPS:?}", g.op));
}
if g.parts.is_empty() {
return Err(format!("'{}' needs at least one part", g.op));
}
for part in &g.parts {
part.validate_at(depth + 1)?;
}
Ok(())
}
Shape::Lathe(l) => {
if l.lathe.len() < 3 {
return Err("lathe needs at least 3 profile points".into());
}
Ok(())
}
}
}
pub fn bounds(&self) -> ([f32; 3], [f32; 3]) {
match self {
Shape::Prim(p) => {
let e = match p.prim.as_str() {
"sphere" => [p.r; 3],
"box" => {
let s = p.size.unwrap_or([1.0; 3]);
[
s[0] / 2.0 + p.rounded,
s[1] / 2.0 + p.rounded,
s[2] / 2.0 + p.rounded,
]
}
"cylinder" | "cone" => {
let r = p.r.max(p.r2);
swizzle_extent([r, p.h / 2.0, r], &p.axis)
}
"capsule" => swizzle_extent([p.r, p.h / 2.0 + p.r, p.r], &p.axis),
"torus" => swizzle_extent([p.r + p.r2, p.r2, p.r + p.r2], &p.axis),
_ => [p.r; 3],
};
let d = (e[0] * e[0] + e[2] * e[2]).sqrt();
let e = if p.rot != 0.0 { [d, e[1], d] } else { e };
(
[p.at[0] - e[0], p.at[1] - e[1], p.at[2] - e[2]],
[p.at[0] + e[0], p.at[1] + e[1], p.at[2] + e[2]],
)
}
Shape::Group(g) => {
let mut it = g.parts.iter().map(|s| s.bounds());
let (mut min, mut max) = it.next().unwrap_or(([0.0; 3], [0.0; 3]));
match g.op.as_str() {
"cut" => {} "intersect" => {
for (bmin, bmax) in it {
for i in 0..3 {
min[i] = min[i].max(bmin[i]);
max[i] = max[i].min(bmax[i]);
}
}
}
_ => {
for (bmin, bmax) in it {
for i in 0..3 {
min[i] = min[i].min(bmin[i]);
max[i] = max[i].max(bmax[i]);
}
}
if g.op == "blend" {
for i in 0..3 {
min[i] -= g.k;
max[i] += g.k;
}
}
}
}
let d = ((max[0] - min[0]).max(max[2] - min[2])) / 2.0;
let (cx, cz) = ((min[0] + max[0]) / 2.0, (min[2] + max[2]) / 2.0);
let (mut min, mut max) = if g.rot != 0.0 {
([cx - d, min[1], cz - d], [cx + d, max[1], cz + d])
} else {
(min, max)
};
for i in 0..3 {
min[i] += g.at[i];
max[i] += g.at[i];
}
(min, max)
}
Shape::Lathe(l) => {
let rmax = l.lathe.iter().map(|p| p[0].abs()).fold(0.0f32, f32::max);
let (ymin, ymax) = l
.lathe
.iter()
.fold((f32::INFINITY, f32::NEG_INFINITY), |(a, b), p| {
(a.min(p[1]), b.max(p[1]))
});
(
[l.at[0] - rmax, l.at[1] + ymin, l.at[2] - rmax],
[l.at[0] + rmax, l.at[1] + ymax, l.at[2] + rmax],
)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_the_minimal_forms_and_validates() {
let s: Shape = serde_json::from_str(
r#"{ "op": "blend", "k": 0.3, "parts": [
{ "prim": "sphere", "r": 1.0, "at": [0, 0.6, 0] },
{ "prim": "box", "size": [2, 0.4, 2] },
{ "lathe": [[0,0],[0.5,0],[0.6,0.8],[0.2,1.2]] }
] }"#,
)
.unwrap();
s.validate().unwrap();
let (min, max) = s.bounds();
assert!(
min[0] < -1.0 && max[1] > 1.0,
"bounds cover the parts: {min:?} {max:?}"
);
let json = serde_json::to_string(&s).unwrap();
let back: Shape = serde_json::from_str(&json).unwrap();
assert_eq!(back, s);
}
#[test]
fn bad_trees_fail_loudly() {
let bad: Shape = serde_json::from_str(r#"{ "prim": "blob", "r": 1.0 }"#).unwrap();
assert!(bad.validate().is_err());
let bad: Shape =
serde_json::from_str(r#"{ "op": "melt", "parts": [{ "prim": "sphere" }] }"#).unwrap();
assert!(bad.validate().is_err());
let bad: Shape = serde_json::from_str(r#"{ "prim": "box" }"#).unwrap();
assert!(bad.validate().is_err(), "box without size");
}
}