From f08bcfe9f3bc03162c12f32a347c0c5022553909 Mon Sep 17 00:00:00 2001 From: Connor Slade Date: Sun, 1 Sep 2024 17:42:36 -0400 Subject: [PATCH] Implement load and saving projects --- Cargo.lock | 11 +++ Cargo.toml | 1 + TODO.md | 5 ++ common/Cargo.toml | 1 + common/src/config.rs | 9 ++- common/src/lib.rs | 1 + common/src/serde_impls.rs | 88 ++++++++++++++++++++ mslicer/Cargo.toml | 1 + mslicer/src/app.rs | 22 ++++- mslicer/src/main.rs | 1 + mslicer/src/project/mesh.rs | 108 +++++++++++++++++++++++++ mslicer/src/project/mod.rs | 69 ++++++++++++++++ mslicer/src/render/rendered_mesh.rs | 10 +++ mslicer/src/windows/models.rs | 14 ++-- mslicer/src/windows/slice_operation.rs | 68 +++++++++------- mslicer/src/windows/top_bar.rs | 68 +++++++++++++++- 16 files changed, 435 insertions(+), 42 deletions(-) create mode 100644 common/src/serde_impls.rs create mode 100644 mslicer/src/project/mesh.rs create mode 100644 mslicer/src/project/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 096f5bc..bf0d22e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -699,6 +699,15 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bit-set" version = "0.5.3" @@ -1157,6 +1166,7 @@ version = "0.1.0" dependencies = [ "nalgebra 0.32.6", "rand 0.8.5", + "serde", ] [[package]] @@ -2864,6 +2874,7 @@ name = "mslicer" version = "0.1.0" dependencies = [ "anyhow", + "bincode", "bytemuck", "chrono", "clone-macro", diff --git a/Cargo.toml b/Cargo.toml index 63a28de..ff50b94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = ["common", "goo_format", "mslicer", "remote_send", "slicer"] [workspace.dependencies] afire = "3.0.0-alpha.3" anyhow = "1.0.86" +bincode = "1.3.3" bitflags = "2.5.0" bytemuck = "1.16.1" chrono = "0.4.38" diff --git a/TODO.md b/TODO.md index 09486ee..d9f1089 100644 --- a/TODO.md +++ b/TODO.md @@ -73,3 +73,8 @@ - [x] Allow dragging slice operation preview - [x] Fix rotation on Z axis - [x] Fix Z translation being doubled +- [ ] Merge goo_format changes into goo crate +- [ ] Implement .ctb format (see ) +- [ ] Undo / Redo +- [x] Close file menu if button clicked +- [ ] Allow dragging in project to load them? diff --git a/common/Cargo.toml b/common/Cargo.toml index f112d1a..6992e11 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -6,3 +6,4 @@ edition = "2021" [dependencies] nalgebra.workspace = true rand.workspace = true +serde.workspace = true diff --git a/common/src/config.rs b/common/src/config.rs index 731a553..e8c7dae 100644 --- a/common/src/config.rs +++ b/common/src/config.rs @@ -1,8 +1,13 @@ use nalgebra::{Vector2, Vector3}; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug)] +use crate::serde_impls::vector3f; + +#[derive(Serialize, Deserialize, Clone, Debug)] pub struct SliceConfig { + #[serde(skip)] pub platform_resolution: Vector2, + #[serde(with = "vector3f")] pub platform_size: Vector3, pub slice_height: f32, @@ -12,7 +17,7 @@ pub struct SliceConfig { pub transition_layers: u32, } -#[derive(Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug)] pub struct ExposureConfig { pub exposure_time: f32, pub lift_distance: f32, diff --git a/common/src/lib.rs b/common/src/lib.rs index 39f7f51..97d4356 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -3,3 +3,4 @@ pub mod image; pub mod misc; pub mod oklab; pub mod serde; +pub mod serde_impls; diff --git a/common/src/serde_impls.rs b/common/src/serde_impls.rs new file mode 100644 index 0000000..06957d8 --- /dev/null +++ b/common/src/serde_impls.rs @@ -0,0 +1,88 @@ +use nalgebra::Vector3; +use serde::{Deserialize, Serialize}; + +pub mod vector3f { + use super::*; + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: serde::Deserializer<'de>, + { + let [x, y, z] = <[f32; 3]>::deserialize(deserializer)?; + Ok(Vector3::new(x, y, z)) + } + + pub fn serialize(data: &Vector3, serializer: S) -> Result + where + S: serde::Serializer, + { + data.as_slice().serialize(serializer) + } +} + +pub mod vector2u { + use nalgebra::Vector2; + + use super::*; + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: serde::Deserializer<'de>, + { + let [x, y] = <[u32; 2]>::deserialize(deserializer)?; + Ok(Vector2::new(x, y)) + } + + pub fn serialize(data: &Vector2, serializer: S) -> Result + where + S: serde::Serializer, + { + data.as_slice().serialize(serializer) + } +} + +pub mod vector3_list { + use super::*; + + pub fn deserialize<'de, D>(deserializer: D) -> Result>, D::Error> + where + D: serde::Deserializer<'de>, + { + let data = Vec::::deserialize(deserializer)?; + Ok(data + .chunks(3) + .map(|chunk| Vector3::new(chunk[0], chunk[1], chunk[2])) + .collect()) + } + + pub fn serialize(data: &[Vector3], serializer: S) -> Result + where + S: serde::Serializer, + { + let out = data.iter().flat_map(|v| v.iter()).collect::>(); + out.serialize(serializer) + } +} + +pub mod index_list { + use super::*; + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: serde::Deserializer<'de>, + { + let data = Vec::::deserialize(deserializer)?; + Ok(data + .chunks(3) + .map(|chunk| [chunk[0], chunk[1], chunk[2]]) + .collect()) + } + + pub fn serialize(data: &[[u32; 3]], serializer: S) -> Result + where + S: serde::Serializer, + { + let out = data.iter().flat_map(|v| v.iter()).collect::>(); + out.serialize(serializer) + } +} diff --git a/mslicer/Cargo.toml b/mslicer/Cargo.toml index 5e17314..4360a12 100644 --- a/mslicer/Cargo.toml +++ b/mslicer/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] anyhow.workspace = true +bincode.workspace = true bytemuck.workspace = true chrono.workspace = true clone-macro.workspace = true diff --git a/mslicer/src/app.rs b/mslicer/src/app.rs index ae6ccf2..ae6a246 100644 --- a/mslicer/src/app.rs +++ b/mslicer/src/app.rs @@ -1,11 +1,13 @@ use std::{ + fs::File, io::{BufRead, Seek}, - path::PathBuf, + path::{Path, PathBuf}, sync::Arc, thread, time::Instant, }; +use anyhow::Result; use clone_macro::clone; use const_format::concatcp; use eframe::Theme; @@ -25,6 +27,7 @@ use crate::{ elephant_foot_fixer::{self}, PluginManager, }, + project::{BorrowedProject, OwnedProject}, remote_print::RemotePrint, render::{camera::Camera, rendered_mesh::RenderedMesh}, slice_operation::{SliceOperation, SliceResult}, @@ -225,6 +228,23 @@ impl App { .with_random_color(), ); } + + pub fn save_project(&self, path: &Path) -> Result<()> { + let meshes = self.meshes.read(); + let project = BorrowedProject::new(&meshes, &self.slice_config); + + let mut file = File::create(path)?; + project.serialize(&mut file)?; + Ok(()) + } + + pub fn load_project(&mut self, path: &Path) -> Result<()> { + let mut file = File::open(path)?; + let project = OwnedProject::deserialize(&mut file)?; + + project.apply(self); + Ok(()) + } } impl eframe::App for App { diff --git a/mslicer/src/main.rs b/mslicer/src/main.rs index 5e25802..7461816 100644 --- a/mslicer/src/main.rs +++ b/mslicer/src/main.rs @@ -13,6 +13,7 @@ const TEXTURE_FORMAT: TextureFormat = TextureFormat::Bgra8Unorm; mod app; mod config; mod plugins; +mod project; mod remote_print; mod render; mod slice_operation; diff --git a/mslicer/src/project/mesh.rs b/mslicer/src/project/mesh.rs new file mode 100644 index 0000000..1dc1a70 --- /dev/null +++ b/mslicer/src/project/mesh.rs @@ -0,0 +1,108 @@ +use egui::Color32; +use nalgebra::Vector3; +use serde::{Deserialize, Serialize}; + +use crate::render::rendered_mesh::RenderedMesh; +use common::serde_impls::{index_list, vector3_list, vector3f}; +use slicer::mesh::Mesh; + +#[derive(Deserialize)] +pub struct OwnedProjectMesh { + info: ProjectMeshInfo, + + #[serde(with = "vector3_list")] + vertices: Vec>, + #[serde(with = "index_list")] + faces: Vec<[u32; 3]>, + #[serde(with = "vector3_list")] + normals: Vec>, +} + +#[derive(Serialize)] +pub struct BorrowedProjectMesh<'a> { + info: ProjectMeshInfo, + + #[serde(with = "vector3_list")] + vertices: &'a [Vector3], + #[serde(with = "index_list")] + faces: &'a [[u32; 3]], + #[serde(with = "vector3_list")] + normals: &'a [Vector3], +} + +#[derive(Serialize, Deserialize)] +pub struct ProjectMeshInfo { + name: String, + #[serde(with = "color32")] + color: Color32, + hidden: bool, + + #[serde(with = "vector3f")] + position: Vector3, + #[serde(with = "vector3f")] + scale: Vector3, + #[serde(with = "vector3f")] + rotation: Vector3, +} + +impl OwnedProjectMesh { + pub fn into_rendered_mesh(self) -> RenderedMesh { + let mut mesh = Mesh::new_uncentred(self.vertices, self.faces, self.normals); + mesh.set_position_unchecked(self.info.position); + mesh.set_scale_unchecked(self.info.scale); + mesh.set_rotation_unchecked(self.info.rotation); + mesh.update_transformation_matrix(); + + RenderedMesh::from_mesh(mesh) + .with_name(self.info.name) + .with_color(self.info.color) + .with_hidden(self.info.hidden) + } +} + +impl<'a> BorrowedProjectMesh<'a> { + pub fn from_rendered_mesh(rendered_mesh: &'a RenderedMesh) -> Self { + Self { + info: ProjectMeshInfo::from_rendered_mesh(rendered_mesh), + + vertices: rendered_mesh.mesh.vertices(), + faces: rendered_mesh.mesh.faces(), + normals: rendered_mesh.mesh.normals(), + } + } +} + +impl ProjectMeshInfo { + pub fn from_rendered_mesh(rendered_mesh: &RenderedMesh) -> Self { + Self { + name: rendered_mesh.name.clone(), + color: rendered_mesh.color, + hidden: rendered_mesh.hidden, + + position: rendered_mesh.mesh.position(), + scale: rendered_mesh.mesh.scale(), + rotation: rendered_mesh.mesh.rotation(), + } + } +} + +pub mod color32 { + use egui::Color32; + + use super::*; + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let [r, g, b, a]: [u8; 4] = <[u8; 4]>::deserialize(deserializer)?; + Ok(Color32::from_rgba_premultiplied(r, g, b, a)) + } + + pub fn serialize(data: &Color32, serializer: S) -> Result + where + S: serde::Serializer, + { + data.to_array().serialize(serializer) + } +} diff --git a/mslicer/src/project/mod.rs b/mslicer/src/project/mod.rs new file mode 100644 index 0000000..4ea7207 --- /dev/null +++ b/mslicer/src/project/mod.rs @@ -0,0 +1,69 @@ +use std::io::{Read, Write}; + +use anyhow::Result; +use serde::{Deserialize, Serialize}; + +use crate::{app::App, render::rendered_mesh::RenderedMesh}; +use common::config::SliceConfig; + +use mesh::{BorrowedProjectMesh, OwnedProjectMesh}; +mod mesh; + +const VERSION: u32 = 0; + +#[derive(Serialize)] +pub struct BorrowedProject<'a> { + meshes: Vec>, + slice_config: &'a SliceConfig, +} + +#[derive(Deserialize)] +pub struct OwnedProject { + meshes: Vec, + slice_config: SliceConfig, +} + +impl<'a> BorrowedProject<'a> { + pub fn new(meshes: &'a [RenderedMesh], slice_config: &'a SliceConfig) -> Self { + let meshes = meshes + .iter() + .map(BorrowedProjectMesh::from_rendered_mesh) + .collect(); + + Self { + meshes, + slice_config, + } + } + + pub fn serialize(&self, writer: &mut Writer) -> Result<()> { + writer.write_all(&VERSION.to_le_bytes())?; + bincode::serialize_into(writer, self)?; + Ok(()) + } +} + +impl OwnedProject { + pub fn deserialize(reader: &mut Reader) -> Result { + let mut version_bytes = [0; 4]; + reader.read_exact(&mut version_bytes)?; + let version = u32::from_le_bytes(version_bytes); + + if version != VERSION { + anyhow::bail!("Invalid version: Expected {VERSION} found {version}"); + } + + Ok(bincode::deserialize_from(reader).unwrap()) + } + + pub fn apply(self, app: &mut App) { + let mut meshes = app.meshes.write(); + *meshes = self + .meshes + .into_iter() + .map(|mesh| mesh.into_rendered_mesh()) + .collect(); + + app.slice_config = self.slice_config; + } +} diff --git a/mslicer/src/render/rendered_mesh.rs b/mslicer/src/render/rendered_mesh.rs index 3887d63..fbf1612 100644 --- a/mslicer/src/render/rendered_mesh.rs +++ b/mslicer/src/render/rendered_mesh.rs @@ -75,6 +75,16 @@ impl RenderedMesh { self } + pub fn with_color(mut self, color: Color32) -> Self { + self.color = color; + self + } + + pub fn with_hidden(mut self, hidden: bool) -> Self { + self.hidden = hidden; + self + } + pub fn with_random_color(mut self) -> Self { self.randomize_color(); self diff --git a/mslicer/src/windows/models.rs b/mslicer/src/windows/models.rs index e461667..522e071 100644 --- a/mslicer/src/windows/models.rs +++ b/mslicer/src/windows/models.rs @@ -1,6 +1,6 @@ use const_format::concatcp; use egui::{Context, Grid, Id, Ui}; -use egui_phosphor::regular::{DICE_THREE, EYE, EYE_SLASH}; +use egui_phosphor::regular::{ARROW_LINE_DOWN, COPY, DICE_THREE, EYE, EYE_SLASH, TRASH}; use slicer::Pos; use crate::{ @@ -54,18 +54,22 @@ pub fn ui(app: &mut App, ui: &mut Ui, _ctx: &Context) { .show(ui, |ui| { ui.label("Actions"); ui.horizontal(|ui| { - ui.button("🗑 Delete") + ui.button(concatcp!(TRASH, " Delete")) .clicked() .then(|| action = Action::Remove(i)); - ui.button("🗋 Duplicate") + ui.button(concatcp!(COPY, " Duplicate")) .clicked() .then(|| action = Action::Duplicate(i)); - ui.button("⬇ Align to Bed") + ui.button(concatcp!(ARROW_LINE_DOWN, " Align to Bed")) .clicked() .then(|| mesh.align_to_bed()); }); ui.end_row(); + ui.label("Name"); + ui.text_edit_singleline(&mut mesh.name); + ui.end_row(); + ui.horizontal(|ui| { ui.label("Position"); ui.add_space(20.0); @@ -103,7 +107,7 @@ pub fn ui(app: &mut App, ui: &mut Ui, _ctx: &Context) { ui.label("Rotation"); let mut rotation = rad_to_deg(mesh.mesh.rotation()); let original_rotation = rotation; - vec3_dragger(ui, rotation.as_mut(), |x| x); + vec3_dragger(ui, rotation.as_mut(), |x| x.suffix("°")); (original_rotation != rotation) .then(|| mesh.mesh.set_rotation(deg_to_rad(rotation))); ui.end_row(); diff --git a/mslicer/src/windows/slice_operation.rs b/mslicer/src/windows/slice_operation.rs index 6e7daa6..9a6fa6f 100644 --- a/mslicer/src/windows/slice_operation.rs +++ b/mslicer/src/windows/slice_operation.rs @@ -1,9 +1,11 @@ use std::{fs::File, io::Write, mem, sync::Arc}; +use const_format::concatcp; use egui::{ style::HandleShape, text::LayoutJob, Align, Button, Context, DragValue, FontSelection, Grid, Id, Layout, ProgressBar, RichText, Sense, Slider, Style, Vec2, Window, }; +use egui_phosphor::regular::{FLOPPY_DISK_BACK, PAPER_PLANE_TILT}; use egui_wgpu::Callback; use goo_format::LayerDecoder; use nalgebra::Vector2; @@ -52,44 +54,48 @@ pub fn ui(app: &mut App, ctx: &Context) { ui.with_layout(Layout::default().with_cross_align(Align::Max), |ui| { ui.horizontal(|ui| { ui.add_enabled_ui(app.remote_print.is_initialized(), |ui| { - ui.menu_button("Send to Printer", |ui| { - let mqtt = app.remote_print.mqtt(); - for printer in app.remote_print.printers().iter() { - let client = mqtt.get_client(&printer.mainboard_id); + ui.menu_button( + concatcp!(PAPER_PLANE_TILT, " Send to Printer"), + |ui| { + let mqtt = app.remote_print.mqtt(); + for printer in app.remote_print.printers().iter() { + let client = mqtt.get_client(&printer.mainboard_id); - let mut layout_job = LayoutJob::default(); - RichText::new(format!("{} ", client.attributes.name)) - .append_to( - &mut layout_job, - &Style::default(), - FontSelection::Default, - Align::LEFT, - ); - RichText::new(&client.attributes.mainboard_id) - .monospace() - .append_to( - &mut layout_job, - &Style::default(), - FontSelection::Default, - Align::LEFT, - ); + let mut layout_job = LayoutJob::default(); + RichText::new(format!("{} ", client.attributes.name)) + .append_to( + &mut layout_job, + &Style::default(), + FontSelection::Default, + Align::LEFT, + ); + RichText::new(&client.attributes.mainboard_id) + .monospace() + .append_to( + &mut layout_job, + &Style::default(), + FontSelection::Default, + Align::LEFT, + ); - let result = app.slice_operation.as_ref().unwrap().result(); - let result = result.as_ref().unwrap(); + let result = + app.slice_operation.as_ref().unwrap().result(); + let result = result.as_ref().unwrap(); - let mut serializer = DynamicSerializer::new(); - result.goo.serialize(&mut serializer); - let data = Arc::new(serializer.into_inner()); + let mut serializer = DynamicSerializer::new(); + result.goo.serialize(&mut serializer); + let data = Arc::new(serializer.into_inner()); - let mainboard_id = printer.mainboard_id.clone(); - if ui.button(layout_job).clicked() { - app.popup.open(name_popup(mainboard_id, data)); + let mainboard_id = printer.mainboard_id.clone(); + if ui.button(layout_job).clicked() { + app.popup.open(name_popup(mainboard_id, data)); + } } - } - }); + }, + ); }); - if ui.button("Save").clicked() { + if ui.button(concatcp!(FLOPPY_DISK_BACK, " Save")).clicked() { let result = app.slice_operation.as_ref().unwrap().result(); let result = result.as_ref().unwrap(); diff --git a/mslicer/src/windows/top_bar.rs b/mslicer/src/windows/top_bar.rs index 62ff762..3d455a8 100644 --- a/mslicer/src/windows/top_bar.rs +++ b/mslicer/src/windows/top_bar.rs @@ -8,11 +8,17 @@ use const_format::concatcp; use egui::{Button, Context, Key, KeyboardShortcut, Modifiers, TopBottomPanel}; use egui_phosphor::regular::STACK; use rfd::FileDialog; +use tracing::error; -use crate::app::App; +use crate::{ + app::App, + ui::popup::{Popup, PopupIcon}, +}; const IMPORT_MODEL_SHORTCUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::CTRL, Key::I); const LOAD_TEAPOT_SHORTCUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::CTRL, Key::T); +const SAVE_PROJECT_SHORTCUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::CTRL, Key::S); +const LOAD_PROJECT_SHORTCUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::CTRL, Key::O); const QUIT_SHORTCUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::CTRL, Key::Q); const SLICE_SHORTCUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::CTRL, Key::R); @@ -21,6 +27,10 @@ pub fn ui(app: &mut App, ctx: &Context) { .then(|| import_model(app)); ctx.input_mut(|x| x.consume_shortcut(&LOAD_TEAPOT_SHORTCUT)) .then(|| import_teapot(app)); + ctx.input_mut(|x| x.consume_shortcut(&SAVE_PROJECT_SHORTCUT)) + .then(|| save(app)); + ctx.input_mut(|x| x.consume_shortcut(&LOAD_PROJECT_SHORTCUT)) + .then(|| load(app)); ctx.input_mut(|x| x.consume_shortcut(&QUIT_SHORTCUT)) .then(quit); ctx.input_mut(|x| x.consume_shortcut(&SLICE_SHORTCUT)) @@ -49,14 +59,34 @@ pub fn ui(app: &mut App, ctx: &Context) { ui.separator(); - let _ = ui.button("Save Project"); - let _ = ui.button("Load Project"); + let save_project_button = ui.add( + Button::new("Save Project") + .shortcut_text(ctx.format_shortcut(&SAVE_PROJECT_SHORTCUT)), + ); + save_project_button.clicked().then(|| save(app)); + + let load_project_button = ui.add( + Button::new("Load Project") + .shortcut_text(ctx.format_shortcut(&LOAD_PROJECT_SHORTCUT)), + ); + load_project_button.clicked().then(|| load(app)); ui.separator(); let quit_button = ui.add(Button::new("Quit").shortcut_text(ctx.format_shortcut(&QUIT_SHORTCUT))); quit_button.clicked().then(quit); + + // Close the menu if a button is clicked + for button in [ + import_model_button, + import_teapot_button, + save_project_button, + load_project_button, + quit_button, + ] { + button.clicked().then(|| ui.close_menu()); + } }); let slicing = match &app.slice_operation { @@ -95,6 +125,38 @@ fn import_teapot(app: &mut App) { app.load_mesh(&mut buf, "stl", "Utah Teapot".into()); } +fn save(app: &mut App) { + if let Some(path) = FileDialog::new() + .add_filter("mslicer project", &["mslicer"]) + .save_file() + { + if let Err(error) = app.save_project(&path) { + error!("Error saving project: {:?}", error); + app.popup.open(Popup::simple( + "Error Saving Project", + PopupIcon::Error, + error.to_string(), + )); + } + } +} + +fn load(app: &mut App) { + if let Some(path) = FileDialog::new() + .add_filter("mslicer project", &["mslicer"]) + .pick_file() + { + if let Err(error) = app.load_project(&path) { + error!("Error loading project: {:?}", error); + app.popup.open(Popup::simple( + "Error Loading Project", + PopupIcon::Error, + error.to_string(), + )); + } + } +} + fn quit() { process::exit(0); }