diff --git a/Cargo.lock b/Cargo.lock index 3c017bd..c703144 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -145,6 +145,8 @@ version = "0.1.0" dependencies = [ "approx", "clap", + "serde", + "serde_derive", "stl_io", ] @@ -175,6 +177,26 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "serde" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "stl_io" version = "0.7.0" diff --git a/Cargo.toml b/Cargo.toml index ce042aa..e8d213e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,4 +6,6 @@ edition = "2021" [dependencies] approx = "0.5.1" clap = { version = "4.5.4", features = ["derive"] } +serde = "1.0.203" +serde_derive = "1.0.203" stl_io = "0.7.0" diff --git a/src/layers.rs b/src/layers.rs new file mode 100644 index 0000000..a15fbf0 --- /dev/null +++ b/src/layers.rs @@ -0,0 +1,94 @@ +use stl_io::{self, Vertex}; + +use crate::mushroom::Mushroom; + +#[derive(Debug, PartialEq, Clone)] +pub struct Layer { + /// Stalk height in millimeters. Zero for default. + pub center: Vertex, + + /// Stalk diameter in millimeters. Zero for default. + pub radius: f32, +} + +impl Layer { + pub fn new() -> Self { + return Self { + center: Vertex::new([1.0, 0.0, 0.0]), + radius: 0.0, + }; + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Layers { + pub base: Vertex, + pub layers: Vec, + pub top: Vertex, +} + +impl Layers { + pub fn max_radius(&self) -> f32 { + let mut max_radius: f32 = 0.0; + for layer in &self.layers { + max_radius = max_radius.max(layer.radius); + } + return max_radius; + } +} + +fn stalk_layers(mushroom: &Mushroom, base: &Vertex, _resolution: f32) -> Vec { + let stalk_top = Vertex::new([base[0], base[1], base[2] + mushroom.stalk_h]); + + return [ + Layer { + center: base.clone(), + radius: mushroom.stalk_r, + }, + Layer { + center: stalk_top, + radius: mushroom.stalk_r, + }, + ] + .to_vec(); +} + +fn cap_layers(mushroom: &Mushroom, base: &Vertex, _resolution: f32) -> Vec { + let top = Vertex::new([base[0], base[1], base[2] + mushroom.cap_h]); + + return [ + Layer { + center: base.clone(), + radius: mushroom.cap_r, + }, + Layer { + center: top, + radius: mushroom.cap_r, + }, + ] + .to_vec(); +} + +pub fn mushroom(mushroom: &Mushroom, resolution: f32) -> Layers { + let _vertical_steps = (mushroom.height() / resolution) as u32; + + let base = Vertex::new([0.0, 0.0, 0.0]); + + // Get layers for the stalk. + let mut stalk_layers = stalk_layers(mushroom, &base, resolution); + + // Get layers for the cap. + let cap_base = stalk_layers.last().unwrap().center; + let mut cap_layers = cap_layers(mushroom, &cap_base, resolution); + + // The very top of the mushroom is directly above the top layer of the cap (or even with it) + // at the exact height of the total mushroom. + let cap_top = &cap_layers.last().unwrap().center; + let top = Vertex::new([cap_top[0], cap_top[1], base[2] + mushroom.height()]); + + // Assemble the final Layers object. + let mut layers: Vec = Vec::new(); + layers.append(&mut stalk_layers); + layers.append(&mut cap_layers); + return Layers { base, top, layers }; +} diff --git a/src/main.rs b/src/main.rs index 0cf65a4..8da02bd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +pub mod layers; pub mod mushroom; pub mod render; @@ -6,8 +7,30 @@ use clap::Parser; use std::fs::OpenOptions; extern crate approx; +pub use serde_derive::{Deserialize, Serialize}; -// TODO: Investigate https://github.com/virtualritz/polyhedron-ops. +fn main() { + let args = Args::parse(); + + // Define the mushroom. + let mushroom = mushroom_from_args(&args); + + // Render the mushroom. + // let mesh = render::cylinder(mushroom.stalk_h, mushroom.stalk_d, 128); + let layers = layers::mushroom(&mushroom, 0.1); + let mesh = render::render(&layers, 0.1); + + // Save output to file. + let mut file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(&args.output) + .unwrap(); + stl_io::write_stl(&mut file, mesh.iter()).unwrap(); + + println!("Generated from: {:?}", mushroom); +} #[derive(Parser, Debug, Clone)] #[command(version, about, long_about = None)] @@ -35,6 +58,10 @@ struct Args { /// Stalk diameter in millimeters. Zero for default. #[arg(long, default_value_t = 0)] cap_d: u32, + + /// Shape of the cap. + #[arg(long, default_value_t, value_enum)] + cap_style: mushroom::CapStyle, } fn mushroom_from_args(args: &Args) -> Mushroom { @@ -48,52 +75,33 @@ fn mushroom_from_args(args: &Args) -> Mushroom { args.height as f32 * 0.66 } }; - let stalk_d: f32 = if args.stalk_d != 0 { + let stalk_r: f32 = if args.stalk_d != 0 { args.stalk_d as f32 } else { args.height as f32 / 10. - }; + } / 2.0; // computer the diamter, and convert to radius. let cap_h: f32 = if args.cap_h != 0 { args.cap_h as f32 } else { args.height as f32 - stalk_h }; - let cap_d: f32 = if args.cap_d != 0 { + let cap_r: f32 = if args.cap_d != 0 { args.cap_d as f32 } else { args.height as f32 - }; + } / 2.0; // computer the diamter, and convert to radius. + + let cap_style = args.cap_style.clone(); Mushroom { stalk_h, - stalk_d, + stalk_r, cap_h, - cap_d, + cap_r, + cap_style, } } -fn main() { - let args = Args::parse(); - - // Define the mushroom. - let mushroom = mushroom_from_args(&args); - - // Render the mushroom. - // let mesh = render::cylinder(mushroom.stalk_h, mushroom.stalk_d, 128); - let mesh = render::mushroom(&mushroom); - - // Save output to file. - let mut file = OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(&args.output) - .unwrap(); - stl_io::write_stl(&mut file, mesh.iter()).unwrap(); - - println!("Generated from: {:?}", mushroom); -} - #[cfg(test)] mod tests { use super::*; @@ -102,9 +110,10 @@ mod tests { fn validate_mushroom_from_args( string_args: I, stalk_h: f32, - stalk_d: f32, + stalk_r: f32, cap_h: f32, - cap_d: f32, + cap_r: f32, + cap_style: mushroom::CapStyle, ) where I: IntoIterator, T: Into + Clone, @@ -115,31 +124,47 @@ mod tests { result, Mushroom { stalk_h, - stalk_d, + stalk_r, cap_h, - cap_d + cap_r, + cap_style, } ); } #[test] fn defaults() { - validate_mushroom_from_args(["prog"], 66., 10., 34., 100.); + validate_mushroom_from_args( + ["prog"], + 66.0, + 5.0, + 34.0, + 50.0, + mushroom::CapStyle::Cylinder, + ); } #[test] fn height_only() { - validate_mushroom_from_args(["prog", "--height", "200"], 132., 20., 68., 200.); + validate_mushroom_from_args( + ["prog", "--height", "200"], + 132.0, + 10.0, + 68.0, + 100.0, + mushroom::CapStyle::Cylinder, + ); } #[test] fn height_stalk_h() { validate_mushroom_from_args( ["prog", "--height", "200", "--stalk-h", "20"], - 20., - 20., - 180., - 200., + 20.0, + 10.0, + 180.0, + 100.0, + mushroom::CapStyle::Cylinder, ); } @@ -147,10 +172,11 @@ mod tests { fn height_cap_h() { validate_mushroom_from_args( ["prog", "--height", "200", "--cap-h", "20"], - 180., - 20., - 20., - 200., + 180.0, + 10.0, + 20.0, + 100.0, + mushroom::CapStyle::Cylinder, ); } @@ -169,11 +195,14 @@ mod tests { "40", "--cap-d", "50", + "--cap-style", + "dome", ], - 20., - 30., - 40., - 50., + 20.0, + 15.0, + 40.0, + 25.0, + mushroom::CapStyle::Dome, ); } } diff --git a/src/mushroom.rs b/src/mushroom.rs index e683a0b..068db84 100644 --- a/src/mushroom.rs +++ b/src/mushroom.rs @@ -1,14 +1,38 @@ +use serde_derive::Serialize; + +#[derive(clap::ValueEnum, Clone, Default, Debug, Serialize, PartialEq)] +pub enum CapStyle { + #[default] + Cylinder, + Dome, + Pointed, +} + #[derive(Debug, PartialEq)] pub struct Mushroom { /// Stalk height in millimeters. Zero for default. pub stalk_h: f32, /// Stalk diameter in millimeters. Zero for default. - pub stalk_d: f32, + pub stalk_r: f32, /// Cap height in millimeters. Zero for default. pub cap_h: f32, /// Stalk diameter in millimeters. Zero for default. - pub cap_d: f32, + pub cap_r: f32, + + pub cap_style: CapStyle, +} + +impl Mushroom { + /// Total height of the mushroom. + pub fn height(&self) -> f32 { + return self.stalk_h + self.cap_h; + } + + /// Maximum radius of the mushroom. + pub fn radius(&self) -> f32 { + return self.stalk_r.max(self.cap_r) / 2.0; + } } diff --git a/src/render.rs b/src/render.rs index 4b4aed1..b4f4045 100644 --- a/src/render.rs +++ b/src/render.rs @@ -1,6 +1,6 @@ use stl_io::{self, Normal, Triangle, Vertex}; -use crate::mushroom::Mushroom; +use crate::layers::{Layer, Layers}; fn new_triangle(p1: &Vertex, p2: &Vertex, p3: &Vertex) -> stl_io::Triangle { // Set Vector U to (Triangle.p2 minus Triangle.p1) @@ -40,10 +40,10 @@ fn vertex_on_circle(center: &Vertex, radius: f32, angle: f32) -> Vertex { return Vertex::new([x, y, z]); } -fn vertexes_on_circle(center: &Vertex, radius: f32, steps: u32) -> Vec { +fn vertexes_on_circle(layer: &Layer, steps: u32) -> Vec { return radian_steps(steps) .iter() - .map(|angle| vertex_on_circle(center, radius, *angle)) + .map(|angle| vertex_on_circle(&layer.center, layer.radius, *angle)) .collect(); } @@ -80,46 +80,35 @@ fn tube_top(center: &Vertex, edge: &Vec) -> Vec { return tube_base(¢er, &edge); } -pub fn mushroom(mushroom: &Mushroom) -> Vec { - // Max size of any flat surface in mm. - let resolution: f32 = 1.0; +pub fn render(layers: &Layers, resolution: f32) -> Vec { + // Top and Bottom must be a single point, and must be at least one middle layer. + assert!(!layers.layers.is_empty()); - // We always use the same number of diameter steps to ensure the number of circular vertices doesn't change, - // which might create small breaks in our model. - let diameter_steps = std::cmp::max( - (mushroom.cap_d * std::f32::consts::TAU * resolution) as u32, - 64, - ); + // How many vertexes per circle. IE: the resolution of the circle. + let circle_steps = + ((layers.max_radius() * std::f32::consts::TAU) / resolution).max(64.0) as u32; - let stalk_base_center = Vertex::new([0.0, 0.0, 0.0]); - let stalk_base_edge = vertexes_on_circle(&stalk_base_center, mushroom.stalk_d, diameter_steps); - let stalk_top_center = Vertex::new([ - stalk_base_center[0], - stalk_base_center[1], - stalk_base_center[2] + mushroom.stalk_h, - ]); - let stalk_top_edge = vertexes_on_circle(&stalk_top_center, mushroom.stalk_d, diameter_steps); - let cap_base_edge = vertexes_on_circle(&stalk_top_center, mushroom.cap_d, diameter_steps); - let cap_top_center = Vertex::new([ - stalk_top_center[0], - stalk_top_center[1], - stalk_top_center[2] + mushroom.cap_h, - ]); - let cap_top_edge = vertexes_on_circle(&cap_top_center, mushroom.cap_d, diameter_steps); + let mut layer_vertexes: Vec> = Vec::new(); - let mut result = Vec::new(); + for layer in &layers.layers { + layer_vertexes.push(vertexes_on_circle(&layer, circle_steps)); + } - result.append(&mut tube_base(&stalk_base_center, &stalk_base_edge)); - result.append(&mut tube_sides(&stalk_base_edge, &stalk_top_edge)); - result.append(&mut tube_sides(&stalk_top_edge, &cap_base_edge)); - result.append(&mut tube_sides(&cap_base_edge, &cap_top_edge)); - result.append(&mut tube_top(&cap_top_center, &cap_top_edge)); + let mut result: Vec = Vec::new(); + + result.append(&mut tube_base(&layers.base, &layer_vertexes[0])); + for i in 0..layer_vertexes.len() - 1 { + result.append(&mut tube_sides(&layer_vertexes[i], &layer_vertexes[i + 1])); + } + result.append(&mut tube_top(&layers.top, &layer_vertexes.last().unwrap())); return result; } #[cfg(test)] mod tests { + use approx::relative_eq; + use super::*; #[test] @@ -196,7 +185,13 @@ mod tests { #[test] fn vertexes_on_circle_basic() { - let val = vertexes_on_circle(&Vertex::new([0.0, 0.0, 0.0]), 1.0, 4); + let val = vertexes_on_circle( + &Layer { + center: Vertex::new([0.0, 0.0, 0.0]), + radius: 1.0, + }, + 4, + ); let expected = [ Vertex::new([1.0, 0.0, 0.0]), @@ -211,7 +206,13 @@ mod tests { #[test] fn vertexes_on_circle_complex() { - let val = vertexes_on_circle(&Vertex::new([4.0, 5.0, 6.0]), 3.0, 4); + let val = vertexes_on_circle( + &Layer { + center: Vertex::new([4.0, 5.0, 6.0]), + radius: 3.0, + }, + 4, + ); let expected = [ Vertex::new([7.0, 5.0, 6.0]), @@ -229,7 +230,13 @@ mod tests { // fn tube_base(center: &Vertex, edge: &Vec) -> Vec { let center = Vertex::new([0.0, 0.0, 0.0]); - let edge = vertexes_on_circle(¢er, 1.0, 4); + let edge = vertexes_on_circle( + &Layer { + center: center.clone(), + radius: 1.0, + }, + 4, + ); let triangles = tube_base(¢er, &edge); @@ -244,7 +251,13 @@ mod tests { #[test] fn tube_top_test() { let center = Vertex::new([0.0, 0.0, 0.0]); - let edge = vertexes_on_circle(¢er, 1.0, 4); + let edge = vertexes_on_circle( + &Layer { + center: center.clone(), + radius: 1.0, + }, + 4, + ); let triangles = tube_top(¢er, &edge); @@ -255,4 +268,38 @@ mod tests { assert_eq!(t.normal, Vertex::new([0.0, 0.0, 1.0])); } } + + #[test] + fn render_test() { + let layers = Layers { + base: Vertex::new([0.0, 0.0, 0.0]), + layers: [ + Layer { + center: Vertex::new([0.0, 0.0, 1.0]), + radius: 1.0, + }, + Layer { + center: Vertex::new([0.0, 0.0, 2.0]), + radius: 1.0, + }, + ] + .to_vec(), + top: Vertex::new([0.0, 0.0, 3.0]), + }; + + let mesh = render(&layers, 10.0); // Large resolution ensures minimum steps of 64. + + // 64 triangles for top, 128 for sides, 64 for bottom. + assert_eq!(mesh.len(), 64 + 128 + 64); + + for i in 0..mesh.len() { + if i < 64 { + assert!(mesh[i].normal[2] < 0.0) // Bottom triangles point down. + } else if i < (64 + 128) { + assert!(mesh[i].normal[2] == 0.0) // side triangles point out. + } else { + assert!(mesh[i].normal[2] > 0.0) // top triangles point up. + } + } + } }