GameObjectUndo.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. //********************************** Banshee Engine (www.banshee3d.com) **************************************************//
  2. //**************** Copyright (c) 2016 Marko Pintera ([email protected]). All rights reserved. **********************//
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Runtime.CompilerServices;
  6. using bs;
  7. namespace bs.Editor
  8. {
  9. /** @addtogroup Utility-Editor
  10. * @{
  11. */
  12. /// <summary>
  13. /// Handles undo & redo operations for changes made on game objects. Game objects can be recorded just before a change
  14. /// is made and the system will calculate the difference between that state and the state at the end of current frame.
  15. /// This difference will then be recorded as a undo/redo operation. The undo/redo operation will also take care of
  16. /// selecting the object & field it is acting upon.
  17. /// </summary>
  18. internal class GameObjectUndo
  19. {
  20. /// <summary>
  21. /// Contains information about a component that needs its diff recorded.
  22. /// </summary>
  23. private struct ComponentToRecord
  24. {
  25. private Component obj;
  26. private string path;
  27. private SerializedObject orgState;
  28. /// <summary>
  29. /// Creates a new object instance, recording the current state of the component.
  30. /// </summary>
  31. /// <param name="obj">Component to record the state of.</param>
  32. /// <param name="path">
  33. /// Path to the field which should be focused when performing the undo/redo operation. This should be the path
  34. /// as provided by <see cref="InspectableField"/>.
  35. /// </param>
  36. internal ComponentToRecord(Component obj, string path)
  37. {
  38. this.obj = obj;
  39. this.path = path;
  40. orgState = SerializedObject.Create(obj);
  41. }
  42. /// <summary>
  43. /// Generates the diff from the previously recorded state and the current state. If there is a difference
  44. /// an undo command is recorded.
  45. /// </summary>
  46. internal void RecordCommand()
  47. {
  48. if (obj.IsDestroyed)
  49. return;
  50. SerializedObject newState = SerializedObject.Create(obj);
  51. SerializedDiff oldToNew = SerializedDiff.Create(orgState, newState);
  52. if (oldToNew == null || oldToNew.IsEmpty)
  53. return;
  54. SerializedDiff newToOld = SerializedDiff.Create(newState, orgState);
  55. UndoRedo.Global.RegisterCommand(new RecordComponentUndo(obj, path, oldToNew, newToOld));
  56. }
  57. }
  58. /// <summary>
  59. /// Contains information about a scene object that needs its diff recorded. Note this will not record the entire
  60. /// scene object, but rather just its name, transform, active state and potentially other similar properties.
  61. /// It's components as well as hierarchy state are ignored.
  62. /// </summary>
  63. private struct SceneObjectHeaderToRecord
  64. {
  65. private SceneObject obj;
  66. private string path;
  67. private SceneObjectState orgState;
  68. /// <summary>
  69. /// Creates a new object instance, recording the current state of the scene object header.
  70. /// </summary>
  71. /// <param name="obj">Scene object to record the state of.</param>
  72. /// <param name="path">
  73. /// Path to the field which should be focused when performing the undo/redo operation.
  74. /// </param>
  75. internal SceneObjectHeaderToRecord(SceneObject obj, string path)
  76. {
  77. this.obj = obj;
  78. this.path = path;
  79. orgState = SceneObjectState.Create(obj);
  80. }
  81. /// <summary>
  82. /// Generates the diff from the previously recorded state and the current state. If there is a difference
  83. /// an undo command is recorded.
  84. /// </summary>
  85. internal void RecordCommand()
  86. {
  87. if (obj.IsDestroyed)
  88. return;
  89. SceneObjectDiff oldToNew = SceneObjectDiff.Create(orgState, SceneObjectState.Create(obj));
  90. if (oldToNew.flags == 0)
  91. return;
  92. SceneObjectDiff newToOld = SceneObjectDiff.Create(SceneObjectState.Create(obj), orgState);
  93. UndoRedo.Global.RegisterCommand(new RecordSceneObjectHeaderUndo(obj, path, oldToNew, newToOld));
  94. }
  95. }
  96. private static List<ComponentToRecord> components = new List<ComponentToRecord>();
  97. private static List<SceneObjectHeaderToRecord> sceneObjectHeaders = new List<SceneObjectHeaderToRecord>();
  98. /// <summary>
  99. /// Records the current state of the provided component, and generates a diff with the next state at the end of the
  100. /// frame. If change is detected an undo operation will be recorded. Generally you want to call this just before
  101. /// you are about to make a change to the component.
  102. /// </summary>
  103. /// <param name="obj">Component to record the state of.</param>
  104. /// <param name="fieldPath">
  105. /// Path to the field which should be focused when performing the undo/redo operation. This should be the path
  106. /// as provided by <see cref="InspectableField"/>.
  107. /// </param>
  108. public static void RecordComponent(Component obj, string fieldPath)
  109. {
  110. ComponentToRecord cmp = new ComponentToRecord(obj, fieldPath);
  111. components.Add(cmp);
  112. }
  113. /// <summary>
  114. /// Records the current state of the provided scene object header, and generates a diff with the next state at the
  115. /// end of the frame. If change is detected an undo operation will be recorded. Generally you want to call this
  116. /// just before you are about to make a change to the scene object header.
  117. ///
  118. /// Note this will not record the entire scene object, but rather just its name, transform, active state and
  119. /// potentially other similar properties. It's components as well as hierarchy state are ignored.
  120. /// </summary>
  121. /// <param name="obj">Scene object to record the state of.</param>
  122. /// <param name="fieldName">
  123. /// Name to the field which should be focused when performing the undo/redo operation.
  124. /// </param>
  125. public static void RecordSceneObjectHeader(SceneObject obj, string fieldName)
  126. {
  127. SceneObjectHeaderToRecord so = new SceneObjectHeaderToRecord(obj, fieldName);
  128. sceneObjectHeaders.Add(so);
  129. }
  130. /// <summary>
  131. /// Generates diffs for any objects that were previously recorded using any of the Record* methods. The diff is
  132. /// generated by comparing the state at the time Record* was called, compared to the current object state.
  133. /// </summary>
  134. public static void ResolveDiffs()
  135. {
  136. foreach (var entry in components)
  137. entry.RecordCommand();
  138. foreach (var entry in sceneObjectHeaders)
  139. entry.RecordCommand();
  140. components.Clear();
  141. sceneObjectHeaders.Clear();
  142. }
  143. }
  144. /// <summary>
  145. /// Contains information about scene object state, excluding information about its components and hierarchy.
  146. /// </summary>
  147. internal struct SceneObjectState
  148. {
  149. internal string name;
  150. internal Vector3 position;
  151. internal Quaternion rotation;
  152. internal Vector3 scale;
  153. internal bool active;
  154. /// <summary>
  155. /// Initializes the state from a scene object.
  156. /// </summary>
  157. /// <param name="so">Scene object to initialize the state from.</param>
  158. /// <returns>New state object.</returns>
  159. internal static SceneObjectState Create(SceneObject so)
  160. {
  161. SceneObjectState state = new SceneObjectState();
  162. state.name = so.Name;
  163. state.position = so.LocalPosition;
  164. state.rotation = so.LocalRotation;
  165. state.scale = so.LocalScale;
  166. state.active = so.Active;
  167. return state;
  168. }
  169. }
  170. /// <summary>
  171. /// Contains the difference between two <see cref="SceneObjectState"/> objects and allows the changes to be applied to
  172. /// a <see cref="SceneObject"/>. The value of the different fields is stored as its own state, while the flags field
  173. /// specified which of the properties is actually different.
  174. /// </summary>
  175. internal struct SceneObjectDiff
  176. {
  177. internal SceneObjectState state;
  178. internal SceneObjectDiffFlags flags;
  179. /// <summary>
  180. /// Creates a diff object storing the difference between two <see cref="SceneObjectState"/> objects.
  181. /// </summary>
  182. /// <param name="oldState">State of the scene object to compare from.</param>
  183. /// <param name="newState">State of the scene object to compare to.</param>
  184. /// <returns>Difference between the two scene object states.</returns>
  185. internal static SceneObjectDiff Create(SceneObjectState oldState, SceneObjectState newState)
  186. {
  187. SceneObjectDiff diff = new SceneObjectDiff();
  188. diff.state = new SceneObjectState();
  189. if (oldState.name != newState.name)
  190. {
  191. diff.state.name = newState.name;
  192. diff.flags |= SceneObjectDiffFlags.Name;
  193. }
  194. if (oldState.position != newState.position)
  195. {
  196. diff.state.position = newState.position;
  197. diff.flags |= SceneObjectDiffFlags.Position;
  198. }
  199. if (oldState.rotation != newState.rotation)
  200. {
  201. diff.state.rotation = newState.rotation;
  202. diff.flags |= SceneObjectDiffFlags.Rotation;
  203. }
  204. if (oldState.scale != newState.scale)
  205. {
  206. diff.state.scale = newState.scale;
  207. diff.flags |= SceneObjectDiffFlags.Scale;
  208. }
  209. if (oldState.active != newState.active)
  210. {
  211. diff.state.active = newState.active;
  212. diff.flags |= SceneObjectDiffFlags.Active;
  213. }
  214. return diff;
  215. }
  216. /// <summary>
  217. /// Applies the diff to an actual scene object.
  218. /// </summary>
  219. /// <param name="sceneObject">Scene object to apply the diff to.</param>
  220. internal void Apply(SceneObject sceneObject)
  221. {
  222. if (flags.HasFlag(SceneObjectDiffFlags.Name))
  223. sceneObject.Name = state.name;
  224. if (flags.HasFlag(SceneObjectDiffFlags.Position))
  225. sceneObject.LocalPosition = state.position;
  226. if (flags.HasFlag(SceneObjectDiffFlags.Rotation))
  227. sceneObject.LocalRotation = state.rotation;
  228. if (flags.HasFlag(SceneObjectDiffFlags.Scale))
  229. sceneObject.LocalScale = state.scale;
  230. if (flags.HasFlag(SceneObjectDiffFlags.Active))
  231. sceneObject.Active = state.active;
  232. }
  233. }
  234. /// <summary>
  235. /// Stores the field changes in a <see cref="SceneObject"/> as a difference between two states. Allows those changes to
  236. /// be reverted and re-applied. Does not record changes to scene object components or hierarchy, but just the fields
  237. /// considered its header (such as name, local transform and active state).
  238. /// </summary>
  239. [SerializeObject]
  240. internal class RecordSceneObjectHeaderUndo : UndoableCommand
  241. {
  242. private SceneObject obj;
  243. private string fieldPath;
  244. private SceneObjectDiff newToOld;
  245. private SceneObjectDiff oldToNew;
  246. private RecordSceneObjectHeaderUndo() { }
  247. /// <summary>
  248. /// Creates the new scene object undo command.
  249. /// </summary>
  250. /// <param name="obj">Scene object on which to apply the performed changes.</param>
  251. /// <param name="fieldPath">
  252. /// Optional path that controls which is the field being modified and should receive input focus when the command
  253. /// is executed. Note that the diffs applied have no restriction on how many fields they can modify at once, but
  254. /// only one field will receive focus.</param>
  255. /// <param name="oldToNew">
  256. /// Difference that can be applied to the old object in order to get the new object state.
  257. /// </param>
  258. /// <param name="newToOld">
  259. /// Difference that can be applied to the new object in order to get the old object state.
  260. /// </param>
  261. public RecordSceneObjectHeaderUndo(SceneObject obj, string fieldPath, SceneObjectDiff oldToNew,
  262. SceneObjectDiff newToOld)
  263. {
  264. this.obj = obj;
  265. this.fieldPath = fieldPath;
  266. this.oldToNew = oldToNew;
  267. this.newToOld = newToOld;
  268. }
  269. /// <inheritdoc/>
  270. protected override void Commit()
  271. {
  272. if (obj == null)
  273. return;
  274. if (obj.IsDestroyed)
  275. {
  276. Debug.LogWarning("Attempting to commit state on a destroyed game-object.");
  277. return;
  278. }
  279. oldToNew.Apply(obj);
  280. FocusOnField();
  281. RefreshInspector();
  282. }
  283. /// <inheritdoc/>
  284. protected override void Revert()
  285. {
  286. if (obj == null)
  287. return;
  288. if (obj.IsDestroyed)
  289. {
  290. Debug.LogWarning("Attempting to revert state on a destroyed game-object.");
  291. return;
  292. }
  293. newToOld.Apply(obj);
  294. FocusOnField();
  295. RefreshInspector();
  296. }
  297. /// <summary>
  298. /// Selects the component's scene object and focuses on the specific field in the inspector, if the inspector
  299. /// window is open.
  300. /// </summary>
  301. private void FocusOnField()
  302. {
  303. if (obj != null)
  304. {
  305. if (Selection.SceneObject != obj)
  306. Selection.SceneObject = obj;
  307. if (!string.IsNullOrEmpty(fieldPath))
  308. {
  309. InspectorWindow inspectorWindow = EditorWindow.GetWindow<InspectorWindow>();
  310. inspectorWindow?.FocusOnField(obj.UUID, fieldPath);
  311. }
  312. }
  313. }
  314. /// <summary>
  315. /// Updates the values of the fields displayed in the inspector window.
  316. /// </summary>
  317. private void RefreshInspector()
  318. {
  319. InspectorWindow inspectorWindow = EditorWindow.GetWindow<InspectorWindow>();
  320. inspectorWindow?.RefreshSceneObjectFields(true);
  321. }
  322. }
  323. /// <summary>
  324. /// Stores the field changes in a <see cref="Component"/> as a difference between two states. Allows those changes to
  325. /// be reverted and re-applied.
  326. /// </summary>
  327. [SerializeObject]
  328. internal class RecordComponentUndo : UndoableCommand
  329. {
  330. private Component obj;
  331. private string fieldPath;
  332. private SerializedDiff newToOld;
  333. private SerializedDiff oldToNew;
  334. private RecordComponentUndo() { }
  335. /// <summary>
  336. /// Creates the new component undo command.
  337. /// </summary>
  338. /// <param name="obj">Component on which to apply the performed changes.</param>
  339. /// <param name="fieldPath">
  340. /// Optional path that controls which is the field being modified and should receive input focus when the command
  341. /// is executed. Note that the diffs applied have no restriction on how many fields they can modify at once, but
  342. /// only one field will receive focus.</param>
  343. /// <param name="oldToNew">
  344. /// Difference that can be applied to the old object in order to get the new object state.
  345. /// </param>
  346. /// <param name="newToOld">
  347. /// Difference that can be applied to the new object in order to get the old object state.
  348. /// </param>
  349. public RecordComponentUndo(Component obj, string fieldPath, SerializedDiff oldToNew, SerializedDiff newToOld)
  350. {
  351. this.obj = obj;
  352. this.fieldPath = fieldPath;
  353. this.oldToNew = oldToNew;
  354. this.newToOld = newToOld;
  355. }
  356. /// <inheritdoc/>
  357. protected override void Commit()
  358. {
  359. if (oldToNew == null || obj == null)
  360. return;
  361. if (obj.IsDestroyed)
  362. {
  363. Debug.LogWarning("Attempting to commit state on a destroyed game-object.");
  364. return;
  365. }
  366. oldToNew.Apply(obj);
  367. FocusOnField();
  368. }
  369. /// <inheritdoc/>
  370. protected override void Revert()
  371. {
  372. if (newToOld == null || obj == null)
  373. return;
  374. if (obj.IsDestroyed)
  375. {
  376. Debug.LogWarning("Attempting to revert state on a destroyed game-object.");
  377. return;
  378. }
  379. newToOld.Apply(obj);
  380. FocusOnField();
  381. }
  382. /// <summary>
  383. /// Selects the component's scene object and focuses on the specific field in the inspector, if the inspector
  384. /// window is open.
  385. /// </summary>
  386. private void FocusOnField()
  387. {
  388. SceneObject so = obj.SceneObject;
  389. if (so != null)
  390. {
  391. if (Selection.SceneObject != so)
  392. Selection.SceneObject = so;
  393. if (!string.IsNullOrEmpty(fieldPath))
  394. {
  395. InspectorWindow inspectorWindow = EditorWindow.GetWindow<InspectorWindow>();
  396. inspectorWindow?.FocusOnField(obj.UUID, fieldPath);
  397. }
  398. }
  399. }
  400. }
  401. /** @} */
  402. }