diff --git a/.gitignore b/.gitignore index a225919..d39ee17 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /*output *.goo *.stl +codebook.toml diff --git a/Cargo.lock b/Cargo.lock index cf82bd8..8588691 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4491,11 +4491,13 @@ name = "slicer" version = "0.1.0" dependencies = [ "anyhow", + "clap", "common", "criterion", "goo_format", "image 0.25.1", "nalgebra 0.32.6", + "num-traits", "obj-rs", "ordered-float", "parking_lot", diff --git a/Cargo.toml b/Cargo.toml index 664a50a..46be9f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ markdown = "0.3.0" md5 = "0.7.0" nalgebra = { version = "0.32.6", features = ["serde-serialize"] } notify-rust = "4.11.0" +num-traits = "0.2.19" obj-rs = "0.7.1" open = "5.3.0" ordered-float = "4.2.0" diff --git a/slicer/Cargo.toml b/slicer/Cargo.toml index a90315e..7d2cf92 100644 --- a/slicer/Cargo.toml +++ b/slicer/Cargo.toml @@ -5,8 +5,10 @@ edition = "2021" [dependencies] anyhow.workspace = true +clap.workspace = true image.workspace = true nalgebra.workspace = true +num-traits.workspace = true obj-rs.workspace = true ordered-float.workspace = true parking_lot.workspace = true diff --git a/slicer/src/args.rs b/slicer/src/args.rs new file mode 100644 index 0000000..8ebfd4c --- /dev/null +++ b/slicer/src/args.rs @@ -0,0 +1,136 @@ +use std::{path::PathBuf, str::FromStr}; + +use anyhow::{Context, Ok, Result}; +use clap::Parser; +use common::{ + config::{ExposureConfig, SliceConfig}, + format::Format, +}; +use nalgebra::{ArrayStorage, Const, Matrix, Scalar, Vector2, Vector3, U1}; +use num_traits::Zero; + +#[derive(Debug, Parser)] +/// mslicer command line interface. +pub struct Args { + #[arg(long, default_value = "11520, 5120", value_parser = vector_value_parser::, )] + /// Resolution of the printer mask display in pixels. + pub platform_resolution: Vector2, + #[arg(long, default_value = "218.88, 122.904, 260.0", value_parser = vector_value_parser::)] + /// Size of the printer display / platform in mm. + pub platform_size: Vector3, + #[arg(long, default_value_t = 0.05)] + /// Layer height in mm. + pub layer_height: f32, + #[arg(long, default_value_t = 3)] + /// Number of 'first layers'. These are layers that obey the --first- + /// exposure config flags. + pub first_layers: u32, + #[arg(long, default_value_t = 10)] + /// Number of transition layers. These are layers that interpolate from the + /// first layer config to the default config. + pub transition_layers: u32, + + #[arg(long, default_value_t = 3.0)] + /// Layer exposure time in seconds. + pub exposure_time: f32, + #[arg(long, default_value_t = 5.0)] + /// Distance to lift the platform after exposing each regular layer, in mm. + pub lift_distance: f32, + #[arg(long, default_value_t = 65.0)] + /// The speed to lift the platform after exposing each regular layer, in + /// mm/min. + pub lift_speed: f32, + #[arg(long, default_value_t = 150.0)] + /// The speed to retract (move down) the platform after exposing each + /// regular layer, in mm/min. + pub retract_speed: f32, + + #[arg(long, default_value_t = 30.0)] + /// First layer exposure time in seconds. + pub first_exposure_time: f32, + #[arg(long, default_value_t = 5.0)] + /// Distance to lift the platform after exposing each first layer, in mm. + pub first_lift_distance: f32, + #[arg(long, default_value_t = 65.0)] + /// The speed to lift the platform after exposing each first layer, in + /// mm/min. + pub first_lift_speed: f32, + #[arg(long, default_value_t = 150.0)] + /// The speed to retract (move down) the platform after exposing each first + /// layer, in mm/min. + pub first_retract_speed: f32, + + #[command(flatten)] + pub model: ModelArgs, + + /// File to save sliced result to. Currently only .goo files can be + /// generated. + pub output: PathBuf, +} + +#[derive(clap::Args, Debug)] +#[group(required = true)] +pub struct ModelArgs { + /// Path to a .stl or .obj file + pub mesh: PathBuf, + + #[arg(long, default_value = "0, 0, 0", value_parser = vector_value_parser::)] + /// Location of the bottom center of model bounding box. The origin is the + /// center of the build plate. + pub position: Vector3, + + #[arg(long, default_value = "0, 0, 0", value_parser = vector_value_parser::)] + /// Rotation of the model in degrees, pitch, roll, yaw. + pub rotation: Vector3, + + #[arg(long, default_value = "1, 1, 1", value_parser = vector_value_parser::)] + /// Scale of the model along the X, Y, and Z axes. + pub scale: Vector3, +} + +impl Args { + pub fn slice_config(&self) -> SliceConfig { + SliceConfig { + format: Format::Goo, + platform_resolution: self.platform_resolution, + platform_size: self.platform_size, + slice_height: self.layer_height, + exposure_config: ExposureConfig { + exposure_time: self.exposure_time, + lift_distance: self.lift_distance, + lift_speed: self.lift_speed, + retract_distance: self.lift_distance, + retract_speed: self.retract_speed, + }, + first_exposure_config: ExposureConfig { + exposure_time: self.first_exposure_time, + lift_distance: self.first_lift_distance, + lift_speed: self.first_lift_speed, + retract_distance: self.first_lift_distance, + retract_speed: self.first_retract_speed, + }, + first_layers: self.first_layers, + transition_layers: self.transition_layers, + } + } +} + +pub fn vector_value_parser( + raw: &str, +) -> Result, U1, ArrayStorage>> +where + T: FromStr + Scalar + Zero, + T::Err: Send + Sync + std::error::Error, +{ + let mut vec = Matrix::, U1, ArrayStorage>::zeros(); + + let mut parts = raw.splitn(N, ','); + for i in 0..N { + let element = parts.next().context("Missing vector element")?.trim(); + vec[i] = element + .parse() + .context("Can't convert element from string")?; + } + + Ok(vec) +} diff --git a/slicer/src/main.rs b/slicer/src/main.rs index 5248f1e..2b02ff7 100644 --- a/slicer/src/main.rs +++ b/slicer/src/main.rs @@ -6,60 +6,46 @@ use std::{ }; use anyhow::Result; -use nalgebra::{Vector2, Vector3}; +use args::Args; +use clap::Parser; +use nalgebra::Vector3; -use common::{ - config::{ExposureConfig, SliceConfig}, - format::Format, - serde::DynamicSerializer, -}; +use common::serde::DynamicSerializer; use goo_format::{File as GooFile, LayerEncoder}; -use slicer::{mesh::load_mesh, slicer::Slicer, Pos}; +use slicer::{mesh::load_mesh, slicer::Slicer}; + +mod args; fn main() -> Result<()> { - const FILE_PATH: &str = "teapot.stl"; - const OUTPUT_PATH: &str = "output.goo"; + let args = Args::parse(); + let slice_config = args.slice_config(); - let slice_config = SliceConfig { - format: Format::Goo, + let ext = args.model.mesh.extension().unwrap().to_string_lossy(); + let file = File::open(&args.model.mesh)?; - platform_resolution: Vector2::new(11_520, 5_120), - platform_size: Vector3::new(218.88, 122.904, 260.0), - slice_height: 0.05, - - exposure_config: ExposureConfig { - exposure_time: 3.0, - ..Default::default() - }, - first_exposure_config: ExposureConfig { - exposure_time: 50.0, - ..Default::default() - }, - first_layers: 10, - transition_layers: 10, - }; - - let file = File::open(FILE_PATH)?; let mut buf = BufReader::new(file); - let mut mesh = load_mesh(&mut buf, "stl")?; - let (min, max) = mesh.bounds(); + let mut mesh = load_mesh(&mut buf, &ext)?; // Scale the model into printer-space (mm => px) - let real_scale = 1.0; - mesh.set_scale(Pos::new( - real_scale / slice_config.platform_size.x * slice_config.platform_resolution.x as f32, - real_scale / slice_config.platform_size.y * slice_config.platform_resolution.y as f32, - real_scale, - )); + mesh.set_scale(args.model.scale.component_div(&Vector3::new( + slice_config.platform_size.x * slice_config.platform_resolution.x as f32, + slice_config.platform_size.y * slice_config.platform_resolution.y as f32, + 1.0, + ))); + + mesh.set_rotation(args.model.rotation); // Center the model + let (min, max) = mesh.bounds(); let center = slice_config.platform_resolution / 2; let mesh_center = (min + max) / 2.0; - mesh.set_position(Vector3::new( - center.x as f32 - mesh_center.x, - center.y as f32 - mesh_center.y, - mesh.position().z - 0.05, - )); + mesh.set_position( + Vector3::new( + center.x as f32 - mesh_center.x, + center.y as f32 - mesh_center.y, + mesh.position().z - 0.05, + ) + args.model.position, + ); println!( "Loaded mesh. {{ vert: {}, face: {} }}", @@ -90,7 +76,7 @@ fn main() -> Result<()> { // Once slicing is complete write to a .goo file let mut serializer = DynamicSerializer::new(); goo.join().unwrap().serialize(&mut serializer); - fs::write(OUTPUT_PATH, serializer.into_inner())?; + fs::write(args.output, serializer.into_inner())?; println!("\nDone. Elapsed: {:.1}s", now.elapsed().as_secs_f32());