diff --git a/Cargo.lock b/Cargo.lock index 61df796..3c017bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,287 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "anstream" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" + +[[package]] +name = "anstyle-parse" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "clap" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" + +[[package]] +name = "colorchoice" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + [[package]] name = "mushroom_generator" version = "0.1.0" +dependencies = [ + "approx", + "clap", + "stl_io", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "proc-macro2" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "stl_io" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d520eb03e632e666ac32f3c0cf018ac6dcd535ab0be1420937e2d896f44f7cf7" +dependencies = [ + "byteorder", + "float-cmp", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" diff --git a/Cargo.toml b/Cargo.toml index 66ce8a2..ce042aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,3 +4,6 @@ version = "0.1.0" edition = "2021" [dependencies] +approx = "0.5.1" +clap = { version = "4.5.4", features = ["derive"] } +stl_io = "0.7.0" diff --git a/src/main.rs b/src/main.rs index e7a11a9..303f7ea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,180 @@ +pub mod mushroom; +pub mod render; + +use self::mushroom::Mushroom; +use clap::Parser; +use std::fs::OpenOptions; + +#[macro_use] +extern crate approx; + +// TODO: Investigate https://github.com/virtualritz/polyhedron-ops. + +#[derive(Parser, Debug, Clone)] +#[command(version, about, long_about = None)] +struct Args { + /// File to save too. + #[arg(long, default_value = "mesh.stl")] + output: String, + + /// Height of total mushroom in millimeters. + #[arg(long, default_value_t = 100)] + height: u32, + + /// Stalk height in millimeters. Zero for default. + #[arg(long, default_value_t = 0)] + stalk_h: u32, + + /// Stalk diameter in millimeters. Zero for default. + #[arg(long, default_value_t = 0)] + stalk_d: u32, + + /// Cap height in millimeters. Zero for default. + #[arg(long, default_value_t = 0)] + cap_h: u32, + + /// Stalk diameter in millimeters. Zero for default. + #[arg(long, default_value_t = 0)] + cap_d: u32, +} + +fn mushroom_from_args(args: &Args) -> Mushroom { + // Default values are all based on height. + let stalk_h: f32 = if args.stalk_h != 0 { + args.stalk_h as f32 + } else { + if args.cap_h != 0 { + (args.height - args.cap_h) as f32 + } else { + args.height as f32 * 0.66 + } + }; + let stalk_d: f32 = if args.stalk_d != 0 { + args.stalk_d as f32 + } else { + args.height as f32 / 10. + }; + 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 { + args.cap_d as f32 + } else { + args.height as f32 + }; + + Mushroom { + stalk_h, + stalk_d, + cap_h, + cap_d, + } +} + fn main() { - println!("Hello, world!"); + 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); + + // 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!("From: {:?}", mushroom); + println!("Generated:\n{:?}", mesh); +} + +#[cfg(test)] +mod tests { + use super::*; + use std::ffi::OsString; + + fn validate_mushroom_from_args( + string_args: I, + stalk_h: f32, + stalk_d: f32, + cap_h: f32, + cap_d: f32, + ) where + I: IntoIterator, + T: Into + Clone, + { + let parsed_args = Args::parse_from(string_args); + let result = mushroom_from_args(&parsed_args); + assert_eq!( + result, + Mushroom { + stalk_h, + stalk_d, + cap_h, + cap_d + } + ); + } + + #[test] + fn defaults() { + validate_mushroom_from_args(["prog"], 66., 10., 34., 100.); + } + + #[test] + fn height_only() { + validate_mushroom_from_args(["prog", "--height", "200"], 132., 20., 68., 200.); + } + + #[test] + fn height_stalk_h() { + validate_mushroom_from_args( + ["prog", "--height", "200", "--stalk-h", "20"], + 20., + 20., + 180., + 200., + ); + } + + #[test] + fn height_cap_h() { + validate_mushroom_from_args( + ["prog", "--height", "200", "--cap-h", "20"], + 180., + 20., + 20., + 200., + ); + } + + #[test] + fn all() { + validate_mushroom_from_args( + [ + "prog", + "--height", + "10", + "--stalk-h", + "20", + "--stalk-d", + "30", + "--cap-h", + "40", + "--cap-d", + "50", + ], + 20., + 30., + 40., + 50., + ); + } } diff --git a/src/mushroom.rs b/src/mushroom.rs new file mode 100644 index 0000000..e683a0b --- /dev/null +++ b/src/mushroom.rs @@ -0,0 +1,14 @@ +#[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, + + /// Cap height in millimeters. Zero for default. + pub cap_h: f32, + + /// Stalk diameter in millimeters. Zero for default. + pub cap_d: f32, +} diff --git a/src/render.rs b/src/render.rs new file mode 100644 index 0000000..a5d43e6 --- /dev/null +++ b/src/render.rs @@ -0,0 +1,233 @@ +use stl_io::{self, Normal, Triangle, Vertex}; + +fn new_triangle(p1: &Vertex, p2: &Vertex, p3: &Vertex) -> stl_io::Triangle { + // Set Vector U to (Triangle.p2 minus Triangle.p1) + // Set Vector V to (Triangle.p3 minus Triangle.p1) + + // Set Normal.x to (multiply U.y by V.z) minus (multiply U.z by V.y) + // Set Normal.y to (multiply U.z by V.x) minus (multiply U.x by V.z) + // Set Normal.z to (multiply U.x by V.y) minus (multiply U.y by V.x) + + let u = [p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2]]; + let v = [p3[0] - p1[0], p3[1] - p1[1], p3[2] - p1[2]]; + + let n_x = (u[1] * v[2]) - (u[2] * v[1]); + let n_y = (u[2] * v[0]) - (u[0] * v[2]); + let n_z = (u[0] * v[1]) - (u[1] * v[0]); + + stl_io::Triangle { + normal: Normal::new([n_x, n_y, n_z]), + vertices: [p1.clone(), p2.clone(), p3.clone()], + } +} + +/// N steps around a circle, measured in radians. +fn radian_steps(steps: u32) -> Vec { + let counter = 0..steps; + let stepsize = std::f64::consts::TAU / steps as f64; + + return counter.map(|i| (i as f64 * stepsize) as f32).collect(); +} + +/// Find a point on the cirle at a given angle (in radians). +fn vertex_on_circle(center: &Vertex, radius: f32, angle: f32) -> Vertex { + let x = center[0] + (radius * angle.cos()); + let y = center[1] + (radius * angle.sin()); + let z = center[2]; + + return Vertex::new([x, y, z]); +} + +fn vertexes_on_circle(center: &Vertex, radius: f32, steps: u32) -> Vec { + return radian_steps(steps) + .iter() + .map(|angle| vertex_on_circle(center, radius, *angle)) + .collect(); +} + +fn tube_base(center: &Vertex, edge: &Vec) -> Vec { + let mut result = Vec::with_capacity(edge.len()); + + for i in 0..edge.len() { + result.push(new_triangle(¢er, &edge[(i + 1) % edge.len()], &edge[i])); + } + + return result; +} + +fn tube_sides(botoom_edge: &Vec, top_edge: &Vec) -> Vec { + assert_eq!(botoom_edge.len(), top_edge.len()); + + let mut result = Vec::with_capacity(2 * botoom_edge.len()); + + for i in 0..botoom_edge.len() { + let j = (i + 1) % botoom_edge.len(); + + result.push(new_triangle(&botoom_edge[i], &top_edge[j], &top_edge[i])); + result.push(new_triangle(&botoom_edge[i], &botoom_edge[j], &top_edge[j])); + } + + return result; +} + +fn tube_top(center: &Vertex, edge: &Vec) -> Vec { + // The top is the same, except the normals are reversed. We can do that by reversing the order of the vertexes. + let mut edge = edge.clone(); + edge.reverse(); + + return tube_base(¢er, &edge); +} + +pub fn cylinder(height: f32, radius: f32, steps: u32) -> Vec { + let base_center = Vertex::new([0.0, 0.0, 0.0]); + let base_edge = vertexes_on_circle(&base_center, radius, steps); + let top_center = Vertex::new([0.0, 0.0, height]); + let top_edge = vertexes_on_circle(&top_center, radius, steps); + + let mut result = Vec::new(); + + result.append(&mut tube_base(&base_center, &base_edge)); + result.append(&mut tube_sides(&base_edge, &top_edge)); + result.append(&mut tube_top(&top_center, &top_edge)); + + return result; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn new_triangle_normal() { + // Simple triangle + let a = Vertex::new([1.0, 0.0, 0.0]); + let b = Vertex::new([0.0, 1.0, 0.0]); + let c = Vertex::new([-1.0, 0.0, 0.0]); + + let tri = new_triangle(&a, &b, &c); + + assert_eq!(tri.normal, Normal::new([0.0, 0.0, 2.0])); + + // Same triangle, reverse order. + let tri = new_triangle(&c, &b, &a); + + assert_eq!(tri.normal, Normal::new([0.0, 0.0, -2.0])); + + // Uneven numbers. + let a = Vertex::new([2.0, 0.0, 0.0]); + let b = Vertex::new([0.0, 3.0, 0.0]); + let c = Vertex::new([-4.0, 0.0, 0.0]); + + let tri_big = new_triangle(&a, &b, &c); + + assert_eq!(tri_big.normal, Normal::new([0.0, 0.0, 18.0])); + } + + #[test] + fn radian_steps_basic() { + assert_eq!(radian_steps(0), []); + assert_eq!(radian_steps(1), [0.0]); + assert_eq!(radian_steps(2), [0.0, std::f32::consts::PI]); + assert_eq!( + radian_steps(3), + [ + 0.0, + std::f32::consts::PI * 2.0 / 3.0, + std::f32::consts::PI * 4.0 / 3.0 + ] + ); + assert_eq!( + radian_steps(4), + [ + 0.0, + std::f32::consts::PI * 0.5, + std::f32::consts::PI, + std::f32::consts::PI * 1.5, + ] + ); + } + + fn validate_vec_vertex_relative_eq(val: &Vec, expected: &Vec) { + let mut pass = true; + + if val.len() != expected.len() { + pass = false; + } + + for i in 0..val.len() { + for j in 0..3 { + if !relative_eq!(val[i][j], expected[i][j]) { + pass = false; + } + } + } + + if !pass { + // We re-compare here (which will always fail) to get the friendly error message, + // which I can't figure out how to generate directly. + assert_eq!(val, expected); + } + } + + #[test] + fn vertexes_on_circle_basic() { + let val = vertexes_on_circle(&Vertex::new([0.0, 0.0, 0.0]), 1.0, 4); + + let expected = [ + Vertex::new([1.0, 0.0, 0.0]), + Vertex::new([0.0, 1.0, 0.0]), + Vertex::new([-1.0, 0.0, 0.0]), + Vertex::new([0.0, -1.0, 0.0]), + ] + .to_vec(); + + validate_vec_vertex_relative_eq(&val, &expected); + } + + #[test] + fn vertexes_on_circle_complex() { + let val = vertexes_on_circle(&Vertex::new([4.0, 5.0, 6.0]), 3.0, 4); + + let expected = [ + Vertex::new([7.0, 5.0, 6.0]), + Vertex::new([4.0, 8.0, 6.0]), + Vertex::new([1.0, 5.0, 6.0]), + Vertex::new([4.0, 2.0, 6.0]), + ] + .to_vec(); + + validate_vec_vertex_relative_eq(&val, &expected); + } + + #[test] + fn tube_base_test() { + // 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 triangles = tube_base(¢er, &edge); + + assert_eq!(triangles.len(), edge.len()); + + for t in triangles { + // All of these triangles should have the same normal (down). + assert_eq!(t.normal, Vertex::new([0.0, 0.0, -1.0])); + } + } + + #[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 triangles = tube_top(¢er, &edge); + + assert_eq!(triangles.len(), edge.len()); + + for t in triangles { + // All of these triangles should have the same normal (up). + assert_eq!(t.normal, Vertex::new([0.0, 0.0, 1.0])); + } + } +}