diff --git a/README.md b/README.md index b659405..7c232da 100644 --- a/README.md +++ b/README.md @@ -2,43 +2,29 @@ Mostly a working level editor, why should you use it? because it exists and allows to make levels without tilemaps... +## Usage + +Left mouse click for mostly everything, when creating a shape you it will build the shape as you go(giving you a visual indicator). + +If you want to cancel a shape mid-build you can right click to undo the shape + +When an item is selected in the items tree window(will be written in gold), clicking `X` will delete it + ## TODO -- [x] Items tree view showing all shapes with their child nodes and purposes(maybe location and such as well?) -- [x] Change shape name in tree view -- [x] Delete shapes in tree view - I should probably make it ask for confirmation... -- [x] Highlight opened shapes in the tree(maybe allow only 1 opened shape? - I dont think egui allows me to do that) -- [x] Drag camera around -- [x] Zome in/out -- [x] Import images: - - [x] Insert images - - [x] Show images in tree view - - [x] Show/Hide images - - [x] Move/Drag images around - - [x] Control images Z value(so we could reorder them) - - [x] Delete images - - [x] Name images - - [x] Scale images -- [x] Show hide shapes -- [x] Control shape Z value -- [x] Snap to grid -- [x] Change grid size -- [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 -- [x] Export -- [x] Import +- [x] Pick default color for shapes +- [x] Undo/Redo history(ctrl + Z/ctrl + shift + Z) +- [ ] Select item by clicking/double clicking on the relevant shape/image ## Quality of life todo - [ ] Double click on shape in tree view will center the relevant shape - [ ] 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 - [ ] Allow to set a relative export path for images(also allow to select when importing/save it in the saved file) +- [ ] Group shapes/images under an empty/another object with an actual tree view ## Maybe, just maybe todo -- [ ] Group shapes/images under an empty/another object with an actual tree view - [ ] Duplicate shapes/images diff --git a/src/create.rs b/src/create.rs index 94cc043..8de5de7 100644 --- a/src/create.rs +++ b/src/create.rs @@ -5,7 +5,9 @@ use bevy_prototype_lyon::prelude::*; pub fn create_sys( mut coms: Commands, + mut undo: ResMut, p_size: Res, + default_color: Res, state: Res, mouse: Res>, wnds: Res, @@ -59,6 +61,9 @@ pub fn create_sys( ms.add_child(*e); } ms.insert(s); + + undo.push(UndoItem::CreateShape { entity: ms.id() }); + *shape = None; } } @@ -76,7 +81,7 @@ pub fn create_sys( else if mouse.just_released(MouseButton::Left) { // We can now spawn a shape in the current mouse position... // Spawn the first point - *shape = Some(create_new_shape(&mut coms, mouse_pos, state.create_shape, p_size.0)); + *shape = Some(create_new_shape(&mut coms, mouse_pos, state.create_shape, p_size.0, **default_color)); } } } @@ -275,10 +280,10 @@ fn update_main_shape_creation( } } -fn create_new_shape(coms: &mut Commands, pos: Vec2, create_shape: CreateShape, p_size: f32) -> ShapeData { +fn create_new_shape(coms: &mut Commands, pos: Vec2, create_shape: CreateShape, p_size: f32, color: Color) -> ShapeData { // Shape draw mode... let draw_mode = DrawMode::Outlined { - fill_mode: FillMode::color(Color::rgba(0.0, 0.5, 0.5, 0.4)), + fill_mode: FillMode::color(color), outline_mode: StrokeMode::new(Color::rgba(0.0, 0.5, 0.5, 0.6), 3.0), }; diff --git a/src/export.rs b/src/export.rs index 12393dc..4c67259 100644 --- a/src/export.rs +++ b/src/export.rs @@ -42,7 +42,7 @@ pub struct ImportItem { pub data: ExportData, } -pub fn toml_save_sys( +pub fn json_save_sys( mut imp_exp: ResMut, shapes: Query<&ShapeData>, images: Query<(&ImageData, &Transform)>, @@ -107,7 +107,6 @@ pub fn import_sys( mut imp_exp: ResMut, assets: Res, p_size: Res, - ) { imp_exp.import = false; let file = rfd::FileDialog::new() diff --git a/src/lib.rs b/src/lib.rs index 2ae3790..430cfc1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +#![feature(let_chains)] + use bevy::prelude::*; use bevy_egui::egui; @@ -7,10 +9,15 @@ pub mod modify; pub mod ui; pub mod infinite_grid; pub mod export; +pub mod undo; +pub use undo::UndoItem; pub use modify::modify_sys; pub use create::create_sys; pub use helpers::*; +#[derive(Debug, Clone, Deref, DerefMut)] +pub struct DefaultColor(pub Color); + #[derive(Debug, Clone)] pub struct SnapGrid { pub width: f32, @@ -41,14 +48,14 @@ impl Default for SnapGrid { #[derive(Clone, Debug)] pub struct PointSize(pub f32); -#[derive(Component, Debug)] +#[derive(Component, Debug, Default, Clone)] pub struct ImageData { pub name: String, pub note: String, pub path: String, } -#[derive(Component, Debug)] +#[derive(Component, Debug, Clone)] pub struct ShapeData { pub name: String, pub shape: CreateShape, @@ -57,6 +64,18 @@ pub struct ShapeData { pub center: Option, pub note: String, } +impl ShapeData { + pub fn shallow_copy(&self) -> Self { + Self { + name: Default::default(), + shape: self.shape, + main_shape: self.main_shape, + edges: Default::default(), + center: Default::default(), + note: Default::default() + } + } +} #[derive(Component)] pub struct MainCamera; @@ -117,4 +136,4 @@ impl Default for ButtonsColors { clicked: egui::Rgba::from_rgb(1.0, 1.0, 1.0) } } -} \ No newline at end of file +} diff --git a/src/main.rs b/src/main.rs index 1afeead..503a6de 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,6 +28,8 @@ fn main() { .insert_resource(SnapGrid::default()) .insert_resource(ui::SelectedItem::default()) .insert_resource(ShouldImportExport::default()) + .insert_resource(DefaultColor(Color::rgba(0.0, 0.5, 0.5, 0.4))) + .insert_resource(undo::UndoStack::default()) ; app @@ -65,12 +67,13 @@ 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| { + .add_system(export::json_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 } })) + .add_system(undo::undo_redo_sys) ; app.run(); @@ -92,11 +95,35 @@ fn configure_visuals(mut egui_ctx: ResMut) { fn delete_selected_item_on_del_sys( mut coms: Commands, + mut undo: ResMut, mut selected: ResMut, keyboard: Res>, + mut shapes: Query<&mut ShapeData>, + mut images: Query<&mut ImageData>, + transforms: Query<&Transform>, + image_handles: Query<&Handle>, + draw_modes: Query<&DrawMode>, ) { if keyboard.just_pressed(KeyCode::Delete) || keyboard.just_pressed(KeyCode::X) { if let Some(e) = selected.0 { + if let Ok(mut sd) = shapes.get_mut(e) { + let mut name = String::new(); + let mut note = String::new(); + std::mem::swap(&mut name, &mut sd.name); + std::mem::swap(&mut note, &mut sd.note); + let edges = sd.edges.iter().map(|e| transforms.get(*e).unwrap().translation.xy()).collect::>(); + let transform = transforms.get(e).map(|t| *t).unwrap_or_default(); + let dm = *draw_modes.get(e).unwrap(); + undo.push(UndoItem::DeleteShape { transform, edges, name, note, shape: sd.shape, dm, old_entity: e }); + } + else if let Ok(mut id) = images.get_mut(e) { + let mut nid = ImageData::default(); + std::mem::swap(&mut nid, &mut *id); + let transform = transforms.get(e).map(|t| *t).unwrap_or_default(); + let handle = image_handles.get(e).unwrap().clone(); + undo.push(UndoItem::DeleteImage { id: nid, transform: transform, handle, old_entity: e }); + } + coms.entity(e).despawn_recursive(); selected.0 = None; } diff --git a/src/modify.rs b/src/modify.rs index 553de11..d4173bc 100644 --- a/src/modify.rs +++ b/src/modify.rs @@ -16,6 +16,8 @@ pub enum Holding { pub fn modify_sys( // Which entity the user currently drags, and which specific part of it(ShapeData entity, path entity) mut holding: Local, + mut held_from: Local, + mut undo: ResMut, mouse: Res>, wnds: Res, scale: Query<&OrthographicProjection, With>, @@ -38,8 +40,8 @@ pub fn modify_sys( for (e, sd) in shapes.iter() { if let Ok(t) = gtransforms.get(sd.center.unwrap()) { let t = t.translation().xy(); - if (mouse_pos - t).length_squared() < (p_size.0 * scale).powi(2) { + *held_from = t; *holding = Holding::Shape(e, sd.center.unwrap()); break; } @@ -49,6 +51,7 @@ pub fn modify_sys( let t = t.translation().xy(); if (mouse_pos - t).length_squared() < (p_size.0 * scale).powi(2) { + *held_from = t; *holding = Holding::Shape(e, *edge); break; } @@ -65,6 +68,7 @@ pub fn modify_sys( // Disregard rotations for now plz let diff = (mouse_pos - t.translation().xy()).abs(); if diff.x < size.x * scale.x && diff.y < size.y * scale.y { + *held_from = t.translation().xy(); *holding = Holding::Image(e, mouse_pos - t.translation().xy()); break; } @@ -73,6 +77,28 @@ pub fn modify_sys( } } else if mouse.just_released(MouseButton::Left) && *holding != Holding::None { + match *holding { + Holding::Shape(shape_data, dot) => { + if let Ok(sd) = shapes.get_component::(shape_data) { + if sd.center == Some(dot) { + undo.push(UndoItem::MoveItem { shape: shape_data, from: *held_from, to: snap.snap_to_grid(mouse_pos) }); + } + else { + undo.push(UndoItem::MoveDot { + shape_data, + dot: sd.edges.iter().position(|e| *e == dot).unwrap_or(0), + from: *held_from, + to: snap.snap_to_grid(mouse_pos), + }); + } + } + }, + Holding::Image(e, off) => { + // ye i know it says MoVeShaPe but really it doesnt matter that much here! + undo.push(UndoItem::MoveItem { shape: e, from: *held_from, to: snap.snap_to_grid(mouse_pos - off) }); + }, + Holding::None => {}, + } *holding = Holding::None; // We just released our sad little dot/shape } else if let Holding::Shape(se, pe) = *holding { diff --git a/src/ui.rs b/src/ui.rs index 662ed02..bbac018 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -9,9 +9,11 @@ pub struct SelectedItem(pub Option); pub fn action_bar_sys( mut coms: Commands, + mut undo: ResMut, assets: Res, mut egui_ctx: ResMut, mut state: ResMut, + mut default_color: ResMut, colors: Res, ) { egui::Window::new("buttons_float") @@ -42,11 +44,13 @@ pub fn action_bar_sys( 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 { + let id = coms.spawn_bundle(SpriteBundle { texture: image, ..Default::default() }) - .insert(ImageData { name, note: String::new(), path }); + .insert(ImageData { name, note: String::new(), path }).id(); + + undo.push(UndoItem::CreateImage { entity: id }); } } @@ -76,6 +80,14 @@ pub fn action_bar_sys( // state.create_shape = CreateShape::Capsule; // } }); + ui.horizontal(|hui| { + hui.label("Color: "); + let c = **default_color; + let mut color = [c.r(), c.g(), c.b(), c.a()]; + if hui.color_edit_button_rgba_unmultiplied(&mut color).changed() { + **default_color = Color::from(color); + } + }); } }); } @@ -197,8 +209,10 @@ pub fn items_tree_sys( } pub fn inspector_sys( - mut selected: ResMut, // Which item is currently selected mut coms: Commands, // For deletion! + mut changes: Local<(f32, String)>, + mut undo: ResMut, + mut selected: ResMut, // Which item is currently selected mut egui_ctx: ResMut, mut shapes: Query<&mut ShapeData>, mut transforms: Query<&mut Transform>, @@ -207,6 +221,7 @@ pub fn inspector_sys( mut draw_modes: Query<&mut DrawMode, With>, mut visible: Query<&mut Visibility, Or<(With, With)>>, mut images: Query<&mut ImageData, With>, + image_handles: Query<&Handle, With>, ) { if let Some(e) = **selected { let mut open = true; @@ -219,40 +234,119 @@ pub fn inspector_sys( .open(&mut open) .resizable(false) .show(egui_ctx.ctx_mut(), |ui| { - if let Ok(mut sd) = shapes.get_mut(e) { + let mut shape = shapes.get_mut(e); + let mut image = images.get_mut(e); + + if shape.is_err() && image.is_err() { + bevy::log::error!("Selected item is not a shape nor an image, so... weird"); + **selected = None; + return; + } + + let name = if let Ok(ref mut sd) = shape { &mut sd.name } + else if let Ok(ref mut id) = image { &mut id.name } + else { unreachable!() }; + ui.horizontal(|hui| { + hui.label("Name:"); + let te = hui.text_edit_singleline(name); + if te.gained_focus() { + changes.1 = name.clone(); + } + if te.lost_focus() { + let mut new = String::new(); + std::mem::swap(&mut new, &mut changes.1); + undo.push(UndoItem::NameChange { entity: e, from: new }); + } + }); + ui.separator(); + if let Ok(mut t) = transforms.get_mut(e) { ui.horizontal(|hui| { - hui.label("Name:"); - hui.text_edit_singleline(&mut sd.name); + hui.label("Translation:"); + let xdrag = hui.add(egui::DragValue::new(&mut t.translation.x)); + if started_edit(&xdrag) { + changes.0 = t.translation.x; + } + else if stopped_edit(&xdrag) { + undo.push(UndoItem::MoveItem { shape: e, from: Vec2::new(changes.0, t.translation.y), to: t.translation.xy() }); + } + let ydrag = hui.add(egui::DragValue::new(&mut t.translation.y)); + if started_edit(&ydrag) { + changes.0 = t.translation.y; + } + else if stopped_edit(&ydrag) { + undo.push(UndoItem::MoveItem { shape: e, from: Vec2::new(t.translation.x , changes.0), to: t.translation.xy() }); + } + }); - ui.separator(); - if let Ok(mut t) = transforms.get_mut(sd.main_shape) { + ui.horizontal(|hui| { + hui.label("Rotation:"); + let mut rot = t.rotation.to_euler(EulerRot::XYZ).2.to_degrees(); + let drag = hui.add(egui::DragValue::new(&mut rot).suffix("°")); + if started_edit(&drag) { + changes.0 = rot; + } + if stopped_edit(&drag) { + undo.push(UndoItem::Rotate { entity: e, from: changes.0, to: rot }); + } + if drag.changed() { + t.rotation = Quat::from_rotation_z(rot.to_radians()); + } + hui.label("Z:"); + let z = hui.add(egui::DragValue::new(&mut t.translation.z).clamp_range(0..=i32::MAX)); + if started_edit(&z) { + changes.0 = t.translation.z; + } + if stopped_edit(&z) { + undo.push(UndoItem::ChangeZ { entity: e, from: changes.0, to: t.translation.z }); + } + }); + if image.is_ok() { ui.horizontal(|hui| { - hui.label("Translation:"); - hui.add(egui::DragValue::new(&mut t.translation.x)); - hui.add(egui::DragValue::new(&mut t.translation.y)); + hui.label("Scale:"); + let xs = hui.add(egui::DragValue::new(&mut t.scale.x).speed(0.01)); + if started_edit(&xs) { + changes.0 = t.scale.x; + } + if stopped_edit(&xs) { + undo.push(UndoItem::ReScale { entity: e, from: Vec2::new(changes.0, t.scale.y), to: t.scale.xy() }); + } + + let ys = hui.add(egui::DragValue::new(&mut t.scale.y).speed(0.01)); + if started_edit(&ys) { + changes.0 = t.scale.y; + } + if stopped_edit(&ys) { + undo.push(UndoItem::ReScale { entity: e, from: Vec2::new(t.scale.x, changes.0), to: t.scale.xy() }); + } }); - ui.horizontal(|hui| { - hui.label("Rotation:"); - let mut rot = t.rotation.to_euler(EulerRot::XYZ).2.to_degrees(); - if hui.add(egui::DragValue::new(&mut rot).suffix("°")).changed() { - t.rotation = Quat::from_rotation_z(rot.to_radians()); - } - hui.label("Z:"); - hui.add(egui::DragValue::new(&mut t.translation.z).clamp_range(0..=i32::MAX)); - }); } - ui.separator(); + } + ui.separator(); + if let Ok(ref mut sd) = shape { for (i, edge) in sd.edges.iter().enumerate() { ui.horizontal(|hui| { hui.label(format!("Edge {}", i)); let gt = global_transforms.get(*edge).unwrap(); let mut gt_x = gt.translation().x; let mut gt_y = gt.translation().y; - let c1 = hui.add(egui::DragValue::new(&mut gt_x)).changed(); - let c2 = hui.add(egui::DragValue::new(&mut gt_y)).changed(); + let rx = hui.add(egui::DragValue::new(&mut gt_x)); + let ry = hui.add(egui::DragValue::new(&mut gt_y)); - if c1 || c2 { + if started_edit(&rx) { + changes.0 = gt_x; + } + if started_edit(&ry) { + changes.0 = gt_y; + } + if stopped_edit(&rx) { + undo.push(UndoItem::MoveDot { shape_data: e, dot: i, from: Vec2::new(changes.0, gt_y), to: Vec2::new(gt_x, gt_y) }); + } + if stopped_edit(&ry) { + undo.push(UndoItem::MoveDot { shape_data: e, dot: i, from: Vec2::new(gt_x, changes.0), to: Vec2::new(gt_x, gt_y) }); + } + + if rx.changed() || ry.changed() { let rot = gt.to_scale_rotation_translation().1; let ang = rot.to_euler(EulerRot::XYZ).2; let delta = Mat2::from_angle(-ang) * (Vec2::new(gt_x, gt_y) - gt.translation().xy()); @@ -277,9 +371,11 @@ pub fn inspector_sys( }); } ui.separator(); - if let Ok(mut v) = visible.get_mut(e) { - ui.checkbox(&mut v.is_visible, "Visible"); - } + } + if let Ok(mut v) = visible.get_mut(e) { + ui.checkbox(&mut v.is_visible, "Visible"); + } + if shape.is_ok() { if let Ok(mut dm) = draw_modes.get_mut(e) { ui.separator(); if let DrawMode::Outlined { fill_mode: f, outline_mode: _ } = &mut *dm { @@ -293,55 +389,54 @@ pub fn inspector_sys( } } ui.separator(); - ui.label("Notes"); - ui.text_edit_multiline(&mut sd.note); - - - ui.separator(); - if ui.button("Delete").clicked() { - coms.entity(e).despawn_recursive(); - **selected = None; - } } - else if let Ok(mut name) = images.get_mut(e) { - ui.horizontal(|hui| { - hui.label("Name:"); - hui.text_edit_singleline(&mut name.name); - }); - ui.separator(); - if let Ok(mut t) = transforms.get_mut(e) { - ui.horizontal(|hui| { - hui.label("Translation:"); - hui.add(egui::DragValue::new(&mut t.translation.x)); - hui.add(egui::DragValue::new(&mut t.translation.y)); - - }); - ui.horizontal(|hui| { - hui.label("Rotation:"); - let mut rot = t.rotation.to_euler(EulerRot::XYZ).2.to_degrees(); - if hui.add(egui::DragValue::new(&mut rot).suffix("°")).changed() { - t.rotation = Quat::from_rotation_z(rot.to_radians()); - } - hui.label("Z:"); - hui.add(egui::DragValue::new(&mut t.translation.z).clamp_range(0..=i32::MAX)); - }); - ui.horizontal(|hui| { - hui.label("Scale:"); - hui.add(egui::DragValue::new(&mut t.scale.x).speed(0.01)); - hui.add(egui::DragValue::new(&mut t.scale.y).speed(0.01)); - }); - } + let note = if let Ok(ref mut sd) = shape { &mut sd.note } + else if let Ok(ref mut id) = image { &mut id.note } + else { unreachable!() }; + ui.label("Notes"); + let ne = ui.text_edit_multiline(note); + if ne.gained_focus() { + changes.1 = note.clone(); + } + if ne.lost_focus() { + let mut new = String::new(); + std::mem::swap(&mut new, &mut changes.1); + undo.push(UndoItem::NoteChange { entity: e, from: new }); + } - ui.separator(); - if ui.button("Delete").clicked() { - coms.entity(e).despawn_recursive(); - **selected = None; + ui.separator(); + if ui.button("Delete").clicked() { + if let Ok(mut sd) = shape { + let mut name = String::new(); + let mut note = String::new(); + std::mem::swap(&mut name, &mut sd.name); + std::mem::swap(&mut note, &mut sd.note); + let edges = sd.edges.iter().map(|e| transforms.get(*e).unwrap().translation.xy()).collect::>(); + let transform = transforms.get(e).map(|t| *t).unwrap_or_default(); + let dm = *draw_modes.get(e).unwrap(); + undo.push(UndoItem::DeleteShape { transform, edges, name, note, shape: sd.shape, dm, old_entity: e }); } + else if let Ok(mut id) = image { + let mut nid = ImageData::default(); + std::mem::swap(&mut nid, &mut *id); + let transform = transforms.get(e).map(|t| *t).unwrap_or_default(); + let handle = image_handles.get(e).unwrap().clone(); + undo.push(UndoItem::DeleteImage { id: nid, transform: transform, handle, old_entity: e }); + } + coms.entity(e).despawn_recursive(); + **selected = None; } }); if !open { **selected = None; } } +} + +fn started_edit(res: &egui::Response) -> bool { + res.drag_started() || res.gained_focus() +} +fn stopped_edit(res :&egui::Response) -> bool { + res.drag_released() || res.lost_focus() } \ No newline at end of file diff --git a/src/undo.rs b/src/undo.rs new file mode 100644 index 0000000..b2764b8 --- /dev/null +++ b/src/undo.rs @@ -0,0 +1,314 @@ +use bevy::math::Vec3Swizzles; +use bevy::prelude::*; +use bevy_prototype_lyon::prelude::*; + +use crate::{ShapeData, ImageData, CreateShape, PointSize}; + +#[derive(Debug, Clone)] +pub enum UndoItem { + Base, + CreateShape { entity: Entity }, + DeleteShape { transform: Transform, edges: Vec, name: String, note: String, shape: CreateShape, dm: DrawMode, old_entity: Entity }, + MoveItem { shape: Entity, from: Vec2, to: Vec2 }, + MoveDot { shape_data: Entity, dot: usize, from: Vec2, to: Vec2 }, + Rotate { entity: Entity, from: f32, to: f32 }, + NameChange { entity: Entity, from: String }, + NoteChange { entity: Entity, from: String }, + ChangeZ { entity: Entity, from: f32, to: f32 }, + ReScale { entity: Entity, from: Vec2, to: Vec2 }, + CreateImage { entity: Entity }, + DeleteImage { id: ImageData, transform: Transform, handle: Handle, old_entity: Entity }, +} +pub struct UndoStack { + items: Vec, + current_action: usize, + last_valid: usize, +} + +impl Default for UndoStack { + fn default() -> Self { + Self { items: [UndoItem::Base].to_vec(), current_action: 0, last_valid: 0 } + } +} +impl UndoStack { + pub fn push(&mut self, item: UndoItem) { + bevy::log::info!("PUSHING: {:?}", item); + if self.current_action < self.items.len() - 1 { + // We need to do a "semi push" + self.current_action += 1; + self.items[self.current_action] = item; + } + else { + // We push a completly new item and might need more space! + self.items.push(item); + self.current_action = self.items.len() - 1; + } + self.last_valid = self.current_action; + } + /// Pop the last item in the stack + pub fn pop(&mut self) -> Option<&mut UndoItem> { + // If this happens we are clearly into invalid territory + assert!(self.current_action < self.items.len()); + if self.current_action > 0 { + let item = &mut self.items[self.current_action]; + self.current_action -= 1; + Some(item) + } + else { + None + } + } + // If exists, "unpops" the last popped item + pub fn unpop(&mut self) -> Option<&mut UndoItem> { + if self.current_action < self.last_valid { + self.current_action += 1; + let item = &mut self.items[self.current_action]; + Some(item) + } + else { + None + } + } +} + +pub fn undo_redo_sys( + mut coms: Commands, + mut stack: ResMut, + keyboard: Res>, + p_size: Res, + mut shapes: Query<&mut ShapeData>, + mut transforms: Query<&mut Transform>, + global_transforms: Query<&GlobalTransform>, + mut images: Query<(&mut ImageData, &Handle)>, + mut paths: Query<&mut bevy_prototype_lyon::prelude::Path>, + draw_modes: Query<&DrawMode>, +) { + if keyboard.just_pressed(KeyCode::Z) && keyboard.pressed(KeyCode::LControl) { + let item = if keyboard.pressed(KeyCode::LShift) { stack.unpop() } else { stack.pop() }; + if let Some(item) = item { + match item { + UndoItem::Base => bevy::log::error!("POP/UNPOP: Got UndoItem::Base as a result!"), + UndoItem::CreateShape { entity } => { + let transform = transforms.get(*entity).map(|t| *t).unwrap_or_default(); + let mut sd = shapes.get_mut(*entity).unwrap(); + let mut name = String::new(); + let mut note = String::new(); + std::mem::swap(&mut name, &mut sd.name); + std::mem::swap(&mut note, &mut sd.note); + let edges = sd.edges.iter().map(|e| transforms.get(*e).unwrap().translation.xy()).collect::>(); + let dm = *draw_modes.get(*entity).unwrap(); + let shape = sd.shape; + + coms.entity(*entity).despawn_recursive(); + *item = UndoItem::DeleteShape { transform, edges, name, note, shape, dm, old_entity: *entity }; + + }, + UndoItem::DeleteShape { + transform, + edges, + name, + note, + shape, + dm, + old_entity, + } => { + let main_shape = spawn_main_shape(&mut coms, *dm, *transform, *shape, edges.clone()); + + let dots_dm = DrawMode::Fill(FillMode::color(Color::RED)); + let dot_shape = shapes::Circle { radius: p_size.0, center: Vec2::ZERO }; + let center = coms.spawn_bundle(GeometryBuilder::build_as( + &dot_shape, + dots_dm, + Transform::from_xyz(0.0, 0.0, 0.5) + )).id(); + let edges = edges.iter().map(|v| { + coms.spawn_bundle(GeometryBuilder::build_as( + &dot_shape, + dots_dm, + Transform::from_translation(v.extend(0.5)) + )).id() + }).collect::>(); + + let mut nname = String::new(); + let mut nnote = String::new(); + std::mem::swap(&mut nname, name); + std::mem::swap(&mut nnote, note); + + let mut main_shape = coms.entity(main_shape); + + edges.iter().for_each(|e| { main_shape.add_child(*e); }); + let sd = ShapeData { name: nname, shape: *shape, main_shape: main_shape.id(), edges, center: Some(center), note: nnote }; + main_shape + .insert(sd) + .add_child(center); + + let oe = *old_entity; + *item = UndoItem::CreateShape { entity: main_shape.id() }; + + let f = |ne: &mut Entity| { if *ne == oe { *ne = main_shape.id(); }}; + for i in stack.items.iter_mut() { + match i { + UndoItem::CreateShape { entity } => f(entity), + UndoItem::MoveDot { shape_data, ..} => f(shape_data), + UndoItem::MoveItem { shape, .. } => f(shape), + UndoItem::Rotate { entity, .. } => f(entity), + UndoItem::NameChange { entity, .. } => f(entity), + UndoItem::NoteChange { entity, .. } => f(entity), + UndoItem::ChangeZ { entity, .. } => f(entity), + UndoItem::ReScale { entity, .. } => f(entity), + _ => {}, + } + } + }, + UndoItem::MoveItem { shape, from, to } => { + if let Ok(mut t) = transforms.get_mut(*shape) { + t.translation = from.extend(t.translation.z); + } + let t = *from; + *from = *to; + *to = t; + }, + UndoItem::MoveDot { shape_data, dot, from, to } => { + let sd = shapes.get(*shape_data).unwrap(); + let dot = sd.edges[*dot]; + let gt = global_transforms.get(dot).unwrap(); + + let rot = gt.to_scale_rotation_translation().1; + let ang = rot.to_euler(EulerRot::XYZ).2; + let delta = Mat2::from_angle(-ang) * (*from - gt.translation().xy()); + if let Ok(mut t) = transforms.get_mut(dot) { + t.translation += delta.extend(0.0); + } + // We need to recalculate the center, and update the points to be the new relevant point from the center + let center_offset = crate::modify::calc_shape_center_offset(&transforms, &*sd); + // Update each edge's offset, and then move the main shape's translation + for edge in sd.edges.iter() { + if let Ok(mut t) = transforms.get_mut(*edge) { + t.translation -= center_offset.extend(0.0); + } + } + if let Ok(mut t) = transforms.get_mut(sd.main_shape) { + t.translation += (Mat2::from_angle(ang) * center_offset).extend(0.0); + } + + // Now we need to update the shape itself + crate::modify::update_main_shape(&mut paths, &transforms, &*sd); + + let t = *from; + *from = *to; + *to = t; + }, + UndoItem::Rotate { entity, from, to } => { + if let Ok(mut t) = transforms.get_mut(*entity) { + t.rotation = Quat::from_rotation_z(from.to_radians()); + } + let t = *from; + *from = *to; + *to = t; + }, + UndoItem::NameChange { entity, from } => { + if let Ok(mut sd) = shapes.get_mut(*entity) { + std::mem::swap(&mut sd.name, from); + } + else if let Ok((mut id, _)) = images.get_mut(*entity) { + std::mem::swap(&mut id.name, from); + } + // No need to update the item, so no need to replace + }, + UndoItem::ChangeZ { entity, from, to } => { + if let Ok(mut t) = transforms.get_mut(*entity) { + t.translation.z = *from; + } + let t = *from; + *from = *to; + *to = t; + }, + UndoItem::ReScale { entity, from, to } => { + if let Ok(mut t) = transforms.get_mut(*entity) { + t.scale = from.extend(1.0); + } + let t = *from; + *from = *to; + *to = t; + }, + UndoItem::CreateImage { entity } => { + if let Ok((mut id, handle)) = images.get_mut(*entity) { + let transform = transforms.get(*entity).map(|t| *t).unwrap_or(Transform::default()); + let mut nid = ImageData::default(); + std::mem::swap(&mut *id, &mut nid); + + coms.entity(*entity).despawn_recursive(); + *item = UndoItem::DeleteImage { id: nid, transform, handle: handle.clone(), old_entity: *entity }; + } + }, + UndoItem::DeleteImage { transform, handle, id, old_entity } => { + let mut nid = ImageData::default(); + std::mem::swap(id, &mut nid); + + let e = coms.spawn_bundle(SpriteBundle { + texture: handle.clone(), + transform: *transform, + ..Default::default() + }) + .insert(nid).id(); + + let oe = *old_entity; + *item = UndoItem::CreateImage { entity: e }; + + // Update each item with the same entity id + let f = |ne: &mut Entity| { if *ne == oe { *ne = e; }}; + for i in stack.items.iter_mut() { + match i { + UndoItem::CreateShape { entity } => f(entity), + UndoItem::MoveItem { shape, .. } => f(shape), + UndoItem::Rotate { entity, .. } => f(entity), + UndoItem::NameChange { entity, .. } => f(entity), + UndoItem::NoteChange { entity, .. } => f(entity), + UndoItem::ChangeZ { entity, .. } => f(entity), + UndoItem::ReScale { entity, .. } => f(entity), + _ => {}, + } + } + }, + UndoItem::NoteChange { entity, from } => { + if let Ok(mut sd) = shapes.get_mut(*entity) { + std::mem::swap(&mut sd.note, from); + } + else if let Ok((mut id, _)) = images.get_mut(*entity) { + std::mem::swap(&mut id.note, from); + } + }, + } + } + else { + bevy::log::info!("Nothing to pop/unpop!"); + } + } +} + +fn spawn_main_shape(coms: &mut Commands, dm: DrawMode, trans: Transform, shape: CreateShape, edges: Vec) -> Entity { + match shape { + CreateShape::Triangle => { + coms.spawn_bundle(GeometryBuilder::build_as( + &shapes::Polygon { points: edges, closed: true }, + dm, + trans + )).id() + }, + CreateShape::Square => { + coms.spawn_bundle(GeometryBuilder::build_as( + &shapes::Rectangle { extents: (edges[0] - edges[1]).abs(), ..Default::default() }, + dm, + trans + )).id() + }, + CreateShape::Circle => { + coms.spawn_bundle(GeometryBuilder::build_as( + &shapes::Circle { radius: edges[0].length(), center: Vec2::ZERO }, + dm, + trans + )).id() + }, + CreateShape::Capsule => unimplemented!("I should prob get rid of this..."), + } +} \ No newline at end of file