From 92e3b9f620fa9b98b2016b78c928a5b2f7307662 Mon Sep 17 00:00:00 2001 From: RustyStriker Date: Mon, 5 Sep 2022 19:42:40 +0300 Subject: [PATCH] undo/redo - 50% --- README.md | 2 +- src/create.rs | 4 ++ src/lib.rs | 2 + src/main.rs | 2 + src/modify.rs | 28 +++++++++++- src/ui.rs | 44 +++++++++++++++--- src/undo.rs | 123 ++++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 197 insertions(+), 8 deletions(-) create mode 100644 src/undo.rs diff --git a/README.md b/README.md index b216275..505028b 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ When an item is selected in the items tree window(will be written in gold), clic ## TODO - [x] Pick default color for shapes -- [ ] Undo/Redo history +- [ ] Undo/Redo history(roughly 50%) - [ ] Select item by clicking/double clicking on the relevant shape/image ## Quality of life todo diff --git a/src/create.rs b/src/create.rs index a25d395..8de5de7 100644 --- a/src/create.rs +++ b/src/create.rs @@ -5,6 +5,7 @@ use bevy_prototype_lyon::prelude::*; pub fn create_sys( mut coms: Commands, + mut undo: ResMut, p_size: Res, default_color: Res, state: Res, @@ -60,6 +61,9 @@ pub fn create_sys( ms.add_child(*e); } ms.insert(s); + + undo.push(UndoItem::CreateShape { entity: ms.id() }); + *shape = None; } } diff --git a/src/lib.rs b/src/lib.rs index 3e289bd..0e55a56 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,8 @@ 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::*; diff --git a/src/main.rs b/src/main.rs index bf01060..eb0c84f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,6 +29,7 @@ fn main() { .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 @@ -72,6 +73,7 @@ fn main() { .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(); diff --git a/src/modify.rs b/src/modify.rs index 553de11..4c5c7d9 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::MoveShape { shape: shape_data, from: *held_from, to: snap.snap_to_grid(mouse_pos) }); + } + else { + undo.push(UndoItem::MoveDot { + shape_data, + dot, + 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::MoveShape { 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 0cea441..263abb9 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -206,8 +206,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, + 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>, @@ -231,20 +233,43 @@ pub fn inspector_sys( if let Ok(mut sd) = shapes.get_mut(e) { ui.horizontal(|hui| { hui.label("Name:"); - hui.text_edit_singleline(&mut sd.name); + let te = hui.text_edit_singleline(&mut sd.name); + if te.lost_focus() { + bevy::log::info!("stopped editing the name!"); + // TODO have name editing please + } }); ui.separator(); if let Ok(mut t) = transforms.get_mut(sd.main_shape) { ui.horizontal(|hui| { hui.label("Translation:"); - hui.add(egui::DragValue::new(&mut t.translation.x)); - hui.add(egui::DragValue::new(&mut t.translation.y)); - + let xdrag = hui.add(egui::DragValue::new(&mut t.translation.x)); + if started_edit(&xdrag) { + *changes = t.translation.x; + } + else if stopped_edit(&xdrag) { + undo.push(UndoItem::MoveShape { shape: e, from: Vec2::new(*changes, t.translation.y), to: t.translation.xy() }); + } + let ydrag = hui.add(egui::DragValue::new(&mut t.translation.y)); + if started_edit(&ydrag) { + *changes = t.translation.y; + } + else if stopped_edit(&ydrag) { + undo.push(UndoItem::MoveShape { shape: e, from: Vec2::new(t.translation.x , *changes), to: t.translation.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() { + let drag = hui.add(egui::DragValue::new(&mut rot).suffix("°")); + if started_edit(&drag) { + *changes = rot; + } + if stopped_edit(&drag) { + undo.push(UndoItem::Rotate { entity: e, from: *changes, to: rot }); + } + if drag.changed() { t.rotation = Quat::from_rotation_z(rot.to_radians()); } hui.label("Z:"); @@ -353,4 +378,11 @@ pub fn inspector_sys( **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..2eac0e8 --- /dev/null +++ b/src/undo.rs @@ -0,0 +1,123 @@ +use bevy::prelude::*; + +use crate::{CreateShape, ShapeData}; + +#[derive(Debug, Clone)] +pub enum UndoItem { + Base, + CreateShape { entity: Entity }, + MoveDot { shape_data: Entity, dot: Entity, from: Vec2, to: Vec2 }, + MoveShape { shape: Entity, from: Vec2, to: Vec2 }, + Rotate { entity: Entity, from: f32, to: f32 }, + NameChange { entity: Entity, from: String }, + DeleteShape { name: String, shape: CreateShape, edges: Vec, transform: Transform, color: Color, note: String }, +} +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>, + mut shapes: Query<&mut ShapeData>, + mut transforms: Query<&mut Transform>, +) { + 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 } => { + bevy::log::error!("NOT IMPLEMENTED : CreateShape"); + }, + UndoItem::MoveDot { shape_data, dot, from, to } => { + bevy::log::error!("NOT IMPLEMENTED : MoveDot"); + }, + UndoItem::MoveShape { 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::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); + } + // No need to update the item, so no need to replace + }, + UndoItem::DeleteShape { + name, + shape, + edges, + transform, + color, + note + } => { + bevy::log::error!("NOT IMPLEMENTED : DeleteShape"); + }, + } + } + else { + bevy::log::info!("Nothing to pop/unpop!"); + } + } +} \ No newline at end of file