// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using FlaxEditor.Actions;
using FlaxEditor.SceneGraph;
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)
{
}
///
/// Selects all scenes.
///
public void SelectAllScenes()
{
// Select all scenes (linked to the root node)
Select(Editor.Scene.Root.ChildNodes);
}
///
/// 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)
throw new ArgumentNullException();
// 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();
}
///
/// Spawns the specified actor to the game (with undo).
///
/// The actor.
/// The parent actor. Set null as default.
public void Spawn(Actor actor, Actor parent = null)
{
bool isPlayMode = Editor.StateMachine.IsPlayMode;
if (Level.IsAnySceneLoaded == false)
throw new InvalidOperationException("Cannot spawn actor when no scene is loaded.");
SpawnBegin?.Invoke();
// Add it
Level.SpawnActor(actor, parent);
// 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.");
// During play in editor mode spawned actors should be dynamic (user can move them)
if (isPlayMode)
actor.StaticFlags = StaticFlags.None;
// Call post spawn action (can possibly setup custom default values)
actorNode.PostSpawn();
// Create undo action
var action = new DeleteActorsAction(new List(1) { actorNode }, true);
Undo.AddAction(action);
// Mark scene as dirty
Editor.Scene.MarkSceneEdited(actor.Scene);
SpawnEnd?.Invoke();
var options = Editor.Options.Options;
// 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 && (actor.StaticFlags & StaticFlags.Navigation) == StaticFlags.Navigation)
{
var bounds = actor.BoxWithChildren;
Navigation.BuildNavMesh(actor.Scene, bounds, options.General.AutoRebuildNavMeshTimeoutMs);
}
}
///
/// 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;
bool isPlayMode = Editor.StateMachine.IsPlayMode;
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();
var options = Editor.Options.Options;
// Auto CSG mesh rebuild
if (!isPlayMode && options.General.AutoRebuildCSG)
{
foreach (var obj in objects)
{
if (obj is ActorNode node && node.Actor is BoxBrush)
node.Actor.Scene.BuildCSG(options.General.AutoRebuildCSGTimeoutMs);
}
}
// Auto NavMesh rebuild
if (!isPlayMode && options.General.AutoRebuildNavMesh)
{
foreach (var obj in objects)
{
if (obj is ActorNode node && node.Actor.Scene && (node.Actor.StaticFlags & StaticFlags.Navigation) == StaticFlags.Navigation)
{
var bounds = node.Actor.BoxWithChildren;
Navigation.BuildNavMesh(node.Actor.Scene, bounds, options.General.AutoRebuildNavMeshTimeoutMs);
}
}
}
}
///
/// 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;
}
// Create paste action
var pasteAction = PasteActorsAction.Paste(data, pasteTargetActor?.ID ?? Guid.Empty);
if (pasteAction != null)
{
OnPasteAction(pasteAction);
}
}
///
/// Cuts the selected objects. Supports undo/redo.
///
public void Cut()
{
Copy();
Delete();
}
///
/// Duplicates the selected objects. Supports undo/redo.
///
public void Duplicate()
{
// 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;
}
// Create paste action (with selecting spawned objects)
var pasteAction = PasteActorsAction.Duplicate(data, Guid.Empty);
if (pasteAction != null)
{
OnPasteAction(pasteAction);
}
}
private void OnPasteAction(PasteActorsAction pasteAction)
{
pasteAction.Do(out _, out var nodeParents);
// Select spawned objects
var selectAction = new SelectionChangeAction(Selection.ToArray(), nodeParents.Cast().ToArray(), OnSelectionUndo);
selectAction.Do();
Undo.AddAction(new MultiUndoAction(pasteAction, selectAction));
OnSelectionChanged();
}
///
/// 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;
}
}
}
}
}