undo/redo - 50%

This commit is contained in:
RustyStriker 2022-09-05 19:42:40 +03:00
parent 0ddb8bdba4
commit 92e3b9f620
7 changed files with 197 additions and 8 deletions

View file

@ -13,7 +13,7 @@ When an item is selected in the items tree window(will be written in gold), clic
## TODO ## TODO
- [x] Pick default color for shapes - [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 - [ ] Select item by clicking/double clicking on the relevant shape/image
## Quality of life todo ## Quality of life todo

View file

@ -5,6 +5,7 @@ use bevy_prototype_lyon::prelude::*;
pub fn create_sys( pub fn create_sys(
mut coms: Commands, mut coms: Commands,
mut undo: ResMut<undo::UndoStack>,
p_size: Res<PointSize>, p_size: Res<PointSize>,
default_color: Res<DefaultColor>, default_color: Res<DefaultColor>,
state: Res<UiState>, state: Res<UiState>,
@ -60,6 +61,9 @@ pub fn create_sys(
ms.add_child(*e); ms.add_child(*e);
} }
ms.insert(s); ms.insert(s);
undo.push(UndoItem::CreateShape { entity: ms.id() });
*shape = None; *shape = None;
} }
} }

View file

@ -9,6 +9,8 @@ pub mod modify;
pub mod ui; pub mod ui;
pub mod infinite_grid; pub mod infinite_grid;
pub mod export; pub mod export;
pub mod undo;
pub use undo::UndoItem;
pub use modify::modify_sys; pub use modify::modify_sys;
pub use create::create_sys; pub use create::create_sys;
pub use helpers::*; pub use helpers::*;

View file

@ -29,6 +29,7 @@ fn main() {
.insert_resource(ui::SelectedItem::default()) .insert_resource(ui::SelectedItem::default())
.insert_resource(ShouldImportExport::default()) .insert_resource(ShouldImportExport::default())
.insert_resource(DefaultColor(Color::rgba(0.0, 0.5, 0.5, 0.4))) .insert_resource(DefaultColor(Color::rgba(0.0, 0.5, 0.5, 0.4)))
.insert_resource(undo::UndoStack::default())
; ;
app app
@ -72,6 +73,7 @@ fn main() {
.add_system(export::import_sys.with_run_criteria(|ie: Res<ShouldImportExport>| { .add_system(export::import_sys.with_run_criteria(|ie: Res<ShouldImportExport>| {
if ie.import { ShouldRun::Yes} else { ShouldRun::No } if ie.import { ShouldRun::Yes} else { ShouldRun::No }
})) }))
.add_system(undo::undo_redo_sys)
; ;
app.run(); app.run();

View file

@ -16,6 +16,8 @@ pub enum Holding {
pub fn modify_sys( pub fn modify_sys(
// Which entity the user currently drags, and which specific part of it(ShapeData entity, path entity) // Which entity the user currently drags, and which specific part of it(ShapeData entity, path entity)
mut holding: Local<Holding>, mut holding: Local<Holding>,
mut held_from: Local<Vec2>,
mut undo: ResMut<undo::UndoStack>,
mouse: Res<Input<MouseButton>>, mouse: Res<Input<MouseButton>>,
wnds: Res<Windows>, wnds: Res<Windows>,
scale: Query<&OrthographicProjection, With<MainCamera>>, scale: Query<&OrthographicProjection, With<MainCamera>>,
@ -38,8 +40,8 @@ pub fn modify_sys(
for (e, sd) in shapes.iter() { for (e, sd) in shapes.iter() {
if let Ok(t) = gtransforms.get(sd.center.unwrap()) { if let Ok(t) = gtransforms.get(sd.center.unwrap()) {
let t = t.translation().xy(); let t = t.translation().xy();
if (mouse_pos - t).length_squared() < (p_size.0 * scale).powi(2) { if (mouse_pos - t).length_squared() < (p_size.0 * scale).powi(2) {
*held_from = t;
*holding = Holding::Shape(e, sd.center.unwrap()); *holding = Holding::Shape(e, sd.center.unwrap());
break; break;
} }
@ -49,6 +51,7 @@ pub fn modify_sys(
let t = t.translation().xy(); let t = t.translation().xy();
if (mouse_pos - t).length_squared() < (p_size.0 * scale).powi(2) { if (mouse_pos - t).length_squared() < (p_size.0 * scale).powi(2) {
*held_from = t;
*holding = Holding::Shape(e, *edge); *holding = Holding::Shape(e, *edge);
break; break;
} }
@ -65,6 +68,7 @@ pub fn modify_sys(
// Disregard rotations for now plz // Disregard rotations for now plz
let diff = (mouse_pos - t.translation().xy()).abs(); let diff = (mouse_pos - t.translation().xy()).abs();
if diff.x < size.x * scale.x && diff.y < size.y * scale.y { 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()); *holding = Holding::Image(e, mouse_pos - t.translation().xy());
break; break;
} }
@ -73,6 +77,28 @@ pub fn modify_sys(
} }
} }
else if mouse.just_released(MouseButton::Left) && *holding != Holding::None { else if mouse.just_released(MouseButton::Left) && *holding != Holding::None {
match *holding {
Holding::Shape(shape_data, dot) => {
if let Ok(sd) = shapes.get_component::<ShapeData>(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 *holding = Holding::None; // We just released our sad little dot/shape
} }
else if let Holding::Shape(se, pe) = *holding { else if let Holding::Shape(se, pe) = *holding {

View file

@ -206,8 +206,10 @@ pub fn items_tree_sys(
} }
pub fn inspector_sys( pub fn inspector_sys(
mut selected: ResMut<SelectedItem>, // Which item is currently selected
mut coms: Commands, // For deletion! mut coms: Commands, // For deletion!
mut changes: Local<f32>,
mut undo: ResMut<undo::UndoStack>,
mut selected: ResMut<SelectedItem>, // Which item is currently selected
mut egui_ctx: ResMut<EguiContext>, mut egui_ctx: ResMut<EguiContext>,
mut shapes: Query<&mut ShapeData>, mut shapes: Query<&mut ShapeData>,
mut transforms: Query<&mut Transform>, mut transforms: Query<&mut Transform>,
@ -231,20 +233,43 @@ pub fn inspector_sys(
if let Ok(mut sd) = shapes.get_mut(e) { if let Ok(mut sd) = shapes.get_mut(e) {
ui.horizontal(|hui| { ui.horizontal(|hui| {
hui.label("Name:"); 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(); ui.separator();
if let Ok(mut t) = transforms.get_mut(sd.main_shape) { if let Ok(mut t) = transforms.get_mut(sd.main_shape) {
ui.horizontal(|hui| { ui.horizontal(|hui| {
hui.label("Translation:"); hui.label("Translation:");
hui.add(egui::DragValue::new(&mut t.translation.x)); let xdrag = hui.add(egui::DragValue::new(&mut t.translation.x));
hui.add(egui::DragValue::new(&mut t.translation.y)); 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| { ui.horizontal(|hui| {
hui.label("Rotation:"); hui.label("Rotation:");
let mut rot = t.rotation.to_euler(EulerRot::XYZ).2.to_degrees(); 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()); t.rotation = Quat::from_rotation_z(rot.to_radians());
} }
hui.label("Z:"); hui.label("Z:");
@ -354,3 +379,10 @@ pub fn inspector_sys(
} }
} }
} }
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()
}

123
src/undo.rs Normal file
View file

@ -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<Vec2>, transform: Transform, color: Color, note: String },
}
pub struct UndoStack {
items: Vec<UndoItem>,
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<UndoStack>,
keyboard: Res<Input<KeyCode>>,
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!");
}
}
}