1386 lines
43 KiB
Rust
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
|
|
}
|