Blacksite/crates/editor/src/history/mod.rs
Rbanh f29712d158
Some checks are pending
CI / Format, lint, test, build (push) Waiting to run
Initial project import
2026-06-05 21:44:45 -04:00

1386 lines
43 KiB
Rust

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<EditorCommand>,
redo_stack: Vec<EditorCommand>,
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<EditorCommand> {
self.undo_stack.pop()
}
fn push_undo(&mut self, command: EditorCommand) {
self.undo_stack.push(command);
}
fn pop_redo(&mut self) -> Option<EditorCommand> {
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::<EditorHistory>()
.init_resource::<TransformEditTracker>()
.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<EditorEntitySnapshot> {
let entity_ref = world.get_entity(entity).ok()?;
if !entity_ref.contains::<LevelObject>() {
return None;
}
let children = entity_ref
.get::<Children>()
.map(|children| {
children
.iter()
.filter_map(|child| snapshot_entity(world, child))
.collect()
})
.unwrap_or_default();
let actor_kind = entity_ref
.get::<ActorKind>()
.copied()
.or_else(|| infer_actor_kind(entity_ref))
.unwrap_or(ActorKind::Empty);
Some(EditorEntitySnapshot {
actor_id: entity_ref.get::<ActorId>().cloned(),
actor_kind,
actor_name: entity_ref.get::<ActorName>().cloned(),
name: entity_ref
.get::<Name>()
.map(|name| name.as_str().to_string()),
transform: entity_ref.get::<Transform>().copied().unwrap_or_default(),
primitive: entity_ref.get::<Primitive>().cloned(),
static_mesh_renderer: entity_ref.get::<StaticMeshRenderer>().cloned(),
material: entity_ref.get::<MaterialDesc>().cloned(),
material_override: entity_ref.get::<MaterialOverride>().cloned(),
rigid_body: entity_ref.get::<RigidBodyDesc>().cloned(),
collider: entity_ref.get::<ColliderDesc>().cloned(),
physics: entity_ref.get::<PhysicsBody>().cloned(),
light: entity_ref.get::<LightDesc>().cloned(),
player_spawn: entity_ref.contains::<PlayerSpawn>(),
model: entity_ref.get::<ModelRef>().cloned(),
prefab: entity_ref.get::<PrefabRef>().cloned(),
prefab_instance: entity_ref.get::<PrefabInstance>().cloned(),
weapon_spawn: entity_ref.get::<WeaponSpawn>().cloned(),
trigger_volume: entity_ref.get::<TriggerVolume>().cloned(),
post_process_volume: entity_ref.get::<PostProcessVolumeDesc>().cloned(),
team_spawn: entity_ref.get::<TeamSpawn>().cloned(),
objective: entity_ref.get::<ObjectiveMarker>().cloned(),
hierarchy_sibling_index: entity_ref
.get::<HierarchySiblingIndex>()
.map(|index| index.0)
.unwrap_or(0),
editor_visibility: entity_ref
.get::<EditorVisibility>()
.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>,
) -> 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::<Entity, With<LevelObject>>();
let entities: Vec<Entity> = 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<EditorEntitySnapshot> = 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<Entity> = 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::<Name>(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::<MaterialDesc>(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::<MaterialOverride>(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::<LightDesc>(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::<PostProcessVolumeDesc>(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::<PhysicsBody>(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::<RigidBodyDesc>(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::<ColliderDesc>(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::<Primitive>(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::<StaticMeshRenderer>(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<Entity>) {
reorder_siblings_with_history(world, &[entity], new_parent, i32::MAX);
}
pub fn reorder_siblings_with_history(
world: &mut World,
entities: &[Entity],
new_parent: Option<Entity>,
insert_index: i32,
) {
let entities: Vec<Entity> = 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::<EditorVisibility>(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::<InspectorOrder>(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<Entity> = 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::<PrefabInstance>(entity).is_none() {
return;
}
if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
entity_mut.remove::<PrefabInstance>();
}
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::<EditorHistory>().pop_undo() else {
return;
};
undo_command(world, &mut command);
world.resource_mut::<EditorHistory>().push_redo(command);
mark_dirty(world);
}
pub fn apply_command_redo(world: &mut World) {
let Some(mut command) = world.resource_mut::<EditorHistory>().pop_redo() else {
return;
};
redo_command(world, &mut command);
world.resource_mut::<EditorHistory>().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::<Transform>(*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::<Transform>(*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<Entity> = 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::<Transform>(*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::<Transform>(*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<MaterialDesc>) {
if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
match material {
Some(value) => {
entity_mut.insert(value.clone());
}
None => {
entity_mut.remove::<MaterialDesc>();
}
}
}
}
fn apply_material_override(
world: &mut World,
entity: Entity,
material_override: &Option<MaterialOverride>,
) {
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::<MaterialOverride>();
}
}
}
}
fn apply_light(world: &mut World, entity: Entity, light: &Option<LightDesc>) {
if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
match light {
Some(value) => {
entity_mut.insert(value.clone());
}
None => {
entity_mut.remove::<LightDesc>();
}
}
}
}
fn apply_post_process_volume(
world: &mut World,
entity: Entity,
volume: &Option<PostProcessVolumeDesc>,
) {
if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
match volume {
Some(value) => {
entity_mut.insert(value.clone());
}
None => {
entity_mut.remove::<PostProcessVolumeDesc>();
}
}
}
}
fn apply_physics(world: &mut World, entity: Entity, physics: &Option<PhysicsBody>) {
if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
match physics {
Some(value) => {
entity_mut.insert(value.clone());
}
None => {
entity_mut.remove::<PhysicsBody>();
}
}
}
}
fn apply_rigid_body(world: &mut World, entity: Entity, rigid_body: &Option<RigidBodyDesc>) {
if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
match rigid_body {
Some(value) => {
entity_mut.insert(*value);
}
None => {
entity_mut.remove::<RigidBodyDesc>();
}
}
}
}
fn apply_collider(world: &mut World, entity: Entity, collider: &Option<ColliderDesc>) {
if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
match collider {
Some(value) => {
entity_mut.insert(value.clone());
}
None => {
entity_mut.remove::<ColliderDesc>();
}
}
}
}
fn apply_primitive(world: &mut World, entity: Entity, primitive: &Option<Primitive>) {
if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
match primitive {
Some(value) => {
entity_mut.insert(value.clone());
}
None => {
entity_mut.remove::<Primitive>();
}
}
}
}
fn apply_static_mesh_renderer(
world: &mut World,
entity: Entity,
renderer: &Option<StaticMeshRenderer>,
) {
if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
match renderer {
Some(value) => {
entity_mut.insert(value.clone());
}
None => {
entity_mut.remove::<StaticMeshRenderer>();
}
}
}
}
fn apply_inspector_order(
world: &mut World,
entity: Entity,
inspector_order: &Option<InspectorOrder>,
) {
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::<InspectorOrder>();
}
}
}
}
fn apply_parent(world: &mut World, entity: Entity, parent: Option<Entity>) {
if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
match parent {
Some(p) => {
entity_mut.insert(ChildOf(p));
}
None => {
entity_mut.remove::<ChildOf>();
}
}
}
}
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::<Primitive>();
}
if snapshot.static_mesh_renderer.is_some() {
entity_mut.remove::<StaticMeshRenderer>();
}
if snapshot.material.is_some() {
entity_mut.remove::<MaterialDesc>();
}
if snapshot.material_override.is_some() {
entity_mut.remove::<MaterialOverride>();
}
if snapshot.rigid_body.is_some() {
entity_mut.remove::<RigidBodyDesc>();
}
if snapshot.collider.is_some() {
entity_mut.remove::<ColliderDesc>();
}
if snapshot.physics.is_some() {
entity_mut.remove::<PhysicsBody>();
}
if snapshot.light.is_some() {
entity_mut.remove::<LightDesc>();
}
if snapshot.player_spawn {
entity_mut.remove::<PlayerSpawn>();
}
if snapshot.model.is_some() {
entity_mut.remove::<ModelRef>();
}
if snapshot.prefab.is_some() {
entity_mut.remove::<PrefabRef>();
}
if snapshot.prefab_instance.is_some() {
entity_mut.remove::<PrefabInstance>();
}
if snapshot.weapon_spawn.is_some() {
entity_mut.remove::<WeaponSpawn>();
}
if snapshot.trigger_volume.is_some() {
entity_mut.remove::<TriggerVolume>();
}
if snapshot.post_process_volume.is_some() {
entity_mut.remove::<PostProcessVolumeDesc>();
}
if snapshot.team_spawn.is_some() {
entity_mut.remove::<TeamSpawn>();
}
if snapshot.objective.is_some() {
entity_mut.remove::<ObjectiveMarker>();
}
}
}
fn handle_history_hotkeys(world: &mut World) {
let (undo, redo, delete, duplicate) = {
let keys = world.resource::<ButtonInput<KeyCode>>();
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<Entity> = world
.get_resource::<UiState>()
.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<Entity> = world
.get_resource::<UiState>()
.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::<TransformEditTracker>();
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::<Transform>(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::<Transform>(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::<Transform>(*entity)
.copied()
.map(|transform| (*entity, transform))
})
.collect()
} else {
vec![(primary, primary_transform)]
}
}
fn push_history(world: &mut World, command: EditorCommand) {
world.resource_mut::<EditorHistory>().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::<SceneIo>() {
io.mark_dirty();
}
}
fn despawn_entity(world: &mut World, entity: Option<Entity>) {
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::<UiState>() {
ui_state.selected_entities.clear();
ui_state.selected_entities.select_replace(entity);
}
world.resource_mut::<SelectedEntity>().0 = Some(entity);
}
fn select_many(world: &mut World, entities: &[Entity]) {
if let Some(mut ui_state) = world.get_resource_mut::<UiState>() {
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::<SelectedEntity>().0 = entities.first().copied();
}
pub fn clear_selection(world: &mut World) {
if let Some(mut ui_state) = world.get_resource_mut::<UiState>() {
ui_state.selected_entities.clear();
}
world.resource_mut::<SelectedEntity>().0 = None;
}
pub fn is_level_object(world: &World, entity: Entity) -> bool {
world
.get_entity(entity)
.is_ok_and(|entity_ref| entity_ref.contains::<LevelObject>())
}
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
}