Implement load and saving projects

This commit is contained in:
Connor Slade
2024-09-01 17:42:36 -04:00
parent ed0c79196f
commit f08bcfe9f3
16 changed files with 435 additions and 42 deletions

11
Cargo.lock generated
View File

@@ -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",

View File

@@ -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"

View File

@@ -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 <https://github.com/cbiffle/catibo/blob/master/doc/cbddlp-ctb.adoc>)
- [ ] Undo / Redo
- [x] Close file menu if button clicked
- [ ] Allow dragging in project to load them?

View File

@@ -6,3 +6,4 @@ edition = "2021"
[dependencies]
nalgebra.workspace = true
rand.workspace = true
serde.workspace = true

View File

@@ -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<u32>,
#[serde(with = "vector3f")]
pub platform_size: Vector3<f32>,
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,

View File

@@ -3,3 +3,4 @@ pub mod image;
pub mod misc;
pub mod oklab;
pub mod serde;
pub mod serde_impls;

88
common/src/serde_impls.rs Normal file
View File

@@ -0,0 +1,88 @@
use nalgebra::Vector3;
use serde::{Deserialize, Serialize};
pub mod vector3f {
use super::*;
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vector3<f32>, D::Error>
where
D: serde::Deserializer<'de>,
{
let [x, y, z] = <[f32; 3]>::deserialize(deserializer)?;
Ok(Vector3::new(x, y, z))
}
pub fn serialize<S>(data: &Vector3<f32>, serializer: S) -> Result<S::Ok, S::Error>
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<Vector2<u32>, D::Error>
where
D: serde::Deserializer<'de>,
{
let [x, y] = <[u32; 2]>::deserialize(deserializer)?;
Ok(Vector2::new(x, y))
}
pub fn serialize<S>(data: &Vector2<u32>, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
data.as_slice().serialize(serializer)
}
}
pub mod vector3_list {
use super::*;
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Vector3<f32>>, D::Error>
where
D: serde::Deserializer<'de>,
{
let data = Vec::<f32>::deserialize(deserializer)?;
Ok(data
.chunks(3)
.map(|chunk| Vector3::new(chunk[0], chunk[1], chunk[2]))
.collect())
}
pub fn serialize<S>(data: &[Vector3<f32>], serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let out = data.iter().flat_map(|v| v.iter()).collect::<Vec<_>>();
out.serialize(serializer)
}
}
pub mod index_list {
use super::*;
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<[u32; 3]>, D::Error>
where
D: serde::Deserializer<'de>,
{
let data = Vec::<u32>::deserialize(deserializer)?;
Ok(data
.chunks(3)
.map(|chunk| [chunk[0], chunk[1], chunk[2]])
.collect())
}
pub fn serialize<S>(data: &[[u32; 3]], serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let out = data.iter().flat_map(|v| v.iter()).collect::<Vec<_>>();
out.serialize(serializer)
}
}

View File

@@ -5,6 +5,7 @@ edition = "2021"
[dependencies]
anyhow.workspace = true
bincode.workspace = true
bytemuck.workspace = true
chrono.workspace = true
clone-macro.workspace = true

View File

@@ -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 {

View File

@@ -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;

108
mslicer/src/project/mesh.rs Normal file
View File

@@ -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<Vector3<f32>>,
#[serde(with = "index_list")]
faces: Vec<[u32; 3]>,
#[serde(with = "vector3_list")]
normals: Vec<Vector3<f32>>,
}
#[derive(Serialize)]
pub struct BorrowedProjectMesh<'a> {
info: ProjectMeshInfo,
#[serde(with = "vector3_list")]
vertices: &'a [Vector3<f32>],
#[serde(with = "index_list")]
faces: &'a [[u32; 3]],
#[serde(with = "vector3_list")]
normals: &'a [Vector3<f32>],
}
#[derive(Serialize, Deserialize)]
pub struct ProjectMeshInfo {
name: String,
#[serde(with = "color32")]
color: Color32,
hidden: bool,
#[serde(with = "vector3f")]
position: Vector3<f32>,
#[serde(with = "vector3f")]
scale: Vector3<f32>,
#[serde(with = "vector3f")]
rotation: Vector3<f32>,
}
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<Color32, D::Error>
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<S>(data: &Color32, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
data.to_array().serialize(serializer)
}
}

View File

@@ -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<BorrowedProjectMesh<'a>>,
slice_config: &'a SliceConfig,
}
#[derive(Deserialize)]
pub struct OwnedProject {
meshes: Vec<OwnedProjectMesh>,
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<Writer: Write>(&self, writer: &mut Writer) -> Result<()> {
writer.write_all(&VERSION.to_le_bytes())?;
bincode::serialize_into(writer, self)?;
Ok(())
}
}
impl OwnedProject {
pub fn deserialize<Reader: Read>(reader: &mut Reader) -> Result<Self> {
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;
}
}

View File

@@ -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

View File

@@ -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();

View File

@@ -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();

View File

@@ -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);
}