diff --git a/Cargo.lock b/Cargo.lock index a43d158..76c32b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,6 +41,18 @@ dependencies = [ "version_check", ] +[[package]] +name = "ahash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57e6e951cfbb2db8de1828d49073a113a29fd7117b1596caa781a258c7e38d72" +dependencies = [ + "cfg-if 1.0.0", + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "0.7.18" @@ -400,9 +412,9 @@ dependencies = [ [[package]] name = "bevy_egui" -version = "0.15.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf44ff770566dca66b805a6829df783f64700bd01d35aec1034dff31b531a4" +checksum = "e92efd442c1c99edb841c21b51248e0b472d9fc92ecd8124bc7bcfb5346760e9" dependencies = [ "arboard", "bevy", @@ -841,7 +853,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6e9aa1866c1cf7ee000f281ce9e90d02d701f5c7380a107252017e58e2f5246" dependencies = [ - "ahash", + "ahash 0.7.6", "getrandom", "hashbrown", "instant", @@ -1386,20 +1398,20 @@ checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" [[package]] name = "egui" -version = "0.18.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb095a8b9feb9b7ff8f00b6776dffcef059538a3f4a91238e03c900e9c9ad9a2" +checksum = "fc9fcd393c3daaaf5909008a1d948319d538b79c51871e4df0993260260a94e4" dependencies = [ - "ahash", + "ahash 0.8.0", "epaint", "nohash-hasher", ] [[package]] name = "emath" -version = "0.18.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c223f58c7e38abe1770f367b969f1b3fbd4704b67666bcb65dbb1adb0980ba72" +checksum = "9542a40106fdba943a055f418d1746a050e1a903a049b030c2b097d4686a33cf" dependencies = [ "bytemuck", ] @@ -1448,12 +1460,12 @@ dependencies = [ [[package]] name = "epaint" -version = "0.18.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c29567088888e8ac3e8f61bbb2ddc820207ebb8d69eefde5bcefa06d65e4e89" +checksum = "5ba04741be7f6602b1a1b28f1082cce45948a7032961c52814f8946b28493300" dependencies = [ "ab_glyph", - "ahash", + "ahash 0.8.0", "atomic_refcell", "bytemuck", "emath", @@ -1901,7 +1913,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash", + "ahash 0.7.6", "serde", ] @@ -3078,6 +3090,8 @@ dependencies = [ "bevy_egui", "bevy_prototype_lyon", "rfd", + "serde", + "serde_json", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 10ea550..0c164d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -bevy = "0.8.0" -bevy_egui = { version = "0.15.0", features = ["manage_clipboard"] } +bevy = "0.8.1" +bevy_egui = { version = "0.16.0", features = ["manage_clipboard"] } bevy_prototype_lyon = "0.6.0" rfd = "0.10.0" +serde = "1.0.144" +serde_json = "1.0.85" diff --git a/README.md b/README.md index bc23094..647bb54 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,8 @@ Eventually, this will be a level editor... But until then, I guess it's just fun - [x] Show/Hide grid(also make a visible grid in the first place) - [x] Make grid fill the screen at all times - [x] Comment/Editor note for shapes -- [ ] Save? (maybe just import and export directly?) -- [ ] Export -- [ ] Import +- [x] Export +- [x] Import ## Quality of life todo @@ -36,6 +35,7 @@ Eventually, this will be a level editor... But until then, I guess it's just fun - [ ] Grab shapes instead of center points - [ ] Reorder items tree - [ ] Select item by clicking/double clicking on the relevant shape/image +- [ ] Allow for more types of export/import ## Maybe, just maybe todo diff --git a/src/export.rs b/src/export.rs new file mode 100644 index 0000000..12393dc --- /dev/null +++ b/src/export.rs @@ -0,0 +1,267 @@ +use std::io::Write; + +use bevy::math::Vec3Swizzles; +use bevy::prelude::*; +use bevy_prototype_lyon::prelude::*; +use crate::*; +use serde::{Serialize, Deserialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum ExportData { + Image { + path: String, + scale: Vec2, + }, + Square { + size: Vec2, + }, + Triangle { + /// Relative to translation! + vertex: [Vec2; 3], + }, + Circle { + radius: f32, + } +} +#[derive(Debug, Serialize)] +pub struct ExportItem<'a> { + pub name: &'a str, + pub note: &'a str, + pub translation: Vec3, + pub rotation: f32, + pub color: Color, + pub data: ExportData, +} +#[derive(Debug, Deserialize)] +pub struct ImportItem { + pub name: String, + pub note: String, + pub translation: Vec3, + pub rotation: f32, + pub color: Color, + pub data: ExportData, +} + +pub fn toml_save_sys( + mut imp_exp: ResMut, + shapes: Query<&ShapeData>, + images: Query<(&ImageData, &Transform)>, + transforms: Query<&Transform>, + draw_modes: Query<&DrawMode>, +) { + imp_exp.export = false; + let file = rfd::FileDialog::new() + .set_directory("./") + .set_title("Just pick where you wanna save that file...") + .save_file(); + + if let Some(path) = file { + let file = std::fs::File::create(path); + if let Ok(mut file) = file { + let items: Vec = images.iter().map(|(id, tr)| { + ExportItem { + name: &id.name, + note: &id.note, + translation: tr.translation, + rotation: tr.rotation.to_euler(EulerRot::XYZ).2, + data: ExportData::Image { path: id.path.clone(), scale: tr.scale.xy() }, + color: Color::WHITE, + } + }) + .chain(shapes.iter().map(|sd| { + let trans = transforms.get(sd.main_shape).unwrap(); + let color = draw_modes.get(sd.main_shape).map(|dm| + if let DrawMode::Outlined { fill_mode: fm, outline_mode: _ } = dm { + fm.color + } else { + Color::WHITE + } + ).unwrap_or(Color::WHITE); + + ExportItem { + name: &sd.name, + note: &sd.note, + translation: trans.translation, + rotation: trans.rotation.to_euler(EulerRot::XYZ).2, + data: get_export_data_for_shape(sd, &transforms), + color, + } + })) + .collect(); + + let json = match serde_json::to_string_pretty(&items) { + Ok(t) => t, + Err(e) => { + println!("json error: {:?}", e); + return; + }, + }; + + file.write(json.as_bytes()).expect("Could not write to save file"); + } + } +} + +pub fn import_sys( + mut coms: Commands, + mut imp_exp: ResMut, + assets: Res, + p_size: Res, + +) { + imp_exp.import = false; + let file = rfd::FileDialog::new() + .set_directory("./") + .set_title("Please pick a valid json file to load from <3") + .pick_file(); + + if let Some(file_path) = file { + let file = std::fs::OpenOptions::new().read(true).open(file_path); + if let Ok(f) = file { + let items: Vec = match serde_json::from_reader(f) { + Ok(itms) => itms, + Err(_) => { + bevy::log::error!("Invalid json file"); + return; + }, + }; + let outline_mode = StrokeMode::new(Color::rgba(0.0, 0.5, 0.5, 0.6), 3.0); + let dots_dm = DrawMode::Fill(FillMode::color(Color::RED)); + let dot_shape = shapes::Circle { radius: p_size.0, center: Vec2::ZERO }; + + for i in items { + let ImportItem { name, note, translation, rotation, color, data } = i; + let transform = Transform::from_translation(translation) + .with_rotation(Quat::from_rotation_z(rotation)); + let center = coms.spawn_bundle(GeometryBuilder::build_as( + &dot_shape, + dots_dm, + Transform::from_xyz(0.0, 0.0, 0.5) + )).id(); + let main_shape_dm = DrawMode::Outlined { fill_mode: FillMode::color(color), outline_mode: outline_mode }; + match data { + ExportData::Image { path, scale } => { + let image: Handle = assets.load(&path); + coms.spawn_bundle(SpriteBundle { + texture: image, + ..Default::default() + }) + .insert(ImageData { name, note, path }) + .insert(transform.with_scale(scale.extend(1.0)) + ); + + coms.entity(center).despawn(); // We dont need the center in an image + }, + ExportData::Square { size } => { + let e1 = coms.spawn_bundle(GeometryBuilder::build_as( + &dot_shape, + dots_dm, + Transform::from_translation((size * 0.5).extend(0.5)) + )).id(); + let e2 = coms.spawn_bundle(GeometryBuilder::build_as( + &dot_shape, + dots_dm, + Transform::from_translation((-size * 0.5).extend(0.5)) + )).id(); + + let main_id = coms.spawn_bundle(GeometryBuilder::build_as( + &shapes::Rectangle { extents: size, ..Default::default() }, + main_shape_dm, + transform + )).id(); + + coms.entity(main_id).insert(ShapeData { + name, + shape: CreateShape::Square, + main_shape: main_id, + edges: Vec::from([e1,e2]), + center: Some(center), + note, + }) + .add_child(center) + .add_child(e1) + .add_child(e2); + }, + ExportData::Triangle { vertex } => { + let main_shape = shapes::Polygon { points: vertex.clone().to_vec(), closed: true }; + let vertex = vertex.map(|v| { + coms.spawn_bundle(GeometryBuilder::build_as( + &dot_shape, + dots_dm, + Transform::from_translation(v.extend(0.5)) + )).id() + }); + + let main_id = coms.spawn_bundle(GeometryBuilder::build_as( + &main_shape, + main_shape_dm, + transform + )).id(); + coms.entity(main_id).insert(ShapeData { + name, + shape: CreateShape::Triangle, + main_shape: main_id, + edges: vertex.clone().to_vec(), + center: Some(center), + note, + }) + .add_child(vertex[0]) + .add_child(vertex[1]) + .add_child(vertex[2]) + .add_child(center); + }, + ExportData::Circle { radius } => { + let edge = coms.spawn_bundle(GeometryBuilder::build_as( + &dot_shape, + dots_dm, + Transform::from_xyz(radius, 0.0, 0.5) + )).id(); + + let main_id = coms.spawn_bundle(GeometryBuilder::build_as( + &shapes::Circle { radius, center: Vec2::ZERO }, + main_shape_dm, + transform + )).id(); + coms.entity(main_id).insert(ShapeData { + name, + shape: CreateShape::Circle, + main_shape: main_id, + edges: [edge].to_vec(), + center: Some(center), + note, + }) + .add_child(center) + .add_child(edge); + }, + } + } + } + } +} + +fn get_export_data_for_shape(sd: &ShapeData, transforms: &Query<&Transform>,) -> ExportData { + match sd.shape { + CreateShape::Triangle => { + assert_eq!(sd.edges.len(), 3); + + let mut edges = sd.edges.iter().take(3).map(|e| transforms.get(*e).unwrap().translation.xy()); + let edges = [edges.next().unwrap(), edges.next().unwrap(), edges.next().unwrap()]; + ExportData::Triangle { vertex: edges } + }, + CreateShape::Square => { + assert_eq!(sd.edges.len(), 2); + let e1 = transforms.get(sd.edges[0]).unwrap().translation.xy(); + let e2 = transforms.get(sd.edges[1]).unwrap().translation.xy(); + + let size = (e1 - e2).abs(); + ExportData::Square { size } + }, + CreateShape::Circle => { + assert_eq!(sd.edges.len(), 1); + + let radius = transforms.get(sd.edges[0]).unwrap().translation.xy().length(); + ExportData::Circle { radius } + }, + CreateShape::Capsule => unimplemented!(), + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index bad0572..2ae3790 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ pub mod helpers; pub mod modify; pub mod ui; pub mod infinite_grid; +pub mod export; pub use modify::modify_sys; pub use create::create_sys; pub use helpers::*; @@ -40,10 +41,12 @@ impl Default for SnapGrid { #[derive(Clone, Debug)] pub struct PointSize(pub f32); -#[derive(Component, Debug, Deref, DerefMut)] -pub struct ImageName(pub String); -#[derive(Component, Debug, Deref, DerefMut)] -pub struct ImageSize(pub Vec2); +#[derive(Component, Debug)] +pub struct ImageData { + pub name: String, + pub note: String, + pub path: String, +} #[derive(Component, Debug)] pub struct ShapeData { @@ -96,6 +99,11 @@ impl UiState { } } } +#[derive(Default, Clone, Debug)] +pub struct ShouldImportExport { + pub import: bool, + pub export: bool, +} #[derive(Debug)] pub struct ButtonsColors { diff --git a/src/main.rs b/src/main.rs index 913e21b..1afeead 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,6 +27,7 @@ fn main() { .insert_resource(PointSize(6.0)) .insert_resource(SnapGrid::default()) .insert_resource(ui::SelectedItem::default()) + .insert_resource(ShouldImportExport::default()) ; app @@ -64,6 +65,12 @@ fn main() { .add_system(delete_selected_item_on_del_sys.with_run_criteria(|mut ec: ResMut| if ec.ctx_mut().wants_keyboard_input() { ShouldRun::No } else { ShouldRun::Yes } )) + .add_system(export::toml_save_sys.with_run_criteria(|ie: Res| { + if ie.export { ShouldRun::Yes } else { ShouldRun::No } + })) + .add_system(export::import_sys.with_run_criteria(|ie: Res| { + if ie.import { ShouldRun::Yes} else { ShouldRun::No } + })) ; app.run(); diff --git a/src/ui.rs b/src/ui.rs index abe8c24..662ed02 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -40,12 +40,13 @@ pub fn action_bar_sys( .pick_file(); if let Some(file) = file { let name = String::from(file.file_name().unwrap().to_str().unwrap_or("Image")); + let path = file.to_str().unwrap().to_string(); let image: Handle = assets.load(file); coms.spawn_bundle(SpriteBundle { texture: image, ..Default::default() }) - .insert(ImageName(name)); + .insert(ImageData { name, note: String::new(), path }); } } @@ -118,17 +119,29 @@ pub fn grid_window_sys( /// TODO: Allow drag and drop to re-order + reparent pub fn items_tree_sys( mut selected: ResMut, + mut imp_exp: ResMut, mut egui_ctx: ResMut, shapes:Query<(Entity, &ShapeData)>, - images: Query<(Entity, &ImageName), With>, + images: Query<(Entity, &ImageData), With>, mut draw_modes: Query<&mut DrawMode, With>, mut visible: Query<&mut Visibility, Or<(With, With)>>, ) { egui::Window::new("Items") .default_pos((10.0,100.0)) + .fixed_size((150.0, f32::INFINITY)) .title_bar(true) .resizable(false) .show(egui_ctx.ctx_mut(), |ui| { + ui.horizontal(|hui| { + if hui.button("Import").clicked() { + imp_exp.import = true; + } + if hui.button("Export").clicked() { + imp_exp.export = true; + } + }); + ui.separator(); + for (e, sd) in shapes.iter() { let entity_selected = if let Some(se) = **selected && se == e { true } else { false }; ui.horizontal(|hui| { @@ -162,7 +175,7 @@ pub fn items_tree_sys( ui.horizontal(|hui| { let color = if entity_selected { egui::Color32::GOLD } else { egui::Color32::WHITE }; - let label = egui::Label::new(egui::RichText::new(&**n).color(color)).sense(egui::Sense::click()); + let label = egui::Label::new(egui::RichText::new(&n.name).color(color)).sense(egui::Sense::click()); if hui.add(label).clicked() { if entity_selected { **selected = None; @@ -180,7 +193,6 @@ pub fn items_tree_sys( } }); } - }); } @@ -194,7 +206,7 @@ pub fn inspector_sys( mut paths: Query<&mut Path>, mut draw_modes: Query<&mut DrawMode, With>, mut visible: Query<&mut Visibility, Or<(With, With)>>, - mut images: Query<&mut ImageName, With>, + mut images: Query<&mut ImageData, With>, ) { if let Some(e) = **selected { let mut open = true; @@ -294,7 +306,7 @@ pub fn inspector_sys( else if let Ok(mut name) = images.get_mut(e) { ui.horizontal(|hui| { hui.label("Name:"); - hui.text_edit_singleline(&mut **name); + hui.text_edit_singleline(&mut name.name); }); ui.separator();