Files
FlaxEngine/Source/Editor/Modules/SceneEditingModule.cs
T
mafiesto4 b3ceda5a6e Revert 10c8454e8f to use more common paste logic in scene tree
(duplicate can paste at the same tree level, paste puts new object into the selection by default)
2026-04-08 22:19:21 +02:00

749 lines
27 KiB
C#

// Copyright (c) Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using FlaxEditor.Actions;
using FlaxEditor.SceneGraph;
using FlaxEditor.SceneGraph.Actors;
using FlaxEngine;
namespace FlaxEditor.Modules
{
/// <summary>
/// Editing scenes module. Manages scene objects selection and editing modes.
/// </summary>
/// <seealso cref="FlaxEditor.Modules.EditorModule" />
public sealed class SceneEditingModule : EditorModule
{
private int _lastSelectionStatus = 0;
/// <summary>
/// The selected objects.
/// </summary>
public readonly List<SceneGraphNode> Selection = new List<SceneGraphNode>(64);
/// <summary>
/// Gets the amount of the selected objects.
/// </summary>
public int SelectionCount => Selection.Count;
/// <summary>
/// Gets a value indicating whether any object is selected.
/// </summary>
public bool HasSthSelected => Selection.Count > 0;
/// <summary>
/// Occurs when selected objects collection gets changed.
/// </summary>
public event Action SelectionChanged;
/// <summary>
/// Occurs before spawning actor to game action.
/// </summary>
public event Action SpawnBegin;
/// <summary>
/// Occurs after spawning actor to game action.
/// </summary>
public event Action SpawnEnd;
/// <summary>
/// Occurs before selection delete action.
/// </summary>
public event Action SelectionDeleteBegin;
/// <summary>
/// Occurs after selection delete action.
/// </summary>
public event Action SelectionDeleteEnd;
internal SceneEditingModule(Editor editor)
: base(editor)
{
}
private void BulkScenesSelectUpdate(bool select = true)
{
// Blank list deselects all
Select(select ? Editor.Scene.Root.ChildNodes : new List<SceneGraphNode>());
}
/// <summary>
/// Selects all scenes.
/// </summary>
public void SelectAllScenes()
{
BulkScenesSelectUpdate(true);
}
/// <summary>
/// Deselects all scenes.
/// </summary>
public void DeselectAllScenes()
{
BulkScenesSelectUpdate(false);
}
/// <summary>
/// Selects the specified actor (finds it's scene graph node).
/// </summary>
/// <param name="actor">The actor.</param>
public void Select(Actor actor)
{
var node = Editor.Scene.GetActorNode(actor);
if (node != null)
Select(node);
}
/// <summary>
/// Selects the specified collection of objects.
/// </summary>
/// <param name="selection">The selection.</param>
/// <param name="additive">if set to <c>true</c> will use additive mode, otherwise will clear previous selection.</param>
public void Select(List<SceneGraphNode> selection, bool additive = false)
{
if (selection == null)
{
Deselect();
return;
}
// Prevent from selecting null nodes
selection.RemoveAll(x => x == null);
// Check if won't change
if (!additive && Selection.Count == selection.Count && Selection.SequenceEqual(selection))
return;
var before = Selection.ToArray();
if (!additive)
Selection.Clear();
Selection.AddRange(selection);
SelectionChange(before);
}
/// <summary>
/// Selects the specified collection of objects.
/// </summary>
/// <param name="selection">The selection.</param>
/// <param name="additive">if set to <c>true</c> will use additive mode, otherwise will clear previous selection.</param>
public void Select(SceneGraphNode[] selection, bool additive = false)
{
if (selection == null)
throw new ArgumentNullException();
Select(selection.ToList(), additive);
}
/// <summary>
/// Selects the specified object.
/// </summary>
/// <param name="selection">The selection.</param>
/// <param name="additive">if set to <c>true</c> will use additive mode, otherwise will clear previous selection.</param>
public void Select(SceneGraphNode selection, bool additive = false)
{
if (selection == null)
throw new ArgumentNullException();
// Check if won't change
if (!additive && Selection.Count == 1 && Selection[0] == selection)
return;
if (additive && Selection.Contains(selection))
return;
var before = Selection.ToArray();
if (!additive)
Selection.Clear();
Selection.Add(selection);
SelectionChange(before);
}
/// <summary>
/// Deselects given object.
/// </summary>
public void Deselect(SceneGraphNode node)
{
if (!Selection.Contains(node))
return;
var before = Selection.ToArray();
Selection.Remove(node);
SelectionChange(before);
}
/// <summary>
/// Clears selected objects collection.
/// </summary>
public void Deselect()
{
// Check if won't change
if (Selection.Count == 0)
return;
var before = Selection.ToArray();
Selection.Clear();
SelectionChange(before);
}
private void SelectionChange(SceneGraphNode[] before)
{
Undo.AddAction(new SelectionChangeAction(before, Selection.ToArray(), OnSelectionUndo));
OnSelectionChanged();
// Display amount of selected actors on the status bar
Editor.UI.RemoveStatusMessage(_lastSelectionStatus);
var objects = Selection.Count;
var actors = CountActors(Selection);
_lastSelectionStatus = Editor.UI.AddStatusMessage($"Selected {objects} {(objects > 1 ? "objects" : "object")} (total {actors} {(actors > 1 ? "actors" : "actor")})");
}
private int CountActors(List<SceneGraphNode> nodes)
{
int result = 0;
foreach (var node in nodes)
result += 1 + CountActors(node.ChildNodes);
return result;
}
private void OnSelectionUndo(SceneGraphNode[] toSelect)
{
Selection.Clear();
if (toSelect != null)
{
for (int i = 0; i < toSelect.Length; i++)
{
if (toSelect[i] != null)
Selection.Add(toSelect[i]);
else
Editor.LogWarning("Null scene graph node to select");
}
}
OnSelectionChanged();
}
private void OnDirty(ActorNode node)
{
var options = Editor.Options.Options;
var isPlayMode = Editor.StateMachine.IsPlayMode;
var actor = node.Actor;
// Auto CSG mesh rebuild
if (!isPlayMode && options.General.AutoRebuildCSG)
{
if (actor is BoxBrush && actor.Scene)
actor.Scene.BuildCSG(options.General.AutoRebuildCSGTimeoutMs);
}
// Auto NavMesh rebuild
if (!isPlayMode && options.General.AutoRebuildNavMesh && actor.Scene && node.AffectsNavigationWithChildren)
{
var bounds = actor.BoxWithChildren;
Navigation.BuildNavMesh(bounds, options.General.AutoRebuildNavMeshTimeoutMs);
}
}
private static bool SelectActorsUsingAsset(Guid assetId, ref Guid id, Dictionary<Guid, bool> scannedAssets)
{
// Check for asset match or try to use cache
if (assetId == id)
return true;
if (scannedAssets.TryGetValue(id, out var result))
return result;
if (id == Guid.Empty || !FlaxEngine.Content.GetAssetInfo(id, out var assetInfo))
return false;
scannedAssets.Add(id, false);
// Skip scene assets
if (assetInfo.TypeName == "FlaxEngine.SceneAsset")
return false;
// Recursive check if this asset contains direct or indirect reference to the given asset
var asset = FlaxEngine.Content.Load<Asset>(assetInfo.ID, 1000);
if (asset)
{
var references = asset.GetReferences();
for (var i = 0; i < references.Length; i++)
{
if (SelectActorsUsingAsset(assetId, ref references[i], scannedAssets))
{
scannedAssets[id] = true;
return true;
}
}
}
return false;
}
private static void SelectActorsUsingAsset(Guid assetId, SceneGraphNode node, List<SceneGraphNode> selection, Dictionary<Guid, bool> scannedAssets)
{
if (node is ActorNode actorNode && actorNode.Actor)
{
// To detect if this actor uses the given asset simply serialize it to json and check used asset ids
// TODO: check scripts too
var json = actorNode.Actor.ToJson();
JsonAssetBase.GetReferences(json, out var ids);
for (var i = 0; i < ids.Length; i++)
{
if (SelectActorsUsingAsset(assetId, ref ids[i], scannedAssets))
{
selection.Add(actorNode);
break;
}
}
}
// Recursive check for children
for (int i = 0; i < node.ChildNodes.Count; i++)
SelectActorsUsingAsset(assetId, node.ChildNodes[i], selection, scannedAssets);
}
/// <summary>
/// Selects the actors using the given asset.
/// </summary>
/// <param name="assetId">The asset ID.</param>
/// <param name="additive">if set to <c>true</c> will use additive mode, otherwise will clear previous selection.</param>
public void SelectActorsUsingAsset(Guid assetId, bool additive = false)
{
// TODO: make it async action with progress
Profiler.BeginEvent("SelectActorsUsingAsset");
var selection = new List<SceneGraphNode>();
var scannedAssets = new Dictionary<Guid, bool>();
SelectActorsUsingAsset(assetId, Editor.Scene.Root, selection, scannedAssets);
Profiler.EndEvent();
Select(selection, additive);
}
/// <summary>
/// Spawns the specified actor to the game (with undo).
/// </summary>
/// <param name="actor">The actor.</param>
/// <param name="parent">The parent actor. Set null as default.</param>
/// <param name="orderInParent">The order under the parent to put the spawned actor.</param>
/// <param name="autoSelect">True if automatically select the spawned actor, otherwise false.</param>
public void Spawn(Actor actor, Actor parent = null, int orderInParent = -1, bool autoSelect = true)
{
bool isPlayMode = Editor.StateMachine.IsPlayMode;
if (Level.IsAnySceneLoaded == false)
throw new InvalidOperationException("Cannot spawn actor when no scene is loaded.");
SpawnBegin?.Invoke();
// During play in editor mode spawned actors should be dynamic (user can move them)
if (isPlayMode)
actor.StaticFlags = StaticFlags.None;
// Add it
Level.SpawnActor(actor, parent);
// Set order if given
if (orderInParent != -1)
actor.OrderInParent = orderInParent;
// Peek spawned node
var actorNode = Editor.Instance.Scene.GetActorNode(actor);
if (actorNode == null)
throw new InvalidOperationException("Failed to create scene node for the spawned actor.");
// Call post spawn action (can possibly setup custom default values)
actorNode.PostSpawn();
// Create undo action
IUndoAction action = new DeleteActorsAction(actorNode, true);
if (autoSelect)
{
var before = Selection.ToArray();
Selection.Clear();
Selection.Add(actorNode);
OnSelectionChanged();
action = new MultiUndoAction(action, new SelectionChangeAction(before, Selection.ToArray(), OnSelectionUndo));
}
Undo.AddAction(action);
// Mark scene as dirty
Editor.Scene.MarkSceneEdited(actor.Scene);
SpawnEnd?.Invoke();
OnDirty(actorNode);
}
/// <summary>
/// Converts the selected actor to another type.
/// </summary>
/// <param name="to">The type to convert in.</param>
public void Convert(Type to)
{
if (!Editor.SceneEditing.HasSthSelected || !(Editor.SceneEditing.Selection[0] is ActorNode))
return;
if (Level.IsAnySceneLoaded == false)
throw new InvalidOperationException("Cannot spawn actor when no scene is loaded.");
var actionList = new IUndoAction[4];
var oldNode = (ActorNode)Editor.SceneEditing.Selection[0];
var old = oldNode.Actor;
var actor = (Actor)FlaxEngine.Object.New(to);
var parent = old.Parent;
var orderInParent = old.OrderInParent;
// Steps:
// - deselect old actor
// - destroy old actor
// - spawn new actor
// - select new actor
SelectionDeleteBegin?.Invoke();
actionList[0] = new SelectionChangeAction(Selection.ToArray(), new SceneGraphNode[0], OnSelectionUndo);
actionList[0].Do();
actionList[1] = new DeleteActorsAction(oldNode.BuildAllNodes().Where(x => x.CanDelete).ToList());
SelectionDeleteEnd?.Invoke();
SpawnBegin?.Invoke();
// Copy properties
actor.Transform = old.Transform;
actor.StaticFlags = old.StaticFlags;
actor.HideFlags = old.HideFlags;
actor.Layer = old.Layer;
actor.Tags = old.Tags;
actor.Name = old.Name;
actor.IsActive = old.IsActive;
// Spawn actor
Level.SpawnActor(actor, parent);
if (parent != null)
actor.OrderInParent = orderInParent;
if (Editor.StateMachine.IsPlayMode)
actor.StaticFlags = StaticFlags.None;
// Move children
var scripts = old.Scripts;
for (var i = scripts.Length - 1; i >= 0; i--)
scripts[i].Actor = actor;
var children = old.Children;
for (var i = children.Length - 1; i >= 0; i--)
children[i].Parent = actor;
var actorNode = Editor.Instance.Scene.GetActorNode(actor);
if (actorNode == null)
throw new InvalidOperationException("Failed to create scene node for the spawned actor.");
actorNode.PostConvert(oldNode);
actorNode.PostSpawn();
Editor.Scene.MarkSceneEdited(actor.Scene);
actionList[1].Do();
actionList[2] = new DeleteActorsAction(actorNode.BuildAllNodes().Where(x => x.CanDelete).ToList(), true);
actionList[3] = new SelectionChangeAction(new SceneGraphNode[0], new SceneGraphNode[] { actorNode }, OnSelectionUndo);
actionList[3].Do();
Undo.AddAction(new MultiUndoAction(actionList, "Convert actor"));
SpawnEnd?.Invoke();
OnDirty(actorNode);
}
/// <summary>
/// Deletes the selected objects. Supports undo/redo.
/// </summary>
public void Delete()
{
// Peek things that can be removed
var objects = Selection.Where(x => x.CanDelete).ToList().BuildAllNodes().Where(x => x.CanDelete).ToList();
if (objects.Count == 0)
return;
var isSceneTreeFocus = Editor.Windows.SceneWin.ContainsFocus;
SelectionDeleteBegin?.Invoke();
// Change selection
var action1 = new SelectionChangeAction(Selection.ToArray(), new SceneGraphNode[0], OnSelectionUndo);
// Delete objects
var action2 = new DeleteActorsAction(objects);
// Merge two actions and perform them
var action = new MultiUndoAction(new IUndoAction[]
{
action1,
action2
}, action2.ActionString);
action.Do();
Undo.AddAction(action);
SelectionDeleteEnd?.Invoke();
if (isSceneTreeFocus)
{
Editor.Windows.SceneWin.Focus();
}
// fix scene window layout
Editor.Windows.SceneWin.PerformLayout();
Editor.Windows.SceneWin.PerformLayout();
}
/// <summary>
/// Copies the selected objects.
/// </summary>
public void Copy()
{
// Peek things that can be copied (copy all actors)
var objects = Selection.Where(x => x.CanCopyPaste).ToList().BuildAllNodes().Where(x => x.CanCopyPaste && x is ActorNode).ToList();
if (objects.Count == 0)
return;
// Serialize actors
var actors = objects.ConvertAll(x => ((ActorNode)x).Actor);
var data = Actor.ToBytes(actors.ToArray());
if (data == null)
{
Editor.LogError("Failed to copy actors data.");
return;
}
// Copy data
Clipboard.RawData = data;
}
/// <summary>
/// Pastes the copied objects. Supports undo/redo.
/// </summary>
public void Paste()
{
Paste(null);
}
/// <summary>
/// Pastes the copied objects. Supports undo/redo.
/// </summary>
/// <param name="pasteTargetActor">The target actor to paste copied data.</param>
public void Paste(Actor pasteTargetActor)
{
// Get clipboard data
var data = Clipboard.RawData;
// Set paste target if only one actor is selected and no target provided
if (pasteTargetActor == null && SelectionCount == 1 && Selection[0] is ActorNode actorNode)
{
pasteTargetActor = actorNode.Actor;
}
// Create paste action
var pasteAction = PasteActorsAction.Paste(data, pasteTargetActor?.ID ?? Guid.Empty);
if (pasteAction != null)
{
pasteAction.Do(out _, out var nodeParents);
// Select spawned objects (parents only)
var selectAction = new SelectionChangeAction(Selection.ToArray(), nodeParents.Cast<SceneGraphNode>().ToArray(), OnSelectionUndo);
selectAction.Do();
// Build single compound undo action that pastes the actors and selects the created objects (parents only)
Undo.AddAction(new MultiUndoAction(pasteAction, selectAction));
OnSelectionChanged();
}
// Scroll to new selected node while pasting
Editor.Windows.SceneWin.ScrollToSelectedNode();
}
/// <summary>
/// Cuts the selected objects. Supports undo/redo.
/// </summary>
public void Cut()
{
Copy();
Delete();
}
/// <summary>
/// Create parent for selected actors.
/// </summary>
public void CreateParentForSelectedActors()
{
List<SceneGraphNode> selection = Editor.SceneEditing.Selection;
// Get Actors but skip scene node
var actors = selection.Where(x => x is ActorNode and not SceneNode).Select(x => ((ActorNode)x).Actor);
var actorsCount = actors.Count();
if (actorsCount == 0)
return;
Vector3 center = Vector3.Zero;
foreach (var actor in actors)
center += actor.Position;
center /= actorsCount;
Actor parent = new EmptyActor
{
Position = center,
};
Editor.SceneEditing.Spawn(parent, null, -1, false);
using (new UndoMultiBlock(Undo, actors, "Reparent actors"))
{
for (int i = 0; i < selection.Count; i++)
{
if (selection[i] is ActorNode node)
{
if (node.ParentNode != node.ParentScene) // If parent node is not a scene
{
if (selection.Contains(node.ParentNode))
{
continue; // If parent and child nodes selected together, don't touch child nodes
}
// Put created node as child of the Parent Node of node
int parentOrder = node.Actor.OrderInParent;
parent.SetParent(node.Actor.Parent, true, true);
parent.OrderInParent = parentOrder;
}
node.Actor.SetParent(parent, true, false);
}
}
}
Editor.SceneEditing.Select(parent);
Editor.Scene.GetActorNode(parent).TreeNode.StartRenaming(Editor.Windows.SceneWin, Editor.Windows.SceneWin.SceneTreePanel);
}
/// <summary>
/// Duplicates the selected objects. Supports undo/redo.
/// </summary>
public void Duplicate()
{
// Peek things that can be copied (copy all actors)
var nodes = Selection.Where(x => x.CanDuplicate).ToList().BuildAllNodes();
if (nodes.Count == 0)
return;
var actors = new List<Actor>();
var newSelection = new List<SceneGraphNode>();
List<IUndoAction> customUndoActions = null;
foreach (var node in nodes)
{
if (node.CanDuplicate)
{
if (node is ActorNode actorNode)
{
actors.Add(actorNode.Actor);
}
else
{
var customDuplicatedObject = node.Duplicate(out var customUndoAction);
if (customDuplicatedObject != null)
newSelection.Add(customDuplicatedObject);
if (customUndoAction != null)
{
if (customUndoActions == null)
customUndoActions = new List<IUndoAction>();
customUndoActions.Add(customUndoAction);
}
}
}
}
if (actors.Count == 0)
{
// Duplicate custom scene graph nodes only without actors
if (newSelection.Count != 0)
{
// Select spawned objects (parents only)
var selectAction = new SelectionChangeAction(Selection.ToArray(), newSelection.ToArray(), OnSelectionUndo);
selectAction.Do();
// Build a single compound undo action that pastes the actors, pastes custom stuff (scene graph extension) and selects the created objects (parents only)
var customUndoActionsCount = customUndoActions?.Count ?? 0;
var undoActions = new IUndoAction[1 + customUndoActionsCount];
for (int i = 0; i < customUndoActionsCount; i++)
undoActions[i] = customUndoActions[i];
undoActions[undoActions.Length - 1] = selectAction;
Undo.AddAction(new MultiUndoAction(undoActions));
OnSelectionChanged();
}
return;
}
// Serialize actors
var data = Actor.ToBytes(actors.ToArray());
if (data == null)
{
Editor.LogError("Failed to copy actors data.");
return;
}
// Create paste action (with selecting spawned objects)
var pasteAction = PasteActorsAction.Duplicate(data, Guid.Empty);
if (pasteAction != null)
{
pasteAction.Do(out _, out var nodeParents);
// Select spawned objects (parents only)
newSelection.Clear();
newSelection.AddRange(nodeParents);
var selectAction = new SelectionChangeAction(Selection.ToArray(), newSelection.ToArray(), OnSelectionUndo);
selectAction.Do();
// Build a single compound undo action that pastes the actors, pastes custom stuff (scene graph extension) and selects the created objects (parents only)
var customUndoActionsCount = customUndoActions?.Count ?? 0;
var undoActions = new IUndoAction[2 + customUndoActionsCount];
undoActions[0] = pasteAction;
for (int i = 0; i < customUndoActionsCount; i++)
undoActions[i + 1] = customUndoActions[i];
undoActions[undoActions.Length - 1] = selectAction;
Undo.AddAction(new MultiUndoAction(undoActions));
OnSelectionChanged();
}
// Scroll to new selected node while duplicating
Editor.Windows.SceneWin.ScrollToSelectedNode();
}
/// <summary>
/// Called when selection gets changed. Invokes the other events and updates editor. Call it when you manually modify selected objects collection.
/// </summary>
public void OnSelectionChanged()
{
SelectionChanged?.Invoke();
}
/// <inheritdoc />
public override void OnInit()
{
// Deselect actors on remove (and actor child nodes)
Editor.Scene.ActorRemoved += Deselect;
Editor.Scene.Root.ActorChildNodesDispose += OnActorChildNodesDispose;
}
private void OnActorChildNodesDispose(ActorNode node)
{
if (Selection.Count == 0)
return;
// TODO: cache if selection contains any actor child node and skip this loop if no need to iterate
// TODO: or build a hash set with selected nodes for quick O(1) checks (cached until selection changes)
// Deselect child nodes
for (int i = 0; i < node.ChildNodes.Count; i++)
{
if (Selection.Contains(node.ChildNodes[i]))
{
Deselect();
return;
}
}
}
}
}