//********************************** Banshee Engine (www.banshee3d.com) **************************************************// //**************** Copyright (c) 2016 Marko Pintera (marko.pintera@gmail.com). All rights reserved. **********************// using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using bs; namespace bs.Editor { /** @addtogroup Utility-Editor * @{ */ /// /// Handles undo & redo operations for changes made on game objects. Game objects can be recorded just before a change /// is made and the system will calculate the difference between that state and the state at the end of current frame. /// This difference will then be recorded as a undo/redo operation. The undo/redo operation will also take care of /// selecting the object & field it is acting upon. /// internal class GameObjectUndo { /// /// Contains information about a component that needs its diff recorded. /// private struct ComponentToRecord { private Component obj; private string path; private SerializedObject orgState; /// /// Creates a new object instance, recording the current state of the component. /// /// Component to record the state of. /// /// Path to the field which should be focused when performing the undo/redo operation. This should be the path /// as provided by . /// internal ComponentToRecord(Component obj, string path) { this.obj = obj; this.path = path; orgState = SerializedObject.Create(obj); } /// /// Generates the diff from the previously recorded state and the current state. If there is a difference /// an undo command is recorded. /// internal void RecordCommand() { if (obj.IsDestroyed) return; SerializedObject newState = SerializedObject.Create(obj); SerializedDiff oldToNew = SerializedDiff.Create(orgState, newState); if (oldToNew == null || oldToNew.IsEmpty) return; SerializedDiff newToOld = SerializedDiff.Create(newState, orgState); UndoRedo.Global.RegisterCommand(new RecordComponentUndo(obj, path, oldToNew, newToOld)); } } /// /// Contains information about scene objects that needs their diff recorded. Note this will not record the entire /// scene object, but rather just its name, transform, active state and potentially other similar properties. /// It's components as well as hierarchy state are ignored. /// private struct SceneObjectHeaderToRecord { private SceneObject[] objs; private string path; private SceneObjectState[] orgStates; /// /// Creates a new object instance, recording the current state of the scene object header. /// /// Scene object to record the state of. /// /// Path to the field which should be focused when performing the undo/redo operation. /// internal SceneObjectHeaderToRecord(SceneObject obj, string path) { this.objs = new [] { obj }; this.path = path; orgStates = new [] { SceneObjectState.Create(obj) }; } /// /// Creates a new object instance, recording the current state of the scene object header for multiple scene /// objects. /// /// Scene objects to record the state of. /// /// Path to the field which should be focused when performing the undo/redo operation. /// internal SceneObjectHeaderToRecord(SceneObject[] objs, string path) { this.objs = objs; this.path = path; orgStates = new SceneObjectState[objs.Length]; for(int i = 0; i < orgStates.Length; i++) orgStates[i] = SceneObjectState.Create(objs[i]); } /// /// Generates the diff from the previously recorded state and the current state. If there is a difference /// an undo command is recorded. /// internal void RecordCommand() { if (objs == null) return; List headers = new List(); for (int i = 0; i < objs.Length; i++) { SceneObject obj = objs[i]; SceneObjectState orgState = orgStates[i]; if (obj.IsDestroyed) continue; SceneObjectDiff oldToNew = SceneObjectDiff.Create(orgState, SceneObjectState.Create(obj)); if (oldToNew.flags == 0) continue; SceneObjectDiff newToOld = SceneObjectDiff.Create(SceneObjectState.Create(obj), orgState); headers.Add(new SceneObjectHeaderUndo(obj, newToOld, oldToNew)); } if (headers.Count > 0) UndoRedo.Global.RegisterCommand(new RecordSceneObjectHeaderUndo(headers, path)); } } /// /// Contains information about a scene object that needs its diff recorded. Unlike /// this will record the entire scene object, /// including its components and optionally the child hierarchy. /// private struct SceneObjectToRecord { private SceneObject obj; private string description; private SerializedSceneObject orgState; /// /// Creates a new object instance, recording the current state of the scene object. /// /// Scene object to record the state of. /// If true, the child scene objects will be recorded as well. /// /// Optional description that describes the change that is happening. /// internal SceneObjectToRecord(SceneObject obj, bool hierarchy, string description) { this.obj = obj; this.description = description; orgState = new SerializedSceneObject(obj, hierarchy); } /// /// Generates the diff from the previously recorded state and the current state. If there is a difference /// an undo command is recorded. /// internal void RecordCommand() { if (obj.IsDestroyed) return; var newState = new SerializedSceneObject(obj); UndoRedo.Global.RegisterCommand(new RecordSceneObjectUndo(obj, orgState, newState, description)); } } private static List components = new List(); private static List sceneObjectHeaders = new List(); private static List sceneObjects = new List(); /// /// Records the current state of the provided component, and generates a diff with the next state at the end of the /// frame. If change is detected an undo operation will be recorded. Generally you want to call this just before /// you are about to make a change to the component. /// /// Component to record the state of. /// /// Path to the field which should be focused when performing the undo/redo operation. This should be the path /// as provided by . /// public static void RecordComponent(Component obj, string fieldPath) { ComponentToRecord cmp = new ComponentToRecord(obj, fieldPath); components.Add(cmp); } /// /// Records the current state of the provided scene object header, and generates a diff with the next state at the /// end of the frame. If change is detected an undo operation will be recorded. Generally you want to call this /// just before you are about to make a change to the scene object header. /// /// Note this will not record the entire scene object, but rather just its name, transform, active state and /// potentially other similar properties. It's components as well as hierarchy state are ignored. /// /// Scene object to record the state of. /// /// Name to the field which should be focused when performing the undo/redo operation. /// public static void RecordSceneObjectHeader(SceneObject obj, string fieldName) { SceneObjectHeaderToRecord so = new SceneObjectHeaderToRecord(obj, fieldName); sceneObjectHeaders.Add(so); } /// /// Records the current state of the provided scene object header, and generates a diff with the next state at the /// end of the frame. If change is detected an undo operation will be recorded. Generally you want to call this /// just before you are about to make a change to the scene object header. /// /// Note this will not record the entire scene object, but rather just its name, transform, active state and /// potentially other similar properties. It's components as well as hierarchy state are ignored. /// /// Scene objects to record the state of. public static void RecordSceneObjectHeader(SceneObject[] objs) { SceneObjectHeaderToRecord so = new SceneObjectHeaderToRecord(objs, null); sceneObjectHeaders.Add(so); } /// /// Records the current state of the provided scene object header, and generates a diff with the next state at the /// end of the frame. If change is detected an undo operation will be recorded. Generally you want to call this /// just before you are about to make a change to the scene object. /// /// Note this records the complete state of a scene object, including its header, its components and optionally its /// child hierarchy. /// /// Scene object to record. /// /// If true the child objects will be recorded as well, otherwise just the provided object. /// /// Optional description specifying the type of changes about to be made. public static void RecordSceneObject(SceneObject obj, bool hierarchy, string description) { SceneObjectToRecord so = new SceneObjectToRecord(obj, hierarchy, description); sceneObjects.Add(so); } /// /// Generates diffs for any objects that were previously recorded using any of the Record* methods. The diff is /// generated by comparing the state at the time Record* was called, compared to the current object state. /// public static void ResolveDiffs() { foreach (var entry in components) entry.RecordCommand(); foreach (var entry in sceneObjectHeaders) entry.RecordCommand(); foreach (var entry in sceneObjects) entry.RecordCommand(); components.Clear(); sceneObjectHeaders.Clear(); sceneObjects.Clear(); } } /// /// Contains information about scene object state, excluding information about its components and hierarchy. /// internal struct SceneObjectState { internal string name; internal Vector3 position; internal Quaternion rotation; internal Vector3 scale; internal bool active; /// /// Initializes the state from a scene object. /// /// Scene object to initialize the state from. /// New state object. internal static SceneObjectState Create(SceneObject so) { SceneObjectState state = new SceneObjectState(); state.name = so.Name; state.position = so.LocalPosition; state.rotation = so.LocalRotation; state.scale = so.LocalScale; state.active = so.Active; return state; } } /// /// Contains the difference between two objects and allows the changes to be applied to /// a . The value of the different fields is stored as its own state, while the flags field /// specified which of the properties is actually different. /// internal struct SceneObjectDiff { internal SceneObjectState state; internal SceneObjectDiffFlags flags; /// /// Creates a diff object storing the difference between two objects. /// /// State of the scene object to compare from. /// State of the scene object to compare to. /// Difference between the two scene object states. internal static SceneObjectDiff Create(SceneObjectState oldState, SceneObjectState newState) { SceneObjectDiff diff = new SceneObjectDiff(); diff.state = new SceneObjectState(); if (oldState.name != newState.name) { diff.state.name = newState.name; diff.flags |= SceneObjectDiffFlags.Name; } if (oldState.position != newState.position) { diff.state.position = newState.position; diff.flags |= SceneObjectDiffFlags.Position; } if (oldState.rotation != newState.rotation) { diff.state.rotation = newState.rotation; diff.flags |= SceneObjectDiffFlags.Rotation; } if (oldState.scale != newState.scale) { diff.state.scale = newState.scale; diff.flags |= SceneObjectDiffFlags.Scale; } if (oldState.active != newState.active) { diff.state.active = newState.active; diff.flags |= SceneObjectDiffFlags.Active; } return diff; } /// /// Applies the diff to an actual scene object. /// /// Scene object to apply the diff to. internal void Apply(SceneObject sceneObject) { if (flags.HasFlag(SceneObjectDiffFlags.Name)) sceneObject.Name = state.name; if (flags.HasFlag(SceneObjectDiffFlags.Position)) sceneObject.LocalPosition = state.position; if (flags.HasFlag(SceneObjectDiffFlags.Rotation)) sceneObject.LocalRotation = state.rotation; if (flags.HasFlag(SceneObjectDiffFlags.Scale)) sceneObject.LocalScale = state.scale; if (flags.HasFlag(SceneObjectDiffFlags.Active)) sceneObject.Active = state.active; } } /// /// Contains information about two separate states of a scene object header. /// internal struct SceneObjectHeaderUndo { internal SceneObject obj; internal SceneObjectDiff newToOld; internal SceneObjectDiff oldToNew; internal SceneObjectHeaderUndo(SceneObject obj, SceneObjectDiff newToOld, SceneObjectDiff oldToNew) { this.obj = obj; this.newToOld = newToOld; this.oldToNew = oldToNew; } } /// /// Stores the field changes in a as a difference between two states. Allows those changes to /// be reverted and re-applied. Does not record changes to scene object components or hierarchy, but just the fields /// considered its header (such as name, local transform and active state). /// [SerializeObject] internal class RecordSceneObjectHeaderUndo : UndoableCommand { private string fieldPath; private List headers; private RecordSceneObjectHeaderUndo() { } /// /// Creates the new scene object header undo command. /// /// Information about recorded states of scene object headers. /// /// Optional path that controls which is the field being modified and should receive input focus when the command /// is executed. Note that the diffs applied have no restriction on how many fields they can modify at once, but /// only one field will receive focus. public RecordSceneObjectHeaderUndo(List headers, string fieldPath) { this.headers = headers; this.fieldPath = fieldPath; } /// protected override void Commit() { if (headers == null) return; foreach (var header in headers) { if (header.obj == null) continue; if (header.obj.IsDestroyed) { Debug.LogWarning("Attempting to commit state on a destroyed game-object."); continue; } header.oldToNew.Apply(header.obj); } FocusOnField(); RefreshInspector(); } /// protected override void Revert() { if (headers == null) return; foreach (var header in headers) { if (header.obj == null) continue; if (header.obj.IsDestroyed) { Debug.LogWarning("Attempting to revert state on a destroyed game-object."); continue; } header.newToOld.Apply(header.obj); } FocusOnField(); RefreshInspector(); } /// /// Selects the component's scene object and focuses on the specific field in the inspector, if the inspector /// window is open. /// private void FocusOnField() { if (headers != null && headers.Count > 0) { if (headers.Count == 1) { if (Selection.SceneObject != headers[0].obj) Selection.SceneObject = headers[0].obj; } else { List objs = new List(); foreach (var header in headers) { if(header.obj != null) objs.Add(header.obj); } Selection.SceneObjects = objs.ToArray(); } if (!string.IsNullOrEmpty(fieldPath)) { InspectorWindow inspectorWindow = EditorWindow.GetWindow(); inspectorWindow?.FocusOnField(headers[0].obj.UUID, fieldPath); } } } /// /// Updates the values of the fields displayed in the inspector window. /// private void RefreshInspector() { InspectorWindow inspectorWindow = EditorWindow.GetWindow(); inspectorWindow?.RefreshSceneObjectFields(true); } } /// /// Stores the field changes in a as a difference between two states. Allows those changes to /// be reverted and re-applied. Unlike this command records the /// full scene object state, including changes to its components and optionally any child objects. /// [SerializeObject] internal class RecordSceneObjectUndo : UndoableCommand { private SceneObject obj; private SerializedSceneObject oldState; private SerializedSceneObject newState; private RecordSceneObjectUndo() { } /// /// Creates the new scene object undo command. /// /// Scene object on which to apply the performed changes. /// Recorded original state of the scene object(s). /// Recorded new state of the scene object(s). /// /// Optional description of the changes made to the scene object between the two states. /// public RecordSceneObjectUndo(SceneObject obj, SerializedSceneObject oldState, SerializedSceneObject newState, string description) { this.obj = obj; this.oldState = oldState; this.newState = newState; } /// protected override void Commit() { newState?.Restore(); FocusOnObject(); RefreshInspector(); } /// protected override void Revert() { oldState?.Restore(); FocusOnObject(); RefreshInspector(); } /// /// Selects the scene object if not already selected. /// private void FocusOnObject() { if (obj != null) { if (Selection.SceneObject != obj) Selection.SceneObject = obj; } } /// /// Updates the values of the fields displayed in the inspector window. /// private void RefreshInspector() { InspectorWindow inspectorWindow = EditorWindow.GetWindow(); inspectorWindow?.RefreshSceneObjectFields(true); } } /// /// Stores the field changes in a as a difference between two states. Allows those changes to /// be reverted and re-applied. /// [SerializeObject] internal class RecordComponentUndo : UndoableCommand { private Component obj; private string fieldPath; private SerializedDiff newToOld; private SerializedDiff oldToNew; private RecordComponentUndo() { } /// /// Creates the new component undo command. /// /// Component on which to apply the performed changes. /// /// Optional path that controls which is the field being modified and should receive input focus when the command /// is executed. Note that the diffs applied have no restriction on how many fields they can modify at once, but /// only one field will receive focus. /// /// Difference that can be applied to the old object in order to get the new object state. /// /// /// Difference that can be applied to the new object in order to get the old object state. /// public RecordComponentUndo(Component obj, string fieldPath, SerializedDiff oldToNew, SerializedDiff newToOld) { this.obj = obj; this.fieldPath = fieldPath; this.oldToNew = oldToNew; this.newToOld = newToOld; } /// protected override void Commit() { if (oldToNew == null || obj == null) return; if (obj.IsDestroyed) { Debug.LogWarning("Attempting to commit state on a destroyed game-object."); return; } oldToNew.Apply(obj); FocusOnField(); } /// protected override void Revert() { if (newToOld == null || obj == null) return; if (obj.IsDestroyed) { Debug.LogWarning("Attempting to revert state on a destroyed game-object."); return; } newToOld.Apply(obj); FocusOnField(); } /// /// Selects the component's scene object and focuses on the specific field in the inspector, if the inspector /// window is open. /// private void FocusOnField() { SceneObject so = obj.SceneObject; if (so != null) { if (Selection.SceneObject != so) Selection.SceneObject = so; if (!string.IsNullOrEmpty(fieldPath)) { InspectorWindow inspectorWindow = EditorWindow.GetWindow(); inspectorWindow?.FocusOnField(obj.UUID, fieldPath); } } } } /** @} */ }