diff --git a/Cargo.toml b/Cargo.toml index 8752c13..87539cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,14 +11,15 @@ description = "In-App editor tools for bevy apps" readme = "README.md" [workspace.dependencies] -bevy_editor_pls = { version = "0.8.0", path = "crates/bevy_editor_pls" } -bevy_editor_pls_core = { version = "0.8.0", path = "crates/bevy_editor_pls_core" } -bevy_editor_pls_default_windows = { version = "0.8.0", path = "crates/bevy_editor_pls_default_windows" } +bevy_editor_pls = { version = "0.8.1", path = "crates/bevy_editor_pls" } +bevy_editor_pls_core = { version = "0.8.1", path = "crates/bevy_editor_pls_core" } +bevy_editor_pls_default_windows = { version = "0.8.1", path = "crates/bevy_editor_pls_default_windows" } -bevy-inspector-egui = "0.23.0" -egui = "0.26" -egui_dock = "0.11" -egui-gizmo = "0.16" +bevy-inspector-egui = "0.24.0" +egui = "0.27" +egui_dock = "0.12" +# used to be egui-gizmo 0.16 +transform-gizmo-bevy = "0.2" [profile.dev.package."*"] opt-level = 2 diff --git a/crates/bevy_editor_pls/Cargo.toml b/crates/bevy_editor_pls/Cargo.toml index 3655b2e..3b32f21 100644 --- a/crates/bevy_editor_pls/Cargo.toml +++ b/crates/bevy_editor_pls/Cargo.toml @@ -24,7 +24,7 @@ bevy_editor_pls_core.workspace = true bevy_editor_pls_default_windows = { workspace = true, optional = true } bevy = { version = "0.13", default-features = false, features = ["x11"] } egui.workspace = true -egui-gizmo.workspace = true +transform-gizmo-bevy.workspace = true # bevy_framepace = { version = "0.12", default-features = false } [dev-dependencies] diff --git a/crates/bevy_editor_pls/src/controls.rs b/crates/bevy_editor_pls/src/controls.rs index 78e0f74..01f9429 100644 --- a/crates/bevy_editor_pls/src/controls.rs +++ b/crates/bevy_editor_pls/src/controls.rs @@ -136,6 +136,8 @@ pub enum Action { PauseUnpauseTime, FocusSelected, + // maybe investigate [GizmoOptions].hotkeys + // https://docs.rs/transform-gizmo-bevy/latest/transform_gizmo_bevy/struct.GizmoHotkeys.html #[cfg(feature = "default_windows")] SetGizmoModeTranslate, #[cfg(feature = "default_windows")] @@ -240,7 +242,7 @@ pub fn editor_controls_system( editor .window_state_mut::() .unwrap() - .gizmo_mode = egui_gizmo::GizmoMode::Translate; + .gizmo_modes = transform_gizmo_bevy::GizmoMode::all_translate(); } if controls.just_pressed( Action::SetGizmoModeRotate, @@ -251,7 +253,7 @@ pub fn editor_controls_system( editor .window_state_mut::() .unwrap() - .gizmo_mode = egui_gizmo::GizmoMode::Rotate; + .gizmo_modes = transform_gizmo_bevy::GizmoMode::all_rotate(); } if controls.just_pressed( Action::SetGizmoModeScale, @@ -262,7 +264,7 @@ pub fn editor_controls_system( editor .window_state_mut::() .unwrap() - .gizmo_mode = egui_gizmo::GizmoMode::Scale; + .gizmo_modes = transform_gizmo_bevy::GizmoMode::all_scale(); } } } diff --git a/crates/bevy_editor_pls/src/lib.rs b/crates/bevy_editor_pls/src/lib.rs index f479693..13c3db7 100644 --- a/crates/bevy_editor_pls/src/lib.rs +++ b/crates/bevy_editor_pls/src/lib.rs @@ -128,6 +128,11 @@ impl Plugin for EditorPlugin { app.add_plugins(bevy::pbr::wireframe::WireframePlugin); + // required for the GizmoWindow + if !app.is_plugin_added::() { + app.add_plugins(transform_gizmo_bevy::TransformGizmoPlugin); + } + app.insert_resource(controls::EditorControls::default_bindings()) .add_systems(Update, controls::editor_controls_system); diff --git a/crates/bevy_editor_pls_core/src/editor_window.rs b/crates/bevy_editor_pls_core/src/editor_window.rs index 89dde47..aac990e 100644 --- a/crates/bevy_editor_pls_core/src/editor_window.rs +++ b/crates/bevy_editor_pls_core/src/editor_window.rs @@ -13,6 +13,7 @@ pub trait EditorWindow: 'static { const DEFAULT_SIZE: (f32, f32) = (0.0, 0.0); fn ui(world: &mut World, cx: EditorWindowContext, ui: &mut egui::Ui); + /// Ui shown in the `Open Window` menu item. By default opens the window as a floating window. fn menu_ui(world: &mut World, mut cx: EditorWindowContext, ui: &mut egui::Ui) { let _ = world; diff --git a/crates/bevy_editor_pls_default_windows/Cargo.toml b/crates/bevy_editor_pls_default_windows/Cargo.toml index 203cb72..18c0095 100644 --- a/crates/bevy_editor_pls_default_windows/Cargo.toml +++ b/crates/bevy_editor_pls_default_windows/Cargo.toml @@ -31,4 +31,4 @@ indexmap = "2" pretty-type-name = "1.0" bevy_mod_debugdump = "0.10" opener = "0.6.0" -egui-gizmo.workspace = true +transform-gizmo-bevy.workspace = true diff --git a/crates/bevy_editor_pls_default_windows/src/cameras/mod.rs b/crates/bevy_editor_pls_default_windows/src/cameras/mod.rs index 56894ec..5982018 100644 --- a/crates/bevy_editor_pls_default_windows/src/cameras/mod.rs +++ b/crates/bevy_editor_pls_default_windows/src/cameras/mod.rs @@ -13,6 +13,7 @@ use bevy_editor_pls_core::{ Editor, EditorEvent, }; use bevy_inspector_egui::egui; +use transform_gizmo_bevy::GizmoCamera; // use bevy_mod_picking::prelude::PickRaycastSource; use crate::hierarchy::{HideInEditor, HierarchyWindow}; @@ -232,6 +233,7 @@ fn spawn_editor_cameras(mut commands: Commands, editor: Res) { HideInEditor, Name::new("Editor Camera 3D Free"), NotInScene, + GizmoCamera, render_layers, )); @@ -254,6 +256,7 @@ fn spawn_editor_cameras(mut commands: Commands, editor: Res) { HideInEditor, Name::new("Editor Camera 3D Pan/Orbit"), NotInScene, + GizmoCamera, render_layers, )); @@ -275,6 +278,7 @@ fn spawn_editor_cameras(mut commands: Commands, editor: Res) { HideInEditor, Name::new("Editor Camera 2D Pan/Zoom"), NotInScene, + GizmoCamera, render_layers, )); } @@ -600,7 +604,7 @@ fn set_main_pass_viewport( } }); - cameras.iter_mut().for_each(|mut cam| { - cam.viewport = viewport.clone(); - }); + cameras + .iter_mut() + .for_each(|mut cam| cam.viewport.clone_from(&viewport)); } diff --git a/crates/bevy_editor_pls_default_windows/src/gizmos.rs b/crates/bevy_editor_pls_default_windows/src/gizmos.rs index 1f5482f..2938b14 100644 --- a/crates/bevy_editor_pls_default_windows/src/gizmos.rs +++ b/crates/bevy_editor_pls_default_windows/src/gizmos.rs @@ -1,28 +1,31 @@ use bevy::{ - ecs::query::QueryFilter, + ecs::{query::QueryFilter, system::RunSystemOnce}, prelude::*, - render::{camera::CameraProjection, view::RenderLayers}, + render::view::RenderLayers, }; use bevy_editor_pls_core::editor_window::{EditorWindow, EditorWindowContext}; -use bevy_inspector_egui::{bevy_inspector::hierarchy::SelectedEntities, egui}; -use egui_gizmo::GizmoMode; +use bevy_inspector_egui::egui; +use transform_gizmo_bevy::GizmoTarget; +use transform_gizmo_bevy::{EnumSet, GizmoMode}; use crate::{ - cameras::{ActiveEditorCamera, CameraWindow, EditorCamera, EDITOR_RENDER_LAYER}, + cameras::{EditorCamera, EDITOR_RENDER_LAYER}, hierarchy::HierarchyWindow, }; pub struct GizmoState { + /// If [false], doesn't show any gizmos pub camera_gizmo_active: bool, - pub gizmo_mode: GizmoMode, + /// Synced with the [transform_gizmo_bevy::GizmoOptions] resource + pub gizmo_modes: EnumSet, } impl Default for GizmoState { fn default() -> Self { Self { camera_gizmo_active: true, - gizmo_mode: GizmoMode::Translate, + gizmo_modes: GizmoMode::all_translate(), } } } @@ -36,16 +39,79 @@ impl EditorWindow for GizmoWindow { fn ui(_world: &mut World, _cx: EditorWindowContext, ui: &mut egui::Ui) { ui.label("Gizmos can currently not be configured"); + // could definitely change some settings here in the future } - fn viewport_toolbar_ui(world: &mut World, cx: EditorWindowContext, ui: &mut egui::Ui) { + /// Called every frame (hopefully), could this invariant (namely being called every frame) be documented, + /// ideally in the [EditorWindow] trait? + fn viewport_toolbar_ui(world: &mut World, cx: EditorWindowContext, _ui: &mut egui::Ui) { let gizmo_state = cx.state::().unwrap(); + // syncs the [GizmoOptions] resource with the current state of the gizmo window + let mut gizmo_options = world.resource_mut::(); + gizmo_options.gizmo_modes = gizmo_state.gizmo_modes; + if gizmo_state.camera_gizmo_active { - if let (Some(hierarchy_state), Some(_camera_state)) = - (cx.state::(), cx.state::()) - { - draw_gizmo(ui, world, &hierarchy_state.selected, gizmo_state.gizmo_mode); + /// Before [hydrate_gizmos] and [deconstruct_gizmos] are run, this system resets the state of all entities that have a [EntityShouldShowGizmo] component. + /// Then, according to selection logic some entities are marked as focussed, and [hydrate_gizmos] and [deconstruct_gizmos] is run to sync the gizmo state with the selection state. + fn reset_gizmos_selected_state( + mut commands: Commands, + entities: Query>, + ) { + for entity in entities.iter() { + commands.entity(entity).remove::(); + } + } + + /// Takes all entities marked with [EntityShouldShowGizmo] and adds the [GizmoTarget] component to them. + fn hydrate_gizmos( + mut commands: Commands, + entities: Query, Without)>, + ) { + for entity in entities.iter() { + if let Some(mut entity) = commands.get_entity(entity) { + trace!( + "Hydrating a gizmo on entity {:?} because it is selected", + entity.id() + ); + // implicitly assumes it is the only gizmo target in the world, + // otherwise setting [GizmoTarget].is_focussed may be necessary + entity.insert(GizmoTarget::default()); + } + } + } + + /// Takes all entities that should have their [GizmoTarget] removed because they are no longer selected. + fn deconstruct_gizmos( + mut commands: Commands, + entities: Query, Without)>, + ) { + for entity in entities.iter() { + if let Some(mut entity) = commands.get_entity(entity) { + entity.remove::(); + debug!( + "Removing GizmoTarget from entity {:?} because it has lost focus", + entity.id() + ); + } + } + } + + if let Some(hierarchy_state) = cx.state::() { + // here should assign the `EntityShouldShowGizmo` component, which is later synced + // with the actual gizmo ui system + + world.run_system_once(reset_gizmos_selected_state); + + let selected_entities = hierarchy_state.selected.iter(); + for entity in selected_entities { + if let Some(mut entity) = world.get_entity_mut(entity) { + entity.insert(EntityShouldShowGizmo); + } + } + + world.run_system_once(hydrate_gizmos); + world.run_system_once(deconstruct_gizmos); } } } @@ -93,9 +159,15 @@ struct GizmoMarkerConfig { camera_material: Handle, } +/// can somebody document what this does? is it a duplicate of [EntityShouldShowGizmo]? #[derive(Component)] struct HasGizmoMarker; +/// When on an entity, this entity should be controllable using some sort of user gizmo. +/// Currently uses [transform_gizmo_bevy], and puts the [GizmoTarget] on the entity. +#[derive(Component)] +struct EntityShouldShowGizmo; + type GizmoMarkerQuery<'w, 's, T, F = ()> = Query<'w, 's, Entity, (With, Without, F)>; @@ -165,56 +237,3 @@ fn add_gizmo_markers( }); } } - -fn draw_gizmo( - ui: &mut egui::Ui, - world: &mut World, - selected_entities: &SelectedEntities, - gizmo_mode: GizmoMode, -) { - let Ok((cam_transform, projection)) = world - .query_filtered::<(&GlobalTransform, &Projection), With>() - .get_single(world) - else { - return; - }; - let view_matrix = Mat4::from(cam_transform.affine().inverse()); - let projection_matrix = projection.get_projection_matrix(); - - if selected_entities.len() != 1 { - return; - } - - for selected in selected_entities.iter() { - let Some(global_transform) = world.get::(selected) else { - continue; - }; - let model_matrix = global_transform.compute_matrix(); - - let Some(result) = egui_gizmo::Gizmo::new(selected) - .model_matrix(model_matrix.into()) - .view_matrix(view_matrix.into()) - .projection_matrix(projection_matrix.into()) - .orientation(egui_gizmo::GizmoOrientation::Local) - .mode(gizmo_mode) - .interact(ui) - else { - continue; - }; - - let global_affine = global_transform.affine(); - - let mut transform = world.get_mut::(selected).unwrap(); - - let parent_affine = global_affine * transform.compute_affine().inverse(); - let inverse_parent_transform = GlobalTransform::from(parent_affine.inverse()); - - let global_transform = Transform { - translation: result.translation.into(), - rotation: result.rotation.into(), - scale: result.scale.into(), - }; - - *transform = (inverse_parent_transform * global_transform).into(); - } -}