use crate::editor_scene::EditorScene;
use crate::project::ProjectState;
use crate::scene_writeback;
use nightshade::ecs::scene::{
AssetUuid, Scene, embed_referenced_meshes, embed_referenced_textures, entity_to_scene_entity,
spawn_scene,
};
use nightshade::ecs::transform::queries::query_descendants;
use nightshade::prelude::*;
pub const MAX_UNDO_DEPTH: usize = 200;
pub struct History<T: Reversible> {
undo_stack: Vec<HistoryEntry<T>>,
redo_stack: Vec<HistoryEntry<T>>,
max_entries: usize,
}
pub struct HistoryEntry<T> {
pub operation: T,
pub description: String,
}
impl<T: Reversible> Default for History<T> {
fn default() -> Self {
Self::new(MAX_UNDO_DEPTH)
}
}
impl<T: Reversible> History<T> {
pub fn new(max_entries: usize) -> Self {
Self {
undo_stack: Vec::new(),
redo_stack: Vec::new(),
max_entries,
}
}
pub fn push(&mut self, operation: T, description: impl Into<String>) {
self.redo_stack.clear();
self.undo_stack.push(HistoryEntry {
operation,
description: description.into(),
});
while self.undo_stack.len() > self.max_entries {
self.undo_stack.remove(0);
}
}
pub fn undo(&mut self, ctx: &mut T::Context<'_>) -> Option<T::Result> {
let entry = self.undo_stack.pop()?;
let (reversed, result) = entry.operation.reverse(ctx);
self.redo_stack.push(HistoryEntry {
operation: reversed,
description: entry.description,
});
Some(result)
}
pub fn redo(&mut self, ctx: &mut T::Context<'_>) -> Option<T::Result> {
let entry = self.redo_stack.pop()?;
let (reversed, result) = entry.operation.reverse(ctx);
self.undo_stack.push(HistoryEntry {
operation: reversed,
description: entry.description,
});
Some(result)
}
pub fn clear(&mut self) {
self.undo_stack.clear();
self.redo_stack.clear();
}
pub fn undo_len(&self) -> usize {
self.undo_stack.len()
}
}
pub trait Reversible: Sized {
type Context<'a>;
type Result;
fn reverse(&self, ctx: &mut Self::Context<'_>) -> (Self, Self::Result);
}
#[derive(Default)]
pub struct UndoResult {
pub select_entity: Option<Entity>,
}
pub struct UndoCtx<'a> {
pub world: &'a mut World,
pub editor_scene: &'a mut EditorScene,
pub project: &'a mut ProjectState,
}
#[derive(Clone)]
pub enum UndoableOperation {
EntityCreated {
captured: Box<CapturedSubtree>,
},
EntityDeleted {
captured: Box<CapturedSubtree>,
},
TransformChanged {
uuid: AssetUuid,
old: LocalTransform,
new: LocalTransform,
},
ComponentChanged {
uuid: AssetUuid,
old: Box<ComponentSnapshot>,
new: Box<ComponentSnapshot>,
},
Batch {
steps: Vec<UndoableOperation>,
},
}
#[derive(Clone)]
pub enum ComponentSnapshot {
Light(Light),
Visibility(bool),
CastsShadow(bool),
Name(String),
ParticleEmitter(Option<Box<nightshade::ecs::particles::components::ParticleEmitter>>),
Decal(Option<Box<nightshade::ecs::decal::components::Decal>>),
AudioSource(Option<Box<nightshade::ecs::audio::components::AudioSource>>),
RigidBody(Option<Box<RigidBodyComponent>>),
Collider(Option<Box<ColliderComponent>>),
CharacterController(Option<Box<CharacterControllerComponent>>),
NavmeshAgent(Option<Box<NavMeshAgent>>),
Camera(Option<nightshade::ecs::camera::components::Camera>),
AnimationPlayer(Option<Box<nightshade::ecs::animation::components::AnimationPlayer>>),
Material {
name: String,
material: Option<Box<nightshade::ecs::material::components::Material>>,
},
Text {
component: Option<Box<nightshade::ecs::text::components::Text>>,
content: Option<String>,
},
GrassRegion(Option<Box<nightshade::ecs::grass::components::GrassRegion>>),
GrassInteractor(Option<nightshade::ecs::grass::components::GrassInteractor>),
RenderLayer(Option<nightshade::ecs::primitives::RenderLayer>),
CullingMask(Option<nightshade::ecs::primitives::CullingMask>),
CameraCullingMask(Option<nightshade::ecs::primitives::CameraCullingMask>),
IgnoreParentScale(bool),
PrefabSource(Option<Box<nightshade::ecs::prefab::components::PrefabSource>>),
}
impl PartialEq for ComponentSnapshot {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Light(a), Self::Light(b)) => {
a.light_type == b.light_type
&& a.color == b.color
&& a.intensity == b.intensity
&& a.range == b.range
&& a.inner_cone_angle == b.inner_cone_angle
&& a.outer_cone_angle == b.outer_cone_angle
&& a.cast_shadows == b.cast_shadows
&& a.shadow_bias == b.shadow_bias
}
(Self::Visibility(a), Self::Visibility(b)) => a == b,
(Self::CastsShadow(a), Self::CastsShadow(b)) => a == b,
(Self::Name(a), Self::Name(b)) => a == b,
(Self::ParticleEmitter(a), Self::ParticleEmitter(b)) => {
snapshot_bytes(a) == snapshot_bytes(b)
}
(Self::Decal(a), Self::Decal(b)) => a == b,
(Self::AudioSource(a), Self::AudioSource(b)) => snapshot_bytes(a) == snapshot_bytes(b),
(Self::RigidBody(a), Self::RigidBody(b)) => snapshot_bytes(a) == snapshot_bytes(b),
(Self::Collider(a), Self::Collider(b)) => snapshot_bytes(a) == snapshot_bytes(b),
(Self::CharacterController(a), Self::CharacterController(b)) => {
snapshot_bytes(a) == snapshot_bytes(b)
}
(Self::NavmeshAgent(a), Self::NavmeshAgent(b)) => {
snapshot_bytes(a) == snapshot_bytes(b)
}
(Self::Camera(a), Self::Camera(b)) => a == b,
(Self::AnimationPlayer(a), Self::AnimationPlayer(b)) => a == b,
(
Self::Material {
name: a_name,
material: a_mat,
},
Self::Material {
name: b_name,
material: b_mat,
},
) => a_name == b_name && a_mat == b_mat,
(
Self::Text {
component: a_component,
content: a_content,
},
Self::Text {
component: b_component,
content: b_content,
},
) => {
snapshot_bytes(a_component) == snapshot_bytes(b_component) && a_content == b_content
}
(Self::GrassRegion(a), Self::GrassRegion(b)) => snapshot_bytes(a) == snapshot_bytes(b),
(Self::GrassInteractor(a), Self::GrassInteractor(b)) => {
snapshot_bytes(a) == snapshot_bytes(b)
}
(Self::RenderLayer(a), Self::RenderLayer(b)) => a == b,
(Self::CullingMask(a), Self::CullingMask(b)) => a == b,
(Self::CameraCullingMask(a), Self::CameraCullingMask(b)) => a == b,
(Self::IgnoreParentScale(a), Self::IgnoreParentScale(b)) => a == b,
(Self::PrefabSource(a), Self::PrefabSource(b)) => {
snapshot_bytes(a) == snapshot_bytes(b)
}
_ => false,
}
}
}
fn snapshot_bytes<T: serde::Serialize>(value: &T) -> Vec<u8> {
bincode::serialize(value).unwrap_or_default()
}
impl ComponentSnapshot {
pub fn capture(world: &World, entity: Entity, kind: SnapshotKind) -> Option<Self> {
match kind {
SnapshotKind::Light => world.core.get_light(entity).cloned().map(Self::Light),
SnapshotKind::Visibility => world
.core
.get_visibility(entity)
.map(|v| Self::Visibility(v.visible)),
SnapshotKind::CastsShadow => Some(Self::CastsShadow(
world.core.entity_has_casts_shadow(entity),
)),
SnapshotKind::Name => Some(Self::Name(
world
.core
.get_name(entity)
.map(|n| n.0.clone())
.unwrap_or_default(),
)),
SnapshotKind::ParticleEmitter => Some(Self::ParticleEmitter(
world
.core
.get_particle_emitter(entity)
.cloned()
.map(Box::new),
)),
SnapshotKind::Decal => Some(Self::Decal(
world.core.get_decal(entity).cloned().map(Box::new),
)),
SnapshotKind::AudioSource => Some(Self::AudioSource(
world.core.get_audio_source(entity).cloned().map(Box::new),
)),
SnapshotKind::RigidBody => Some(Self::RigidBody(
world.core.get_rigid_body(entity).cloned().map(Box::new),
)),
SnapshotKind::Collider => Some(Self::Collider(
world.core.get_collider(entity).cloned().map(Box::new),
)),
SnapshotKind::CharacterController => Some(Self::CharacterController(
world
.core
.get_character_controller(entity)
.cloned()
.map(Box::new),
)),
SnapshotKind::NavmeshAgent => Some(Self::NavmeshAgent(
world.core.get_navmesh_agent(entity).cloned().map(Box::new),
)),
SnapshotKind::Camera => Some(Self::Camera(world.core.get_camera(entity).copied())),
SnapshotKind::AnimationPlayer => Some(Self::AnimationPlayer(
world
.core
.get_animation_player(entity)
.cloned()
.map(Box::new),
)),
SnapshotKind::Material => {
let material_ref = world.core.get_material_ref(entity)?;
let name = material_ref.name.clone();
let material = nightshade::ecs::material::resources::material_registry_iter(
&world.resources.assets.material_registry,
)
.find(|(entry_name, _)| entry_name.as_str() == name.as_str())
.map(|(_, material)| Box::new(material.clone()));
Some(Self::Material { name, material })
}
SnapshotKind::Text => {
let component = world.core.get_text(entity).cloned().map(Box::new);
let content = component
.as_ref()
.and_then(|text| world.resources.text.cache.get_text(text.text_index))
.map(str::to_string);
Some(Self::Text { component, content })
}
SnapshotKind::GrassRegion => Some(Self::GrassRegion(
world.core.get_grass_region(entity).cloned().map(Box::new),
)),
SnapshotKind::GrassInteractor => Some(Self::GrassInteractor(
world.core.get_grass_interactor(entity).cloned(),
)),
SnapshotKind::RenderLayer => Some(Self::RenderLayer(
world.core.get_render_layer(entity).copied(),
)),
SnapshotKind::CullingMask => Some(Self::CullingMask(
world.core.get_culling_mask(entity).copied(),
)),
SnapshotKind::CameraCullingMask => Some(Self::CameraCullingMask(
world.core.get_camera_culling_mask(entity).copied(),
)),
SnapshotKind::IgnoreParentScale => Some(Self::IgnoreParentScale(
world.core.entity_has_ignore_parent_scale(entity),
)),
SnapshotKind::PrefabSource => Some(Self::PrefabSource(
world.core.get_prefab_source(entity).cloned().map(Box::new),
)),
}
}
pub fn apply(&self, world: &mut World, entity: Entity) {
use nightshade::ecs::world::{
ANIMATION_PLAYER, AUDIO_SOURCE, CAMERA, CAMERA_CULLING_MASK, CASTS_SHADOW, COLLIDER,
CULLING_MASK, DECAL, GRASS_INTERACTOR, GRASS_REGION, IGNORE_PARENT_SCALE,
NAVMESH_AGENT, PARTICLE_EMITTER, RENDER_LAYER, RIGID_BODY, TEXT, VISIBILITY,
};
match self {
Self::Light(light) => {
if let Some(target) = world.core.get_light_mut(entity) {
*target = light.clone();
}
}
Self::Visibility(visible) => {
world.core.add_components(entity, VISIBILITY);
world
.core
.set_visibility(entity, Visibility { visible: *visible });
}
Self::CastsShadow(value) => {
if *value {
world.core.add_components(entity, CASTS_SHADOW);
world.core.set_casts_shadow(entity, CastsShadow);
} else {
world.core.remove_components(entity, CASTS_SHADOW);
}
}
Self::Name(text) => {
world.core.set_name(entity, Name(text.clone()));
}
Self::ParticleEmitter(value) => {
apply_option_component(
world,
entity,
value.as_deref().cloned(),
PARTICLE_EMITTER,
|world, entity, value| world.core.set_particle_emitter(entity, value),
);
}
Self::Decal(value) => {
apply_option_component(
world,
entity,
value.as_deref().cloned(),
DECAL,
|world, entity, value| world.core.set_decal(entity, value),
);
}
Self::AudioSource(value) => {
apply_option_component(
world,
entity,
value.as_deref().cloned(),
AUDIO_SOURCE,
|world, entity, value| world.core.set_audio_source(entity, value),
);
}
Self::RigidBody(value) => {
apply_option_component(
world,
entity,
value.as_deref().cloned(),
RIGID_BODY,
|world, entity, value| world.core.set_rigid_body(entity, value),
);
}
Self::Collider(value) => {
apply_option_component(
world,
entity,
value.as_deref().cloned(),
COLLIDER,
|world, entity, value| world.core.set_collider(entity, value),
);
}
Self::CharacterController(value) => {
apply_option_component(
world,
entity,
value.as_deref().cloned(),
nightshade::ecs::world::CHARACTER_CONTROLLER,
|world, entity, value| world.core.set_character_controller(entity, value),
);
}
Self::NavmeshAgent(value) => {
apply_option_component(
world,
entity,
value.as_deref().cloned(),
NAVMESH_AGENT,
|world, entity, value| world.core.set_navmesh_agent(entity, value),
);
}
Self::Camera(value) => {
apply_option_component(world, entity, *value, CAMERA, |world, entity, value| {
world.core.set_camera(entity, value)
});
}
Self::AnimationPlayer(value) => {
apply_option_component(
world,
entity,
value.as_deref().cloned(),
ANIMATION_PLAYER,
|world, entity, value| world.core.set_animation_player(entity, value),
);
}
Self::Material { name, material } => {
if let Some(material) = material.as_deref() {
queue_ecs_command(
world,
nightshade::ecs::world::commands::EcsCommand::ReloadMaterial {
name: name.clone(),
material: Box::new(material.clone()),
},
);
}
}
Self::Text { component, content } => {
apply_option_component(
world,
entity,
component.as_deref().cloned(),
TEXT,
|world, entity, value| world.core.set_text(entity, value),
);
if let (Some(component), Some(content)) = (component.as_deref(), content.as_deref())
{
world
.resources
.text
.cache
.set_text(component.text_index, content);
}
}
Self::GrassRegion(value) => {
apply_option_component(
world,
entity,
value.as_deref().cloned(),
GRASS_REGION,
|world, entity, value| world.core.set_grass_region(entity, value),
);
}
Self::GrassInteractor(value) => {
apply_option_component(
world,
entity,
value.clone(),
GRASS_INTERACTOR,
|world, entity, value| world.core.set_grass_interactor(entity, value),
);
}
Self::RenderLayer(value) => {
apply_option_component(
world,
entity,
*value,
RENDER_LAYER,
|world, entity, value| world.core.set_render_layer(entity, value),
);
}
Self::CullingMask(value) => {
apply_option_component(
world,
entity,
*value,
CULLING_MASK,
|world, entity, value| world.core.set_culling_mask(entity, value),
);
}
Self::CameraCullingMask(value) => {
apply_option_component(
world,
entity,
*value,
CAMERA_CULLING_MASK,
|world, entity, value| world.core.set_camera_culling_mask(entity, value),
);
}
Self::IgnoreParentScale(present) => {
if *present {
world.core.add_components(entity, IGNORE_PARENT_SCALE);
world.core.set_ignore_parent_scale(
entity,
nightshade::ecs::transform::components::IgnoreParentScale,
);
} else {
world.core.remove_components(entity, IGNORE_PARENT_SCALE);
}
}
Self::PrefabSource(value) => {
apply_option_component(
world,
entity,
value.as_deref().cloned(),
nightshade::ecs::world::PREFAB_SOURCE,
|world, entity, value| world.core.set_prefab_source(entity, value),
);
}
}
}
}
fn apply_option_component<T, F>(
world: &mut World,
entity: Entity,
value: Option<T>,
mask: u64,
set: F,
) where
F: FnOnce(&mut World, Entity, T),
{
if let Some(value) = value {
world.core.add_components(entity, mask);
set(world, entity, value);
} else {
world.core.remove_components(entity, mask);
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub enum SnapshotKind {
Light,
Visibility,
CastsShadow,
Name,
ParticleEmitter,
Decal,
AudioSource,
RigidBody,
Collider,
CharacterController,
NavmeshAgent,
Camera,
AnimationPlayer,
Material,
Text,
GrassRegion,
GrassInteractor,
RenderLayer,
CullingMask,
CameraCullingMask,
IgnoreParentScale,
PrefabSource,
}
#[derive(Clone)]
pub struct CapturedSubtree {
pub scene: Scene,
pub root_uuids: Vec<AssetUuid>,
pub root_parents: Vec<Option<AssetUuid>>,
}
impl Reversible for UndoableOperation {
type Context<'a> = UndoCtx<'a>;
type Result = UndoResult;
fn reverse(&self, ctx: &mut Self::Context<'_>) -> (Self, Self::Result) {
match self {
UndoableOperation::EntityCreated { captured } => {
for uuid in &captured.root_uuids {
if let Some(entity) = ctx.editor_scene.entity_for(*uuid) {
scene_writeback::remove_entity(ctx.project, ctx.editor_scene, entity);
for descendant in query_descendants(ctx.world, entity) {
scene_writeback::remove_entity(
ctx.project,
ctx.editor_scene,
descendant,
);
}
despawn_recursive_immediate(ctx.world, entity);
}
}
(
UndoableOperation::EntityDeleted {
captured: captured.clone(),
},
UndoResult::default(),
)
}
UndoableOperation::EntityDeleted { captured } => {
let restored_first = recreate_subtree(ctx, captured);
(
UndoableOperation::EntityCreated {
captured: captured.clone(),
},
UndoResult {
select_entity: restored_first,
},
)
}
UndoableOperation::TransformChanged { uuid, old, new } => {
let entity = ctx.editor_scene.entity_for(*uuid);
if let Some(entity) = entity {
ctx.world.core.set_local_transform(entity, *old);
mark_local_transform_dirty(ctx.world, entity);
scene_writeback::sync_entity(ctx.project, ctx.editor_scene, ctx.world, entity);
}
(
UndoableOperation::TransformChanged {
uuid: *uuid,
old: *new,
new: *old,
},
UndoResult {
select_entity: entity,
},
)
}
UndoableOperation::ComponentChanged { uuid, old, new } => {
let entity = ctx.editor_scene.entity_for(*uuid);
if let Some(entity) = entity {
old.apply(ctx.world, entity);
scene_writeback::sync_entity(ctx.project, ctx.editor_scene, ctx.world, entity);
}
(
UndoableOperation::ComponentChanged {
uuid: *uuid,
old: new.clone(),
new: old.clone(),
},
UndoResult {
select_entity: entity,
},
)
}
UndoableOperation::Batch { steps } => {
let mut reversed_steps: Vec<UndoableOperation> = Vec::with_capacity(steps.len());
let mut final_result = UndoResult::default();
for step in steps.iter().rev() {
let (reversed, result) = step.reverse(ctx);
if result.select_entity.is_some() {
final_result = result;
}
reversed_steps.push(reversed);
}
(
UndoableOperation::Batch {
steps: reversed_steps,
},
final_result,
)
}
}
}
}
pub fn capture_subtree(
world: &World,
editor_scene: &mut EditorScene,
root: Entity,
) -> CapturedSubtree {
let mut order: Vec<Entity> = vec![root];
order.extend(query_descendants(world, root));
let mut uuids: std::collections::HashMap<Entity, AssetUuid> = std::collections::HashMap::new();
for entity in &order {
let uuid = match editor_scene.uuid_for(*entity) {
Some(uuid) => uuid,
None => AssetUuid::random(),
};
uuids.insert(*entity, uuid);
}
let mut scene = Scene::new("undo_capture");
for entity in &order {
let uuid = uuids[entity];
let parent_entity = world.core.get_parent(*entity).and_then(|parent| parent.0);
let parent_uuid = parent_entity.and_then(|parent| uuids.get(&parent).copied());
let scene_entity = entity_to_scene_entity(world, *entity, uuid, parent_uuid);
scene.add_entity(scene_entity);
}
embed_referenced_meshes(world, &mut scene);
embed_referenced_textures(world, &mut scene);
scene.compute_spawn_order();
let root_uuid = uuids[&root];
let root_parent = world
.core
.get_parent(root)
.and_then(|parent| parent.0)
.and_then(|parent| editor_scene.uuid_for(parent));
CapturedSubtree {
scene,
root_uuids: vec![root_uuid],
root_parents: vec![root_parent],
}
}
fn recreate_subtree(ctx: &mut UndoCtx<'_>, captured: &CapturedSubtree) -> Option<Entity> {
let result = match spawn_scene(ctx.world, &captured.scene, None) {
Ok(result) => result,
Err(error) => {
tracing::error!("undo recreate failed: {error}");
return None;
}
};
for (uuid, entity) in &result.uuid_to_entity {
ctx.editor_scene.insert(*uuid, *entity);
}
for (root_uuid, parent_uuid) in captured.root_uuids.iter().zip(captured.root_parents.iter()) {
let Some(child) = result.uuid_to_entity.get(root_uuid).copied() else {
continue;
};
let parent_entity = parent_uuid.and_then(|uuid| ctx.editor_scene.entity_for(uuid));
if let Some(parent) = parent_entity {
ctx.world.core.set_parent(
child,
nightshade::ecs::transform::components::Parent(Some(parent)),
);
}
}
for entity in result.uuid_to_entity.values() {
scene_writeback::add_entity(ctx.project, ctx.editor_scene, ctx.world, *entity);
}
captured
.root_uuids
.first()
.and_then(|uuid| result.uuid_to_entity.get(uuid).copied())
}
pub type UndoHistory = History<UndoableOperation>;
#[derive(Default)]
pub struct GizmoDragTracker {
prev_active: bool,
tracked_uuid: Option<AssetUuid>,
start_transform: Option<LocalTransform>,
}
pub fn poll_gizmo_drag(
tracker: &mut GizmoDragTracker,
history: &mut UndoHistory,
editor_scene: &EditorScene,
world: &mut World,
) {
let gizmos = &world.resources.user_interface.gizmos;
let active = gizmos.translation_drag.is_some()
|| gizmos.scale_drag.is_some()
|| gizmos.rotation_drag.is_some()
|| gizmos.planar_translation_drag.is_some()
|| gizmos.planar_scale_drag.is_some();
let target = world.resources.graphics.bounding_volume_selected_entity;
if active && !tracker.prev_active {
if let Some(entity) = target
&& let Some(uuid) = editor_scene.uuid_for(entity)
&& let Some(transform) = world.core.get_local_transform(entity).copied()
{
tracker.tracked_uuid = Some(uuid);
tracker.start_transform = Some(transform);
} else {
tracker.tracked_uuid = None;
tracker.start_transform = None;
}
} else if !active
&& tracker.prev_active
&& let (Some(uuid), Some(start)) =
(tracker.tracked_uuid.take(), tracker.start_transform.take())
&& let Some(entity) = editor_scene.entity_for(uuid)
&& let Some(end) = world.core.get_local_transform(entity).copied()
&& start != end
{
history.push(
UndoableOperation::TransformChanged {
uuid,
old: start,
new: end,
},
"Transform",
);
}
tracker.prev_active = active;
}