diff --git a/.gitignore b/.gitignore index fa4b5bde..92422ca1 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ docs/.obsidian/* editor.ron .idea/* crates/prefab/test*.ron -**/.DS_Store \ No newline at end of file +**/.DS_Store +assets/models/TrackATest1_sp.glb \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index a68f36cc..49d54cad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5270,6 +5270,7 @@ dependencies = [ "space_editor_game_view", "space_editor_ui", "space_prefab", + "transform-gizmo-bevy", "workspace-hakari", ] @@ -5821,8 +5822,8 @@ dependencies = [ [[package]] name = "transform-gizmo" -version = "0.5.0" -source = "git+https://github.com/jakobhellermann/transform-gizmo.git?branch=bevy-0.16#a1fbba9c06f8817f4cf041104dadf55c801c9fcc" +version = "0.6.0" +source = "git+https://github.com/MiniMinerX/transform-gizmo?branch=dev-0.16-3#55559e6dfcf02a54e4a06c2d9ff873fbcbd2f5cd" dependencies = [ "ahash", "ecolor", @@ -5836,8 +5837,8 @@ dependencies = [ [[package]] name = "transform-gizmo-bevy" -version = "0.5.0" -source = "git+https://github.com/jakobhellermann/transform-gizmo.git?branch=bevy-0.16#a1fbba9c06f8817f4cf041104dadf55c801c9fcc" +version = "0.6.0" +source = "git+https://github.com/MiniMinerX/transform-gizmo?branch=dev-0.16-3#55559e6dfcf02a54e4a06c2d9ff873fbcbd2f5cd" dependencies = [ "bevy_app", "bevy_asset", @@ -5863,8 +5864,8 @@ dependencies = [ [[package]] name = "transform-gizmo-egui" -version = "0.5.0" -source = "git+https://github.com/jakobhellermann/transform-gizmo.git?branch=bevy-0.16#a1fbba9c06f8817f4cf041104dadf55c801c9fcc" +version = "0.6.0" +source = "git+https://github.com/MiniMinerX/transform-gizmo?branch=dev-0.16-3#55559e6dfcf02a54e4a06c2d9ff873fbcbd2f5cd" dependencies = [ "egui", "transform-gizmo", diff --git a/Cargo.toml b/Cargo.toml index 82c80488..fae1a794 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,18 @@ repository = "https://github.com/rewin123/space_editor" homepage = "https://github.com/rewin123/space_editor" [workspace.dependencies] -bevy = { version = "0.16.0"} +bevy = { version = "0.16.0", features = [ + "bevy_gltf", + "bevy_log", + "bevy_window", + "bevy_state", + "bevy_sprite", + "reflect_documentation", + "reflect_functions", + "bevy_gizmos", + "bevy_picking", + "bevy_mesh_picking_backend", +] } # Editor Crates space_prefab = { version = "0.8.0", path = "crates/prefab" } @@ -83,13 +94,14 @@ bevy-inspector-egui = {version = "0.31.0", features = [ bevy_mod_billboard = { branch = "dev-0.16", git = "https://github.com/MiniMinerX/bevy_mod_billboard.git"} bevy_asset_loader = "0.23.0-rc.3" bevy_panorbit_camera = "0.26" -# bevy_panorbit_camera = { branch = "reflect_component", git = "https://github.com/MiniMinerX/bevy_panorbit_camera.git"} +# bevy_panorbit_camera = { branch = "dev-0.16-reflect-cam-2", git = "https://github.com/MiniMinerX/bevy_panorbit_camera.git"} + bevy_mod_outline = {git = "https://github.com/komadori/bevy_mod_outline.git", branch = "bevy-0.16"} # below has not been updated to bevy 0.16 -transform-gizmo-bevy = {git = "https://github.com/jakobhellermann/transform-gizmo.git", branch = "bevy-0.16", package = "transform-gizmo-bevy"} -transform-gizmo-egui = {git = "https://github.com/jakobhellermann/transform-gizmo.git", branch = "bevy-0.16", package = "transform-gizmo-egui"} +transform-gizmo-bevy = {git = "https://github.com/MiniMinerX/transform-gizmo", branch = "dev-0.16-3"} +transform-gizmo-egui = {git = "https://github.com/MiniMinerX/transform-gizmo", branch = "dev-0.16-3"} #transform-gizmo-egui = {git = "https://github.com/MiniMinerX/transform-gizmo.git"} @@ -106,6 +118,7 @@ bevy.workspace = true space_editor_ui.workspace = true space_prefab.workspace = true game_app.workspace = true +transform-gizmo-bevy.workspace = true # Tabs space_editor_game_view.workspace = true diff --git a/assets/scenes/Scene0.scn.ron b/assets/scenes/Scene0.scn.ron index 2b8b9638..048c18c8 100644 --- a/assets/scenes/Scene0.scn.ron +++ b/assets/scenes/Scene0.scn.ron @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ca5bc50a0083a10b30a31f79b93e57de1a6b4a4b5f1e9b2e14bd6f41fd57609 -size 3386 +oid sha256:979ff68eb678794704172be125d51654943d87b65a8f03e67a8c58c98d88dd48 +size 40067 diff --git a/crates/editor_core/src/gltf_unpack.rs b/crates/editor_core/src/gltf_unpack.rs index 6b0deb45..0fec2a83 100644 --- a/crates/editor_core/src/gltf_unpack.rs +++ b/crates/editor_core/src/gltf_unpack.rs @@ -11,10 +11,14 @@ use super::{BackgroundTask, BackgroundTaskStorage}; /// Event to handle GLTF path pub struct EditorUnpackGltf { pub path: String, + pub parent: Option, } #[derive(Event, Clone)] -struct GltfLoaded(Handle); +struct GltfLoaded { + handle: Handle, + parent: Option, +} pub struct UnpackGltfPlugin; @@ -34,8 +38,8 @@ impl Plugin for UnpackGltfPlugin { #[reflect(Component)] struct GltfHolder(Handle); -#[derive(Resource, Default)] -struct GltfSceneQueue(Vec>); +#[derive(Resource, Default)] // Handle then parent +struct GltfSceneQueue(Vec<(Handle, Option)>); fn unpack_gltf_event( mut events: EventReader, @@ -49,7 +53,7 @@ fn unpack_gltf_event( event.path.clone(), handle.clone().untyped(), )); - queue.0.push(handle); + queue.0.push((handle, event.parent)); } events.clear(); } @@ -60,9 +64,11 @@ fn queue_push( mut events: EventWriter, assets: Res, ) { - if !queue.0.is_empty() && matches!(assets.get_load_state(&queue.0[0]), Some(LoadState::Loaded)) - { - events.write(GltfLoaded(queue.0.remove(0))); + if let Some((handle, parent)) = queue.0.first().cloned() { + if matches!(assets.get_load_state(&handle), Some(LoadState::Loaded)) { + events.write(GltfLoaded { handle, parent }); + queue.0.remove(0); + } } } @@ -86,8 +92,8 @@ fn unpack_gltf(world: &mut World) { }; let mut command_queue = CommandQueue::default(); - for gltf in loaded_scenes.iter() { - let handle: Handle = gltf.0.clone(); + for gltf_loaded in loaded_scenes.iter() { + let handle: Handle = gltf_loaded.handle.clone(); let gltf_path = if let Some(path) = handle.path() { path.clone() } else { @@ -97,7 +103,7 @@ fn unpack_gltf(world: &mut World) { let Some(gltf) = world .get_resource::>() - .and_then(|gltfs| gltfs.get(&gltf.0)) + .and_then(|gltfs| gltfs.get(&gltf_loaded.handle)) else { world.send_event(space_shared::toast::ToastMessage::new( "Gltf asset not found or empty", @@ -175,7 +181,11 @@ fn unpack_gltf(world: &mut World) { }; for root in roots.iter() { - spawn_node(&mut commands, root, gltf, &ctx); + let entity = spawn_node(&mut commands, root, gltf, &ctx); + + if let Some(parent) = gltf_loaded.parent { + commands.entity(parent).add_child(entity); + } } } diff --git a/crates/editor_core/src/lib.rs b/crates/editor_core/src/lib.rs index 640b5faa..36511577 100644 --- a/crates/editor_core/src/lib.rs +++ b/crates/editor_core/src/lib.rs @@ -51,8 +51,8 @@ impl Plugin for EditorCore { app.add_systems(Update, editor_event_listener); //app.auto_reflected_undo::(); - app.auto_reflected_undo::(); - app.auto_undo::(); + //app.auto_reflected_undo::(); + //app.auto_undo::(); } } @@ -100,8 +100,11 @@ fn editor_event_listener( EditorEvent::StartGame => { start_game_state.set(EditorState::GamePrepare); } - EditorEvent::LoadGltfAsPrefab(path) => { - gltf_events.write(gltf_unpack::EditorUnpackGltf { path: path.clone() }); + EditorEvent::LoadGltfAsPrefab{path, parent} => { + gltf_events.write(gltf_unpack::EditorUnpackGltf { + path: path.clone(), + parent: *parent, + }); } } } diff --git a/crates/editor_game_view/src/lib.rs b/crates/editor_game_view/src/lib.rs index b45667c6..71991499 100644 --- a/crates/editor_game_view/src/lib.rs +++ b/crates/editor_game_view/src/lib.rs @@ -214,27 +214,39 @@ pub fn set_camera_viewport( local.0 = Some(viewport_rect); let scale_factor = window.scale_factor(); - debug!( - "Window scale factor: {} egui scale factor: {}", - scale_factor, context_settings.scale_factor - ); let mut viewport_pos = viewport_rect.left_top().to_vec2() * scale_factor; let mut viewport_size = viewport_rect.size() * scale_factor; + // Ensure position is non-negative viewport_pos.x = viewport_pos.x.max(0.0); viewport_pos.y = viewport_pos.y.max(0.0); - viewport_size.x = viewport_size - .x - .min(window.width().mul_add(scale_factor, -viewport_pos.x)); - viewport_size.y = viewport_size - .y - .min(window.height().mul_add(scale_factor, -viewport_pos.y)); - - if (viewport_size.x <= 0.0) || (viewport_size.y <= 0.0) { + // Calculate maximum allowed size based on window dimensions and position + let window_width = window.width() * scale_factor; + let window_height = window.height() * scale_factor; + + // IMPORTANT: Ensure viewport fits WITHIN the render target + // Subtract 1 pixel to ensure it's contained, not equal + let max_width = (window_width - viewport_pos.x - 1.0).max(0.0); + let max_height = (window_height - viewport_pos.y - 1.0).max(0.0); + + viewport_size.x = viewport_size.x.min(max_width); + viewport_size.y = viewport_size.y.min(max_height); + + // Ensure minimum size of 1x1 + if viewport_size.x < 1.0 || viewport_size.y < 1.0 { return; } + + // Additional safety check: ensure the viewport is fully contained + if viewport_pos.x + viewport_size.x >= window_width || + viewport_pos.y + viewport_size.y >= window_height { + // Adjust size to fit + viewport_size.x = (window_width - viewport_pos.x - 1.0).max(1.0); + viewport_size.y = (window_height - viewport_pos.y - 1.0).max(1.0); + } + cam.viewport = Some(bevy::render::camera::Viewport { physical_position: UVec2::new(viewport_pos.x as u32, viewport_pos.y as u32), physical_size: UVec2::new(viewport_size.x as u32, viewport_size.y as u32), @@ -251,4 +263,4 @@ fn set_non_ui_areas( if let Some(viewport_rect) = game_view.viewport_rect { non_ui_areas.areas.push(viewport_rect); } -} \ No newline at end of file +} diff --git a/crates/editor_ui/src/camera_plugin.rs b/crates/editor_ui/src/camera_plugin.rs index f8db156f..a3ca4959 100644 --- a/crates/editor_ui/src/camera_plugin.rs +++ b/crates/editor_ui/src/camera_plugin.rs @@ -81,18 +81,20 @@ pub fn update_pan_orbit( } } -type PlayModeCameraFilter = (Without, With); -type EditorModeCameraFilter = (With, Without); +//type PlayModeCameraFilter = (Without, With); +//type EditorModeCameraFilter = (With, Without); /// System to change camera from editor camera to game play camera (if exist) pub fn change_camera_in_play( - mut editor_cameras: Query<&mut Camera, EditorModeCameraFilter>, - mut play_cameras: Query<&mut Camera, PlayModeCameraFilter>, + //mut editor_cameras: Query<&mut Camera, EditorModeCameraFilter>, + //mut play_cameras: Query<&mut Camera, PlayModeCameraFilter>, + mut editor_only_cameras: Query<&mut Camera, (With, Without)>, + mut play_cameras: Query<&mut Camera, With>, primary_window: Query<&mut Window, With>, mut toast: EventWriter, ) { if !play_cameras.is_empty() { - editor_cameras.iter_mut().for_each(|mut cam| { + editor_only_cameras.iter_mut().for_each(|mut cam| { cam.is_active = false; }); play_cameras.iter_mut().for_each(|mut cam| { @@ -131,14 +133,14 @@ pub fn change_camera_in_play( /// System to change camera from game camera to editor camera (if exist) pub fn change_camera_in_editor( - mut editor_cameras: Query<&mut Camera, EditorModeCameraFilter>, - mut play_cameras: Query<&mut Camera, PlayModeCameraFilter>, + mut editor_cameras: Query<&mut Camera, With>, + mut play_only_cameras: Query<&mut Camera, (With, Without)>, ) { for mut ecam in editor_cameras.iter_mut() { ecam.is_active = true; } - for mut play_cam in play_cameras.iter_mut() { + for mut play_cam in play_only_cameras.iter_mut() { play_cam.is_active = false; } } @@ -164,7 +166,7 @@ pub fn draw_camera_gizmo( (&GlobalTransform, &Projection), ( With, - Without, + //Without, Without, Without, ), @@ -173,11 +175,14 @@ pub fn draw_camera_gizmo( for (transform, _projection) in cameras.iter() { let pink = Color::srgb(1.0, 0.41, 0.71); + let scale = 0.4; + let scale2 = 0.4 / 1.5; + let transform = transform.compute_transform(); - let cuboid_transform = transform.with_scale(Vec3::new(1.0, 1.0, 2.0)); + let cuboid_transform = transform.with_scale(Vec3::new(1.0 * scale2, 1.0 * scale2, 2.0 * scale2)); gizmos.cuboid(cuboid_transform, pink); - let scale = 1.5; + gizmos.line( transform.translation, diff --git a/crates/editor_ui/src/camera_view.rs b/crates/editor_ui/src/camera_view.rs index 3ed71fbb..482506ac 100644 --- a/crates/editor_ui/src/camera_view.rs +++ b/crates/editor_ui/src/camera_view.rs @@ -123,7 +123,7 @@ impl EditorTab for CameraViewTab { let mut camera_query = world.query_filtered::, With, - Without, + //Without, )>(); if camera_query.iter(world).count() == 1 { @@ -267,7 +267,7 @@ impl EditorTab for CameraViewTab { fn clean_camera_view_tab( mut ui_state: ResMut, - mut cameras: Query<(&mut Camera, &mut GlobalTransform), Without>, + mut cameras: Query<(&mut Camera, &mut GlobalTransform) /*, Without */ >, ) { let Some(real_cam_entity) = ui_state.real_camera else { return; @@ -296,7 +296,7 @@ fn set_camera_viewport( primary_window: Query<&mut Window, With>, mut cameras: Query< (&mut Camera, &mut GlobalTransform, &mut Transform), - Without, + //Without, >, mut ctxs: EguiContexts, images: Res>, diff --git a/crates/editor_ui/src/hierarchy.rs b/crates/editor_ui/src/hierarchy.rs index 4f46ce9a..0a667c2c 100644 --- a/crates/editor_ui/src/hierarchy.rs +++ b/crates/editor_ui/src/hierarchy.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use bevy::{ecs::query::QueryFilter, platform::collections::HashMap, prelude::*}; use bevy_egui::{ - egui::{collapsing_header::CollapsingState, TextEdit}, + egui::{collapsing_header::CollapsingState, TextEdit, TextStyle}, *, }; use space_editor_core::prelude::*; @@ -70,12 +70,6 @@ pub fn show_hierarchy( mut state: ResMut, auto_children: Query<(), With>, ) { - let mut all: Vec<_> = if state.show_editor_entities { - all_entities.iter().collect() - } else { - query.iter().collect() - }; - all.sort_by_key(|a| a.0); let ui = &mut ui.0; ui.horizontal(|ui| { let button_size = ui @@ -95,41 +89,58 @@ pub fn show_hierarchy( ui.spacing(); let lower_filter = state.entity_filter.to_lowercase(); - egui::ScrollArea::vertical().show(ui, |ui| { - for (entity, _name, _children, parent) in all.iter().filter(|(_, name, _, _)| { + // Collect and filter entities once + let filtered_entities: Vec<_> = if state.show_editor_entities { + all_entities.iter().filter(|(_, name, _, parent)| { + parent.is_none() && // Only root entities name.map(|n| n.to_lowercase()) .unwrap_or_else(|| "entity".to_string()) .contains(&lower_filter) - }) { - if parent.is_none() { - if state.show_editor_entities { - draw_entity::<()>( - &mut commands, - ui, - &all_entities, - *entity, - &mut selected, - &mut clone_events, - &mut changes, - &auto_children, - ); - } else { - draw_entity::>( - &mut commands, - ui, - &query, - *entity, - &mut selected, - &mut clone_events, - &mut changes, - &auto_children, - ); + }).collect() + } else { + query.iter().filter(|(_, name, _, parent)| { + parent.is_none() && // Only root entities + name.map(|n| n.to_lowercase()) + .unwrap_or_else(|| "entity".to_string()) + .contains(&lower_filter) + }).collect() + }; + // Use virtual scrolling for performance + let text_style = TextStyle::Body; + let row_height = ui.text_style_height(&text_style); + + egui::ScrollArea::vertical() + .auto_shrink(false) + .show_rows(ui, row_height, filtered_entities.len(), |ui, row_range| { + for row in row_range { + if let Some((entity, _name, _children, _parent)) = filtered_entities.get(row) { + if state.show_editor_entities { + draw_entity::<()>( + &mut commands, + ui, + &all_entities, + *entity, + &mut selected, + &mut clone_events, + &mut changes, + &auto_children, + ); + } else { + draw_entity::>( + &mut commands, + ui, + &query, + *entity, + &mut selected, + &mut clone_events, + &mut changes, + &auto_children, + ); + } } } - } - }); + }); } - type DrawIter<'a> = ( Entity, Option<&'a Name>, @@ -162,7 +173,7 @@ fn draw_entity( CollapsingState::load_with_default_open( ui.ctx(), ui.make_persistent_id(entity_name.clone()), - true, + false, ) .show_header(ui, |ui| { let mut entity_name = egui::RichText::new(entity_name.clone()); diff --git a/crates/editor_ui/src/lib.rs b/crates/editor_ui/src/lib.rs index 8eee1913..95809c56 100644 --- a/crates/editor_ui/src/lib.rs +++ b/crates/editor_ui/src/lib.rs @@ -139,6 +139,8 @@ pub mod prelude { pub use crate::EditorPlugin; pub use crate::editor_tab_name::*; + + } /// External dependencies for editor crate diff --git a/crates/editor_ui/src/menu_toolbars.rs b/crates/editor_ui/src/menu_toolbars.rs index 9d53285e..8da763ad 100644 --- a/crates/editor_ui/src/menu_toolbars.rs +++ b/crates/editor_ui/src/menu_toolbars.rs @@ -479,7 +479,10 @@ pub fn top_menu( if path.starts_with("assets/") { path = path.replace("assets/", ""); - editor_events.write(EditorEvent::LoadGltfAsPrefab(path)); + editor_events.write(EditorEvent::LoadGltfAsPrefab{ + path: path, + parent: None, + }); } } } else { diff --git a/crates/editor_ui/src/selection.rs b/crates/editor_ui/src/selection.rs index a21d2b46..9fa1eb0f 100644 --- a/crates/editor_ui/src/selection.rs +++ b/crates/editor_ui/src/selection.rs @@ -1,5 +1,6 @@ use crate::*; use bevy::{color::palettes::tailwind::{PINK_100, RED_500}, picking::pointer::PointerInteraction, prelude::*}; +use transform_gizmo_bevy::GizmoTarget; @@ -9,12 +10,12 @@ pub fn plugin(app: &mut App) { app.add_plugins(MeshPickingPlugin); } - app.add_observer(on_pointer_click); + //app.add_observer(on_pointer_click); app.add_systems( Update, - (delete_selected, reemit_pointer_click, auto_add_markers) + (delete_selected, reemit_pointer_click)// auto_add_markers) ); app.add_systems( @@ -24,12 +25,12 @@ pub fn plugin(app: &mut App) { app.add_event::(); - app.add_observer(select_listener); - app.add_observer(recursive_add_markers); + //app.add_observer(select_listener); + //app.add_observer(recursive_add_markers); app.insert_resource(MeshPickingSettings { - require_markers: false, - ray_cast_visibility: RayCastVisibility::Any + require_markers: true, + ray_cast_visibility: RayCastVisibility::VisibleInView }); } @@ -119,7 +120,11 @@ pub fn select_listener( parents: Query<&ChildOf>, pan_orbit_state: ResMut, keyboard: Res>, + gizmo_query: Query<&GizmoTarget>, ) { + if gizmo_query.iter().any(|gizmo| gizmo.is_active()) { + return; + } if !pan_orbit_state.0 { trigger.propagate(false); @@ -142,6 +147,8 @@ pub fn select_listener( } + + /// This event used for selecting entities #[derive(Event, Clone)] pub struct SelectEvent; diff --git a/crates/editor_ui/src/ui_plugin.rs b/crates/editor_ui/src/ui_plugin.rs index c995590d..f83eafca 100644 --- a/crates/editor_ui/src/ui_plugin.rs +++ b/crates/editor_ui/src/ui_plugin.rs @@ -178,15 +178,15 @@ impl Plugin for EditorUiCore { clear_and_load_on_start, ); - app.add_systems( - Update, - ( - draw_camera_gizmo, + //app.add_systems( + // Update, + // ( + //draw_camera_gizmo, //draw_light_gizmo, //selection::delete_selected, - ) - .run_if(in_state(EditorState::Editor).and(in_state(ShowEditorUi::Show))), - ); + // ) + // .run_if(in_state(EditorState::Editor).and(in_state(ShowEditorUi::Show))), + //); if self.disable_no_editor_cams { app.add_systems( diff --git a/crates/prefab/src/editor_registry/mod.rs b/crates/prefab/src/editor_registry/mod.rs index 5c8dfdc8..60908bf9 100644 --- a/crates/prefab/src/editor_registry/mod.rs +++ b/crates/prefab/src/editor_registry/mod.rs @@ -309,7 +309,7 @@ impl EditorRegistryExt for App { self.world_mut().register_component::(); self.register_type::(); - self.auto_reflected_undo::(); + //self.auto_reflected_undo::(); self } diff --git a/crates/prefab/src/load.rs b/crates/prefab/src/load.rs index 04a04aee..e29bdf40 100644 --- a/crates/prefab/src/load.rs +++ b/crates/prefab/src/load.rs @@ -1,4 +1,4 @@ -use bevy::prelude::*; +use bevy::{platform::collections::HashSet, prelude::*}; use bevy_scene_hook::SceneHook; use space_shared::PrefabMarker; @@ -48,15 +48,15 @@ impl Plugin for LoadPlugin { app.add_systems( Update, - load_prefab.after(bevy_scene_hook::Systems::SceneHookRunner), + ( + conflict_resolve, + load_prefab, + ApplyDeferred, + auto_children, + ) + .chain() + .after(bevy_scene_hook::Systems::SceneHookRunner), ); - app.add_systems( - Update, - conflict_resolve - .after(bevy_scene_hook::Systems::SceneHookRunner) - .before(load_prefab), - ); - app.add_systems(Update, auto_children); } } @@ -102,18 +102,20 @@ fn load_prefab( } commands.entity(e).remove::(); } - + let scene: Handle = assets.load(&l.path); let id = commands .spawn(DynamicSceneRoot(scene)) .insert(SceneHook::new(move |_e, cmd| { - cmd.insert(PrefabAutoChild); + cmd.insert((PrefabAutoChild)); })) .insert(PrefabAutoChild) .id(); commands.entity(e).add_children(&[id]); + + } } @@ -126,6 +128,7 @@ fn conflict_resolve( } } + fn auto_children( mut commands: Commands, query: Query<(Entity, &ChildrenPrefab)>, @@ -133,15 +136,20 @@ fn auto_children( ) { for (e, children) in query.iter() { let mut cmds = commands.entity(e); - for child in children.0.iter() { + for child in children.entities.iter() { + println!("Adding child: {:?}", child); if existing_entity.contains(*child) { cmds.add_child(*child); + } else { + println!("nonexistent entity"); } } cmds.remove::(); } } + + #[cfg(test)] mod test { use super::*; diff --git a/crates/prefab/src/plugins.rs b/crates/prefab/src/plugins.rs index 447362db..f89ef19d 100644 --- a/crates/prefab/src/plugins.rs +++ b/crates/prefab/src/plugins.rs @@ -6,6 +6,7 @@ use bevy::{ use bevy_scene_hook::HookPlugin; use space_shared::toast::ToastMessage; use space_shared::{LightAreaToggle, PrefabMarker}; +use std::collections::HashSet; use crate::{ component, editor_registry::EditorRegistryExt, load, prelude::EditorRegistryPlugin, save, @@ -17,6 +18,18 @@ use load::*; use save::*; use spawn_system::*; +// Resource to track entities that need mesh loading +#[derive(Resource, Default)] +pub struct PendingMeshLoads { + pub entities: HashSet, +} + +// Resource to track entities that need material loading +#[derive(Resource, Default)] +pub struct PendingMaterialLoads { + pub entities: HashSet, +} + /// This plugin contains all components and logic of prefabs pub struct PrefabPlugin; @@ -33,6 +46,8 @@ impl Plugin for BasePrefabPlugin { #[cfg(not(tarpaulin_include))] fn build(&self, app: &mut App) { app.init_state::(); + app.init_resource::(); + app.init_resource::(); if !app.is_plugin_added::() { app.add_plugins(HookPlugin); @@ -113,15 +128,26 @@ impl Plugin for BasePrefabPlugin { app.register_type::(); app.editor_registry::(); - app.add_systems( - Update, - sync_asset_mesh.in_set(PrefabSet::DetectPrefabChange), - ); + //app.add_systems( + // Update, + // sync_asset_mesh.in_set(PrefabSet::DetectPrefabChange), + //); + app.add_observer(on_asset_mesh_added_tracker); + //app.add_observer(on_asset_mesh_changed); + app.add_observer(on_asset_mesh_removed); app.editor_registry::(); + //app.add_systems( + // Update, + // sync_asset_material.in_set(PrefabSet::DetectPrefabChange), + //); + app.add_observer(on_asset_material_added_tracker); + //app.add_observer(on_asset_material_changed); + app.add_observer(on_asset_material_removed); + app.add_systems( Update, - sync_asset_material.in_set(PrefabSet::DetectPrefabChange), + (batched_sync_asset_mesh, batched_sync_asset_material).in_set(PrefabSet::DetectPrefabChange), ); //material registration @@ -130,9 +156,9 @@ impl Plugin for BasePrefabPlugin { app.register_type::(); //camera - app.editor_registry::(); - app.editor_registry::(); - app.editor_registry::(); + //app.editor_registry::(); + //app.editor_registry::(); + //app.editor_registry::(); app.editor_registry::(); //app.editor_registry::(); app.editor_registry::(); @@ -311,6 +337,7 @@ fn remove_computed_visibility( } } +/* fn sync_asset_mesh( mut commands: Commands, changed: Query<(Entity, &AssetMesh), Changed>, @@ -350,6 +377,237 @@ fn sync_asset_material( } } +// AssetMesh observers +fn on_asset_mesh_added( + trigger: Trigger, + mut commands: Commands, + query: Query<&AssetMesh>, + assets: Res, +) { + let entity = trigger.target(); + if let Ok(asset_mesh) = query.get(entity) { + info!("Loading mesh for entity {:?}: {}", entity, asset_mesh.path); + commands.entity(entity).insert(Mesh3d(assets.load::(&asset_mesh.path))); + } +} + +fn on_asset_mesh_changed( + trigger: Trigger, + mut commands: Commands, + query: Query<&AssetMesh>, + assets: Res, +) { + let entity = trigger.target(); + if let Ok(asset_mesh) = query.get(entity) { + info!("Updating mesh for entity {:?}: {}", entity, asset_mesh.path); + commands.entity(entity).insert(Mesh3d(assets.load::(&asset_mesh.path))); + } +} + +fn on_asset_mesh_removed( + trigger: Trigger, + mut commands: Commands, +) { + let entity = trigger.target(); + if let Ok(mut cmd) = commands.get_entity(entity) { + cmd.remove::(); + info!("Removed mesh handle for entity {:?}", entity); + } +} + +// AssetMaterial observers +fn on_asset_material_added( + trigger: Trigger, + mut commands: Commands, + query: Query<&AssetMaterial>, + assets: Res, +) { + let entity = trigger.target(); + if let Ok(asset_material) = query.get(entity) { + info!("Loading material for entity {:?}: {}", entity, asset_material.path); + commands.entity(entity).insert(MeshMaterial3d( + assets.load::(&asset_material.path), + )); + } +} + +fn on_asset_material_changed( + trigger: Trigger, + mut commands: Commands, + query: Query<&AssetMaterial>, + assets: Res, +) { + let entity = trigger.target(); + if let Ok(asset_material) = query.get(entity) { + info!("Updating material for entity {:?}: {}", entity, asset_material.path); + commands.entity(entity).insert(MeshMaterial3d( + assets.load::(&asset_material.path), + )); + } +} + +fn on_asset_material_removed( + trigger: Trigger, + mut commands: Commands, +) { + let entity = trigger.target(); + if let Ok(mut cmd) = commands.get_entity(entity) { + cmd.remove::>(); + info!("Removed material handle for entity {:?}", entity); + } +} +*/ + +// Modified observer - just tracks the entity, doesn't load immediately +fn on_asset_mesh_added_tracker( + trigger: Trigger, + mut pending_loads: ResMut, + query: Query<&AssetMesh>, +) { + let entity = trigger.target(); + if query.get(entity).is_ok() { + info!("Queuing mesh load for entity {:?}", entity); + pending_loads.entities.insert(entity); + } +} + +// Batched sync system - processes all pending loads at once +fn batched_sync_asset_mesh( + mut commands: Commands, + mut pending_loads: ResMut, + query: Query<&AssetMesh>, + assets: Res, +) { + if pending_loads.entities.is_empty() { + return; + } + + // Group entities by their mesh path to avoid duplicate handles + let mut path_to_entities: std::collections::HashMap> = std::collections::HashMap::new(); + + // Collect valid entities and group by path + let mut valid_entities = Vec::new(); + for &entity in &pending_loads.entities { + if let Ok(asset_mesh) = query.get(entity) { + path_to_entities + .entry(asset_mesh.path.clone()) + .or_default() + .push(entity); + valid_entities.push(entity); + } + } + + // Load each unique mesh path once and apply to all entities that need it + for (path, entities) in path_to_entities { + info!("Loading mesh '{}' for {} entities", path, entities.len()); + let mesh_handle = assets.load::(&path); + + for entity in entities { + commands.entity(entity).insert(Mesh3d(mesh_handle.clone())); + } + } + + // Clear the pending loads + pending_loads.entities.clear(); + + info!("Batched mesh loading completed for {} entities", valid_entities.len()); +} + +// Keep the removal observer as-is since it's immediate +fn on_asset_mesh_removed( + trigger: Trigger, + mut commands: Commands, +) { + let entity = trigger.target(); + if let Ok(mut cmd) = commands.get_entity(entity) { + cmd.remove::(); + info!("Removed mesh handle for entity {:?}", entity); + } +} + +// Similar pattern for materials +fn on_asset_material_added_tracker( + trigger: Trigger, + mut pending_loads: ResMut, + query: Query<&AssetMaterial>, +) { + let entity = trigger.target(); + if query.get(entity).is_ok() { + info!("Queuing material load for entity {:?}", entity); + pending_loads.entities.insert(entity); + } +} + +fn batched_sync_asset_material( + mut commands: Commands, + mut pending_loads: ResMut, + query: Query<&AssetMaterial>, + assets: Res, +) { + if pending_loads.entities.is_empty() { + return; + } + + // Group entities by their material path + let mut path_to_entities: std::collections::HashMap> = std::collections::HashMap::new(); + + let mut valid_entities = Vec::new(); + for &entity in &pending_loads.entities { + if let Ok(asset_material) = query.get(entity) { + path_to_entities + .entry(asset_material.path.clone()) + .or_default() + .push(entity); + valid_entities.push(entity); + } + } + + // Load each unique material path once and apply to all entities + for (path, entities) in path_to_entities { + info!("Loading material '{}' for {} entities", path, entities.len()); + let material_handle = assets.load::(&path); + + for entity in entities { + commands.entity(entity).insert(MeshMaterial3d(material_handle.clone())); + } + } + + // Clear the pending loads + pending_loads.entities.clear(); + + info!("Batched material loading completed for {} entities", valid_entities.len()); +} + +fn on_asset_material_removed( + trigger: Trigger, + mut commands: Commands, +) { + let entity = trigger.target(); + if let Ok(mut cmd) = commands.get_entity(entity) { + cmd.remove::>(); + info!("Removed material handle for entity {:?}", entity); + } +} + +// Optional: Add a system to handle changes to existing AssetMesh components +fn on_asset_mesh_changed_tracker( + trigger: Trigger, + mut pending_loads: ResMut, +) { + let entity = trigger.target(); + info!("Queuing mesh update for entity {:?}", entity); + pending_loads.entities.insert(entity); +} + +fn on_asset_material_changed_tracker( + trigger: Trigger, + mut pending_loads: ResMut, +) { + let entity = trigger.target(); + info!("Queuing material update for entity {:?}", entity); + pending_loads.entities.insert(entity); +} + #[cfg(test)] mod test { use super::*; diff --git a/crates/prefab/src/save.rs b/crates/prefab/src/save.rs index 38dacf77..14589892 100644 --- a/crates/prefab/src/save.rs +++ b/crates/prefab/src/save.rs @@ -6,28 +6,39 @@ use std::{any::TypeId, fs, io::Write}; use crate::prelude::{EditorRegistry, EditorRegistryExt, SceneAutoChild}; -#[derive(Reflect, Default, Component, Clone)] +#[derive(Reflect, Default, Component, Clone, MapEntities)] #[reflect(Component, MapEntities)] /// Component that holds children entity/prefab information /// that should be serialized -pub struct ChildrenPrefab(pub Vec); +pub struct ChildrenPrefab{ + #[entities] + pub entities: Vec, +} impl ChildrenPrefab { pub fn from_children(children: &Children) -> Self { - Self(children.to_vec()) + ChildrenPrefab { + entities: children.to_vec(), + } } } +/* impl MapEntities for ChildrenPrefab { #[cfg(not(tarpaulin_include))] fn map_entities(&mut self, entity_mapper: &mut M) { - self.0 = self - .0 - .iter() - .map(|e| entity_mapper.get_mapped(*e)) - .collect(); + // ================================================================================= + // DIAGNOSTIC LOGS: Check your console for these messages when loading a prefab. + // ================================================================================= + warn!("ChildrenPrefab::map_entities CALLED. This is a good sign!"); + for entity in self.0.iter_mut() { + let old_id = *entity; + *entity = entity_mapper.get_mapped(old_id); + info!(" Mapping child {:?} -> {:?}", old_id, *entity); + } } } + */ struct SaveResourcesPrefabPlugin; @@ -139,6 +150,13 @@ pub fn serialize_scene(world: &mut World) { .with_resource_filter(SceneFilter::Allowlist(HashSet::from_iter( allow_types.iter().cloned(), ))) + .with_component_filter(SceneFilter::Allowlist(HashSet::from_iter( + allow_types.iter().cloned(), + ))) + // Deny test for standard material + //.deny_component::>() + //.deny_component:: + .extract_entities(entities.iter().copied()); let scene = builder.build(); @@ -312,11 +330,11 @@ mod tests { let child_id = commands.spawn_empty().id(); commands .spawn(PrefabMarker) - .insert(ChildrenPrefab(vec![child_id])); + .insert(ChildrenPrefab{entities: vec![child_id]}); let child_id = commands.spawn_empty().id(); commands .spawn(PrefabMarker) - .insert(ChildrenPrefab(vec![child_id])); + .insert(ChildrenPrefab{entities: vec![child_id]}); commands.spawn(PrefabMarker); }) .add_systems(Update, delete_prepared_children); @@ -338,7 +356,7 @@ mod tests { let children = query.single(&world).unwrap(); let prefab = ChildrenPrefab::from_children(children); - assert_eq!(prefab.0.len(), 1); + assert_eq!(prefab.entities.len(), 1); } #[test] diff --git a/crates/prefab/src/spawn_system.rs b/crates/prefab/src/spawn_system.rs index a98bb3fa..c33b6522 100644 --- a/crates/prefab/src/spawn_system.rs +++ b/crates/prefab/src/spawn_system.rs @@ -85,7 +85,7 @@ fn recursive_path( entity: Entity, path: Vec, ) { - // commands.entity(entity).insert(ChildPath(path.clone())); + //commands.entity(entity).insert(ChildPath(path.clone())); if let Ok(children) = q_children.get(entity) { for (i, child_entity) in children.iter().enumerate() { diff --git a/crates/shared/src/lib.rs b/crates/shared/src/lib.rs index a53bb425..e98a44bc 100644 --- a/crates/shared/src/lib.rs +++ b/crates/shared/src/lib.rs @@ -66,7 +66,10 @@ pub enum EditorPrefabPath { pub enum EditorEvent { Load(EditorPrefabPath), Save(EditorPrefabPath), - LoadGltfAsPrefab(String), + LoadGltfAsPrefab{ + path: String, + parent: Option, + }, StartGame, } diff --git a/crates/undo/src/lib.rs b/crates/undo/src/lib.rs index ef3265c5..d5a7f5e7 100644 --- a/crates/undo/src/lib.rs +++ b/crates/undo/src/lib.rs @@ -26,6 +26,7 @@ impl Plugin for UndoPlugin { app.add_event::(); app.add_event::(); + /* app.configure_sets( PostUpdate, (UndoSet::PerType, UndoSet::UpdateAll, UndoSet::Remapping) @@ -44,6 +45,7 @@ impl Plugin for UndoPlugin { .chain() .in_set(UndoSet::UpdateAll), ); + */ } } @@ -821,10 +823,8 @@ fn apply_for_every_typed_field( } bevy::reflect::ReflectMut::Set(_s) => {} bevy::reflect::ReflectMut::Opaque(_s) => {} - // bevy::reflect::ReflectMut::Function(_s) => {}, - //bevy::reflect::ReflectMut::Value(_v) => { - //do nothing. Value was checked before - //} + bevy::reflect::ReflectMut::Function(_s) => {}, + //bevy::reflect::ReflectMut::Value(_v) => {} } } } diff --git a/examples/gltf_unpack_example.rs b/examples/gltf_unpack_example.rs index 085c4f40..7fe361f6 100644 --- a/examples/gltf_unpack_example.rs +++ b/examples/gltf_unpack_example.rs @@ -6,11 +6,26 @@ fn main() { .add_plugins((DefaultPlugins, SpaceEditorPlugin)) .add_systems(Startup, simple_editor_setup) .add_systems(Startup, setup) + .register_type::() + + .run(); } -fn setup(mut editor_events: EventWriter) { - editor_events.send(EditorEvent::LoadGltfAsPrefab( - "models/low_poly_fighter_2.gltf".to_string(), - )); +fn setup( + mut editor_events: EventWriter, + mut commands: Commands, +) { + + let test_parent = commands.spawn(( + Name::from("Test Parent"), + PrefabMarker, + Transform::default(), + Visibility::default() + )).id(); + + editor_events.write(EditorEvent::LoadGltfAsPrefab{ + path: "models/colone.glb".to_string(), + parent: Some(test_parent) + }); } diff --git a/examples/space_example.rs b/examples/space_example.rs index 56afbb67..7aec31c0 100644 --- a/examples/space_example.rs +++ b/examples/space_example.rs @@ -5,13 +5,49 @@ use bevy::{ use ext::bevy_inspector_egui::quick::WorldInspectorPlugin; use space_editor::prelude::*; +use space_editor_game_view::gizmo_tool; +use space_editor_ui::ext::bevy_panorbit_camera::PanOrbitCamera; +use transform_gizmo_bevy::{mouse_interact::MouseGizmoInteractionPlugin, picking::TransformGizmoPickingPlugin, GizmoCamera, GizmoTarget, TransformGizmoPlugin}; + fn main() { App::default() .add_plugins(DefaultPlugins.set(LogPlugin { - level: Level::TRACE, + level: Level::DEBUG, ..default() })) .add_plugins(SpaceEditorPlugin) .add_systems(Startup, simple_editor_setup) + + .add_plugins(TransformGizmoPlugin) + + .add_systems( + Update, + disable_pan_orbit_on_gizmo + .after(update_pan_orbit) + .in_set(EditorSet::Editor), + ) + + .register_type::() + .register_type::() + .editor_registry::() + .editor_registry::() + .register_type::() + .run(); } + + +fn disable_pan_orbit_on_gizmo( + mut pan_orbit_cams: Query<&mut PanOrbitCamera, With>, + gizmo_targets: Query<&GizmoTarget>, +) { + for mut cam in pan_orbit_cams.iter_mut() { + for gizmo_target in gizmo_targets.iter() { + if gizmo_target.is_active() { + cam.enabled = false; + //debug!("Disabling PanOrbitCamera for GizmoTarget: {:?}", gizmo_target); + return; + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index ded18708..9d3b085a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,8 @@ pub mod prelude { pub use crate::SpaceEditorPlugin; pub use space_editor_ui::prelude::*; + // Game editor tab egui editing + pub use space_editor_game_view::GameViewTab; } pub use space_editor_ui;