Initial checkpoint.

The system is capable of parsing it's command
line, and of generating a mesh representing
the mushroom's stalk.
This commit is contained in:
dongar committed 2024-06-09 17:36:19 -05:00
1 parent 6be55d2ebe
commit b49610be92
5 files changed
+709 -1

No files matched your search

+178 -1
View File
@@ -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<I, T>(
string_args: I,
stalk_h: f32,
stalk_d: f32,
cap_h: f32,
cap_d: f32,
) where
I: IntoIterator<Item = T>,
T: Into<OsString> + 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.,
);
}
}
+14
View File
@@ -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,
}
+233
View File
@@ -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<f32> {
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<Vertex> {
return radian_steps(steps)
.iter()
.map(|angle| vertex_on_circle(center, radius, *angle))
.collect();
}
fn tube_base(center: &Vertex, edge: &Vec<Vertex>) -> Vec<Triangle> {
let mut result = Vec::with_capacity(edge.len());
for i in 0..edge.len() {
result.push(new_triangle(&center, &edge[(i + 1) % edge.len()], &edge[i]));
}
return result;
}
fn tube_sides(botoom_edge: &Vec<Vertex>, top_edge: &Vec<Vertex>) -> Vec<Triangle> {
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<Vertex>) -> Vec<Triangle> {
// 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(&center, &edge);
}
pub fn cylinder(height: f32, radius: f32, steps: u32) -> Vec<Triangle> {
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<Vertex>, expected: &Vec<Vertex>) {
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<Vertex>) -> Vec<Triangle> {
let center = Vertex::new([0.0, 0.0, 0.0]);
let edge = vertexes_on_circle(&center, 1.0, 4);
let triangles = tube_base(&center, &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(&center, 1.0, 4);
let triangles = tube_top(&center, &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]));
}
}
}