Support loading .obj and .mtl files
This commit is contained in:
11
Cargo.lock
generated
11
Cargo.lock
generated
@@ -2581,6 +2581,16 @@ dependencies = [
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "obj-rs"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a502182538a65adc7e3843f820eb5a4bd3ff395017e0ba6e3ccad404e9ea1da3"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc"
|
||||
version = "0.2.7"
|
||||
@@ -3449,6 +3459,7 @@ dependencies = [
|
||||
"common",
|
||||
"goo_format",
|
||||
"nalgebra",
|
||||
"obj-rs",
|
||||
"ordered-float",
|
||||
"rayon",
|
||||
"stl_io",
|
||||
|
4
TODO.md
4
TODO.md
@@ -26,4 +26,6 @@
|
||||
- [ ] Fix slicing rotated objects
|
||||
- [ ] 0-pad slice preview layer display so its width doesn't change as you move the slider
|
||||
- [ ] Preload layer 1 in slice preview
|
||||
- [ ] Better camera movement (left click to orbit, right click to change orbit point)
|
||||
- [x] Better camera movement (left click to orbit, right click to change orbit point)
|
||||
- [ ] Don't crash when object is outside bounds
|
||||
- [ ] Change centerpoint of model to be at z=0
|
@@ -10,88 +10,86 @@ use crate::{
|
||||
|
||||
pub fn ui(app: &mut App, ctx: &Context, _frame: &mut Frame) {
|
||||
if let Some(result) = app.slice_result.lock().unwrap().as_mut() {
|
||||
Window::new("Slice Preview")
|
||||
.resizable([true, true])
|
||||
.show(ctx, move |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(
|
||||
Slider::new(&mut result.slice_preview_layer, 1..=result.goo.layers.len())
|
||||
.vertical()
|
||||
.show_value(false),
|
||||
Window::new("Slice Preview").show(ctx, move |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(
|
||||
Slider::new(&mut result.slice_preview_layer, 1..=result.goo.layers.len())
|
||||
.vertical()
|
||||
.show_value(false),
|
||||
);
|
||||
|
||||
result.slice_preview_layer =
|
||||
result.slice_preview_layer.clamp(1, result.goo.layers.len());
|
||||
let new_preview = if result.last_preview_layer != result.slice_preview_layer {
|
||||
result.last_preview_layer = result.slice_preview_layer;
|
||||
let (width, height) = (
|
||||
result.goo.header.x_resolution as u32,
|
||||
result.goo.header.y_resolution as u32,
|
||||
);
|
||||
|
||||
result.slice_preview_layer =
|
||||
result.slice_preview_layer.clamp(1, result.goo.layers.len());
|
||||
let new_preview = if result.last_preview_layer != result.slice_preview_layer {
|
||||
result.last_preview_layer = result.slice_preview_layer;
|
||||
let (width, height) = (
|
||||
result.goo.header.x_resolution as u32,
|
||||
result.goo.header.y_resolution as u32,
|
||||
);
|
||||
let layer_data = &result.goo.layers[result.slice_preview_layer - 1].data;
|
||||
let decoder = LayerDecoder::new(layer_data);
|
||||
|
||||
let layer_data = &result.goo.layers[result.slice_preview_layer - 1].data;
|
||||
let decoder = LayerDecoder::new(layer_data);
|
||||
|
||||
let mut image = vec![0; (width * height) as usize];
|
||||
let mut pixel = 0;
|
||||
for run in decoder {
|
||||
for _ in 0..run.length {
|
||||
image[pixel] = run.value;
|
||||
pixel += 1;
|
||||
}
|
||||
let mut image = vec![0; (width * height) as usize];
|
||||
let mut pixel = 0;
|
||||
for run in decoder {
|
||||
for _ in 0..run.length {
|
||||
image[pixel] = run.value;
|
||||
pixel += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Some(image)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Some(image)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
result.preview_scale = result.preview_scale.max(0.1);
|
||||
egui::Frame::canvas(ui.style()).show(ui, |ui| {
|
||||
let available_size = ui.available_size();
|
||||
let (rect, _response) = ui.allocate_exact_size(
|
||||
Vec2::new(
|
||||
available_size.x,
|
||||
available_size.x / result.goo.header.x_resolution as f32
|
||||
* result.goo.header.y_resolution as f32,
|
||||
),
|
||||
Sense::drag(),
|
||||
);
|
||||
let callback = Callback::new_paint_callback(
|
||||
rect,
|
||||
SlicePreviewRenderCallback {
|
||||
dimensions: Vector2::new(
|
||||
result.goo.header.x_resolution as u32,
|
||||
result.goo.header.y_resolution as u32,
|
||||
),
|
||||
offset: result.preview_offset,
|
||||
scale: result.preview_scale,
|
||||
new_preview,
|
||||
},
|
||||
);
|
||||
ui.painter().add(callback);
|
||||
});
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(
|
||||
DragValue::new(&mut result.slice_preview_layer)
|
||||
.clamp_range(1..=result.goo.layers.len())
|
||||
.custom_formatter(|n, _| format!("{}/{}", n, result.goo.layers.len())),
|
||||
result.preview_scale = result.preview_scale.max(0.1);
|
||||
egui::Frame::canvas(ui.style()).show(ui, |ui| {
|
||||
let available_size = ui.available_size();
|
||||
let (rect, _response) = ui.allocate_exact_size(
|
||||
Vec2::new(
|
||||
available_size.x,
|
||||
available_size.x / result.goo.header.x_resolution as f32
|
||||
* result.goo.header.y_resolution as f32,
|
||||
),
|
||||
Sense::drag(),
|
||||
);
|
||||
result.slice_preview_layer +=
|
||||
ui.button(RichText::new("+").monospace()).clicked() as usize;
|
||||
result.slice_preview_layer -=
|
||||
ui.button(RichText::new("-").monospace()).clicked() as usize;
|
||||
|
||||
ui.separator();
|
||||
ui.label("Offset");
|
||||
vec2_dragger(ui, result.preview_offset.as_mut(), |x| x);
|
||||
|
||||
ui.separator();
|
||||
ui.label("Scale");
|
||||
ui.add(DragValue::new(&mut result.preview_scale));
|
||||
let callback = Callback::new_paint_callback(
|
||||
rect,
|
||||
SlicePreviewRenderCallback {
|
||||
dimensions: Vector2::new(
|
||||
result.goo.header.x_resolution as u32,
|
||||
result.goo.header.y_resolution as u32,
|
||||
),
|
||||
offset: result.preview_offset,
|
||||
scale: result.preview_scale,
|
||||
new_preview,
|
||||
},
|
||||
);
|
||||
ui.painter().add(callback);
|
||||
});
|
||||
});
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(
|
||||
DragValue::new(&mut result.slice_preview_layer)
|
||||
.clamp_range(1..=result.goo.layers.len())
|
||||
.custom_formatter(|n, _| format!("{}/{}", n, result.goo.layers.len())),
|
||||
);
|
||||
result.slice_preview_layer +=
|
||||
ui.button(RichText::new("+").monospace()).clicked() as usize;
|
||||
result.slice_preview_layer -=
|
||||
ui.button(RichText::new("-").monospace()).clicked() as usize;
|
||||
|
||||
ui.separator();
|
||||
ui.label("Offset");
|
||||
vec2_dragger(ui, result.preview_offset.as_mut(), |x| x);
|
||||
|
||||
ui.separator();
|
||||
ui.label("Scale");
|
||||
ui.add(DragValue::new(&mut result.preview_scale));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
use std::thread;
|
||||
use std::{fs::File, io::BufReader, thread};
|
||||
|
||||
use clone_macro::clone;
|
||||
use eframe::Frame;
|
||||
@@ -21,11 +21,19 @@ pub fn ui(app: &mut App, ctx: &Context, _frame: &mut Frame) {
|
||||
ui.menu_button("🖹 File", |ui| {
|
||||
if ui.button("Import Model").clicked() {
|
||||
// TODO: async
|
||||
if let Some(path) = FileDialog::new().add_filter("STL", &["stl"]).pick_file() {
|
||||
if let Some(path) = FileDialog::new()
|
||||
.add_filter("Mesh", &["stl", "obj"])
|
||||
.pick_file()
|
||||
{
|
||||
let name = path.file_name().unwrap().to_str().unwrap().to_string();
|
||||
let ext = path.extension();
|
||||
let format = ext
|
||||
.expect("Selected file has no extension")
|
||||
.to_string_lossy();
|
||||
|
||||
let mut file = std::fs::File::open(path).unwrap();
|
||||
let model = slicer::mesh::load_mesh(&mut file, "stl").unwrap();
|
||||
let file = File::open(&path).unwrap();
|
||||
let mut buf = BufReader::new(file);
|
||||
let model = slicer::mesh::load_mesh(&mut buf, &format).unwrap();
|
||||
|
||||
app.meshes
|
||||
.write()
|
||||
|
@@ -14,3 +14,4 @@ stl_io = "0.7.0"
|
||||
|
||||
common = { path = "../common" }
|
||||
goo_format = { path = "../goo_format" }
|
||||
obj-rs = "0.7.1"
|
||||
|
@@ -1,6 +1,6 @@
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::{stdout, Write},
|
||||
io::{stdout, BufReader, Write},
|
||||
thread,
|
||||
time::Instant,
|
||||
};
|
||||
@@ -36,8 +36,9 @@ fn main() -> Result<()> {
|
||||
first_layers: 10,
|
||||
};
|
||||
|
||||
let mut file = File::open(FILE_PATH)?;
|
||||
let mut mesh = load_mesh(&mut file, "stl")?;
|
||||
let file = File::open(FILE_PATH)?;
|
||||
let mut buf = BufReader::new(file);
|
||||
let mut mesh = load_mesh(&mut buf, "stl")?;
|
||||
let (min, max) = mesh.minmax_point();
|
||||
|
||||
let real_scale = 1.0;
|
||||
|
@@ -1,7 +1,8 @@
|
||||
use std::io::{Read, Seek};
|
||||
use std::io::{BufRead, Seek};
|
||||
|
||||
use anyhow::Result;
|
||||
use nalgebra::Matrix4;
|
||||
use obj::{Obj, Position};
|
||||
|
||||
use crate::Pos;
|
||||
|
||||
@@ -137,11 +138,11 @@ impl Mesh {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_mesh<T: Read + Seek>(reader: &mut T, format: &str) -> Result<Mesh> {
|
||||
match format {
|
||||
pub fn load_mesh<T: BufRead + Seek>(reader: &mut T, format: &str) -> Result<Mesh> {
|
||||
Ok(match format {
|
||||
"stl" => {
|
||||
let model = stl_io::read_stl(reader)?;
|
||||
Ok(Mesh {
|
||||
Mesh {
|
||||
vertices: model
|
||||
.vertices
|
||||
.iter()
|
||||
@@ -160,10 +161,26 @@ pub fn load_mesh<T: Read + Seek>(reader: &mut T, format: &str) -> Result<Mesh> {
|
||||
.collect(),
|
||||
..Default::default()
|
||||
}
|
||||
.center_vertices())
|
||||
}
|
||||
_ => Err(anyhow::anyhow!("Unsupported format: {}", format)),
|
||||
"obj" | "mtl" => {
|
||||
let model: Obj<Position> = obj::load_obj(reader)?;
|
||||
Mesh {
|
||||
vertices: model
|
||||
.vertices
|
||||
.iter()
|
||||
.map(|v| Pos::new(v.position[0], v.position[1], v.position[2]))
|
||||
.collect(),
|
||||
faces: model
|
||||
.indices
|
||||
.chunks_exact(3)
|
||||
.map(|i| [i[0] as u32, i[1] as u32, i[2] as u32])
|
||||
.collect(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
_ => return Err(anyhow::anyhow!("Unsupported format: {}", format)),
|
||||
}
|
||||
.center_vertices())
|
||||
}
|
||||
|
||||
impl Default for Mesh {
|
||||
|
Reference in New Issue
Block a user