Skip to content
82 changes: 77 additions & 5 deletions rmf_site_editor/src/site/change_plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@

use crate::site::SiteUpdateSet;
use bevy::prelude::*;
use std::fmt::Debug;
use std::{fmt::Debug, sync::Arc};

use super::{UndoBuffer, UndoEvent};

/// The Change component is used as an event to indicate that the value of a
/// component should change for some entity. Using these events instead of
Expand Down Expand Up @@ -58,28 +60,98 @@ impl<T: Component + Clone + Debug> Default for ChangePlugin<T> {
}
}

/// This is a changelog used for the undo/redo system
struct ChangeLog<T: Component + Clone + Debug>
{
entity: Entity,
from: Option<T>,
to: T
}

#[derive(Resource)]
struct ChangeHistory<T: Component + Clone + Debug> {
pub(crate) revisions: std::collections::HashMap<usize, ChangeLog<T>>
}

impl<T: Component + Clone + Debug> Default for ChangeHistory<T> {
fn default() -> Self {
Self {
revisions: Default::default(),
}
}
}


impl<T: Component + Clone + Debug> Plugin for ChangePlugin<T> {
fn build(&self, app: &mut App) {
app.add_event::<Change<T>>().add_systems(
app.add_event::<Change<T>>()
.init_resource::<ChangeHistory<T>>()
.add_systems(
PreUpdate,
update_changed_values::<T>.in_set(SiteUpdateSet::ProcessChanges),
(update_changed_values::<T>.in_set(SiteUpdateSet::ProcessChanges),
undo_change::<T>.in_set(SiteUpdateSet::ProcessChanges)) // TODO do this on another stage

);
}
}

fn undo_change<T: Component + Clone + Debug>(
mut commands: Commands,
mut values: Query<&mut T>,
change_history: ResMut<ChangeHistory<T>>,
mut undo_cmds: EventReader<UndoEvent>,
) {
for undo in undo_cmds.read() {
let Some(change) = change_history.revisions.get(&undo.action_id) else {
continue;
};

if let Ok(mut component_to_change) = values.get_mut(change.entity) {
if let Some(old_value) = &change.from {
*component_to_change = old_value.clone();
}
else {
commands.entity(change.entity).remove::<T>();
}
}
else {
error!("Undo history corrupted.");
}
}
}

fn update_changed_values<T: Component + Clone + Debug>(
mut commands: Commands,
mut values: Query<&mut T>,
mut changes: EventReader<Change<T>>,
mut undo_buffer: ResMut<UndoBuffer>,
mut change_history: ResMut<ChangeHistory<T>>
) {
for change in changes.read() {
if let Ok(mut new_value) = values.get_mut(change.for_element) {
*new_value = change.to_value.clone();
if let Ok(mut component_to_change) = values.get_mut(change.for_element) {
change_history.revisions.insert(
undo_buffer.get_next_revision(),
ChangeLog {
entity: change.for_element,
to: change.to_value.clone(),
from: Some(component_to_change.clone())
}

);
*component_to_change = change.to_value.clone();
} else {
if change.allow_insert {
commands
.entity(change.for_element)
.insert(change.to_value.clone());
change_history.revisions.insert(
undo_buffer.get_next_revision(),
ChangeLog {
entity: change.for_element,
to: change.to_value.clone(),
from: None
}
);
} else {
error!(
"Unable to change {} data to {:?} for entity {:?} \
Expand Down
4 changes: 4 additions & 0 deletions rmf_site_editor/src/site/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ pub use site_visualizer::*;
pub mod texture;
pub use texture::*;

pub mod undo_plugin;
pub use undo_plugin::*;

pub mod util;
pub use util::*;

Expand Down Expand Up @@ -212,6 +215,7 @@ impl Plugin for SitePlugin {
ChangePlugin::<Scale>::default(),
ChangePlugin::<Distance>::default(),
ChangePlugin::<Texture>::default(),
UndoPlugin::default()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: Move to lib.rs in case we want to run in headless mode

))
.add_plugins((
ChangePlugin::<DoorType>::default(),
Expand Down
77 changes: 77 additions & 0 deletions rmf_site_editor/src/site/undo_plugin.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
use bevy::{ecs::event, prelude::*};
use bevy_impulse::event_streaming_service;
use std::fmt::Debug;

use crate::{EditMenu, MenuEvent, MenuItem};

#[derive(Event, Debug, Clone, Copy)]
pub struct UndoEvent {
pub action_id: usize
}

#[derive(Resource)]
struct UndoMenu{
menu_entity: Entity
}
///TODO(arjo): Decouple
impl FromWorld for UndoMenu {
fn from_world(world: &mut World) -> Self {
let undo_label = world.spawn(
MenuItem::Text("Undo".into())).id();
let edit_menu = world.resource::<EditMenu>().get();
world.entity_mut(edit_menu).push_children(&[undo_label]);
Self {
menu_entity: undo_label
}
}
}

fn watch_undo_click(
mut reader: EventReader<MenuEvent>,
menu_handle: Res<UndoMenu>,
mut undo_buffer: ResMut<UndoBuffer>,
mut event_writer: EventWriter<UndoEvent>)
{
for menu_click in reader.read() {
if menu_click.clicked() && menu_click.source() == menu_handle.menu_entity {
let Some(undo_item) = undo_buffer.undo_last() else {
continue;
};
event_writer.send(UndoEvent {
action_id: undo_item
});
}
}
}

#[derive(Resource, Default)]
pub struct UndoBuffer {
pub prev_value: usize,
pub undo_stack: Vec<usize>
}

impl UndoBuffer {
pub fn get_next_revision(&mut self) -> usize {
self.prev_value += 1;
self.undo_stack.push(self.prev_value);
self.prev_value
}

pub fn undo_last(&mut self) -> Option<usize> {
self.undo_stack.pop()
}
}

#[derive(Default)]
pub struct UndoPlugin;

impl Plugin for UndoPlugin {
fn build(&self, app: &mut App) {
app
.init_resource::<EditMenu>()
.init_resource::<UndoMenu>()
.init_resource::<UndoBuffer>()
.add_event::<UndoEvent>()
.add_systems(Update,(watch_undo_click));
}
}
24 changes: 24 additions & 0 deletions rmf_site_editor/src/widgets/menu_bar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,29 @@ impl FromWorld for ViewMenu {
}
}

#[derive(Resource)]
pub struct EditMenu {
/// Map of menu items
menu_item: Entity,
}

impl EditMenu {
pub fn get(&self) -> Entity {
return self.menu_item;
}
}

impl FromWorld for EditMenu {
fn from_world(world: &mut World) -> Self {
let menu_item = world
.spawn(Menu {
text: "Edit".to_string(),
})
.id();
Self { menu_item }
}
}

#[non_exhaustive]
#[derive(Event)]
pub enum MenuEvent {
Expand Down Expand Up @@ -304,6 +327,7 @@ fn top_menu_bar(
true,
);
});

ui.menu_button("View", |ui| {
render_sub_menu(
ui,
Expand Down
Loading