Refactor undo logic for actors reparenting in Editor

#1741
This commit is contained in:
Wojtek Figat
2024-03-06 19:01:36 +01:00
parent 07e25bb24c
commit c561d684eb
4 changed files with 257 additions and 200 deletions

View File

@@ -316,41 +316,7 @@ namespace FlaxEditor.SceneGraph
{
base.OnParentChanged();
// Update UI (special case if actor is spawned and added to existing scene tree)
var parentTreeNode = (parentNode as ActorNode)?.TreeNode;
if (parentTreeNode != null && !parentTreeNode.IsLayoutLocked)
{
parentTreeNode.IsLayoutLocked = true;
_treeNode.Parent = parentTreeNode;
_treeNode.IndexInParent = _actor.OrderInParent;
parentTreeNode.IsLayoutLocked = false;
// Skip UI update if node won't be in a view
if (parentTreeNode.IsCollapsed)
{
TreeNode.UnlockChildrenRecursive();
}
else
{
// Try to perform layout at the level where it makes it the most performant (the least computations)
var tree = parentTreeNode.ParentTree;
if (tree != null)
{
if (tree.Parent is FlaxEngine.GUI.Panel treeParent)
treeParent.PerformLayout();
else
tree.PerformLayout();
}
else
{
parentTreeNode.PerformLayout();
}
}
}
else
{
_treeNode.Parent = parentTreeNode;
}
_treeNode.OnParentChanged(_actor, parentNode as ActorNode);
}
/// <inheritdoc />

View File

@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FlaxEditor.Actions;
using FlaxEditor.Content;
using FlaxEditor.GUI;
using FlaxEditor.GUI.Drag;
@@ -14,7 +15,6 @@ using FlaxEditor.Windows.Assets;
using FlaxEngine;
using FlaxEngine.GUI;
using FlaxEngine.Utilities;
using Object = FlaxEngine.Object;
namespace FlaxEditor.SceneGraph.GUI
{
@@ -82,8 +82,51 @@ namespace FlaxEditor.SceneGraph.GUI
UpdateText();
}
internal void OnParentChanged(Actor actor, ActorNode parentNode)
{
// Update cached value
_orderInParent = actor.OrderInParent;
// Update UI (special case if actor is spawned and added to existing scene tree)
var parentTreeNode = parentNode?.TreeNode;
if (parentTreeNode != null && !parentTreeNode.IsLayoutLocked)
{
parentTreeNode.IsLayoutLocked = true;
Parent = parentTreeNode;
IndexInParent = _orderInParent;
parentTreeNode.IsLayoutLocked = false;
// Skip UI update if node won't be in a view
if (parentTreeNode.IsCollapsed)
{
UnlockChildrenRecursive();
}
else
{
// Try to perform layout at the level where it makes it the most performant (the least computations)
var tree = parentTreeNode.ParentTree;
if (tree != null)
{
if (tree.Parent is Panel treeParent)
treeParent.PerformLayout();
else
tree.PerformLayout();
}
else
{
parentTreeNode.PerformLayout();
}
}
}
else
{
Parent = parentTreeNode;
}
}
internal void OnOrderInParentChanged()
{
// Use cached value to check if we need to update UI layout (and update siblings order at once)
if (Parent is ActorTreeNode parent)
{
var anyChanged = false;
@@ -419,134 +462,6 @@ namespace FlaxEditor.SceneGraph.GUI
_dragHandlers.OnDragLeave();
}
[Serializable]
private class ReparentAction : IUndoAction
{
[Serialize]
private Guid[] _ids;
[Serialize]
private int _actorsCount;
[Serialize]
private Guid[] _prefabIds;
[Serialize]
private Guid[] _prefabObjectIds;
public ReparentAction(Actor actor)
: this(new List<Actor> { actor })
{
}
public ReparentAction(List<Actor> actors)
{
var allActors = new List<Actor>(Mathf.NextPowerOfTwo(actors.Count));
for (int i = 0; i < actors.Count; i++)
{
GetAllActors(allActors, actors[i]);
}
var allScripts = new List<Script>(allActors.Capacity);
GetAllScripts(allActors, allScripts);
int allCount = allActors.Count + allScripts.Count;
_actorsCount = allActors.Count;
_ids = new Guid[allCount];
_prefabIds = new Guid[allCount];
_prefabObjectIds = new Guid[allCount];
for (int i = 0; i < allActors.Count; i++)
{
_ids[i] = allActors[i].ID;
_prefabIds[i] = allActors[i].PrefabID;
_prefabObjectIds[i] = allActors[i].PrefabObjectID;
}
for (int i = 0; i < allScripts.Count; i++)
{
int j = _actorsCount + i;
_ids[j] = allScripts[i].ID;
_prefabIds[j] = allScripts[i].PrefabID;
_prefabObjectIds[j] = allScripts[i].PrefabObjectID;
}
}
public ReparentAction(Script script)
{
_actorsCount = 0;
_ids = new Guid[] { script.ID };
_prefabIds = new Guid[] { script.PrefabID };
_prefabObjectIds = new Guid[] { script.PrefabObjectID };
}
private void GetAllActors(List<Actor> allActors, Actor actor)
{
allActors.Add(actor);
for (int i = 0; i < actor.ChildrenCount; i++)
{
var child = actor.GetChild(i);
if (!allActors.Contains(child))
{
GetAllActors(allActors, child);
}
}
}
private void GetAllScripts(List<Actor> allActors, List<Script> allScripts)
{
for (int i = 0; i < allActors.Count; i++)
{
var actor = allActors[i];
for (int j = 0; j < actor.ScriptsCount; j++)
{
allScripts.Add(actor.GetScript(j));
}
}
}
/// <inheritdoc />
public string ActionString => string.Empty;
/// <inheritdoc />
public void Do()
{
// Note: prefab links are broken by the C++ backend on actor reparenting
}
/// <inheritdoc />
public void Undo()
{
// Restore links
for (int i = 0; i < _actorsCount; i++)
{
var actor = Object.Find<Actor>(ref _ids[i]);
if (actor != null && _prefabIds[i] != Guid.Empty)
{
Actor.Internal_LinkPrefab(Object.GetUnmanagedPtr(actor), ref _prefabIds[i], ref _prefabObjectIds[i]);
}
}
for (int i = _actorsCount; i < _ids.Length; i++)
{
var script = Object.Find<Script>(ref _ids[i]);
if (script != null && _prefabIds[i] != Guid.Empty)
{
Script.Internal_LinkPrefab(Object.GetUnmanagedPtr(script), ref _prefabIds[i], ref _prefabObjectIds[i]);
}
}
}
/// <inheritdoc />
public void Dispose()
{
_ids = null;
_prefabIds = null;
_prefabObjectIds = null;
}
}
/// <inheritdoc />
protected override DragDropEffect OnDragDropHeader(DragData data)
{
@@ -593,46 +508,24 @@ namespace FlaxEditor.SceneGraph.GUI
// Drag actors
if (_dragActors != null && _dragActors.HasValidDrag)
{
bool worldPositionLock = Root.GetKey(KeyboardKeys.Control) == false;
var singleObject = _dragActors.Objects.Count == 1;
if (singleObject)
{
var targetActor = _dragActors.Objects[0].Actor;
var customAction = targetActor.HasPrefabLink ? new ReparentAction(targetActor) : null;
using (new UndoBlock(ActorNode.Root.Undo, targetActor, "Change actor parent", customAction))
{
targetActor.SetParent(newParent, worldPositionLock, true);
targetActor.OrderInParent = newOrder;
}
}
else
{
var targetActors = _dragActors.Objects.ConvertAll(x => x.Actor);
var customAction = targetActors.Any(x => x.HasPrefabLink) ? new ReparentAction(targetActors) : null;
using (new UndoMultiBlock(ActorNode.Root.Undo, targetActors, "Change actors parent", customAction))
{
for (int i = 0; i < targetActors.Count; i++)
{
var targetActor = targetActors[i];
targetActor.SetParent(newParent, worldPositionLock, true);
targetActor.OrderInParent = newOrder;
}
}
}
bool worldPositionsStays = Root.GetKey(KeyboardKeys.Control) == false;
var objects = new SceneObject[_dragActors.Objects.Count];
for (int i = 0; i < objects.Length; i++)
objects[i] = _dragActors.Objects[i].Actor;
var action = new ParentActorsAction(objects, newParent, newOrder, worldPositionsStays);
ActorNode.Root.Undo?.AddAction(action);
action.Do();
result = DragDropEffect.Move;
}
// Drag scripts
else if (_dragScripts != null && _dragScripts.HasValidDrag)
{
foreach (var script in _dragScripts.Objects)
{
var customAction = script.HasPrefabLink ? new ReparentAction(script) : null;
using (new UndoBlock(ActorNode.Root.Undo, script, "Change script parent", customAction))
{
script.SetParent(newParent, true);
}
}
var objects = new SceneObject[_dragScripts.Objects.Count];
for (int i = 0; i < objects.Length; i++)
objects[i] = _dragScripts.Objects[i];
var action = new ParentActorsAction(objects, newParent, newOrder);
ActorNode.Root.Undo?.AddAction(action);
action.Do();
Select();
result = DragDropEffect.Move;
}

View File

@@ -0,0 +1,199 @@
// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using FlaxEngine;
using Object = FlaxEngine.Object;
namespace FlaxEditor.Actions
{
/// <summary>
/// Implementation of <see cref="IUndoAction"/> used to change parent for <see cref="Actor"/> or <see cref="Script"/>.
/// </summary>
/// <seealso cref="FlaxEditor.IUndoAction" />
[Serializable]
class ParentActorsAction : IUndoAction
{
private struct Item
{
public Guid ID;
public Guid Parent;
public int OrderInParent;
public Transform LocalTransform;
}
[Serialize]
private bool _worldPositionsStays;
[Serialize]
private Guid _newParent;
[Serialize]
private int _newOrder;
[Serialize]
private Item[] _items;
[Serialize]
private Guid[] _idsForPrefab;
[Serialize]
private Guid[] _prefabIds;
[Serialize]
private Guid[] _prefabObjectIds;
public ParentActorsAction(SceneObject[] objects, Actor newParent, int newOrder, bool worldPositionsStays = true)
{
// Sort source objects to provide deterministic behavior
Array.Sort(objects, SortObjects);
// Cache initial state for undo
_worldPositionsStays = worldPositionsStays;
_newParent = newParent.ID;
_newOrder = newOrder;
_items = new Item[objects.Length];
for (int i = 0; i < objects.Length; i++)
{
var obj = objects[i];
_items[i] = new Item
{
ID = obj.ID,
Parent = obj.Parent?.ID ?? Guid.Empty,
OrderInParent = obj.OrderInParent,
LocalTransform = obj is Actor actor ? actor.LocalTransform : Transform.Identity,
};
}
// Collect all objects that have prefab links so they can be restored on undo
var prefabs = new List<SceneObject>();
for (int i = 0; i < objects.Length; i++)
GetAllPrefabs(prefabs, objects[i]);
if (prefabs.Count != 0)
{
// Cache ids of all objects
_idsForPrefab = new Guid[prefabs.Count];
_prefabIds = new Guid[prefabs.Count];
_prefabObjectIds = new Guid[prefabs.Count];
for (int i = 0; i < prefabs.Count; i++)
{
var obj = prefabs[i];
_idsForPrefab[i] = obj.ID;
_prefabIds[i] = obj.PrefabID;
_prefabObjectIds[i] = obj.PrefabObjectID;
}
}
}
private static int SortObjects(SceneObject a, SceneObject b)
{
// By parent
var aParent = Object.GetUnmanagedPtr(a.Parent);
var bParent = Object.GetUnmanagedPtr(b.Parent);
if (aParent == bParent)
{
// By index in parent
var aOrder = a.OrderInParent;
var bOrder = b.OrderInParent;
return aOrder.CompareTo(bOrder);
}
return aParent.CompareTo(bParent);
}
private static void GetAllPrefabs(List<SceneObject> result, SceneObject obj)
{
if (result.Contains(obj))
return;
if (obj.HasPrefabLink)
result.Add(obj);
if (obj is Actor actor)
{
for (int i = 0; i < actor.ScriptsCount; i++)
GetAllPrefabs(result, actor.GetScript(i));
for (int i = 0; i < actor.ChildrenCount; i++)
GetAllPrefabs(result, actor.GetChild(i));
}
}
public string ActionString => "Change parent";
public void Do()
{
// Perform action
var newParent = Object.Find<Actor>(ref _newParent);
if (newParent == null)
{
Editor.LogError("Missing actor to change objects parent.");
return;
}
var order = _newOrder;
var scenes = new HashSet<Scene> { newParent.Scene };
for (int i = 0; i < _items.Length; i++)
{
var item = _items[i];
var obj = Object.Find<SceneObject>(ref item.ID);
if (obj != null)
{
scenes.Add(obj.Parent.Scene);
if (obj is Actor actor)
actor.SetParent(newParent, _worldPositionsStays, true);
else
obj.Parent = newParent;
if (order != -1)
obj.OrderInParent = order++;
}
}
// Prefab links are broken by the C++ backend on actor reparenting
// Mark scenes as edited
foreach (var scene in scenes)
Editor.Instance.Scene.MarkSceneEdited(scene);
}
public void Undo()
{
// Restore state
for (int i = 0; i < _items.Length; i++)
{
var item = _items[i];
var obj = Object.Find<SceneObject>(ref item.ID);
if (obj != null)
{
var parent = Object.Find<Actor>(ref item.Parent);
if (parent != null)
obj.Parent = parent;
if (obj is Actor actor)
actor.LocalTransform = item.LocalTransform;
}
}
for (int j = 0; j < _items.Length; j++) // TODO: find a better way ensure the order is properly restored when moving back multiple objects
for (int i = 0; i < _items.Length; i++)
{
var item = _items[i];
var obj = Object.Find<SceneObject>(ref item.ID);
if (obj != null)
obj.OrderInParent = item.OrderInParent;
}
// Restore prefab links (if any was in use)
if (_idsForPrefab != null)
{
for (int i = 0; i < _idsForPrefab.Length; i++)
{
var obj = Object.Find<SceneObject>(ref _idsForPrefab[i]);
if (obj != null && _prefabIds[i] != Guid.Empty)
SceneObject.Internal_LinkPrefab(Object.GetUnmanagedPtr(obj), ref _prefabIds[i], ref _prefabObjectIds[i]);
}
}
}
public void Dispose()
{
_items = null;
_idsForPrefab = null;
_prefabIds = null;
_prefabObjectIds = null;
}
}
}

View File

@@ -2,7 +2,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FlaxEditor.SceneGraph;
using FlaxEngine;