undo/redo - 50%
This commit is contained in:
parent
0ddb8bdba4
commit
92e3b9f620
7 changed files with 197 additions and 8 deletions
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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::*;
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
42
src/ui.rs
42
src/ui.rs
|
@ -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
123
src/undo.rs
Normal 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!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue