/// <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;
            }

            // Change selection
            var action1 = new SelectionChangeAction(Selection.ToArray(), new SceneGraphNode[0]);

            // 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);

            // Auto CSG mesh rebuild
            foreach (var obj in objects)
            {
                if (obj is ActorNode node && node.Actor is BoxBrush)
                {
                    node.Actor.Scene.BuildCSG();
                }
            }
        }
Beispiel #2
0
        /// <summary>
        /// Spawns the specified actor.
        /// </summary>
        /// <param name="actor">The actor.</param>
        /// <param name="parent">The parent.</param>
        public void Spawn(Actor actor, Actor parent)
        {
            if (actor == null)
            {
                throw new ArgumentNullException(nameof(actor));
            }

            // Link it
            actor.Parent = parent ?? throw new ArgumentNullException(nameof(parent));

            // Peek spawned node
            var actorNode = SceneGraphFactory.FindNode(actor.ID) as ActorNode ?? SceneGraphFactory.BuildActorNode(actor);

            if (actorNode == null)
            {
                throw new InvalidOperationException("Failed to create scene node for the spawned actor.");
            }

            var parentNode = SceneGraphFactory.FindNode(parent.ID) as ActorNode;

            actorNode.ParentNode = parentNode ?? throw new InvalidOperationException("Missing scene graph node for the spawned parent actor.");

            // Call post spawn action (can possibly setup custom default values)
            actorNode.PostSpawn();

            // Create undo action
            var action = new CustomDeleteActorsAction(new List <SceneGraphNode>(1)
            {
                actorNode
            }, true);

            Undo.AddAction(action);
        }
        /// <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>
        public void Spawn(Actor actor, Actor parent = null)
        {
            if (SceneManager.IsAnySceneLoaded == false)
            {
                throw new InvalidOperationException("Cannot spawn actor when no scene is loaded.");
            }

            // Add it
            SceneManager.SpawnActor(actor, parent);

            // Create undo action
            var actorNode = Editor.Instance.Scene.GetActorNode(actor);
            var action    = new DeleteActorsAction(new List <SceneGraphNode>(1)
            {
                actorNode
            }, true);

            Undo.AddAction(action);

            // Auto CSG mesh rebuild
            if (actor is BoxBrush && actor.Scene)
            {
                actor.Scene.BuildCSG();
            }
        }
Beispiel #4
0
        /// <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>
        public void Spawn(Actor actor, Actor parent = null)
        {
            if (SceneManager.IsAnySceneLoaded == false)
            {
                throw new InvalidOperationException("Cannot spawn actor when no scene is loaded.");
            }

            // Add it
            SceneManager.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.");
            }

            // Call post spawn action (can possibly setup custom default values)
            actorNode.PostSpawn();

            // Create undo action
            var action = new DeleteActorsAction(new List <SceneGraphNode>(1)
            {
                actorNode
            }, true);

            Undo.AddAction(action);

            // Auto CSG mesh rebuild
            if (actor is BoxBrush && actor.Scene)
            {
                actor.Scene.BuildCSG();
            }
        }
Beispiel #5
0
        /// <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();
            }
        }
Beispiel #6
0
        /// <summary>
        /// Deletes selected objects.
        /// </summary>
        public void Delete()
        {
            // Peek things that can be removed
            var objects = Selection.Where(x => x.CanDelete && x != Graph.Main).ToList().BuildAllNodes().Where(x => x.CanDelete).ToList();

            if (objects.Count == 0)
            {
                return;
            }

            // Change selection
            var action1 = new SelectionChangeAction(Selection.ToArray(), new SceneGraphNode[0], OnSelectionUndo);

            // Delete objects
            var action2 = new CustomDeleteActorsAction(objects);

            // Merge actions and perform them
            var action = new MultiUndoAction(new IUndoAction[]
            {
                action1,
                action2
            }, action2.ActionString);

            action.Do();
            Undo.AddAction(action);
        }
Beispiel #7
0
        /// <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;
            }

            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;
                        node.Actor.Scene.BuildNavMesh(bounds, options.General.AutoRebuildNavMeshTimeoutMs);
                    }
                }
            }
        }
        /// <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="autoSelect">True if automatically select the spawned actor, otherwise false.</param>
        public void Spawn(Actor actor, Actor parent = null, 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();

            // 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
            IUndoAction action = new DeleteActorsAction(new List <SceneGraphNode>(1)
            {
                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);
        }
Beispiel #9
0
        private void OnPasteAcction(PasteActorsAction pasteAction)
        {
            pasteAction.Do(out _, out var nodeParents);

            // Select spawned objects
            var selectAction = new SelectionChangeAction(Selection.ToArray(), nodeParents.Cast <SceneGraphNode>().ToArray(), OnSelectionUndo);

            selectAction.Do();

            Undo.AddAction(new MultiUndoAction(pasteAction, selectAction));
            OnSelectionChanges();
        }
Beispiel #10
0
        /// <summary>
        /// Changes the root object of the prefab.
        /// </summary>
        private void SetRoot()
        {
            var oldRoot = Graph.MainActor;
            var newRoot = ((ActorNode)Selection[0]).Actor;

            Deselect();

            var action = new SetRootAction(this, oldRoot, newRoot);

            action.Do();
            Undo.AddAction(action);
        }
Beispiel #11
0
        /// <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>
        public void Spawn(Actor actor, Actor parent = null)
        {
            bool isPlayMode = Editor.StateMachine.IsPlayMode;

            if (SceneManager.IsAnySceneLoaded == false)
            {
                throw new InvalidOperationException("Cannot spawn actor when no scene is loaded.");
            }

            // Add it
            SceneManager.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 <SceneGraphNode>(1)
            {
                actorNode
            }, true);

            Undo.AddAction(action);

            // Mark scene as dirty
            Editor.Scene.MarkSceneEdited(actor.Scene);

            // Auto CSG mesh rebuild
            if (isPlayMode && !Editor.Options.Options.General.AutoRebuildCSG)
            {
                if (actor is BoxBrush && actor.Scene)
                {
                    actor.Scene.BuildCSG(Editor.Options.Options.General.AutoRebuildCSGTimeoutMs);
                }
            }
        }
Beispiel #12
0
        /// <summary>
        /// Breaks any prefab links for the selected objects. Supports undo/redo.
        /// </summary>
        public void BreakLinks()
        {
            // Skip in invalid states
            if (!Editor.StateMachine.CurrentState.CanEditScene)
            {
                return;
            }

            // Get valid objects (the top ones, C++ backend will process the child objects)
            var selection = Editor.SceneEditing.Selection.Where(x => x is ActorNode actorNode && actorNode.HasPrefabLink).ToList().BuildNodesParents();

            if (selection.Count == 0)
            {
                return;
            }

            // Perform action
            if (Editor.StateMachine.CurrentState.CanUseUndoRedo)
            {
                if (selection.Count == 1)
                {
                    var action = BreakPrefabLinkAction.Break(((ActorNode)selection[0]).Actor);
                    Undo.AddAction(action);
                    action.Do();
                }
                else
                {
                    var actions = new IUndoAction[selection.Count];
                    for (int i = 0; i < selection.Count; i++)
                    {
                        var action = BreakPrefabLinkAction.Break(((ActorNode)selection[i]).Actor);
                        actions[i] = action;
                        action.Do();
                    }
                    Undo.AddAction(new MultiUndoAction(actions));
                }
            }
            else
            {
                for (int i = 0; i < selection.Count; i++)
                {
                    ((ActorNode)selection[i]).Actor.BreakPrefabLink();
                }
            }
        }
Beispiel #13
0
        private void OnPrefabCreated(ContentItem contentItem)
        {
            if (contentItem is PrefabItem prefabItem)
            {
                PrefabCreated?.Invoke(prefabItem);
            }

            // Skip in invalid states
            if (!Editor.StateMachine.CurrentState.CanEditScene)
            {
                return;
            }

            // Record undo for prefab creating (backend links the target instance with the prefab)
            if (Editor.Undo.Enabled)
            {
                var selection = Editor.SceneEditing.Selection.Where(x => x is ActorNode).ToList().BuildNodesParents();
                if (selection.Count == 0)
                {
                    return;
                }

                if (selection.Count == 1)
                {
                    var action = BreakPrefabLinkAction.Linked(((ActorNode)selection[0]).Actor);
                    Undo.AddAction(action);
                }
                else
                {
                    var actions = new IUndoAction[selection.Count];
                    for (int i = 0; i < selection.Count; i++)
                    {
                        var action = BreakPrefabLinkAction.Linked(((ActorNode)selection[i]).Actor);
                        actions[i] = action;
                    }
                    Undo.AddAction(new MultiUndoAction(actions));
                }
            }

            Editor.Instance.Windows.PropertiesWin.Presenter.BuildLayout();
        }
        /// <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();

            OnDirty(objects);

            if (isSceneTreeFocus)
            {
                Editor.Windows.SceneWin.Focus();
            }
        }
Beispiel #15
0
        /// <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.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();
            }
        }
Beispiel #16
0
        /// <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];
            Actor old = ((ActorNode)Editor.SceneEditing.Selection[0]).Actor;
            Actor actor = (Actor)FlaxEngine.Object.New(to);
            var parent = old.Parent;
            var orderInParent = old.OrderInParent;

            SelectionDeleteBegin?.Invoke();

            actionList[0] = new SelectionChangeAction(Selection.ToArray(), new SceneGraphNode[0], OnSelectionUndo);
            actionList[0].Do();

            actionList[1] = new DeleteActorsAction(new List<SceneGraphNode>
            {
                Editor.Instance.Scene.GetActorNode(old)
            });
            actionList[1].Do();

            SelectionDeleteEnd?.Invoke();

            SpawnBegin?.Invoke();

            // Copy properties
            actor.Transform = old.Transform;
            actor.StaticFlags = old.StaticFlags;
            actor.HideFlags = old.HideFlags;
            actor.Layer = old.Layer;
            actor.Tag = old.Tag;
            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
            for (var i = old.ScriptsCount - 1; i >= 0; i--)
            {
                var script = old.Scripts[i];
                script.Actor = actor;
                Guid newid = Guid.NewGuid();
                FlaxEngine.Object.Internal_ChangeID(FlaxEngine.Object.GetUnmanagedPtr(script), ref newid);
            }
            for (var i = old.Children.Length - 1; i >= 0; i--)
            {
                old.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.PostSpawn();
            Editor.Scene.MarkSceneEdited(actor.Scene);

            actionList[2] = new DeleteActorsAction(new List<SceneGraphNode>
            {
                actorNode
            }, true);

            actionList[3] = new SelectionChangeAction(new SceneGraphNode[0], new SceneGraphNode[] { actorNode }, OnSelectionUndo);
            actionList[3].Do();

            var actions = new MultiUndoAction(actionList);
            Undo.AddAction(actions);

            SpawnEnd?.Invoke();

            OnDirty(actorNode);
        }
Beispiel #17
0
        private void SelectionChange(SceneGraphNode[] before)
        {
            Undo.AddAction(new SelectionChangeAction(before, Selection.ToArray(), OnSelectionUndo));

            OnSelectionChanged();
        }
        /// <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.Tag         = old.Tag;
            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>
        /// Called when selection gets changed.
        /// </summary>
        /// <param name="before">The selection before the change.</param>
        public void OnSelectionChanged(SceneGraphNode[] before)
        {
            Undo.AddAction(new SelectionChangeAction(before, Selection.ToArray(), OnSelectionUndo));

            OnSelectionChanges();
        }
        /// <summary>
        /// Shows the secondary context menu.
        /// </summary>
        /// <param name="location">The location in the Surface Space.</param>
        /// <param name="controlUnderMouse">The Surface Control that is under the cursor. Used to customize the menu.</param>
        public virtual void ShowSecondaryCM(Vector2 location, SurfaceControl controlUnderMouse)
        {
            var selection = SelectedNodes;

            if (selection.Count == 0)
            {
                return;
            }

            // Create secondary context menu
            var menu = new FlaxEditor.GUI.ContextMenu.ContextMenu();

            if (_onSave != null)
            {
                menu.AddButton("Save", _onSave).Enabled = CanEdit;
                menu.AddSeparator();
            }
            _cmCopyButton = menu.AddButton("Copy", Copy);
            menu.AddButton("Paste", Paste).Enabled = CanEdit && CanPaste();
            _cmDuplicateButton         = menu.AddButton("Duplicate", Duplicate);
            _cmDuplicateButton.Enabled = CanEdit;
            var canRemove = CanEdit && selection.All(node => (node.Archetype.Flags & NodeFlags.NoRemove) == 0);

            menu.AddButton("Cut", Cut).Enabled       = canRemove;
            menu.AddButton("Delete", Delete).Enabled = canRemove;

            if (_supportsDebugging)
            {
                menu.AddSeparator();
                if (selection.Count == 1)
                {
                    menu.AddButton(selection[0].Breakpoint.Set ? "Delete breakpoint" : "Add breakpoint", () =>
                    {
                        foreach (var node in Nodes)
                        {
                            if (node.IsSelected)
                            {
                                node.Breakpoint.Set     = !node.Breakpoint.Set;
                                node.Breakpoint.Enabled = true;
                                OnNodeBreakpointEdited(node);
                                break;
                            }
                        }
                    });
                    menu.AddButton("Toggle breakpoint", () =>
                    {
                        foreach (var node in Nodes)
                        {
                            if (node.IsSelected)
                            {
                                node.Breakpoint.Enabled = !node.Breakpoint.Enabled;
                                OnNodeBreakpointEdited(node);
                                break;
                            }
                        }
                    }).Enabled = selection[0].Breakpoint.Set;
                }
                menu.AddSeparator();
                menu.AddButton("Delete all breakpoints", () =>
                {
                    foreach (var node in Nodes)
                    {
                        if (node.Breakpoint.Set)
                        {
                            node.Breakpoint.Set = false;
                            OnNodeBreakpointEdited(node);
                        }
                    }
                }).Enabled = Nodes.Any(x => x.Breakpoint.Set);
                menu.AddButton("Enable all breakpoints", () =>
                {
                    foreach (var node in Nodes)
                    {
                        if (!node.Breakpoint.Enabled)
                        {
                            node.Breakpoint.Enabled = true;
                            OnNodeBreakpointEdited(node);
                        }
                    }
                }).Enabled = Nodes.Any(x => x.Breakpoint.Set && !x.Breakpoint.Enabled);
                menu.AddButton("Disable all breakpoints", () =>
                {
                    foreach (var node in Nodes)
                    {
                        if (node.Breakpoint.Enabled)
                        {
                            node.Breakpoint.Enabled = false;
                            OnNodeBreakpointEdited(node);
                        }
                    }
                }).Enabled = Nodes.Any(x => x.Breakpoint.Set && x.Breakpoint.Enabled);
            }
            menu.AddSeparator();

            _cmFormatNodesConnectionButton         = menu.AddButton("Format node(s)", () => { FormatGraph(SelectedNodes); });
            _cmFormatNodesConnectionButton.Enabled = CanEdit && HasNodesSelection;

            _cmRemoveNodeConnectionsButton = menu.AddButton("Remove all connections to that node(s)", () =>
            {
                var nodes = ((List <SurfaceNode>)menu.Tag);

                if (Undo != null)
                {
                    var actions = new List <IUndoAction>(nodes.Count);
                    foreach (var node in nodes)
                    {
                        var action = new EditNodeConnections(Context, node);
                        node.RemoveConnections();
                        action.End();
                        actions.Add(action);
                    }
                    Undo.AddAction(new MultiUndoAction(actions, actions[0].ActionString));
                }
                else
                {
                    foreach (var node in nodes)
                    {
                        node.RemoveConnections();
                    }
                }

                MarkAsEdited();
            });
            _cmRemoveNodeConnectionsButton.Enabled = CanEdit;
            _cmRemoveBoxConnectionsButton          = menu.AddButton("Remove all connections to that box", () =>
            {
                var boxUnderMouse = (Box)_cmRemoveBoxConnectionsButton.Tag;
                if (Undo != null)
                {
                    var action = new EditNodeConnections(Context, boxUnderMouse.ParentNode);
                    boxUnderMouse.RemoveConnections();
                    action.End();
                    Undo.AddAction(action);
                }
                else
                {
                    boxUnderMouse.RemoveConnections();
                }
                MarkAsEdited();
            });
            _cmRemoveBoxConnectionsButton.Enabled = CanEdit;
            {
                var boxUnderMouse = GetChildAtRecursive(location) as Box;
                _cmRemoveBoxConnectionsButton.Enabled = boxUnderMouse != null && boxUnderMouse.HasAnyConnection;
                _cmRemoveBoxConnectionsButton.Tag     = boxUnderMouse;
            }

            controlUnderMouse?.OnShowSecondaryContextMenu(menu, controlUnderMouse.PointFromParent(location));

            OnShowSecondaryContextMenu(menu, controlUnderMouse);

            // Show secondary context menu
            _cmStartPos = location;
            menu.Tag    = selection;
            menu.MaximumItemsInViewCount = 24;
            menu.Show(this, location);
        }
        /// <summary>
        /// Formats a graph where all nodes are connected
        /// </summary>
        /// <param name="nodes">List of connected nodes</param>
        protected void FormatConnectedGraph(List <SurfaceNode> nodes)
        {
            if (nodes.Count <= 1 || !CanEdit)
            {
                return;
            }

            var boundingBox = GetNodesBounds(nodes);

            var nodeData = nodes.ToDictionary(n => n, n => new NodeFormattingData {
            });

            // Rightmost nodes with none of our nodes to the right of them
            var endNodes = nodes
                           .Where(n => !n.GetBoxes().Any(b => b.IsOutput && b.Connections.Any(c => nodeData.ContainsKey(c.ParentNode))))
                           .OrderBy(n => n.Top) // Keep their relative order
                           .ToList();

            // Longest path layering
            int maxLayer = SetLayers(nodeData, endNodes);

            // Set the vertical offsets
            int maxOffset = SetOffsets(nodeData, endNodes, maxLayer);

            // Layout the nodes

            // Get the largest nodes in the Y and X direction
            float[] widths  = new float[maxLayer + 1];
            float[] heights = new float[maxOffset + 1];
            for (int i = 0; i < nodes.Count; i++)
            {
                if (nodeData.TryGetValue(nodes[i], out var data))
                {
                    if (nodes[i].Width > widths[data.Layer])
                    {
                        widths[data.Layer] = nodes[i].Width;
                    }
                    if (nodes[i].Height > heights[data.Offset])
                    {
                        heights[data.Offset] = nodes[i].Height;
                    }
                }
            }

            Vector2 minDistanceBetweenNodes = new Vector2(30, 30);

            // Figure out the node positions (aligned to a grid)
            float[] nodeXPositions = new float[widths.Length];
            for (int i = 1; i < widths.Length; i++)
            {
                // Go from right to left (backwards) through the nodes
                nodeXPositions[i] = nodeXPositions[i - 1] + minDistanceBetweenNodes.X + widths[i];
            }

            float[] nodeYPositions = new float[heights.Length];
            for (int i = 1; i < heights.Length; i++)
            {
                // Go from top to bottom through the nodes
                nodeYPositions[i] = nodeYPositions[i - 1] + heights[i - 1] + minDistanceBetweenNodes.Y;
            }

            // Set the node positions
            var undoActions      = new List <MoveNodesAction>();
            var topRightPosition = endNodes[0].Location;

            for (int i = 0; i < nodes.Count; i++)
            {
                if (nodeData.TryGetValue(nodes[i], out var data))
                {
                    Vector2 newLocation   = new Vector2(-nodeXPositions[data.Layer], nodeYPositions[data.Offset]) + topRightPosition;
                    Vector2 locationDelta = newLocation - nodes[i].Location;
                    nodes[i].Location = newLocation;

                    if (Undo != null)
                    {
                        undoActions.Add(new MoveNodesAction(Context, new[] { nodes[i].ID }, locationDelta));
                    }
                }
            }

            MarkAsEdited(false);
            Undo?.AddAction(new MultiUndoAction(undoActions, "Format nodes"));
        }
        /// <summary>
        /// Pastes the copied items.
        /// </summary>
        public void Paste()
        {
            if (!CanEdit)
            {
                return;
            }
            var data = Clipboard.Text;

            if (data == null || data.Length < 2)
            {
                return;
            }

            try
            {
                // Load Mr Json
                var model = FlaxEngine.Json.JsonSerializer.Deserialize <DataModel>(data);
                if (model.Nodes == null)
                {
                    model.Nodes = new DataModelNode[0];
                }

                // Build the nodes IDs mapping (need to generate new IDs for the pasted nodes and preserve the internal connections)
                var idsMapping = new Dictionary <uint, uint>();
                for (int i = 0; i < model.Nodes.Length; i++)
                {
                    uint result = 1;
                    while (true)
                    {
                        bool valid = true;
                        if (idsMapping.ContainsValue(result))
                        {
                            result++;
                            valid = false;
                        }
                        else
                        {
                            for (int j = 0; j < Nodes.Count; j++)
                            {
                                if (Nodes[j].ID == result)
                                {
                                    result++;
                                    valid = false;
                                    break;
                                }
                            }
                        }

                        if (valid)
                        {
                            break;
                        }
                    }

                    idsMapping.Add(model.Nodes[i].ID, result);
                }

                // Find controls upper left location
                Vector2 upperLeft = new Vector2(model.Nodes[0].X, model.Nodes[0].Y);
                for (int i = 1; i < model.Nodes.Length; i++)
                {
                    upperLeft.X = Mathf.Min(upperLeft.X, model.Nodes[i].X);
                    upperLeft.Y = Mathf.Min(upperLeft.Y, model.Nodes[i].Y);
                }

                // Create nodes
                var nodes     = new Dictionary <uint, SurfaceNode>();
                var nodesData = new Dictionary <uint, DataModelNode>();
                for (int i = 0; i < model.Nodes.Length; i++)
                {
                    var nodeData = model.Nodes[i];

                    // Peek type
                    if (!NodeFactory.GetArchetype(NodeArchetypes, nodeData.GroupID, nodeData.TypeID, out var groupArchetype, out var nodeArchetype))
                    {
                        throw new InvalidOperationException("Unknown node type.");
                    }

                    // Validate given node type
                    if (!CanUseNodeType(nodeArchetype))
                    {
                        continue;
                    }

                    // Create
                    var node = NodeFactory.CreateNode(idsMapping[nodeData.ID], Context, groupArchetype, nodeArchetype);
                    if (node == null)
                    {
                        throw new InvalidOperationException("Failed to create node.");
                    }
                    Nodes.Add(node);
                    nodes.Add(nodeData.ID, node);
                    nodesData.Add(nodeData.ID, nodeData);

                    // Initialize
                    if (nodeData.Values != null && node.Values.Length > 0)
                    {
                        if (node.Values != null && node.Values.Length == nodeData.Values.Length)
                        {
                            // Copy and fix values (Json deserializes may output them in a different format)
                            for (int l = 0; l < node.Values.Length; l++)
                            {
                                var src = nodeData.Values[l].Value;
                                var dst = node.Values[l];

                                if (nodeData.Values[l].EnumTypeName != null)
                                {
                                    var enumType = TypeUtils.GetManagedType(nodeData.Values[l].EnumTypeName);
                                    if (enumType != null)
                                    {
                                        src = Enum.ToObject(enumType, src);
                                    }
                                }
                                else if (src is JToken token)
                                {
                                    if (dst is Vector2)
                                    {
                                        src = new Vector2(token["X"].Value <float>(),
                                                          token["Y"].Value <float>());
                                    }
                                    else if (dst is Vector3)
                                    {
                                        src = new Vector3(token["X"].Value <float>(),
                                                          token["Y"].Value <float>(),
                                                          token["Z"].Value <float>());
                                    }
                                    else if (dst is Vector4)
                                    {
                                        src = new Vector4(token["X"].Value <float>(),
                                                          token["Y"].Value <float>(),
                                                          token["Z"].Value <float>(),
                                                          token["W"].Value <float>());
                                    }
                                    else if (dst is Color)
                                    {
                                        src = new Color(token["R"].Value <float>(),
                                                        token["G"].Value <float>(),
                                                        token["B"].Value <float>(),
                                                        token["A"].Value <float>());
                                    }
                                    else
                                    {
                                        Editor.LogWarning("Unknown pasted node value token: " + token);
                                        src = dst;
                                    }
                                }
                                else if (src is double asDouble)
                                {
                                    src = (float)asDouble;
                                }
                                else if (dst is Guid)
                                {
                                    src = Guid.Parse((string)src);
                                }
                                else if (dst is int)
                                {
                                    src = Convert.ToInt32(src);
                                }
                                else if (dst is float)
                                {
                                    src = Convert.ToSingle(src);
                                }
                                else if (dst is byte[] && src is string s)
                                {
                                    src = Convert.FromBase64String(s);
                                }

                                node.Values[l] = src;
                            }
                        }
                        else
                        {
                            Editor.LogWarning("Invalid node custom values.");
                        }
                    }

                    Context.OnControlLoaded(node);
                }

                // Setup connections
                foreach (var e in nodes)
                {
                    var node     = e.Value;
                    var nodeData = nodesData[e.Key];
                    if (nodeData.Boxes != null)
                    {
                        foreach (var boxData in nodeData.Boxes)
                        {
                            var box = node.GetBox(boxData.ID);
                            if (box == null || boxData.BoxIDs == null || boxData.NodeIDs == null || boxData.BoxIDs.Length != boxData.NodeIDs.Length)
                            {
                                continue;
                            }

                            for (int i = 0; i < boxData.NodeIDs.Length; i++)
                            {
                                if (nodes.TryGetValue(boxData.NodeIDs[i], out var targetNode) &&
                                    targetNode.TryGetBox(boxData.BoxIDs[i], out var targetBox))
                                {
                                    box.Connections.Add(targetBox);
                                }
                            }
                        }
                    }
                }

                // Arrange controls
                foreach (var e in nodes)
                {
                    var node     = e.Value;
                    var nodeData = nodesData[e.Key];
                    var pos      = new Vector2(nodeData.X, nodeData.Y) - upperLeft;
                    node.Location = ViewPosition + pos + _mousePos / ViewScale;
                }

                // Post load
                foreach (var node in nodes)
                {
                    node.Value.OnSurfaceLoaded();
                }

                // Add undo action
                if (Undo != null && nodes.Count > 0)
                {
                    var actions = new List <IUndoAction>();
                    foreach (var node in nodes)
                    {
                        var action = new AddRemoveNodeAction(node.Value, true);
                        actions.Add(action);
                    }
                    foreach (var node in nodes)
                    {
                        var action = new EditNodeConnections(Context, node.Value);
                        action.End();
                        actions.Add(action);
                    }
                    Undo.AddAction(new MultiUndoAction(actions, nodes.Count == 1 ? "Paste node" : "Paste nodes"));
                }

                // Select those nodes
                Select(nodes.Values);

                MarkAsEdited();
            }
            catch (Exception ex)
            {
                Editor.LogWarning("Failed to paste Visject Surface nodes");
                Editor.LogWarning(ex);
                MessageBox.Show("Failed to paste Visject Surface nodes. " + ex.Message, "Paste failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }
        /// <summary>
        /// Shows the secondary context menu.
        /// </summary>
        /// <param name="location">The location in the Surface Space.</param>
        /// <param name="controlUnderMouse">The Surface Control that is under the cursor. Used to customize the menu.</param>
        public virtual void ShowSecondaryCM(Vector2 location, SurfaceControl controlUnderMouse)
        {
            var selection = SelectedNodes;

            if (selection.Count == 0)
            {
                return;
            }

            // Create secondary context menu
            var menu = new FlaxEditor.GUI.ContextMenu.ContextMenu();

            menu.AddButton("Save", _onSave);
            menu.AddSeparator();
            _cmCopyButton = menu.AddButton("Copy", Copy);
            menu.AddButton("Paste", Paste).Enabled = CanPaste();
            _cmDuplicateButton = menu.AddButton("Duplicate", Duplicate);
            var canRemove = selection.All(node => (node.Archetype.Flags & NodeFlags.NoRemove) == 0);

            menu.AddButton("Cut", Cut).Enabled       = canRemove;
            menu.AddButton("Delete", Delete).Enabled = canRemove;
            menu.AddSeparator();
            _cmRemoveNodeConnectionsButton = menu.AddButton("Remove all connections to that node(s)", () =>
            {
                var nodes = ((List <SurfaceNode>)menu.Tag);

                if (Undo != null)
                {
                    var actions = new List <IUndoAction>(nodes.Count);
                    foreach (var node in nodes)
                    {
                        var action = new EditNodeConnections(Context, node);
                        node.RemoveConnections();
                        action.End();
                        actions.Add(action);
                    }
                    Undo.AddAction(new MultiUndoAction(actions, actions[0].ActionString));
                }
                else
                {
                    foreach (var node in nodes)
                    {
                        node.RemoveConnections();
                    }
                }

                MarkAsEdited();
            });
            _cmRemoveBoxConnectionsButton = menu.AddButton("Remove all connections to that box", () =>
            {
                var boxUnderMouse = (Box)_cmRemoveBoxConnectionsButton.Tag;

                if (Undo != null)
                {
                    var action = new EditNodeConnections(Context, boxUnderMouse.ParentNode);
                    boxUnderMouse.RemoveConnections();
                    action.End();
                    Undo.AddAction(action);
                }
                else
                {
                    boxUnderMouse.RemoveConnections();
                }

                MarkAsEdited();
            });
            {
                var boxUnderMouse = GetChildAtRecursive(location) as Box;
                _cmRemoveBoxConnectionsButton.Enabled = boxUnderMouse != null && boxUnderMouse.HasAnyConnection;
                _cmRemoveBoxConnectionsButton.Tag     = boxUnderMouse;
            }
            controlUnderMouse?.OnShowSecondaryContextMenu(menu, controlUnderMouse.PointFromParent(location));
            OnShowSecondaryContextMenu(menu, controlUnderMouse);

            // Show secondary context menu
            _cmStartPos = location;
            menu.Tag    = selection;
            menu.Show(this, location);
        }