//! Spawn helpers, bookmarks, hierarchy tree, and asset labeling. use bevy::prelude::*; use bevy_inspector_egui::bevy_inspector::hierarchy::SelectedEntities; use shared::{ AuthoringLightKind, LevelObject, LightDesc, MaterialDesc, PlayerSpawn, PrimitiveShape, ProjectSun, }; use sim::Player; use crate::assets::{snapshot_for_asset, EditorAsset, EditorAssetKind}; use crate::camera::EditorCamera; use crate::gizmos::{EditorGizmoMode, EditorGizmoSpace}; use crate::history::{set_material_group_with_history, spawn_with_history, EditorEntitySnapshot}; use crate::play::default_player_spawn; use crate::scene_io::SceneIo; use crate::viewport::{recall_camera_bookmark, save_camera_bookmark, CameraBookmarks}; pub use super::play_controls::{toggle_play_mode, toggle_play_paused, toggle_possession}; pub use super::selection_ops::{ delete_selection, duplicate_selection, entity_name, focus_editor_camera_on_selection, is_level_object, selected_level_entities, }; pub fn set_gizmo_mode(world: &mut World, mode: EditorGizmoMode) { *world.resource_mut::() = mode; } pub fn set_gizmo_space(world: &mut World, space: EditorGizmoSpace) { *world.resource_mut::() = space; } pub fn viewport_save_bookmark(world: &mut World) { let scene_key = world .resource::() .active_path .as_ref() .map(|path| path.display().to_string()) .unwrap_or_else(|| "__unsaved__".into()); let Some(transform) = world .query_filtered::<&Transform, With>() .single(world) .ok() .copied() else { return; }; let mut bookmarks = world.resource_mut::(); save_camera_bookmark(&mut bookmarks, &scene_key, transform); } pub fn viewport_recall_bookmark(world: &mut World) { let scene_key = world .resource::() .active_path .as_ref() .map(|path| path.display().to_string()) .unwrap_or_else(|| "__unsaved__".into()); let saved = { let bookmarks = world.resource::(); match recall_camera_bookmark(bookmarks, &scene_key) { Some(transform) => transform, None => return, } }; for mut transform in world .query_filtered::<&mut Transform, With>() .iter_mut(world) { *transform = saved; } } pub fn primitive_snapshot(name: &str, shape: PrimitiveShape, origin: Vec3) -> EditorEntitySnapshot { let asset = EditorAsset { label: name.to_string(), path: None, folder_path: crate::assets::BUILTINS_FOLDER.to_string(), kind: EditorAssetKind::Primitive(shape), }; snapshot_for_asset(&asset, origin).expect("primitive assets are spawnable") } pub fn light_snapshot(name: &str, translation: Vec3) -> EditorEntitySnapshot { EditorEntitySnapshot { actor_id: None, actor_kind: shared::ActorKind::Light, actor_name: None, name: Some(name.to_string()), transform: Transform::from_translation(translation), primitive: None, brush: None, static_mesh_renderer: None, material: None, material_override: None, rigid_body: None, collider: None, physics: None, light: Some(LightDesc::default()), animation_controller: None, audio_source: None, audio_listener: None, player_spawn: false, model: None, prefab: None, prefab_instance: None, weapon_spawn: None, trigger_volume: None, post_process_volume: None, team_spawn: None, objective: None, hierarchy_sibling_index: 0, editor_visibility: shared::EditorVisibility::default(), children: Vec::new(), } } pub fn ensure_player_spawn_for_edit(world: &mut World) -> Entity { if let Some(entity) = world .query_filtered::>() .iter(world) .next() { return entity; } let transform = world .query_filtered::<&Transform, With>() .iter(world) .next() .copied() .unwrap_or_else(default_player_spawn); spawn_with_history( world, EditorEntitySnapshot { actor_id: None, actor_kind: shared::ActorKind::PlayerSpawn, actor_name: None, name: Some("Player Start".to_string()), transform, primitive: None, brush: None, static_mesh_renderer: None, material: None, material_override: None, rigid_body: None, collider: None, physics: None, light: None, animation_controller: None, audio_source: None, audio_listener: None, player_spawn: true, model: None, prefab: None, prefab_instance: None, weapon_spawn: None, trigger_volume: None, post_process_volume: None, team_spawn: None, objective: None, hierarchy_sibling_index: 0, editor_visibility: shared::EditorVisibility::default(), children: Vec::new(), }, ) } /// Removes all authored [`LightDesc`] so project sun/ambient drive outdoor lighting. pub fn reset_scene_lighting_to_project_defaults(world: &mut World) { let mut query = world.query_filtered::>(); let with_lights: Vec = query .iter(world) .filter(|entity| world.get::(*entity).is_some()) .collect(); for entity in with_lights { if let Ok(mut entity_mut) = world.get_entity_mut(entity) { entity_mut.remove::(); } } world .resource_mut::() .mark_dirty(); } /// Removes authored directional lights so [`ProjectSun`] drives outdoor lighting. pub fn use_project_sun(world: &mut World) { let mut query = world.query_filtered::<(Entity, &LightDesc), With>(); let directionals: Vec = query .iter(world) .filter(|(_, light)| matches!(light.kind, AuthoringLightKind::Directional)) .map(|(e, _)| e) .collect(); for entity in directionals { if let Ok(mut entity_mut) = world.get_entity_mut(entity) { entity_mut.remove::(); } } world .resource_mut::() .mark_dirty(); } pub fn create_scene_sun_override_from_project_settings(world: &mut World) -> Entity { let (transform, light) = { let sun_illuminance = world .resource::() .rendering .sun_illuminance; let transform = world .query_filtered::<&Transform, With>() .iter(world) .next() .copied() .unwrap_or_else(|| { Transform::from_xyz(0.0, 0.0, 0.0) .looking_to(Vec3::new(-0.35, -0.85, -0.4), Vec3::Y) }); let mut light = LightDesc::for_kind(AuthoringLightKind::Directional); light.intensity = sun_illuminance; (transform, light) }; spawn_with_history( world, EditorEntitySnapshot { actor_id: None, actor_kind: shared::ActorKind::Light, actor_name: None, name: Some("Scene Sun".to_string()), transform, primitive: None, brush: None, static_mesh_renderer: None, material: None, material_override: None, rigid_body: None, collider: None, physics: None, light: Some(light), animation_controller: None, audio_source: None, audio_listener: None, player_spawn: false, model: None, prefab: None, prefab_instance: None, weapon_spawn: None, trigger_volume: None, post_process_volume: None, team_spawn: None, objective: None, hierarchy_sibling_index: 0, editor_visibility: shared::EditorVisibility::default(), children: Vec::new(), }, ) } pub fn asset_label(asset: &EditorAsset) -> String { let prefix = match asset.kind { EditorAssetKind::Primitive(_) => "Primitive", EditorAssetKind::Light(_) => "Light", EditorAssetKind::Model => "Model", EditorAssetKind::Texture => "Texture", EditorAssetKind::Material => "Material", EditorAssetKind::AudioClip => "Audio Clip", EditorAssetKind::Level => "Level", EditorAssetKind::Prefab => "Prefab", EditorAssetKind::PostProcessVolume => "Post Process Volume", EditorAssetKind::PostProcessEffect => "Post FX", EditorAssetKind::RenderingProfile => "Rendering Profile", EditorAssetKind::ShaderSchema => "Shader Schema", }; format!("{prefix}: {}", asset.label) } pub fn apply_texture_to_selection( world: &mut World, asset: &EditorAsset, selected: &SelectedEntities, ) -> Result<(), String> { let path = asset .path .clone() .ok_or_else(|| format!("Texture has no source path: {}", asset.label))?; let changes = selected_level_entities(world, selected) .into_iter() .map(|entity| { let mut material = world .get::(entity) .cloned() .unwrap_or_default(); material.base_color_texture = Some(path.clone()); (entity, material) }) .collect::>(); set_material_group_with_history(world, changes); Ok(()) } pub fn apply_material_asset_to_selection( world: &mut World, asset: &EditorAsset, selected: &SelectedEntities, ) -> Result<(), String> { crate::assets::materials::apply_material_asset_to_selection(world, asset, selected) } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum HierarchyNodeKind { Authored, Runtime, Generated, }