Start on slicer CLI

This commit is contained in:
Connor Slade
2025-04-04 15:11:02 -04:00
parent bd5a1aaae0
commit ccc7424992
6 changed files with 170 additions and 42 deletions

1
.gitignore vendored
View File

@@ -2,3 +2,4 @@
/*output /*output
*.goo *.goo
*.stl *.stl
codebook.toml

2
Cargo.lock generated
View File

@@ -4491,11 +4491,13 @@ name = "slicer"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap",
"common", "common",
"criterion", "criterion",
"goo_format", "goo_format",
"image 0.25.1", "image 0.25.1",
"nalgebra 0.32.6", "nalgebra 0.32.6",
"num-traits",
"obj-rs", "obj-rs",
"ordered-float", "ordered-float",
"parking_lot", "parking_lot",

View File

@@ -29,6 +29,7 @@ markdown = "0.3.0"
md5 = "0.7.0" md5 = "0.7.0"
nalgebra = { version = "0.32.6", features = ["serde-serialize"] } nalgebra = { version = "0.32.6", features = ["serde-serialize"] }
notify-rust = "4.11.0" notify-rust = "4.11.0"
num-traits = "0.2.19"
obj-rs = "0.7.1" obj-rs = "0.7.1"
open = "5.3.0" open = "5.3.0"
ordered-float = "4.2.0" ordered-float = "4.2.0"

View File

@@ -5,8 +5,10 @@ edition = "2021"
[dependencies] [dependencies]
anyhow.workspace = true anyhow.workspace = true
clap.workspace = true
image.workspace = true image.workspace = true
nalgebra.workspace = true nalgebra.workspace = true
num-traits.workspace = true
obj-rs.workspace = true obj-rs.workspace = true
ordered-float.workspace = true ordered-float.workspace = true
parking_lot.workspace = true parking_lot.workspace = true

136
slicer/src/args.rs Normal file
View File

@@ -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::<u32, 2>, )]
/// Resolution of the printer mask display in pixels.
pub platform_resolution: Vector2<u32>,
#[arg(long, default_value = "218.88, 122.904, 260.0", value_parser = vector_value_parser::<f32, 3>)]
/// Size of the printer display / platform in mm.
pub platform_size: Vector3<f32>,
#[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::<f32, 3>)]
/// Location of the bottom center of model bounding box. The origin is the
/// center of the build plate.
pub position: Vector3<f32>,
#[arg(long, default_value = "0, 0, 0", value_parser = vector_value_parser::<f32, 3>)]
/// Rotation of the model in degrees, pitch, roll, yaw.
pub rotation: Vector3<f32>,
#[arg(long, default_value = "1, 1, 1", value_parser = vector_value_parser::<f32, 3>)]
/// Scale of the model along the X, Y, and Z axes.
pub scale: Vector3<f32>,
}
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<T, const N: usize>(
raw: &str,
) -> Result<Matrix<T, Const<N>, U1, ArrayStorage<T, N, 1>>>
where
T: FromStr + Scalar + Zero,
T::Err: Send + Sync + std::error::Error,
{
let mut vec = Matrix::<T, Const<N>, U1, ArrayStorage<T, N, 1>>::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)
}

View File

@@ -6,60 +6,46 @@ use std::{
}; };
use anyhow::Result; use anyhow::Result;
use nalgebra::{Vector2, Vector3}; use args::Args;
use clap::Parser;
use nalgebra::Vector3;
use common::{ use common::serde::DynamicSerializer;
config::{ExposureConfig, SliceConfig},
format::Format,
serde::DynamicSerializer,
};
use goo_format::{File as GooFile, LayerEncoder}; 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<()> { fn main() -> Result<()> {
const FILE_PATH: &str = "teapot.stl"; let args = Args::parse();
const OUTPUT_PATH: &str = "output.goo"; let slice_config = args.slice_config();
let slice_config = SliceConfig { let ext = args.model.mesh.extension().unwrap().to_string_lossy();
format: Format::Goo, 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 buf = BufReader::new(file);
let mut mesh = load_mesh(&mut buf, "stl")?; let mut mesh = load_mesh(&mut buf, &ext)?;
let (min, max) = mesh.bounds();
// Scale the model into printer-space (mm => px) // Scale the model into printer-space (mm => px)
let real_scale = 1.0; mesh.set_scale(args.model.scale.component_div(&Vector3::new(
mesh.set_scale(Pos::new( slice_config.platform_size.x * slice_config.platform_resolution.x as f32,
real_scale / slice_config.platform_size.x * slice_config.platform_resolution.x as f32, slice_config.platform_size.y * slice_config.platform_resolution.y as f32,
real_scale / slice_config.platform_size.y * slice_config.platform_resolution.y as f32, 1.0,
real_scale, )));
));
mesh.set_rotation(args.model.rotation);
// Center the model // Center the model
let (min, max) = mesh.bounds();
let center = slice_config.platform_resolution / 2; let center = slice_config.platform_resolution / 2;
let mesh_center = (min + max) / 2.0; let mesh_center = (min + max) / 2.0;
mesh.set_position(Vector3::new( mesh.set_position(
center.x as f32 - mesh_center.x, Vector3::new(
center.y as f32 - mesh_center.y, center.x as f32 - mesh_center.x,
mesh.position().z - 0.05, center.y as f32 - mesh_center.y,
)); mesh.position().z - 0.05,
) + args.model.position,
);
println!( println!(
"Loaded mesh. {{ vert: {}, face: {} }}", "Loaded mesh. {{ vert: {}, face: {} }}",
@@ -90,7 +76,7 @@ fn main() -> Result<()> {
// Once slicing is complete write to a .goo file // Once slicing is complete write to a .goo file
let mut serializer = DynamicSerializer::new(); let mut serializer = DynamicSerializer::new();
goo.join().unwrap().serialize(&mut serializer); 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()); println!("\nDone. Elapsed: {:.1}s", now.elapsed().as_secs_f32());