use bevy::prelude::*; use shared::{ infer_actor_kind, ActorId, ActorKind, ActorName, ColliderDesc, EditorVisibility, HierarchySiblingIndex, InspectorOrder, LevelObject, LightDesc, MaterialDesc, MaterialOverride, ModelRef, ObjectiveMarker, PhysicsBody, PlayerSpawn, PostProcessVolumeDesc, PrefabInstance, PrefabRef, Primitive, RigidBodyDesc, StaticMeshRenderer, TeamSpawn, TriggerVolume, WeaponSpawn, }; use crate::scene_io::SceneIo; use crate::selection::SelectedEntity; use crate::state::scene_tools_active; use crate::ui::hierarchy_ops::{ apply_sibling_change, apply_sibling_change_new, next_sibling_index, reorder_entities_under_parent, would_create_cycle, }; use crate::ui::UiState; mod commands; pub use commands::{EditorCommand, EditorEntitySnapshot, SiblingChange}; #[derive(Resource, Debug, Default)] pub struct EditorHistory { undo_stack: Vec, redo_stack: Vec, pub status: String, } impl EditorHistory { pub fn push(&mut self, command: EditorCommand) { self.undo_stack.push(command); self.redo_stack.clear(); self.status = format!("Undo: {} command(s), redo cleared", self.undo_stack.len()); } pub fn can_undo(&self) -> bool { !self.undo_stack.is_empty() } pub fn can_redo(&self) -> bool { !self.redo_stack.is_empty() } fn pop_undo(&mut self) -> Option { self.undo_stack.pop() } fn push_undo(&mut self, command: EditorCommand) { self.undo_stack.push(command); } fn pop_redo(&mut self) -> Option { self.redo_stack.pop() } fn push_redo(&mut self, command: EditorCommand) { self.redo_stack.push(command); } pub fn clear(&mut self) { self.undo_stack.clear(); self.redo_stack.clear(); self.status = "History cleared".to_string(); } } #[derive(Resource, Debug, Default)] struct TransformEditTracker { active: Option<(Entity, Transform, Vec<(Entity, Transform)>)>, } pub struct EditorHistoryPlugin; impl Plugin for EditorHistoryPlugin { fn build(&self, app: &mut App) { app.init_resource::() .init_resource::() .add_systems( Update, (handle_history_hotkeys, capture_gizmo_transform_edits) .chain() .run_if(scene_tools_active), ); } } pub fn snapshot_entity(world: &World, entity: Entity) -> Option { let entity_ref = world.get_entity(entity).ok()?; if !entity_ref.contains::() { return None; } let children = entity_ref .get::() .map(|children| { children .iter() .filter_map(|child| snapshot_entity(world, child)) .collect() }) .unwrap_or_default(); let actor_kind = entity_ref .get::() .copied() .or_else(|| infer_actor_kind(entity_ref)) .unwrap_or(ActorKind::Empty); Some(EditorEntitySnapshot { actor_id: entity_ref.get::().cloned(), actor_kind, actor_name: entity_ref.get::().cloned(), name: entity_ref .get::() .map(|name| name.as_str().to_string()), transform: entity_ref.get::().copied().unwrap_or_default(), primitive: entity_ref.get::().cloned(), static_mesh_renderer: entity_ref.get::().cloned(), material: entity_ref.get::().cloned(), material_override: entity_ref.get::().cloned(), rigid_body: entity_ref.get::().cloned(), collider: entity_ref.get::().cloned(), physics: entity_ref.get::().cloned(), light: entity_ref.get::().cloned(), player_spawn: entity_ref.contains::(), model: entity_ref.get::().cloned(), prefab: entity_ref.get::().cloned(), prefab_instance: entity_ref.get::().cloned(), weapon_spawn: entity_ref.get::().cloned(), trigger_volume: entity_ref.get::().cloned(), post_process_volume: entity_ref.get::().cloned(), team_spawn: entity_ref.get::().cloned(), objective: entity_ref.get::().cloned(), hierarchy_sibling_index: entity_ref .get::() .map(|index| index.0) .unwrap_or(0), editor_visibility: entity_ref .get::() .copied() .unwrap_or_default(), children, }) } pub fn spawn_snapshot(world: &mut World, snapshot: &EditorEntitySnapshot) -> Entity { spawn_snapshot_with_parent(world, snapshot, None) } fn new_actor_id() -> ActorId { ActorId::new(uuid::Uuid::new_v4().to_string()) } fn spawn_snapshot_with_parent( world: &mut World, snapshot: &EditorEntitySnapshot, parent: Option, ) -> Entity { let mut entity_mut = world.spawn((LevelObject, snapshot.actor_kind, snapshot.transform)); entity_mut.insert( snapshot .actor_id .clone() .filter(|id| !id.0.trim().is_empty()) .unwrap_or_else(new_actor_id), ); if let Some(actor_name) = &snapshot.actor_name { entity_mut.insert(actor_name.clone()); } if let Some(name) = &snapshot.name { entity_mut.insert(Name::new(name.clone())); } if let Some(primitive) = &snapshot.primitive { entity_mut.insert(primitive.clone()); } if let Some(renderer) = &snapshot.static_mesh_renderer { entity_mut.insert(renderer.clone()); } if let Some(material) = &snapshot.material { entity_mut.insert(material.clone()); } if let Some(material_override) = &snapshot.material_override { entity_mut.insert(material_override.clone()); } if let Some(rigid_body) = &snapshot.rigid_body { entity_mut.insert(*rigid_body); } if let Some(collider) = &snapshot.collider { entity_mut.insert(collider.clone()); } if let Some(physics) = &snapshot.physics { entity_mut.insert(physics.clone()); } if let Some(light) = &snapshot.light { entity_mut.insert(light.clone()); } if snapshot.player_spawn { entity_mut.insert(PlayerSpawn); } if let Some(model) = &snapshot.model { entity_mut.insert(model.clone()); } if let Some(prefab) = &snapshot.prefab { entity_mut.insert(prefab.clone()); } if let Some(instance) = &snapshot.prefab_instance { entity_mut.insert(instance.clone()); } if let Some(weapon) = &snapshot.weapon_spawn { entity_mut.insert(weapon.clone()); } if let Some(trigger) = &snapshot.trigger_volume { entity_mut.insert(trigger.clone()); } if let Some(pp) = &snapshot.post_process_volume { entity_mut.insert(pp.clone()); } if let Some(team) = &snapshot.team_spawn { entity_mut.insert(team.clone()); } if let Some(objective) = &snapshot.objective { entity_mut.insert(objective.clone()); } entity_mut.insert(HierarchySiblingIndex(snapshot.hierarchy_sibling_index)); entity_mut.insert(snapshot.editor_visibility); if let Some(parent) = parent { entity_mut.insert(ChildOf(parent)); } let entity = entity_mut.id(); for child in &snapshot.children { spawn_snapshot_with_parent(world, child, Some(entity)); } entity } pub fn clear_level_objects(world: &mut World) { let mut query = world.query_filtered::>(); let entities: Vec = query.iter(world).collect(); for entity in entities { if let Ok(entity_mut) = world.get_entity_mut(entity) { entity_mut.despawn(); } } } pub fn spawn_with_history(world: &mut World, mut snapshot: EditorEntitySnapshot) -> Entity { snapshot.hierarchy_sibling_index = next_sibling_index(world, None); let entity = spawn_snapshot(world, &snapshot); push_history( world, EditorCommand::Spawn { snapshot, entity: Some(entity), }, ); select_one(world, entity); entity } pub fn delete_entities_with_history(world: &mut World, entities: &[Entity]) { let mut deleted = Vec::new(); for entity in entities { if let Some(snapshot) = snapshot_entity(world, *entity) { deleted.push((snapshot, *entity)); } } for (_, entity) in &deleted { if let Ok(entity_mut) = world.get_entity_mut(*entity) { entity_mut.despawn(); } } for (snapshot, entity) in deleted { push_history( world, EditorCommand::Despawn { snapshot, entity: Some(entity), }, ); } clear_selection(world); } pub fn duplicate_entities_with_history(world: &mut World, entities: &[Entity]) { let snapshots: Vec = entities .iter() .filter_map(|entity| snapshot_entity(world, *entity)) .map(|mut snapshot| { snapshot.transform.translation += Vec3::new(1.0, 0.0, 1.0); if let Some(name) = &mut snapshot.name { name.push_str(" Copy"); } snapshot }) .collect(); if snapshots.is_empty() { return; } let entities: Vec = snapshots .iter() .map(|snapshot| spawn_snapshot(world, snapshot)) .collect(); push_history( world, EditorCommand::Duplicate { snapshots, entities: entities.clone(), }, ); select_many(world, &entities); } pub fn rename_entity_with_history(world: &mut World, entity: Entity, new_name: String) { let old = world .get::(entity) .map(|name| name.as_str().to_string()) .unwrap_or_default(); if old == new_name { return; } if let Ok(mut entity_mut) = world.get_entity_mut(entity) { entity_mut.insert(Name::new(new_name.clone())); } push_history( world, EditorCommand::Rename { entity, old, new: new_name, }, ); } pub fn set_material_with_history(world: &mut World, entity: Entity, new: MaterialDesc) { if !is_level_object(world, entity) { return; } let old = world.get::(entity).cloned(); if old.as_ref().is_some_and(|old| material_eq(old, &new)) { return; } if let Ok(mut entity_mut) = world.get_entity_mut(entity) { entity_mut.insert(new.clone()); } push_history(world, EditorCommand::SetMaterial { entity, old, new }); } pub fn set_material_override_with_history( world: &mut World, entity: Entity, new: MaterialOverride, ) { let old = world.get::(entity).cloned(); if old.as_ref() == Some(&new) { return; } if let Ok(mut entity_mut) = world.get_entity_mut(entity) { entity_mut.insert(new.clone()); } push_history( world, EditorCommand::SetMaterialOverride { entity, old, new }, ); } pub fn set_light_with_history(world: &mut World, entity: Entity, new: LightDesc) { if !is_level_object(world, entity) { return; } let old = world.get::(entity).cloned(); if let Ok(mut entity_mut) = world.get_entity_mut(entity) { entity_mut.insert(new.clone()); } push_history(world, EditorCommand::SetLight { entity, old, new }); } pub fn set_post_process_volume_with_history( world: &mut World, entity: Entity, new: PostProcessVolumeDesc, ) { if !is_level_object(world, entity) { return; } let old = world.get::(entity).cloned(); if let Ok(mut entity_mut) = world.get_entity_mut(entity) { entity_mut.insert(new.clone()); } push_history( world, EditorCommand::SetPostProcessVolume { entity, old, new }, ); } pub fn set_physics_with_history(world: &mut World, entity: Entity, new: PhysicsBody) { if !is_level_object(world, entity) { return; } let old = world.get::(entity).cloned(); if let Ok(mut entity_mut) = world.get_entity_mut(entity) { entity_mut.insert(new.clone()); } push_history(world, EditorCommand::SetPhysics { entity, old, new }); } pub fn set_rigid_body_with_history(world: &mut World, entity: Entity, new: RigidBodyDesc) { let old = world.get::(entity).copied(); if old == Some(new) { return; } if let Ok(mut entity_mut) = world.get_entity_mut(entity) { entity_mut.insert(new); } push_history(world, EditorCommand::SetRigidBody { entity, old, new }); } pub fn set_collider_with_history(world: &mut World, entity: Entity, new: ColliderDesc) { let old = world.get::(entity).cloned(); if old.as_ref() == Some(&new) { return; } if let Ok(mut entity_mut) = world.get_entity_mut(entity) { entity_mut.insert(new.clone()); } push_history(world, EditorCommand::SetCollider { entity, old, new }); } pub fn set_primitive_with_history(world: &mut World, entity: Entity, new: Primitive) { if !is_level_object(world, entity) { return; } let old = world.get::(entity).cloned(); if let Ok(mut entity_mut) = world.get_entity_mut(entity) { entity_mut.insert(new.clone()); } push_history(world, EditorCommand::SetPrimitive { entity, old, new }); } pub fn set_static_mesh_renderer_with_history( world: &mut World, entity: Entity, new: StaticMeshRenderer, ) { if !is_level_object(world, entity) { return; } let old = world.get::(entity).cloned(); if old.as_ref() == Some(&new) { return; } if let Ok(mut entity_mut) = world.get_entity_mut(entity) { entity_mut.insert(new.clone()); } push_history( world, EditorCommand::SetStaticMeshRenderer { entity, old, new }, ); } pub fn reparent_with_history(world: &mut World, entity: Entity, new_parent: Option) { reorder_siblings_with_history(world, &[entity], new_parent, i32::MAX); } pub fn reorder_siblings_with_history( world: &mut World, entities: &[Entity], new_parent: Option, insert_index: i32, ) { let entities: Vec = entities .iter() .copied() .filter(|entity| is_level_object(world, *entity)) .collect(); if entities.is_empty() { return; } if would_create_cycle(world, &entities, new_parent) { return; } let changes = reorder_entities_under_parent(world, &entities, new_parent, insert_index); if changes.is_empty() { return; } push_history(world, EditorCommand::ReorderSibling { changes }); } pub fn set_editor_visibility_with_history( world: &mut World, entity: Entity, new: EditorVisibility, ) { if !is_level_object(world, entity) { return; } let old = world .get::(entity) .copied() .unwrap_or_default(); if old.visible == new.visible { return; } if let Ok(mut entity_mut) = world.get_entity_mut(entity) { entity_mut.insert(new); } push_history( world, EditorCommand::SetEditorVisibility { entity, old, new }, ); } pub fn set_inspector_order_with_history(world: &mut World, entity: Entity, new: InspectorOrder) { if !is_level_object(world, entity) { return; } let old = world.get::(entity).cloned(); if old.as_ref() == Some(&new) { return; } if let Ok(mut entity_mut) = world.get_entity_mut(entity) { entity_mut.insert(new.clone()); } push_history(world, EditorCommand::SetInspectorOrder { entity, old, new }); } pub fn group_selection_with_history(world: &mut World, entities: &[Entity]) { let entities: Vec = entities .iter() .copied() .filter(|entity| is_level_object(world, *entity)) .collect(); if entities.is_empty() { return; } let snapshot = EditorEntitySnapshot { actor_id: None, actor_kind: ActorKind::Empty, actor_name: None, name: Some("Group".to_string()), transform: Transform::default(), primitive: None, static_mesh_renderer: None, material: None, material_override: None, rigid_body: None, collider: None, physics: None, light: 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: next_sibling_index(world, None), editor_visibility: EditorVisibility::default(), children: Vec::new(), }; let group = spawn_snapshot(world, &snapshot); push_history( world, EditorCommand::Spawn { snapshot, entity: Some(group), }, ); reorder_siblings_with_history(world, &entities, Some(group), i32::MAX); } pub fn create_empty_child_with_history(world: &mut World, parent: Entity) { if !is_level_object(world, parent) { return; } let snapshot = EditorEntitySnapshot { actor_id: None, actor_kind: ActorKind::Empty, actor_name: None, name: Some("Empty".to_string()), transform: Transform::default(), primitive: None, static_mesh_renderer: None, material: None, material_override: None, rigid_body: None, collider: None, physics: None, light: 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: next_sibling_index(world, Some(parent)), editor_visibility: EditorVisibility::default(), children: Vec::new(), }; let entity = spawn_snapshot_with_parent(world, &snapshot, Some(parent)); push_history( world, EditorCommand::Spawn { snapshot, entity: Some(entity), }, ); select_one(world, entity); } pub fn unpack_prefab_instance(world: &mut World, entity: Entity) { if world.get::(entity).is_none() { return; } if let Ok(mut entity_mut) = world.get_entity_mut(entity) { entity_mut.remove::(); } mark_dirty(world); } pub fn set_transform_with_history( world: &mut World, entity: Entity, old: Transform, new: Transform, ) { if transform_nearly_eq(&old, &new) { return; } push_history(world, EditorCommand::SetTransform { entity, old, new }); } pub fn set_actor_kind_with_history( world: &mut World, entity: Entity, old: ActorKind, new: ActorKind, ) { if old == new { return; } push_history(world, EditorCommand::SetActorKind { entity, old, new }); } pub fn apply_actor_kind(world: &mut World, entity: Entity, kind: ActorKind) { if let Ok(mut entity_mut) = world.get_entity_mut(entity) { entity_mut.insert(kind); } } pub fn apply_command_undo(world: &mut World) { let Some(mut command) = world.resource_mut::().pop_undo() else { return; }; undo_command(world, &mut command); world.resource_mut::().push_redo(command); mark_dirty(world); } pub fn apply_command_redo(world: &mut World) { let Some(mut command) = world.resource_mut::().pop_redo() else { return; }; redo_command(world, &mut command); world.resource_mut::().push_undo(command); mark_dirty(world); } fn undo_command(world: &mut World, command: &mut EditorCommand) { match command { EditorCommand::Spawn { entity, .. } => { despawn_entity(world, entity.take()); clear_selection(world); } EditorCommand::Despawn { snapshot, entity } => { let spawned = spawn_snapshot(world, snapshot); *entity = Some(spawned); select_one(world, spawned); } EditorCommand::Duplicate { entities, .. } => { for entity in std::mem::take(entities) { despawn_entity(world, Some(entity)); } clear_selection(world); } EditorCommand::Rename { entity, old, .. } => { if let Ok(mut entity_mut) = world.get_entity_mut(*entity) { entity_mut.insert(Name::new(old.clone())); } } EditorCommand::SetTransform { entity, old, .. } => { if let Some(mut transform) = world.get_mut::(*entity) { *transform = *old; } } EditorCommand::SetMaterial { entity, old, .. } => { apply_material(world, *entity, old); } EditorCommand::SetMaterialOverride { entity, old, .. } => { apply_material_override(world, *entity, old); } EditorCommand::SetLight { entity, old, .. } => { apply_light(world, *entity, old); } EditorCommand::SetPostProcessVolume { entity, old, .. } => { apply_post_process_volume(world, *entity, old); } EditorCommand::SetPhysics { entity, old, .. } => { apply_physics(world, *entity, old); } EditorCommand::SetRigidBody { entity, old, .. } => { apply_rigid_body(world, *entity, old); } EditorCommand::SetCollider { entity, old, .. } => { apply_collider(world, *entity, old); } EditorCommand::SetPrimitive { entity, old, .. } => { apply_primitive(world, *entity, old); } EditorCommand::SetStaticMeshRenderer { entity, old, .. } => { apply_static_mesh_renderer(world, *entity, old); } EditorCommand::SetTransformGroup { entities, olds, .. } => { for (entity, old) in entities.iter().zip(olds.iter()) { if let Some(mut transform) = world.get_mut::(*entity) { *transform = *old; } } } EditorCommand::Reparent { entity, old_parent, .. } => { apply_parent(world, *entity, *old_parent); } EditorCommand::SetActorKind { entity, old, .. } => { apply_actor_kind(world, *entity, *old); } EditorCommand::ReorderSibling { changes } => { for change in changes.iter().rev() { apply_sibling_change(world, change); } } EditorCommand::SetEditorVisibility { entity, old, .. } => { if let Ok(mut entity_mut) = world.get_entity_mut(*entity) { entity_mut.insert(*old); } } EditorCommand::SetInspectorOrder { entity, old, .. } => { apply_inspector_order(world, *entity, old); } EditorCommand::AddComponent { entity, snapshot } => { remove_authored_components(world, *entity, snapshot); } EditorCommand::RemoveComponent { entity, snapshot } => { apply_authored_components(world, *entity, snapshot); } } } fn redo_command(world: &mut World, command: &mut EditorCommand) { match command { EditorCommand::Spawn { snapshot, entity } => { let spawned = spawn_snapshot(world, snapshot); *entity = Some(spawned); select_one(world, spawned); } EditorCommand::Despawn { entity, .. } => { despawn_entity(world, entity.take()); clear_selection(world); } EditorCommand::Duplicate { snapshots, entities, } => { let spawned: Vec = snapshots .iter() .map(|snapshot| spawn_snapshot(world, snapshot)) .collect(); *entities = spawned.clone(); select_many(world, &spawned); } EditorCommand::Rename { entity, new, .. } => { if let Ok(mut entity_mut) = world.get_entity_mut(*entity) { entity_mut.insert(Name::new(new.clone())); } } EditorCommand::SetTransform { entity, new, .. } => { if let Some(mut transform) = world.get_mut::(*entity) { *transform = *new; } } EditorCommand::SetMaterial { entity, new, .. } => { if let Ok(mut entity_mut) = world.get_entity_mut(*entity) { entity_mut.insert(new.clone()); } } EditorCommand::SetMaterialOverride { entity, new, .. } => { if let Ok(mut entity_mut) = world.get_entity_mut(*entity) { entity_mut.insert(new.clone()); } } EditorCommand::SetLight { entity, new, .. } => { if let Ok(mut entity_mut) = world.get_entity_mut(*entity) { entity_mut.insert(new.clone()); } } EditorCommand::SetPostProcessVolume { entity, new, .. } => { if let Ok(mut entity_mut) = world.get_entity_mut(*entity) { entity_mut.insert(new.clone()); } } EditorCommand::SetPhysics { entity, new, .. } => { if let Ok(mut entity_mut) = world.get_entity_mut(*entity) { entity_mut.insert(new.clone()); } } EditorCommand::SetRigidBody { entity, new, .. } => { if let Ok(mut entity_mut) = world.get_entity_mut(*entity) { entity_mut.insert(*new); } } EditorCommand::SetCollider { entity, new, .. } => { if let Ok(mut entity_mut) = world.get_entity_mut(*entity) { entity_mut.insert(new.clone()); } } EditorCommand::SetPrimitive { entity, new, .. } => { if let Ok(mut entity_mut) = world.get_entity_mut(*entity) { entity_mut.insert(new.clone()); } } EditorCommand::SetStaticMeshRenderer { entity, new, .. } => { if let Ok(mut entity_mut) = world.get_entity_mut(*entity) { entity_mut.insert(new.clone()); } } EditorCommand::SetTransformGroup { entities, news, .. } => { for (entity, new) in entities.iter().zip(news.iter()) { if let Some(mut transform) = world.get_mut::(*entity) { *transform = *new; } } } EditorCommand::Reparent { entity, new_parent, .. } => { apply_parent(world, *entity, *new_parent); } EditorCommand::SetActorKind { entity, new, .. } => { apply_actor_kind(world, *entity, *new); } EditorCommand::ReorderSibling { changes } => { for change in changes { apply_sibling_change_new(world, change); } } EditorCommand::SetEditorVisibility { entity, new, .. } => { if let Ok(mut entity_mut) = world.get_entity_mut(*entity) { entity_mut.insert(*new); } } EditorCommand::SetInspectorOrder { entity, new, .. } => { if let Ok(mut entity_mut) = world.get_entity_mut(*entity) { entity_mut.insert(new.clone()); } } EditorCommand::AddComponent { entity, snapshot } => { apply_authored_components(world, *entity, snapshot); } EditorCommand::RemoveComponent { entity, snapshot } => { remove_authored_components(world, *entity, snapshot); } } } fn apply_material(world: &mut World, entity: Entity, material: &Option) { if let Ok(mut entity_mut) = world.get_entity_mut(entity) { match material { Some(value) => { entity_mut.insert(value.clone()); } None => { entity_mut.remove::(); } } } } fn apply_material_override( world: &mut World, entity: Entity, material_override: &Option, ) { if let Ok(mut entity_mut) = world.get_entity_mut(entity) { match material_override { Some(value) => { entity_mut.insert(value.clone()); } None => { entity_mut.remove::(); } } } } fn apply_light(world: &mut World, entity: Entity, light: &Option) { if let Ok(mut entity_mut) = world.get_entity_mut(entity) { match light { Some(value) => { entity_mut.insert(value.clone()); } None => { entity_mut.remove::(); } } } } fn apply_post_process_volume( world: &mut World, entity: Entity, volume: &Option, ) { if let Ok(mut entity_mut) = world.get_entity_mut(entity) { match volume { Some(value) => { entity_mut.insert(value.clone()); } None => { entity_mut.remove::(); } } } } fn apply_physics(world: &mut World, entity: Entity, physics: &Option) { if let Ok(mut entity_mut) = world.get_entity_mut(entity) { match physics { Some(value) => { entity_mut.insert(value.clone()); } None => { entity_mut.remove::(); } } } } fn apply_rigid_body(world: &mut World, entity: Entity, rigid_body: &Option) { if let Ok(mut entity_mut) = world.get_entity_mut(entity) { match rigid_body { Some(value) => { entity_mut.insert(*value); } None => { entity_mut.remove::(); } } } } fn apply_collider(world: &mut World, entity: Entity, collider: &Option) { if let Ok(mut entity_mut) = world.get_entity_mut(entity) { match collider { Some(value) => { entity_mut.insert(value.clone()); } None => { entity_mut.remove::(); } } } } fn apply_primitive(world: &mut World, entity: Entity, primitive: &Option) { if let Ok(mut entity_mut) = world.get_entity_mut(entity) { match primitive { Some(value) => { entity_mut.insert(value.clone()); } None => { entity_mut.remove::(); } } } } fn apply_static_mesh_renderer( world: &mut World, entity: Entity, renderer: &Option, ) { if let Ok(mut entity_mut) = world.get_entity_mut(entity) { match renderer { Some(value) => { entity_mut.insert(value.clone()); } None => { entity_mut.remove::(); } } } } fn apply_inspector_order( world: &mut World, entity: Entity, inspector_order: &Option, ) { if let Ok(mut entity_mut) = world.get_entity_mut(entity) { match inspector_order { Some(value) => { entity_mut.insert(value.clone()); } None => { entity_mut.remove::(); } } } } fn apply_parent(world: &mut World, entity: Entity, parent: Option) { if let Ok(mut entity_mut) = world.get_entity_mut(entity) { match parent { Some(p) => { entity_mut.insert(ChildOf(p)); } None => { entity_mut.remove::(); } } } } fn apply_authored_components(world: &mut World, entity: Entity, snapshot: &EditorEntitySnapshot) { if let Ok(mut entity_mut) = world.get_entity_mut(entity) { entity_mut.insert(snapshot.actor_kind); if let Some(v) = &snapshot.primitive { entity_mut.insert(v.clone()); } if let Some(v) = &snapshot.static_mesh_renderer { entity_mut.insert(v.clone()); } if let Some(v) = &snapshot.material { entity_mut.insert(v.clone()); } if let Some(v) = &snapshot.material_override { entity_mut.insert(v.clone()); } if let Some(v) = &snapshot.rigid_body { entity_mut.insert(*v); } if let Some(v) = &snapshot.collider { entity_mut.insert(v.clone()); } if let Some(v) = &snapshot.physics { entity_mut.insert(v.clone()); } if let Some(v) = &snapshot.light { entity_mut.insert(v.clone()); } if snapshot.player_spawn { entity_mut.insert(PlayerSpawn); } if let Some(v) = &snapshot.model { entity_mut.insert(v.clone()); } if let Some(v) = &snapshot.prefab { entity_mut.insert(v.clone()); } if let Some(v) = &snapshot.prefab_instance { entity_mut.insert(v.clone()); } if let Some(v) = &snapshot.weapon_spawn { entity_mut.insert(v.clone()); } if let Some(v) = &snapshot.trigger_volume { entity_mut.insert(v.clone()); } if let Some(v) = &snapshot.post_process_volume { entity_mut.insert(v.clone()); } if let Some(v) = &snapshot.team_spawn { entity_mut.insert(v.clone()); } if let Some(v) = &snapshot.objective { entity_mut.insert(v.clone()); } } } fn remove_authored_components(world: &mut World, entity: Entity, snapshot: &EditorEntitySnapshot) { if let Ok(mut entity_mut) = world.get_entity_mut(entity) { if snapshot.primitive.is_some() { entity_mut.remove::(); } if snapshot.static_mesh_renderer.is_some() { entity_mut.remove::(); } if snapshot.material.is_some() { entity_mut.remove::(); } if snapshot.material_override.is_some() { entity_mut.remove::(); } if snapshot.rigid_body.is_some() { entity_mut.remove::(); } if snapshot.collider.is_some() { entity_mut.remove::(); } if snapshot.physics.is_some() { entity_mut.remove::(); } if snapshot.light.is_some() { entity_mut.remove::(); } if snapshot.player_spawn { entity_mut.remove::(); } if snapshot.model.is_some() { entity_mut.remove::(); } if snapshot.prefab.is_some() { entity_mut.remove::(); } if snapshot.prefab_instance.is_some() { entity_mut.remove::(); } if snapshot.weapon_spawn.is_some() { entity_mut.remove::(); } if snapshot.trigger_volume.is_some() { entity_mut.remove::(); } if snapshot.post_process_volume.is_some() { entity_mut.remove::(); } if snapshot.team_spawn.is_some() { entity_mut.remove::(); } if snapshot.objective.is_some() { entity_mut.remove::(); } } } fn handle_history_hotkeys(world: &mut World) { let (undo, redo, delete, duplicate) = { let keys = world.resource::>(); let ctrl = keys.any_pressed([KeyCode::ControlLeft, KeyCode::ControlRight]); let shift = keys.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]); ( ctrl && keys.just_pressed(KeyCode::KeyZ) && !shift, ctrl && (keys.just_pressed(KeyCode::KeyY) || (shift && keys.just_pressed(KeyCode::KeyZ))), keys.just_pressed(KeyCode::Delete) || keys.just_pressed(KeyCode::Backspace), ctrl && keys.just_pressed(KeyCode::KeyD), ) }; if undo { apply_command_undo(world); return; } if redo { apply_command_redo(world); return; } let selection: Vec = world .get_resource::() .map(|ui_state| { ui_state .selected_entities .iter() .filter(|entity| is_level_object(world, *entity)) .collect() }) .unwrap_or_default(); if delete && !selection.is_empty() { delete_entities_with_history(world, &selection); } else if duplicate && !selection.is_empty() { duplicate_entities_with_history(world, &selection); } } fn capture_gizmo_transform_edits(world: &mut World) { let selection: Vec = world .get_resource::() .map(|ui| { ui.selected_entities .iter() .filter(|entity| is_level_object(world, *entity)) .collect() }) .unwrap_or_default(); let mut query = world.query::<( Entity, &Transform, &transform_gizmo_bevy::prelude::GizmoTarget, )>(); let active = query .iter(world) .find(|(_, _, target)| target.is_active()) .map(|(entity, transform, _)| (entity, *transform)); let start_group = active.map(|(entity, transform)| selected_transforms(world, &selection, entity, transform)); let finished_edit = { let mut tracker = world.resource_mut::(); if tracker.active.is_none() { if let (Some((entity, transform)), Some(group)) = (active, start_group) { tracker.active = Some((entity, transform, group)); return; } } match tracker.active.take() { Some((primary, old, group)) if active.is_none() => Some((primary, old, group)), Some(state) if active.is_some() => { tracker.active = Some(state); None } _ => None, } }; if let Some((primary, old_primary, group)) = finished_edit { let Some(new_primary) = world.get::(primary).copied() else { return; }; if group.len() <= 1 { set_transform_with_history(world, primary, old_primary, new_primary); return; } let translation_delta = new_primary.translation - old_primary.translation; let rotation_delta = new_primary.rotation * old_primary.rotation.inverse(); let scale_ratio = new_primary.scale / old_primary.scale; let mut entities = Vec::new(); let mut olds = Vec::new(); let mut news = Vec::new(); for (entity, old) in group { let new = Transform { translation: old.translation + translation_delta, rotation: rotation_delta * old.rotation, scale: old.scale * scale_ratio, }; if let Some(mut transform) = world.get_mut::(entity) { *transform = new; } entities.push(entity); olds.push(old); news.push(new); } if !entities.is_empty() { push_history( world, EditorCommand::SetTransformGroup { entities, olds, news, }, ); } } } fn selected_transforms( world: &World, selection: &[Entity], primary: Entity, primary_transform: Transform, ) -> Vec<(Entity, Transform)> { if selection.len() > 1 && selection.contains(&primary) { selection .iter() .filter_map(|entity| { world .get::(*entity) .copied() .map(|transform| (*entity, transform)) }) .collect() } else { vec![(primary, primary_transform)] } } fn push_history(world: &mut World, command: EditorCommand) { world.resource_mut::().push(command); mark_dirty(world); } pub fn push_command(world: &mut World, command: EditorCommand) { push_history(world, command); } fn mark_dirty(world: &mut World) { if let Some(mut io) = world.get_resource_mut::() { io.mark_dirty(); } } fn despawn_entity(world: &mut World, entity: Option) { if let Some(entity) = entity { if let Ok(entity_mut) = world.get_entity_mut(entity) { entity_mut.despawn(); } } } fn select_one(world: &mut World, entity: Entity) { if let Some(mut ui_state) = world.get_resource_mut::() { ui_state.selected_entities.clear(); ui_state.selected_entities.select_replace(entity); } world.resource_mut::().0 = Some(entity); } fn select_many(world: &mut World, entities: &[Entity]) { if let Some(mut ui_state) = world.get_resource_mut::() { ui_state.selected_entities.clear(); for (index, entity) in entities.iter().enumerate() { if index == 0 { ui_state.selected_entities.select_replace(*entity); } else { ui_state.selected_entities.select_maybe_add(*entity, true); } } } world.resource_mut::().0 = entities.first().copied(); } pub fn clear_selection(world: &mut World) { if let Some(mut ui_state) = world.get_resource_mut::() { ui_state.selected_entities.clear(); } world.resource_mut::().0 = None; } pub fn is_level_object(world: &World, entity: Entity) -> bool { world .get_entity(entity) .is_ok_and(|entity_ref| entity_ref.contains::()) } fn transform_nearly_eq(a: &Transform, b: &Transform) -> bool { a.translation.abs_diff_eq(b.translation, 0.0001) && a.rotation.abs_diff_eq(b.rotation, 0.0001) && a.scale.abs_diff_eq(b.scale, 0.0001) } pub(crate) fn material_eq(a: &MaterialDesc, b: &MaterialDesc) -> bool { a.shader == b.shader && a.base_color.r == b.base_color.r && a.base_color.g == b.base_color.g && a.base_color.b == b.base_color.b && a.base_color.a == b.base_color.a && a.metallic == b.metallic && a.roughness == b.roughness && a.emissive_color.r == b.emissive_color.r && a.emissive_color.g == b.emissive_color.g && a.emissive_color.b == b.emissive_color.b && a.emissive_color.a == b.emissive_color.a && a.emissive_intensity == b.emissive_intensity && a.base_color_texture == b.base_color_texture && a.emissive_texture == b.emissive_texture && a.normal_map_texture == b.normal_map_texture && a.metallic_roughness_texture == b.metallic_roughness_texture && a.material_asset_path == b.material_asset_path && a.parameters == b.parameters && a.textures == b.textures }