Files
sbox-public/engine/Sandbox.Tools/Scene/Session/SceneEditorSession.Undo.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

852 lines
24 KiB
C#

using Sandbox.Helpers;
using System;
using System.Text.Json.Nodes;
namespace Editor;
public partial class SceneEditorSession
{
public UndoSystem UndoSystem { get; } = new UndoSystem();
internal bool IsUndoScopeOpen = false;
private void InitUndo()
{
UndoSystem.Initialize();
// annoy everyone as much as possible
UndoSystem.OnUndo = ( x ) =>
{
if ( EditorPreferences.UndoSounds )
{
EditorUtility.PlayRawSound( "sounds/editor/success.wav" );
}
HasUnsavedChanges = true;
};
UndoSystem.OnRedo = ( x ) =>
{
if ( EditorPreferences.UndoSounds )
{
EditorUtility.PlayRawSound( "sounds/editor/success.wav" );
}
HasUnsavedChanges = true;
};
}
/// <summary>
/// Take a full scene snapshot for the undo system. This is usually a last resort, if you can't do anything more incremental.
/// </summary>
[Obsolete( "Manual full scene undo snapshots are no longer working use UndoScope or AddUndo" )]
public void FullUndoSnapshot( string title )
{
}
/// <summary>
/// Push the current selection into the undo system
/// </summary>
[Obsolete( "Manual selections snapshots are no longer working use UndoScope or AddUndo" )]
public void PushUndoSelection()
{
}
[Obsolete( "EditLog is no longer working use UndoScope or AddUndo" )]
void Scene.ISceneEditorSession.OnEditLog( string name, object source )
{
}
public void AddUndo( string name, Action undo, Action redo )
{
UndoSystem.Insert( name, undo, redo );
}
public ISceneUndoScope UndoScope( string name )
{
return new SceneUndoScope( this, name );
}
}
internal sealed class SceneUndoSnapshot : IDisposable
{
sealed record ScopeSnapshot( JsonObject Scene, SelectionSnapshot Selection, ComponentSnapshot ComponentSnapshot, GameObjectSnapshot GameObjectSnapshot )
{
public bool Equals( ScopeSnapshot other )
{
if ( other == null )
return false;
if ( Scene == other.Scene &&
Selection == other.Selection &&
ComponentSnapshot == other.ComponentSnapshot &&
GameObjectSnapshot == other.GameObjectSnapshot )
{
return true;
}
return false;
}
public override int GetHashCode()
{
return HashCode.Combine( Scene, Selection, ComponentSnapshot, GameObjectSnapshot );
}
}
sealed record SelectionSnapshot
{
public readonly GameObjectReference[] SelectedGoRefs;
public readonly ComponentReference[] SelectedComponentRefs;
public struct ObjectReference
{
public Type Type;
public JsonNode Node;
}
public readonly ObjectReference[] SelectedObjectRefs;
private readonly SceneEditorSession _session;
public SelectionSnapshot( SceneEditorSession session )
{
_session = session;
SelectedGoRefs = _session.Selection.OfType<GameObject>().Select( GameObjectReference.FromInstance ).ToArray();
SelectedComponentRefs = _session.Selection.OfType<Component>().Select( ComponentReference.FromInstance ).ToArray();
SelectedObjectRefs = _session.Selection
.Where( x => x is not GameObject and not Component and not null )
.Select( x => new ObjectReference { Type = x.GetType(), Node = Json.ToNode( x ) } )
.ToArray();
}
public void Restore( Scene scene )
{
_session.Selection.Clear();
foreach ( var goRef in SelectedGoRefs )
{
var go = goRef.Resolve( scene, true );
if ( go.IsValid() )
{
_session.Selection.Add( go );
}
}
foreach ( var compRef in SelectedComponentRefs )
{
var comp = compRef.Resolve( scene );
if ( comp.IsValid() )
{
_session.Selection.Add( comp );
}
}
// We have not garantuee those are still exist and are valid but we try
foreach ( var objRef in SelectedObjectRefs )
{
var obj = Json.FromNode( objRef.Node, objRef.Type );
if ( obj is IValid x && !x.IsValid() )
{
continue;
}
_session.Selection.Add( obj );
}
}
public bool Equals( SelectionSnapshot other )
{
if ( other == null )
return false;
if ( SelectedGoRefs.Select( x => x.GameObjectId ).SequenceEqual( other.SelectedGoRefs.Select( x => x.GameObjectId ) ) &&
SelectedComponentRefs.Select( x => x.GameObjectId ).SequenceEqual( other.SelectedComponentRefs.Select( x => x.GameObjectId ) ) &&
SelectedObjectRefs.SequenceEqual( other.SelectedObjectRefs ) )
{
return true;
}
return false;
}
public override int GetHashCode()
{
return HashCode.Combine( SelectedGoRefs, SelectedComponentRefs, SelectedObjectRefs );
}
}
sealed record ComponentSnapshot
{
public readonly JsonNode[] State;
public readonly ComponentReference[] ComponentRefs;
public ComponentSnapshot( IEnumerable<Component> components )
{
ComponentRefs = components.Select( ComponentReference.FromInstance ).ToArray();
var serializeOptions = new GameObject.SerializeOptions { };
State = components.Select( comp => comp.Serialize( serializeOptions ) ).ToArray();
}
public void Restore( Scene scene )
{
for ( int i = 0; i < State.Length; i++ )
{
Component comp = null;
comp = ComponentRefs[i].Resolve( scene );
if ( comp is null )
{
continue;
}
comp.Deserialize( State[i].AsObject() );
}
}
public void PostRestore( Scene scene )
{
foreach ( var compRef in ComponentRefs )
{
var comp = compRef.Resolve( scene );
if ( comp.IsValid() )
{
compRef.Resolve( scene )?.PostDeserialize();
}
}
}
public bool Equals( ComponentSnapshot other )
{
if ( other == null )
return false;
if ( State == other.State &&
ComponentRefs.Select( x => x.ComponentId ).SequenceEqual( other.ComponentRefs.Select( x => x.ComponentId ) ) )
{
return true;
}
return false;
}
public override int GetHashCode()
{
return HashCode.Combine( State, ComponentRefs );
}
}
sealed record GameObjectSnapshot
{
public readonly List<JsonObject> State;
public readonly List<GameObjectReference> GameObjectRefs;
public readonly List<GameObjectReference> GameObjectNextSiblingRefs;
public readonly List<GameObjectReference> GameObjectParentRefs;
public GameObjectSnapshot( Dictionary<GameObject, GameObjectUndoFlags> gameObjects )
{
var goCount = gameObjects.Count;
GameObjectRefs = new( goCount );
State = new( goCount );
GameObjectNextSiblingRefs = new( goCount );
GameObjectParentRefs = new( goCount );
foreach ( var (go, flags) in gameObjects )
{
if ( !go.IsValid() )
{
Log.Info( $"Undo GameObjectSnapshot: GameObject queued for snapshot is not valid" );
continue;
}
var serializeOptions = new GameObject.SerializeOptions { IgnoreChildren = !flags.HasFlag( GameObjectUndoFlags.Children ), IgnoreComponents = !flags.HasFlag( GameObjectUndoFlags.Components ) };
GameObjectRefs.Add( GameObjectReference.FromInstance( go ) );
if ( go.IsOutermostPrefabInstanceRoot ) go.PrefabInstance.RefreshPatch();
State.Add( go.Serialize( serializeOptions ) );
GameObjectNextSiblingRefs.Add( go.GetNextSibling( false ).IsValid() ? GameObjectReference.FromInstance( go.GetNextSibling( false ) ) : GameObjectReference.FromId( Guid.Empty ) );
GameObjectParentRefs.Add( go.Parent.IsValid() ? GameObjectReference.FromInstance( go.Parent ) : GameObjectReference.FromId( Guid.Empty ) );
}
}
public void Restore( Scene scene, HashSet<GameObjectReference> createdGos = null )
{
for ( int i = 0; i < State.Count; i++ )
{
GameObject go = null;
if ( createdGos is not null && createdGos.Contains( GameObjectRefs[i] ) )
{
go = new GameObject();
// just need the id for now to ensure references are resolved when doing full deserialize later
go.DeserializeId( State[i] );
}
else
{
go = GameObjectRefs[i].Resolve( scene );
}
if ( go is null )
{
continue;
}
}
}
public void PostRestore( Scene scene )
{
// second pass fully desiarizes and restores hierachy
for ( int i = 0; i < State.Count; i++ )
{
GameObject go = GameObjectRefs[i].Resolve( scene );
if ( go is null )
{
continue;
}
go.Deserialize( State[i], new GameObject.DeserializeOptions { IsRefreshing = true } );
}
RestoreHierachy( scene );
}
private void RestoreHierachy( Scene scene )
{
// You could probably also sort the siblings in a smart way, but we will just brought force it in n^2
for ( int j = 0; j < State.Count; j++ )
{
for ( int i = 0; i < State.Count; i++ )
{
GameObject go = GameObjectRefs[i].Resolve( scene );
if ( go is null )
{
continue;
}
// restore position in scene hierachy
var prevSibling = GameObjectNextSiblingRefs[i].Resolve( scene );
var parent = GameObjectParentRefs[i].Resolve( scene );
if ( prevSibling is not null )
{
prevSibling.AddSibling( go, true, false );
}
else if ( parent is not null )
{
go.SetParent( parent, false );
}
else
{
go.SetParent( scene );
}
}
}
}
public bool Equals( GameObjectSnapshot other )
{
if ( other == null )
return false;
if ( State == other.State &&
GameObjectRefs.Select( x => x.GameObjectId ).SequenceEqual( other.GameObjectRefs.Select( x => x.GameObjectId ) ) &&
GameObjectNextSiblingRefs.Select( x => x.GameObjectId ).SequenceEqual( other.GameObjectNextSiblingRefs.Select( x => x.GameObjectId ) ) &&
GameObjectParentRefs.Select( x => x.GameObjectId ).SequenceEqual( other.GameObjectParentRefs.Select( x => x.GameObjectId ) ) )
{
return true;
}
return false;
}
public override int GetHashCode()
{
return HashCode.Combine( State, GameObjectNextSiblingRefs, GameObjectParentRefs );
}
}
private readonly SceneEditorSession _session;
private readonly string _name;
private ScopeSnapshot _initialState;
private HashSet<GameObject> _createdGameObjects = new();
private HashSet<Component> _createdComponents = new();
private Dictionary<GameObject, GameObjectUndoFlags> _initalCapturedGameObjects = new();
private HashSet<Component> _initialCapturedComponents = new();
private Dictionary<GameObject, GameObjectReference> _destroyedGameObjects { get; } = new();
private Dictionary<Component, ComponentReference> _destroyedComponents { get; } = new();
private bool _captureDestructions = false;
private bool _captureComponentCreations = false;
private bool _captureGameObjectCreations = false;
private bool _captureSelections = false;
private bool _captureComponentChanges => _initialCapturedComponents.Count > 0;
private bool _captureGameObjectChanges => _initalCapturedGameObjects.Count > 0;
internal SceneUndoSnapshot( SceneUndoScope builder )
{
_session = builder.Session;
using var sceneScope = _session.Scene.Push();
_name = builder.Name;
_session.IsUndoScopeOpen = true;
// resolve builder contents
foreach ( var (gos, flags) in builder.CapturedGameObjects )
{
foreach ( var go in gos )
{
// Need to capture the prefab root and only the prefab root if we edited an instance
if ( go.IsPrefabInstance )
{
_initalCapturedGameObjects[go.OutermostPrefabInstanceRoot] = GameObjectUndoFlags.All;
continue;
}
if ( _initalCapturedGameObjects.ContainsKey( go ) )
{
_initalCapturedGameObjects[go] |= flags;
}
else
{
_initalCapturedGameObjects[go] = flags;
}
}
}
foreach ( var destroyedGo in builder.DestroyedGameObjects )
{
// if destroyed go is part of prefab we need to update it's instance cache
if ( destroyedGo.IsPrefabInstance && !destroyedGo.IsOutermostPrefabInstanceRoot )
{
_initalCapturedGameObjects[destroyedGo.OutermostPrefabInstanceRoot] = GameObjectUndoFlags.All;
}
// If we delete a nested instance we need to let a potential parent instance know and need to capture it.
else if ( destroyedGo.IsOutermostPrefabInstanceRoot && destroyedGo.Parent.IsValid() && destroyedGo.Parent.IsPrefabInstance )
{
_initalCapturedGameObjects[destroyedGo.Parent.OutermostPrefabInstanceRoot] = GameObjectUndoFlags.All;
}
_destroyedGameObjects[destroyedGo] = GameObjectReference.FromInstance( destroyedGo );
}
foreach ( var destroyedComp in builder.DestroyedComponents )
{
// if destroyed component is part of prefab we need to update it's instance cache
if ( destroyedComp.GameObject.IsPrefabInstance )
{
_initalCapturedGameObjects[destroyedComp.GameObject.OutermostPrefabInstanceRoot] = GameObjectUndoFlags.All;
}
_destroyedComponents[destroyedComp] = ComponentReference.FromInstance( destroyedComp );
}
foreach ( var comp in builder.CapturedComponents )
{
// Need to capture the prefab root and only the prefab root if we edited an instance
if ( comp.GameObject.IsPrefabInstance )
{
_initalCapturedGameObjects[comp.GameObject.OutermostPrefabInstanceRoot] = GameObjectUndoFlags.All;
continue;
}
// only add if parent is not already watched or does not have component flag
if ( !_initalCapturedGameObjects.ContainsKey( comp.GameObject ) || !_initalCapturedGameObjects[comp.GameObject].Contains( GameObjectUndoFlags.Components ) )
{
_initialCapturedComponents.Add( comp );
}
}
_captureDestructions = builder.CaptureDeletions;
_captureComponentCreations = builder.CaptureComponentCreations;
_captureGameObjectCreations = builder.CaptureGameObjectCreations;
_captureSelections = builder.CaptureSelections;
// Undo snapshots
JsonObject scene = null;
// if deletion is requested, we need to capture the whole scene
if ( _captureDestructions )
{
scene = _session.Scene.Serialize();
}
SelectionSnapshot selection = null;
if ( _captureSelections || _captureDestructions )
{
selection = new SelectionSnapshot( _session );
}
ComponentSnapshot componentSnapshot = null;
// If deletion was requested we already captured the whole scene, no point in capturing individual components
if ( !_captureDestructions && _initialCapturedComponents.Count > 0 )
{
componentSnapshot = new ComponentSnapshot( _initialCapturedComponents );
}
// If deletion was requested we already captured the whole scene, no point in capturing individual Fos
GameObjectSnapshot gameObjectSnapshot = null;
if ( !_captureDestructions && _initalCapturedGameObjects.Count > 0 )
{
gameObjectSnapshot = new GameObjectSnapshot( _initalCapturedGameObjects );
}
_initialState = new ScopeSnapshot( scene, selection, componentSnapshot, gameObjectSnapshot );
if ( _captureGameObjectCreations )
{
_session.Scene.Directory.OnGameObjectAdded += OnGameObjectAdded;
}
if ( _captureComponentCreations )
{
_session.Scene.Directory.OnComponentAdded += OnComponentAdded;
}
}
private bool _alreadyDisposed = false;
public void Dispose()
{
if ( _alreadyDisposed )
{
return;
}
using var sceneScope = _session.Scene.Push();
// Redo snapshots
// we don't capture the scene here again because that is never needed for redo
SelectionSnapshot selection = null;
if ( _captureSelections || _captureDestructions )
{
selection = new SelectionSnapshot( _session );
}
Dictionary<GameObject, GameObjectUndoFlags> disposeWatchedGameObjects = new();
// add all gos still valid and not destroyed
foreach ( var (go, flags) in _initalCapturedGameObjects )
{
if ( go.IsPrefabInstance )
{
disposeWatchedGameObjects[go.OutermostPrefabInstanceRoot] = GameObjectUndoFlags.All;
}
// We may have moved this object to a different prefab instance => update prefabroot instead of it
if ( go.Parent.IsValid() && go.Parent.IsPrefabInstance )
{
disposeWatchedGameObjects[go.Parent.OutermostPrefabInstanceRoot] = GameObjectUndoFlags.All;
continue;
}
if ( !go.IsValid() || go.IsDestroyed )
{
continue;
}
if ( !disposeWatchedGameObjects.ContainsKey( go ) )
{
disposeWatchedGameObjects.Add( go, flags );
}
else
{
disposeWatchedGameObjects[go] |= flags;
}
}
// add all created gos
HashSet<GameObjectReference> createdGameObjectRefs = new();
foreach ( var go in _createdGameObjects )
{
if ( !go.IsValid() || go.IsDestroyed )
{
continue;
}
// Need to capture the prefab root and only the prefab root if we edited an instance
if ( go.IsPrefabInstance )
{
disposeWatchedGameObjects[go.OutermostPrefabInstanceRoot] = GameObjectUndoFlags.All;
// If we are a prefabinstanceroot and ahve been added to a prefab we also need to capture the outer prefab
if ( go.IsOutermostPrefabInstanceRoot && go.Parent.IsValid() && go.Parent.IsPrefabInstance )
{
disposeWatchedGameObjects[go.Parent.OutermostPrefabInstanceRoot] = GameObjectUndoFlags.All;
}
}
else
{
disposeWatchedGameObjects[go] = GameObjectUndoFlags.All;
}
createdGameObjectRefs.Add( GameObjectReference.FromInstance( go ) );
}
// for all components that have been created capture their parent go
HashSet<ComponentReference> createdComponentRefs = new();
foreach ( var component in _createdComponents )
{
if ( !component.IsValid() )
{
// can hapen if component was created and immediatly destoryed within undo scope
continue;
}
// Need to capture the prefab root and only the prefab root if we edited an instance
if ( component.GameObject.IsPrefabInstance )
{
disposeWatchedGameObjects[component.GameObject.OutermostPrefabInstanceRoot] = GameObjectUndoFlags.All;
}
else if ( disposeWatchedGameObjects.ContainsKey( component.GameObject ) )
{
disposeWatchedGameObjects[component.GameObject] |= GameObjectUndoFlags.Components;
}
else
{
disposeWatchedGameObjects[component.GameObject] = GameObjectUndoFlags.Components;
}
createdComponentRefs.Add( ComponentReference.FromInstance( component ) );
}
HashSet<Component> disposeWatchedComponents = new();
foreach ( var comp in _initialCapturedComponents )
{
if ( !comp.IsValid() )
{
continue;
}
// Need to capture the prefab root and only the prefab root if we edited an instance
if ( comp.GameObject.IsPrefabInstance )
{
disposeWatchedGameObjects[comp.GameObject.OutermostPrefabInstanceRoot] = GameObjectUndoFlags.All;
}
// only add if parent is not already watched or does not have component flag
else if ( !_initalCapturedGameObjects.ContainsKey( comp.GameObject ) || !_initalCapturedGameObjects[comp.GameObject].Contains( GameObjectUndoFlags.Components ) )
{
disposeWatchedComponents.Add( comp );
}
}
ComponentSnapshot componentSnapshot = null;
if ( disposeWatchedComponents.Count > 0 )
{
componentSnapshot = new ComponentSnapshot( disposeWatchedComponents );
}
GameObjectSnapshot gameObjectSnapshot = null;
if ( disposeWatchedGameObjects.Count > 0 )
{
gameObjectSnapshot = new GameObjectSnapshot( disposeWatchedGameObjects );
}
var disposeState = new ScopeSnapshot( null, selection, componentSnapshot, gameObjectSnapshot );
var destroyedGameObjectRefs = _destroyedGameObjects.Select( x => x.Value ).ToArray();
var destroyedComponentRefs = _destroyedComponents.Select( x => x.Value ).ToArray();
var prefabInstanceRootsRequiringRefresh = new HashSet<GameObject>();
_session.Scene.Directory.OnComponentAdded -= OnComponentAdded;
_session.Scene.Directory.OnGameObjectAdded -= OnGameObjectAdded;
// if nothing changed, don't add an undo
if ( _initialState == disposeState )
{
return;
}
// check if we have non selection changes
if ( _captureComponentChanges || _captureGameObjectChanges || _captureDestructions || _captureGameObjectCreations || _captureComponentCreations )
{
_session.HasUnsavedChanges = true;
}
// copy we want to avoid capture this
var preChangeStateCopy = _initialState;
_session.AddUndo( _name,
() =>
{
using var sceneScope = _session.Scene.Push();
// for undo we need to restore the state to the pre change state
// first check if we have a scene to restore
if ( preChangeStateCopy.Scene != null )
{
_session.Scene.Clear();
using ( CallbackBatch.Isolated() )
{
_session.Scene.Deserialize( preChangeStateCopy.Scene );
}
// do a tick immediately, to make sure everything is up to date
// if we don't do this we can get flickers when undo is called
_session.Scene.EditorTick( RealTime.Now, RealTime.Delta );
}
else
{
using var batch = CallbackBatch.Batch();
preChangeStateCopy.GameObjectSnapshot?.Restore( _session.Scene );
preChangeStateCopy.ComponentSnapshot?.Restore( _session.Scene );
preChangeStateCopy.ComponentSnapshot?.PostRestore( _session.Scene );
preChangeStateCopy.GameObjectSnapshot?.PostRestore( _session.Scene );
// delete created components
foreach ( var compRef in createdComponentRefs )
{
compRef.Resolve( _session.Scene )?.Destroy();
}
// delete created gos
foreach ( var goRef in createdGameObjectRefs )
{
goRef.Resolve( _session.Scene )?.Destroy();
}
}
// At last restore selection
preChangeStateCopy.Selection?.Restore( _session.Scene );
},
() =>
{
if ( disposeState.Scene != null )
{
throw new InvalidOperationException( "Redo should never use scene snapshots." );
}
using var sceneScope = _session.Scene.Push();
using var batch = CallbackBatch.Batch();
// delete destroyed components
foreach ( var compRef in destroyedComponentRefs )
{
compRef.Resolve( _session.Scene )?.Destroy();
}
// delete destroyed gos
foreach ( var goRef in destroyedGameObjectRefs )
{
goRef.Resolve( _session.Scene )?.Destroy();
}
// Restore Gos and pass created gos which need to be created first
disposeState.GameObjectSnapshot?.Restore( _session.Scene, createdGameObjectRefs );
disposeState.ComponentSnapshot?.Restore( _session.Scene );
// Do actual deserialization
disposeState.GameObjectSnapshot?.PostRestore( _session.Scene );
disposeState.ComponentSnapshot?.PostRestore( _session.Scene );
// restore selection
if ( disposeState.Selection != null )
{
disposeState.Selection.Restore( _session.Scene );
}
} );
_alreadyDisposed = true;
_session.IsUndoScopeOpen = false;
}
private void OnComponentAdded( Component comp )
{
_createdComponents.Add( comp );
}
private void OnGameObjectAdded( GameObject go )
{
_createdGameObjects.Add( go );
}
}
internal class SceneUndoScope : ISceneUndoScope
{
internal SceneEditorSession Session { get; }
internal string Name { get; }
internal bool CaptureSelections { get; private set; }
internal bool CaptureGameObjectCreations { get; private set; }
internal bool CaptureComponentCreations { get; private set; }
internal List<(GameObject[], GameObjectUndoFlags)> CapturedGameObjects { get; } = new();
internal List<Component> CapturedComponents { get; } = new();
internal List<GameObject> DestroyedGameObjects { get; } = new();
internal List<Component> DestroyedComponents { get; } = new();
internal bool CaptureDeletions => DestroyedGameObjects.Count > 0 || DestroyedComponents.Count > 0;
public SceneUndoScope( SceneEditorSession session, string name )
{
Session = session;
Name = name;
// Always capture selections
// If we ever want to make selections capture optional,
// it should be opt out (IngoreSelections) rather than opt in.
CaptureSelections = true;
}
public ISceneUndoScope WithGameObjectCreations()
{
CaptureGameObjectCreations = true;
return this;
}
public ISceneUndoScope WithGameObjectDestructions( IEnumerable<GameObject> gameObjects )
{
DestroyedGameObjects.AddRange( gameObjects );
return this;
}
public ISceneUndoScope WithGameObjectDestructions( GameObject gameObject )
{
DestroyedGameObjects.Add( gameObject );
return this;
}
public ISceneUndoScope WithGameObjectChanges( IEnumerable<GameObject> objects, GameObjectUndoFlags flags )
{
CapturedGameObjects.Add( (objects.ToArray(), flags) );
return this;
}
public ISceneUndoScope WithGameObjectChanges( GameObject gameObject, GameObjectUndoFlags flags )
{
CapturedGameObjects.Add( (new[] { gameObject }, flags) );
return this;
}
public ISceneUndoScope WithComponentCreations()
{
CaptureComponentCreations = true;
return this;
}
public ISceneUndoScope WithComponentDestructions( IEnumerable<Component> components )
{
DestroyedComponents.AddRange( components );
return this;
}
public ISceneUndoScope WithComponentDestructions( Component component )
{
DestroyedComponents.Add( component );
return this;
}
public ISceneUndoScope WithComponentChanges( IEnumerable<Component> components )
{
foreach ( var comp in components )
{
CapturedComponents.Add( comp );
}
return this;
}
public ISceneUndoScope WithComponentChanges( Component component )
{
CapturedComponents.Add( component );
return this;
}
public IDisposable Push()
{
var snapshot = new SceneUndoSnapshot( this );
return snapshot;
}
}