Implement load and saving projects
This commit is contained in:
11
Cargo.lock
generated
11
Cargo.lock
generated
@@ -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",
|
||||
|
@@ -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"
|
||||
|
5
TODO.md
5
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 <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?
|
||||
|
@@ -6,3 +6,4 @@ edition = "2021"
|
||||
[dependencies]
|
||||
nalgebra.workspace = true
|
||||
rand.workspace = true
|
||||
serde.workspace = true
|
||||
|
@@ -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,
|
||||
|
@@ -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
88
common/src/serde_impls.rs
Normal 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)
|
||||
}
|
||||
}
|
@@ -5,6 +5,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
bincode.workspace = true
|
||||
bytemuck.workspace = true
|
||||
chrono.workspace = true
|
||||
clone-macro.workspace = true
|
||||
|
@@ -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 {
|
||||
|
@@ -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
108
mslicer/src/project/mesh.rs
Normal 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)
|
||||
}
|
||||
}
|
69
mslicer/src/project/mod.rs
Normal file
69
mslicer/src/project/mod.rs
Normal 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;
|
||||
}
|
||||
}
|
@@ -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
|
||||
|
@@ -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();
|
||||
|
@@ -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();
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
|
Reference in New Issue
Block a user