// 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
{
///
/// Editing scenes module. Manages scene objects selection and editing modes.
///
///
public sealed class SceneEditingModule : EditorModule
{
///
/// The selected objects.
///
public readonly List Selection = new List(64);
///
/// Gets the amount of the selected objects.
///
public int SelectionCount => Selection.Count;
///
/// Gets a value indicating whether any object is selected.
///
public bool HasSthSelected => Selection.Count > 0;
///
/// Occurs when selected objects collection gets changed.
///
public event Action SelectionChanged;
///
/// Occurs before spawning actor to game action.
///
public event Action SpawnBegin;
///
/// Occurs after spawning actor to game action.
///
public event Action SpawnEnd;
///
/// Occurs before selection delete action.
///
public event Action SelectionDeleteBegin;
///
/// Occurs after selection delete action.
///
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());
}
///
/// Selects all scenes.
///
public void SelectAllScenes()
{
BulkScenesSelectUpdate(true);
}
///
/// Deselects all scenes.
///
public void DeselectAllScenes()
{
BulkScenesSelectUpdate(false);
}
///
/// Selects the specified actor (finds it's scene graph node).
///
/// The actor.
public void Select(Actor actor)
{
var node = Editor.Scene.GetActorNode(actor);
if (node != null)
Select(node);
}
///
/// Selects the specified collection of objects.
///
/// The selection.
/// if set to true will use additive mode, otherwise will clear previous selection.
public void Select(List 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);
}
///
/// Selects the specified collection of objects.
///
/// The selection.
/// if set to true will use additive mode, otherwise will clear previous selection.
public void Select(SceneGraphNode[] selection, bool additive = false)
{
if (selection == null)
throw new ArgumentNullException();
Select(selection.ToList(), additive);
}
///
/// Selects the specified object.
///
/// The selection.
/// if set to true will use additive mode, otherwise will clear previous selection.
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);
}
///
/// Deselects given object.
///
public void Deselect(SceneGraphNode node)
{
if (!Selection.Contains(node))
return;
var before = Selection.ToArray();
Selection.Remove(node);
SelectionChange(before);
}
///
/// Clears selected objects collection.
///
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();
}
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(actor.Scene, bounds, options.General.AutoRebuildNavMeshTimeoutMs);
}
}
private static bool SelectActorsUsingAsset(Guid assetId, ref Guid id, Dictionary 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(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 selection, Dictionary 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);
}
///
/// Selects the actors using the given asset.
///
/// The asset ID.
/// if set to true will use additive mode, otherwise will clear previous selection.
public void SelectActorsUsingAsset(Guid assetId, bool additive = false)
{
// TODO: make it async action with progress
Profiler.BeginEvent("SelectActorsUsingAsset");
var selection = new List();
var scannedAssets = new Dictionary();
SelectActorsUsingAsset(assetId, Editor.Scene.Root, selection, scannedAssets);
Profiler.EndEvent();
Select(selection, additive);
}
///
/// Spawns the specified actor to the game (with undo).
///
/// The actor.
/// The parent actor. Set null as default.
/// The order under the parent to put the spawned actor.
/// True if automatically select the spawned actor, otherwise false.
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);
}
///
/// Converts the selected actor to another type.
///
/// The type to convert in.
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);
}
///
/// Deletes the selected objects. Supports undo/redo.
///
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();
}
///
/// Copies the selected objects.
///
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;
}
///
/// Pastes the copied objects. Supports undo/redo.
///
public void Paste()
{
Paste(null);
}
///
/// Pastes the copied objects. Supports undo/redo.
///
/// The target actor to paste copied data.
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.Scene == actorNode.Actor ? actorNode.Actor : actorNode.Actor.Parent;
}
// 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().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();
}
///
/// Cuts the selected objects. Supports undo/redo.
///
public void Cut()
{
Copy();
Delete();
}
///
/// Create parent for selected actors.
///
public void CreateParentForSelectedActors()
{
List 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);
}
///
/// Duplicates the selected objects. Supports undo/redo.
///
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();
var newSelection = new List();
List 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();
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();
}
///
/// Called when selection gets changed. Invokes the other events and updates editor. Call it when you manually modify selected objects collection.
///
public void OnSelectionChanged()
{
SelectionChanged?.Invoke();
}
///
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)
{
// TODO: cache if selection contains any actor child node and skip this loop if no need to iterate
// Deselect child nodes
for (int i = 0; i < node.ChildNodes.Count; i++)
{
if (Selection.Contains(node.ChildNodes[i]))
{
Deselect();
return;
}
}
}
}
}