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