From 65266fd50bc83558d7301ec7f80a11f48ef2a7c9 Mon Sep 17 00:00:00 2001 From: rewin Date: Thu, 23 Nov 2023 13:41:27 +0300 Subject: [PATCH] First undo/redo logic and background task indicator (#72) * Fix compilation with bevy 0.12 Only compilation, many bugs exists * FIxed omponents name and visible defaults * Fix picking, fix platformer scene * Fix platformer example * Fix physics spawn and simple spawn examples * cargo fmt * cargo clippy * Update Cargo.toml * cargo fmt again * Del shortcut * Added commands to tools and tabs traits * Sorted names in inspector and fix xpbd debug render * cargo fmt * Cargo clippy * Fix compiling without features * Persistance logic * Hide persistance under feature * Cache open states * Loading indicator * Undo redo base * added undo plugin * First worked undo redo for transforms * Merge branches --------- Co-authored-by: a.yamaev --- .gitignore | 1 + Cargo.toml | 2 + src/editor/core/gltf_unpack.rs | 10 +- src/editor/core/mod.rs | 26 ++- src/editor/core/persistance.rs | 300 +++++++++++++++++++++++++ src/editor/core/settings.rs | 1 - src/editor/core/task_storage.rs | 45 ++++ src/editor/core/undo.rs | 373 ++++++++++++++++++++++++++++++++ src/editor/mod.rs | 3 +- src/editor/ui/bot_menu.rs | 17 +- src/editor/ui/change_chain.rs | 43 ++++ src/editor/ui/game_view.rs | 16 +- src/editor/ui/mod.rs | 28 ++- src/editor/ui/settings.rs | 15 ++ 14 files changed, 862 insertions(+), 18 deletions(-) create mode 100644 src/editor/core/persistance.rs delete mode 100644 src/editor/core/settings.rs create mode 100644 src/editor/core/task_storage.rs create mode 100644 src/editor/core/undo.rs create mode 100644 src/editor/ui/change_chain.rs diff --git a/.gitignore b/.gitignore index 12127356..4660861a 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ Cargo.lock docs/.obsidian/* .vscode/launch.json +editor.ron diff --git a/Cargo.toml b/Cargo.toml index 40841e40..9c378d18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ bevy_egui = "0.23" egui-gizmo = "0.12" bevy-scene-hook = "9" ron = "0.8" +serde = "1" bevy_panorbit_camera = "0.9" bevy-inspector-egui = { version = "0.21", features = ["bevy_pbr", "highlight_changes"]} pretty-type-name = "1.0" @@ -48,6 +49,7 @@ opt-level = 3 [features] bevy_xpbd_3d = ["dep:bevy_xpbd_3d", "bevy_xpbd_3d/debug-plugin", "bevy_xpbd_3d/3d", "bevy_xpbd_3d/collider-from-mesh", "bevy_xpbd_3d/f32"] default = ["bevy_xpbd_3d"] +persistance_editor = [] [[example]] name = "platformer" diff --git a/src/editor/core/gltf_unpack.rs b/src/editor/core/gltf_unpack.rs index 560db208..fb7a86f6 100644 --- a/src/editor/core/gltf_unpack.rs +++ b/src/editor/core/gltf_unpack.rs @@ -11,6 +11,8 @@ use crate::{ PrefabMarker, }; +use super::{BackgroundTask, BackgroundTaskStorage}; + #[derive(Event)] pub struct EditorUnpackGltf { pub path: String, @@ -47,9 +49,15 @@ fn unpack_gltf_event( mut events: EventReader, assets: Res, mut queue: ResMut, + mut background_tasks: ResMut, ) { for event in events.read() { - queue.0.push(assets.load(event.path.clone())); + let handle = assets.load(event.path.clone()); + background_tasks.tasks.push(BackgroundTask::AssetLoading( + event.path.clone(), + handle.clone().untyped(), + )); + queue.0.push(handle); } events.clear(); } diff --git a/src/editor/core/mod.rs b/src/editor/core/mod.rs index e22f2716..11981e13 100644 --- a/src/editor/core/mod.rs +++ b/src/editor/core/mod.rs @@ -7,8 +7,18 @@ use load::*; pub mod tool; pub use tool::*; +pub mod task_storage; +pub use task_storage::*; + +pub mod undo; +pub use undo::*; + +#[cfg(feature = "persistance_editor")] +pub mod persistance; +#[cfg(feature = "persistance_editor")] +pub use persistance::*; + pub mod gltf_unpack; -pub mod settings; use bevy::prelude::*; @@ -24,6 +34,12 @@ impl Plugin for EditorCore { fn build(&self, app: &mut App) { app.add_plugins(gltf_unpack::UnpackGltfPlugin); + #[cfg(feature = "persistance_editor")] + app.add_plugins(PersistancePlugin); + + app.add_plugins(BackgroundTaskStoragePlugin); + app.add_plugins(UndoPlugin); + app.add_event::(); app.init_resource::(); @@ -67,12 +83,18 @@ fn editor_event_listener( mut start_game_state: ResMut>, cache: ResMut, mut gltf_events: EventWriter, + mut background_tasks: ResMut, ) { for event in events.read() { match event { EditorEvent::Load(path) => match path { EditorPrefabPath::File(path) => { - load_server.scene = Some(assets.load(path.to_string())) + let handle = assets.load(path.to_string()); + background_tasks.tasks.push(BackgroundTask::AssetLoading( + path.to_string(), + handle.clone().untyped(), + )); + load_server.scene = Some(handle); } EditorPrefabPath::MemoryCahce => { load_server.scene = cache.scene.clone(); diff --git a/src/editor/core/persistance.rs b/src/editor/core/persistance.rs new file mode 100644 index 00000000..9b7d36a4 --- /dev/null +++ b/src/editor/core/persistance.rs @@ -0,0 +1,300 @@ +// This part of code is used for saving and loading settings and window state + +use std::any::TypeId; + +use bevy::{ + prelude::*, + reflect::{ + serde::{ReflectSerializer, TypedReflectDeserializer, UntypedReflectDeserializer}, + GetTypeRegistration, TypeRegistry, + }, + utils::HashMap, + window::WindowCloseRequested, +}; +use ron::{ser::PrettyConfig, *}; +use serde::de::DeserializeSeed; + +pub struct PersistancePlugin; + +#[derive(SystemSet, Hash, PartialEq, Clone, Debug, Eq)] +pub enum PersistanceSet { + EventReader, + ResourceProcess, + Collect, +} + +impl Plugin for PersistancePlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .init_resource::(); + + app.add_event::(); + app.add_event::(); + + app.configure_sets( + Update, + ( + PersistanceSet::EventReader, + PersistanceSet::ResourceProcess, + PersistanceSet::Collect, + ) + .chain(), + ); + + app.add_systems(Startup, persistance_startup_load); + app.add_systems(PreUpdate, persistance_save_on_close); + + app.add_systems( + Update, + persistance_start.in_set(PersistanceSet::EventReader), + ); + app.add_systems(Update, persistance_end.in_set(PersistanceSet::Collect)); + + app.persistance_resource::(); + } +} + +fn persistance_save_on_close( + mut events: EventWriter, + settings: Res, + mut close_events: EventReader, +) { + if settings.save_on_close { + if close_events.read().next().is_some() { + events.send(PersistanceEvent::Save); + } + } +} + +fn persistance_startup_load( + mut events: EventWriter, + settings: Res, +) { + if settings.load_on_startup { + events.send(PersistanceEvent::Load); + } +} + +fn persistance_start( + mut events: EventReader, + mut broadcast: EventWriter, + mut persistance: ResMut, +) { + for event in events.read() { + match event { + PersistanceEvent::Save => { + broadcast.send(PersistanceResourceBroadcastEvent::Pack); + persistance.mode = PersistanceMode::Saving; + persistance.save_counter = 0; + } + PersistanceEvent::Load => { + match &persistance.source { + PersistanceDataSource::File(path) => { + let Ok(file) = std::fs::File::open(path) else { + warn!("Persistance file not found"); + continue; + }; + let data: HashMap = ron::de::from_reader(file).unwrap(); + persistance.data = data; + } + PersistanceDataSource::Memory => { + //do nothing + } + } + + broadcast.send(PersistanceResourceBroadcastEvent::Unpack); + persistance.mode = PersistanceMode::Loading; + persistance.load_counter = 0; + } + } + } +} + +fn persistance_end(mut persistance: ResMut) { + let mode = persistance.mode.clone(); + match mode { + PersistanceMode::Saving => { + persistance.mode = PersistanceMode::None; + if persistance.save_counter != persistance.target_count { + error!( + "Persistance saving error: {} of {} resources were saved", + persistance.save_counter, persistance.target_count + ); + } + + match &persistance.source { + PersistanceDataSource::File(path) => { + let mut file = std::fs::File::create(path).unwrap(); + ron::ser::to_writer_pretty( + &mut file, + &persistance.data, + PrettyConfig::default(), + ) + .unwrap(); + } + PersistanceDataSource::Memory => { + //do nothing + } + } + {} + } + PersistanceMode::Loading => { + persistance.mode = PersistanceMode::None; + if persistance.load_counter == persistance.target_count { + error!( + "Persistance loading error: {} of {} resources were loaded", + persistance.load_counter, persistance.target_count + ); + } + } + _ => {} + } +} + +#[derive(Resource, Reflect)] +#[reflect(Resource)] +pub struct PersistanceSettings { + pub load_on_startup: bool, + pub save_on_close: bool, +} + +impl Default for PersistanceSettings { + fn default() -> Self { + Self { + load_on_startup: true, + save_on_close: true, + } + } +} + +#[derive(Default, Clone)] +enum PersistanceMode { + Saving, + Loading, + #[default] + None, +} + +/// ['PersistanceRegistry'] contains lambda functions for loading/unloading editor state +/// At the moment of closing the window or starting the game mode, +/// all necessary data is saved to a file/memory, and then restored when the editor mode is opened. +/// When the restored resource is loaded, the ['PersistenceLoaded'] event is generated +/// +/// ['PersistenceLoaded']: crate::editor::core::persistance::PersistanceLoaded +#[derive(Resource, Default)] +pub struct PersistanceRegistry { + source: PersistanceDataSource, + data: HashMap, + load_counter: usize, + save_counter: usize, + target_count: usize, + mode: PersistanceMode, +} + +#[derive(Event, Default)] +pub struct PersistanceLoaded { + _phantom: std::marker::PhantomData, +} + +#[derive(Event)] +pub enum PersistanceEvent { + Save, + Load, +} + +#[derive(Event)] +enum PersistanceResourceBroadcastEvent { + Unpack, + Pack, +} + +#[derive(Reflect, Clone)] +#[reflect(Default)] +pub enum PersistanceDataSource { + File(String), + Memory, +} + +impl Default for PersistanceDataSource { + fn default() -> Self { + Self::File("editor.ron".to_string()) + } +} + +pub trait AppPersistanceExt { + fn persistance_resource( + &mut self, + ) -> &mut Self; +} + +impl AppPersistanceExt for App { + fn persistance_resource( + &mut self, + ) -> &mut Self { + self.world + .resource_mut::() + .target_count += 1; + + self.register_type::(); + self.add_event::>(); + + self.add_systems( + Update, + persistance_resource_system::.in_set(PersistanceSet::ResourceProcess), + ); + + self + } +} + +fn persistance_resource_system< + T: Default + Reflect + FromReflect + Resource + GetTypeRegistration, +>( + mut events: EventReader, + mut persistance: ResMut, + mut resource: ResMut, + registry: Res, + mut persistance_loaded: EventWriter>, +) { + for event in events.read() { + match event { + PersistanceResourceBroadcastEvent::Pack => { + let type_registry = registry.read(); + let serializer = ReflectSerializer::new(resource.as_ref(), &type_registry); + let data = ron::to_string(&serializer).unwrap(); + persistance.data.insert( + T::get_type_registration() + .type_info() + .type_path() + .to_string(), + data, + ); + persistance.save_counter += 1; + } + PersistanceResourceBroadcastEvent::Unpack => { + let Some(data) = persistance + .data + .get(T::get_type_registration().type_info().type_path()) + else { + warn!( + "Persistance resource {} not found", + T::get_type_registration().type_info().type_path() + ); + continue; + }; + let type_registry = registry.read(); + let deserializer = UntypedReflectDeserializer::new(&type_registry); + let reflected_value = deserializer + .deserialize(&mut ron::Deserializer::from_str(data).unwrap()) + .unwrap(); + + let converted = ::from_reflect(&*reflected_value).unwrap(); + *resource = converted; + resource.set_changed(); + + persistance_loaded.send(PersistanceLoaded::::default()); + persistance.load_counter += 1; + } + } + } +} diff --git a/src/editor/core/settings.rs b/src/editor/core/settings.rs deleted file mode 100644 index 8b137891..00000000 --- a/src/editor/core/settings.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/editor/core/task_storage.rs b/src/editor/core/task_storage.rs new file mode 100644 index 00000000..e487077d --- /dev/null +++ b/src/editor/core/task_storage.rs @@ -0,0 +1,45 @@ +use bevy::{asset::LoadState, prelude::*}; + +pub struct BackgroundTaskStoragePlugin; + +impl Plugin for BackgroundTaskStoragePlugin { + fn build(&self, app: &mut App) { + app.init_resource::(); + + app.add_systems(PostUpdate, update_storage); + } +} + +#[derive(Resource, Default)] +pub struct BackgroundTaskStorage { + pub tasks: Vec, +} + +pub enum BackgroundTask { + AssetLoading(String, UntypedHandle), + None, +} + +fn update_storage(mut storage: ResMut, assets: Res) { + if storage.tasks.len() > 0 { + let mut need_remove_task = false; + match &storage.tasks[0] { + BackgroundTask::AssetLoading(path, handle) => { + let load_state = assets.get_load_state(handle.id()); + if load_state == Some(LoadState::Loaded) + || load_state == None + || load_state == Some(LoadState::Failed) + { + need_remove_task = true; + } + } + BackgroundTask::None => { + need_remove_task = true; + } + } + + if need_remove_task { + storage.tasks.remove(0); + } + } +} diff --git a/src/editor/core/undo.rs b/src/editor/core/undo.rs new file mode 100644 index 00000000..feca0ff0 --- /dev/null +++ b/src/editor/core/undo.rs @@ -0,0 +1,373 @@ +use std::{fmt::format, sync::Arc}; + +use bevy::{ + prelude::*, + utils::{HashMap, HashSet}, +}; + +use crate::PrefabMarker; + +pub struct UndoPlugin; + +impl Plugin for UndoPlugin { + fn build(&self, app: &mut App) { + app.init_resource::(); + + app.add_event::(); + app.add_event::(); + + app.add_systems( + PostUpdate, + (clear_one_frame_ignore, update_change_chain, undo_redo_logic).chain(), + ); + + app.auto_undo::(); + } +} + +#[derive(Component)] +pub struct OneFrameUndoIgnore { + pub counter: i32, +} + +impl Default for OneFrameUndoIgnore { + fn default() -> Self { + Self { counter: 3 } + } +} + +fn update_change_chain(mut change_chain: ResMut, mut events: EventReader) { + for event in events.read() { + change_chain.changes.push(event.change.clone()); + change_chain.changes_for_redo.clear(); + } +} + +fn clear_one_frame_ignore( + mut commands: Commands, + mut query: Query<(Entity, &mut OneFrameUndoIgnore)>, +) { + for (e, mut ignore) in query.iter_mut() { + ignore.counter -= 1; + if ignore.counter <= 0 { + commands.entity(e).remove::(); + } + } +} + +fn undo_redo_logic(world: &mut World) { + world.resource_scope::, _>(|world, mut events| { + world.resource_scope::(|world, mut change_chain| { + { + let mut reader = events.get_reader(); + for event in reader.read(&events) { + match event { + UndoRedo::Undo => { + if let Some(change) = change_chain.changes.pop() { + change.revert(world, &change_chain.entity_remap).unwrap(); + change_chain.changes_for_redo.push(change); + } + } + UndoRedo::Redo => { + if let Some(redo_change) = change_chain.changes_for_redo.pop() { + redo_change + .apply(world, &change_chain.entity_remap) + .unwrap(); + change_chain.changes.push(redo_change); + } + } + } + } + } + events.clear(); + }); + }); +} + +#[derive(Resource, Default)] +pub struct ChangeChain { + pub changes: Vec>, + pub changes_for_redo: Vec>, + entity_remap: HashMap, +} + +impl ChangeChain { + pub fn undo(&mut self, world: &mut World) { + if let Some(change) = self.changes.pop() { + let res = change.revert(world, &self.entity_remap).unwrap(); + self.changes_for_redo.push(change); + self.update_remap(res); + } + } + + pub fn redo(&mut self, world: &mut World) { + if let Some(change) = self.changes_for_redo.pop() { + let res = change.apply(world, &self.entity_remap).unwrap(); + self.changes.push(change); + self.update_remap(res); + } + } + + fn update_remap(&mut self, result: ChangeResult) { + match result { + ChangeResult::Success => {} + ChangeResult::SuccessWithRemap(new_remap) => { + for (prev, new) in new_remap { + self.entity_remap.insert(prev, new); + } + } + } + } +} + +pub fn get_entity_with_remap( + entity: Entity, + world: &World, + entity_remap: &HashMap, +) -> Entity { + *entity_remap.get(&entity).unwrap_or(&entity) +} + +pub trait EditorChange { + fn revert( + &self, + world: &mut World, + entity_remap: &HashMap, + ) -> Result; + fn apply( + &self, + world: &mut World, + entity_remap: &HashMap, + ) -> Result; + fn debug_text(&self) -> String; +} + +pub enum ChangeResult { + Success, + SuccessWithRemap(Vec<(Entity, Entity)>), +} + +#[derive(Event)] +pub enum UndoRedo { + Undo, + Redo, +} + +#[derive(Event)] +pub struct NewChange { + pub change: Arc, +} + +pub struct ComponentChange { + old_value: T, + new_value: T, + entity: Entity, +} + +impl EditorChange for ComponentChange { + fn revert( + &self, + world: &mut World, + entity_remap: &HashMap, + ) -> Result { + let e = get_entity_with_remap(self.entity, world, entity_remap); + world + .entity_mut(e) + .insert(self.old_value.clone()) + .insert(OneFrameUndoIgnore::default()); + Ok(ChangeResult::Success) + } + + fn apply( + &self, + world: &mut World, + entity_remap: &HashMap, + ) -> Result { + world + .entity_mut(get_entity_with_remap(self.entity, world, entity_remap)) + .insert(self.new_value.clone()) + .insert(OneFrameUndoIgnore::default()); + Ok(ChangeResult::Success) + } + + fn debug_text(&self) -> String { + format!("ComponentChange for entity {:?}", self.entity) + } +} + +pub struct NewEntityChange { + entity: Entity, +} + +impl EditorChange for NewEntityChange { + fn revert( + &self, + world: &mut World, + entity_remap: &HashMap, + ) -> Result { + world + .entity_mut(get_entity_with_remap(self.entity, world, entity_remap)) + .despawn(); + Ok(ChangeResult::Success) + } + + fn apply( + &self, + world: &mut World, + entity_remap: &HashMap, + ) -> Result { + let new_entity = world.spawn_empty().id(); + Ok(ChangeResult::SuccessWithRemap(vec![( + self.entity, + new_entity, + )])) + } + + fn debug_text(&self) -> String { + format!("NewEntityChange for entity {:?}", self.entity) + } +} + +pub struct RemoveEntityChange { + entity: Entity, + scene: DynamicScene, +} + +impl EditorChange for RemoveEntityChange { + fn revert( + &self, + world: &mut World, + entity_remap: &HashMap, + ) -> Result { + let mut entity_map = HashMap::new(); + + self.scene.write_to_world(world, &mut entity_map).unwrap(); + let vec_changes = entity_map + .into_iter() + .map(|(prev, new)| (prev, new)) + .collect::>(); + + Ok(ChangeResult::SuccessWithRemap(vec_changes)) + } + + fn apply( + &self, + world: &mut World, + entity_remap: &HashMap, + ) -> Result { + world + .entity_mut(get_entity_with_remap(self.entity, world, entity_remap)) + .despawn(); + Ok(ChangeResult::Success) + } + + fn debug_text(&self) -> String { + format!("RemoveEntityChange for entity {:?}", self.entity) + } +} + +#[derive(Component)] +pub struct ChangedMarker { + _phantom: std::marker::PhantomData, +} + +impl Default for ChangedMarker { + fn default() -> Self { + Self { + _phantom: std::marker::PhantomData, + } + } +} + +#[derive(Resource)] +pub struct AutoUndoStorage { + pub storage: HashMap, +} + +impl Default for AutoUndoStorage { + fn default() -> Self { + Self { + storage: HashMap::new(), + } + } +} + +pub trait AppAutoUndo { + fn auto_undo(&mut self) -> &mut Self; +} + +impl AppAutoUndo for App { + fn auto_undo(&mut self) -> &mut Self { + self.world.insert_resource(AutoUndoStorage::::default()); + + self.add_systems( + PostUpdate, + ( + auto_undo_update_cache::, + auto_undo_add_init::, + auto_undo_system_changed::, + auto_undo_system::, + ) + .chain(), + ); + + self + } +} + +fn auto_undo_update_cache( + mut storage: ResMut>, + ignored_query: Query<(Entity, &T), With>, +) { + for (e, data) in ignored_query.iter() { + storage.storage.insert(e, data.clone()); + } +} + +fn auto_undo_add_init( + mut storage: ResMut>, + query: Query<(Entity, &T), (With, Added, Without)>, +) { + for (e, data) in query.iter() { + storage.storage.insert(e, data.clone()); + } +} + +fn auto_undo_system_changed( + mut commands: Commands, + mut storage: ResMut>, + query: Query, Changed, Without)>, +) { + for entity in query.iter() { + commands + .entity(entity) + .insert(ChangedMarker::::default()); + } +} + +fn auto_undo_system( + mut commands: Commands, + mut storage: ResMut>, + mut query: Query<(Entity, &mut T), With>>, + mut new_change: EventWriter, +) { + for (e, data) in query.iter_mut() { + if !data.is_changed() { + commands.entity(e).remove::>(); + + if let Some(prev_value) = storage.storage.get(&e) { + new_change.send(NewChange { + change: Arc::new(ComponentChange { + old_value: prev_value.clone(), + new_value: data.clone(), + entity: e, + }), + }); + info!("Auto undo change for entity {:?}", e); + } else { + } + + storage.storage.insert(e, data.clone()); + } + } +} diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 8eb6df29..a6e7a089 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -41,6 +41,7 @@ impl Plugin for EditorPlugin { if !app.is_plugin_added::() { app.add_plugins(bevy_egui::EguiPlugin); } + app.add_plugins(core::EditorCore); #[cfg(feature = "bevy_xpbd_3d")] { @@ -68,8 +69,6 @@ impl Plugin for EditorPlugin { app.insert_resource(PanOrbitEnabled(true)); - app.add_plugins(core::EditorCore); - app.add_systems( Startup, (set_start_state, apply_state_transition::) diff --git a/src/editor/ui/bot_menu.rs b/src/editor/ui/bot_menu.rs index ec406a76..8a7131c5 100644 --- a/src/editor/ui/bot_menu.rs +++ b/src/editor/ui/bot_menu.rs @@ -2,7 +2,7 @@ use bevy::prelude::*; use bevy_egui::*; use crate::{ - editor::core::{EditorEvent, EditorPrefabPath}, + editor::core::{BackgroundTask, BackgroundTaskStorage, EditorEvent, EditorPrefabPath}, prefab::PrefabPlugin, EditorSet, EditorState, }; @@ -65,6 +65,7 @@ pub fn bot_menu( mut events: EventReader, mut menu_state: ResMut, mut editor_events: EventWriter, + background_tasks: Res, ) { let ctx = ctxs.ctx_mut(); egui::TopBottomPanel::bottom("bot menu").show(ctx, |ui| { @@ -181,6 +182,20 @@ pub fn bot_menu( if ui.button("▶").clicked() { editor_events.send(EditorEvent::StartGame); } + + ui.with_layout(egui::Layout::right_to_left(egui::Align::RIGHT), |ui| { + if background_tasks.tasks.len() > 0 { + //Spinning circle + ui.spinner(); + + match &background_tasks.tasks[0] { + BackgroundTask::AssetLoading(path, _) => { + ui.label(format!("Loading {}", path)); + } + BackgroundTask::None => {} + } + } + }); }); }); diff --git a/src/editor/ui/change_chain.rs b/src/editor/ui/change_chain.rs new file mode 100644 index 00000000..17a6e09b --- /dev/null +++ b/src/editor/ui/change_chain.rs @@ -0,0 +1,43 @@ +use bevy::prelude::*; + +use super::{editor_tab::EditorTab, EditorUiAppExt}; +use crate::editor::core::ChangeChain; + +pub struct ChangeChainViewPlugin; + +impl Plugin for ChangeChainViewPlugin { + fn build(&self, app: &mut App) { + app.editor_tab_by_trait( + super::editor_tab::EditorTabName::Other("Change Chain".to_string()), + ChangeChainView::default(), + ); + } +} + +#[derive(Resource)] +pub struct ChangeChainView {} + +impl Default for ChangeChainView { + fn default() -> Self { + Self {} + } +} + +impl EditorTab for ChangeChainView { + fn ui( + &mut self, + ui: &mut bevy_egui::egui::Ui, + commands: &mut bevy::prelude::Commands, + world: &mut bevy::prelude::World, + ) { + let change_chain = world.resource::(); + + for change in change_chain.changes.iter() { + ui.label(change.debug_text()); + } + } + + fn title(&self) -> bevy_egui::egui::WidgetText { + "Change Chain".into() + } +} diff --git a/src/editor/ui/game_view.rs b/src/editor/ui/game_view.rs index 79e39da1..b88681ee 100644 --- a/src/editor/ui/game_view.rs +++ b/src/editor/ui/game_view.rs @@ -2,7 +2,11 @@ use bevy::{prelude::*, window::PrimaryWindow}; use bevy_egui::egui::{self}; use egui_gizmo::GizmoMode; -use crate::{editor::core::EditorTool, prelude::EditorTab, EditorCameraMarker}; +use crate::{ + editor::core::{EditorTool, UndoRedo}, + prelude::EditorTab, + EditorCameraMarker, +}; #[derive(Resource)] pub struct GameViewTab { @@ -27,6 +31,16 @@ impl Default for GameViewTab { impl EditorTab for GameViewTab { fn ui(&mut self, ui: &mut bevy_egui::egui::Ui, commands: &mut Commands, world: &mut World) { + if ui.input_mut(|i| i.key_released(egui::Key::Z) && i.modifiers.ctrl && !i.modifiers.shift) + { + world.send_event(UndoRedo::Undo); + info!("Undo command"); + } + if ui.input_mut(|i| i.key_released(egui::Key::Z) && i.modifiers.ctrl && i.modifiers.shift) { + world.send_event(UndoRedo::Redo); + info!("Redo command"); + } + self.viewport_rect = Some(ui.clip_rect()); //Draw FPS diff --git a/src/editor/ui/mod.rs b/src/editor/ui/mod.rs index 55e53bab..a8632e2f 100644 --- a/src/editor/ui/mod.rs +++ b/src/editor/ui/mod.rs @@ -25,6 +25,9 @@ pub use settings::*; pub mod tools; pub use tools::*; +pub mod change_chain; +pub use change_chain::*; + pub mod debug_panels; use bevy::{ecs::system::CommandQueue, prelude::*, utils::HashMap, window::PrimaryWindow}; @@ -35,7 +38,7 @@ use crate::{EditorSet, EditorState}; use self::tools::gizmo::GizmoTool; use super::{ - core::{SelectedPlugin, ToolExt}, + core::{SelectedPlugin, ToolExt, UndoRedo}, update_pan_orbit, }; @@ -99,6 +102,7 @@ impl Plugin for EditorUiPlugin { app.world.resource_mut::().active_tool = Some(0); app.add_plugins(settings::SettingsWindowPlugin); + app.add_plugins(ChangeChainViewPlugin); if self.use_standard_layout { let mut editor = app.world.resource_mut::(); @@ -183,14 +187,16 @@ impl EditorUi { let cell = world.as_unsafe_world_cell(); let mut command_queue = CommandQueue::default(); - let mut commands = unsafe { Commands::new(&mut command_queue, cell.world()) }; - - let mut tab_viewer = EditorTabViewer { - commands: &mut commands, - world: unsafe { cell.world_mut() }, - registry: &mut self.registry, - visible, - tab_commands: vec![], + let mut commands = Commands::new(&mut command_queue, unsafe { cell.world() }); + + let mut tab_viewer = unsafe { + EditorTabViewer { + commands: &mut commands, + world: cell.world_mut(), + registry: &mut self.registry, + visible, + tab_commands: vec![], + } }; DockArea::new(&mut self.tree) @@ -215,7 +221,9 @@ impl EditorUi { } } - command_queue.apply(unsafe { cell.world_mut() }); + unsafe { + command_queue.apply(cell.world_mut()); + } } } diff --git a/src/editor/ui/settings.rs b/src/editor/ui/settings.rs index 2e50e184..b7096693 100644 --- a/src/editor/ui/settings.rs +++ b/src/editor/ui/settings.rs @@ -3,6 +3,9 @@ use bevy_egui::*; use crate::prelude::{EditorTab, EditorTabName}; +#[cfg(feature = "persistance_editor")] +use crate::prelude::editor::core::AppPersistanceExt; + use super::EditorUiAppExt; pub struct SettingsWindowPlugin; @@ -10,6 +13,18 @@ pub struct SettingsWindowPlugin; impl Plugin for SettingsWindowPlugin { fn build(&self, app: &mut App) { app.editor_tab_by_trait(EditorTabName::Settings, SettingsWindow::default()); + + #[cfg(feature = "bevy_xpbd_3d")] + { + #[cfg(feature = "persistance_editor")] + { + app.persistance_resource::(); + app.register_type::>(); + app.register_type::>(); + app.register_type::>(); + app.register_type::<[f32; 4]>(); + } + } } }