// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using System.Linq; using FlaxEditor.GUI; using FlaxEditor.GUI.Drag; using FlaxEditor.Options; using FlaxEditor.Scripting; using FlaxEditor.Surface.Archetypes; using FlaxEditor.Surface.ContextMenu; using FlaxEditor.Surface.GUI; using FlaxEditor.Surface.Undo; using FlaxEngine; using FlaxEngine.GUI; namespace FlaxEditor.Surface { /// /// Visject Surface control for editing Nodes Graph. /// /// /// [HideInEditor] public partial class VisjectSurface : ContainerControl, IParametersDependantNode { private static readonly List NavUpdateCache = new List(8); /// /// The surface control. /// protected SurfaceRootControl _rootControl; private float _targetScale = 1.0f; private float _moveViewWithMouseDragSpeed = 1.0f; private bool _canEdit = true; private readonly bool _supportsDebugging; private bool _isReleasing; private VisjectCM _activeVisjectCM; private GroupArchetype _customNodesGroup; private List _customNodes; private List _batchedUndoActions; private Action _onSave; private int _selectedConnectionIndex; internal int _isUpdatingBoxTypes; /// /// True if surface supports implicit casting of the FlaxEngine.Object types into Boolean value (as simple validate check). /// protected bool _supportsImplicitCastFromObjectToBoolean = false; /// /// The left mouse down flag. /// protected bool _leftMouseDown; /// /// The right mouse down flag. /// protected bool _rightMouseDown; /// /// The middle mouse down flag. /// protected bool _middleMouseDown; /// /// The left mouse down position. /// protected Float2 _leftMouseDownPos = Float2.Minimum; /// /// The right mouse down position. /// protected Float2 _rightMouseDownPos = Float2.Minimum; /// /// The middle mouse down position. /// protected Float2 _middleMouseDownPos = Float2.Minimum; /// /// The mouse position. /// protected Float2 _mousePos = Float2.Minimum; /// /// The mouse movement amount. /// protected float _mouseMoveAmount; /// /// The is moving selection flag. /// protected bool _isMovingSelection; /// /// The moving selection view position. /// protected Float2 _movingSelectionViewPos; /// /// The connection start. /// protected IConnectionInstigator _connectionInstigator; /// /// The last connection instigator under mouse. /// protected IConnectionInstigator _lastInstigatorUnderMouse; /// /// The primary context menu. /// protected VisjectCM _cmPrimaryMenu; /// /// The context menu start position. /// protected Float2 _cmStartPos = Float2.Minimum; /// /// Occurs when selection gets changed. /// public event Action SelectionChanged; /// /// The surface owner. /// public readonly IVisjectSurfaceOwner Owner; /// /// The style used by the surface. /// public readonly SurfaceStyle Style; /// /// The undo system to use for the history actions recording (optional, can be null). /// public readonly FlaxEditor.Undo Undo; /// /// If false, the surface editing is blocked (UI display in read-only mode). /// public bool CanEdit { get => _canEdit; set { if (_canEdit == value) return; _canEdit = value; for (int i = 0; i < _rootControl.Children.Count; i++) { if (_rootControl.Children[i] is SurfaceControl control) { control.OnSurfaceCanEditChanged(value); } } } } /// /// Gets a value indicating whether surface is edited. /// public bool IsEdited => RootContext.IsModified; /// /// Gets the current context surface root control (nodes and all other surface elements container). /// public SurfaceRootControl SurfaceRoot => _rootControl; /// /// Gets or sets the view position (upper left corner of the view) in the surface space. /// public Float2 ViewPosition { get => _rootControl.Location / -ViewScale; set => _rootControl.Location = value * -ViewScale; } /// /// Gets or sets the view center position (middle point of the view) in the surface space. /// public Float2 ViewCenterPosition { get => (_rootControl.Location - Size * 0.5f) / -ViewScale; set => _rootControl.Location = Size * 0.5f + value * -ViewScale; } /// /// Gets or sets the view scale. /// public float ViewScale { get => _targetScale; set { // Clamp value = Mathf.Clamp(value, 0.05f, 1.6f); // Check if value will change if (Mathf.Abs(value - _targetScale) > 0.0001f) { // Set new target scale _targetScale = value; } // disable view scale animation _rootControl.Scale = new Float2(_targetScale); } } /// /// Gets a value indicating whether user is selecting nodes. /// public bool IsSelecting => _leftMouseDown && !_isMovingSelection && _connectionInstigator == null; /// /// Gets a value indicating whether user is moving selected nodes. /// public bool IsMovingSelection => _leftMouseDown && _isMovingSelection && _connectionInstigator == null; /// /// Gets a value indicating whether user is connecting nodes. /// public bool IsConnecting => _connectionInstigator != null; /// /// Gets a value indicating whether the left mouse button is down. /// public bool IsLeftMouseButtonDown => _leftMouseDown; /// /// Gets a value indicating whether the right mouse button is down. /// public bool IsRightMouseButtonDown => _rightMouseDown; /// /// Gets a value indicating whether this Surface supports debugging. /// public bool SupportsDebugging => _supportsDebugging; /// /// Returns true if any node is selected by the user (one or more). /// public bool HasNodesSelection { get { for (int i = 0; i < Nodes.Count; i++) { if (Nodes[i].IsSelected) return true; } return false; } } /// /// Gets the list of the selected nodes. /// /// /// Don't call it too often. It does memory allocation and iterates over the surface controls to find selected nodes in the graph. /// public List SelectedNodes { get { var selection = new List(); for (int i = 0; i < Nodes.Count; i++) { if (Nodes[i].IsSelected) selection.Add(Nodes[i]); } return selection; } } /// /// Gets the list of the selected controls (comments and nodes). /// /// /// Don't call it too often. It does memory allocation and iterates over the surface controls to find selected nodes in the graph. /// public List SelectedControls { get { var selection = new List(); for (int i = 0; i < _rootControl.Children.Count; i++) { if (_rootControl.Children[i] is SurfaceControl control && control.IsSelected) selection.Add(control); } return selection; } } /// /// Gets the list of the surface comments. /// /// /// Don't call it too often. It does memory allocation and iterates over the surface controls to find comments in the graph. /// public List Comments => _context?.Comments; /// /// The current surface context nodes collection. Read-only. /// public List Nodes => _context?.Nodes; /// /// The surface node descriptors collection. /// public readonly List NodeArchetypes; /// /// Occurs when node gets spawned in the surface (via UI). /// public event Action NodeSpawned; /// /// Occurs when node gets removed from the surface (via UI). /// public event Action NodeDeleted; /// /// Occurs when node values gets modified in the surface (via UI). /// public event Action NodeValuesEdited; /// /// Occurs when node breakpoint state gets modified in the surface (via UI). /// public event Action NodeBreakpointEdited; /// /// Occurs when two nodes gets connected (via UI). /// public event Action NodesConnected; /// /// Occurs when two nodes gets disconnected (via UI). /// public event Action NodesDisconnected; /// /// Initializes a new instance of the class. /// /// The owner. /// The save action called when user wants to save the surface. /// The undo/redo to use for the history actions recording. Optional, can be null to disable undo support. /// The custom surface style. Use null to create the default style. /// The custom surface node types. Pass null to use the default nodes set. /// True if surface supports debugging features (breakpoints, etc.). public VisjectSurface(IVisjectSurfaceOwner owner, Action onSave = null, FlaxEditor.Undo undo = null, SurfaceStyle style = null, List groups = null, bool supportsDebugging = false) { AnchorPreset = AnchorPresets.StretchAll; Offsets = Margin.Zero; AutoFocus = false; // Disable to prevent autofocus and event handling on OnMouseDown event CullChildren = false; _supportsDebugging = supportsDebugging; Owner = owner; Style = style ?? SurfaceStyle.CreateStyleHandler(Editor.Instance); if (Style == null) throw new InvalidOperationException("Missing visject surface style."); NodeArchetypes = groups ?? NodeFactory.DefaultGroups; Undo = undo; _onSave = onSave; // Initialize with the root context OpenContext(owner); RootContext.Modified += OnRootContextModified; // Setup input actions InputActions = new InputActionsContainer(new[] { new InputActionsContainer.Binding(options => options.Delete, Delete), new InputActionsContainer.Binding(options => options.SelectAll, SelectAll), new InputActionsContainer.Binding(options => options.DeselectAll, DeselectAll), new InputActionsContainer.Binding(options => options.Copy, Copy), new InputActionsContainer.Binding(options => options.Paste, Paste), new InputActionsContainer.Binding(options => options.Cut, Cut), new InputActionsContainer.Binding(options => options.Duplicate, Duplicate), }); Context.ControlSpawned += OnSurfaceControlSpawned; Context.ControlDeleted += OnSurfaceControlDeleted; SelectionChanged += () => { _selectedConnectionIndex = 0; }; // Init drag handlers DragHandlers.Add(_dragAssets = new DragAssets(ValidateDragItem)); DragHandlers.Add(_dragParameters = new DragNames(SurfaceParameter.DragPrefix, ValidateDragParameter)); ScriptsBuilder.ScriptsReloadBegin += OnScriptsReloadBegin; } private void OnScriptsReloadBegin() { _activeVisjectCM = null; _cmPrimaryMenu?.Dispose(); _cmPrimaryMenu = null; } /// /// Gets the display name of the connection type used in the surface. /// /// The graph connection type. /// The display name (for UI). public virtual string GetTypeName(ScriptType type) { return type == ScriptType.Null ? null : type.Name; } /// /// Gets the debugger tooltip for the given box. /// /// The box. /// The result tooltip text. /// True if processed output text should be used to display it on a box tooltip for debugger UI, otherwise false. public virtual bool GetBoxDebuggerTooltip(Elements.Box box, out string text) { text = null; return false; } private void OnRootContextModified(VisjectSurfaceContext context, bool graphEdited) { Owner.OnSurfaceEditedChanged(); if (graphEdited) { Owner.OnSurfaceGraphEdited(); } } /// /// Gets the custom nodes group archetype with custom nodes archetypes. May be null if no custom nodes in use. /// /// The custom nodes or null if no used. public GroupArchetype GetCustomNodes() { return _customNodesGroup; } /// /// Adds the custom nodes archetypes to the surface (user can spawn them and surface can deserialize). /// /// Custom nodes has to have a node logic typename in DefaultValues[0] and group name in DefaultValues[1]. /// The archetypes. public void AddCustomNodes(IEnumerable archetypes) { if (_customNodes == null) { // First time setup _customNodes = new List(archetypes); _customNodesGroup = new GroupArchetype { GroupID = Custom.GroupID, Name = "Custom", Color = Color.Wheat }; } else { // Add more nodes _customNodes.AddRange(archetypes); } // Update collection _customNodesGroup.Archetypes = _customNodes; } /// /// Updates the navigation bar of the toolstrip from window that uses this surface. Updates the navigation bar panel buttons to match the current view path. /// /// The navigation bar to update. /// The toolstrip to use as layout reference. /// True if skip showing nav button if the current context is the root location (user has no option to change context). public void UpdateNavigationBar(NavigationBar navigationBar, ToolStrip toolStrip, bool hideIfRoot = true) { if (navigationBar == null || toolStrip == null) return; bool wasLayoutLocked = navigationBar.IsLayoutLocked; navigationBar.IsLayoutLocked = true; // Remove previous buttons navigationBar.DisposeChildren(); // Spawn buttons var nodes = NavUpdateCache; nodes.Clear(); var context = Context; if (hideIfRoot && context == RootContext) context = null; while (context != null) { nodes.Add(context); context = context.Parent; } float x = NavigationBar.DefaultButtonsMargin; float h = toolStrip.ItemsHeight - 2 * ToolStrip.DefaultMarginV; for (int i = nodes.Count - 1; i >= 0; i--) { var button = new VisjectContextNavigationButton(this, nodes[i].Context, x, ToolStrip.DefaultMarginV, h); button.PerformLayout(); x += button.Width + NavigationBar.DefaultButtonsMargin; navigationBar.AddChild(button); } nodes.Clear(); // Update navigationBar.IsLayoutLocked = wasLayoutLocked; navigationBar.PerformLayout(); } /// /// Gets all nodes bounds or empty if surface is empty. /// public Rectangle AllNodesBounds { get { var area = Rectangle.Empty; if (Nodes.Count != 0) { area = Nodes[0].Bounds; for (int i = 1; i < Nodes.Count; i++) area = Rectangle.Union(area, Nodes[i].Bounds); } return area; } } /// /// Gets a value indicating whether surface parameters are not read-only and can be modified in a surface graph. /// public virtual bool CanSetParameters => false; /// /// Gets a value indicating whether surface supports/allows live previewing graph modifications due to value sliders and color pickers. True by default but disabled for shader surfaces that generate and compile shader source at flight. /// public virtual bool CanLivePreviewValueChanges => true; /// /// Determines whether the specified node archetype can be used in the surface. /// /// The nodes group archetype identifier. /// The node archetype identifier. /// True if can use this node archetype, otherwise false. public bool CanUseNodeType(ushort groupID, ushort typeID) { var result = false; var nodeArchetypes = NodeArchetypes ?? NodeFactory.DefaultGroups; if (NodeFactory.GetArchetype(nodeArchetypes, groupID, typeID, out var groupArchetype, out var nodeArchetype)) { var flags = nodeArchetype.Flags; nodeArchetype.Flags &= ~NodeFlags.NoSpawnViaGUI; nodeArchetype.Flags &= ~NodeFlags.NoSpawnViaPaste; result = CanUseNodeType(groupArchetype, nodeArchetype); nodeArchetype.Flags = flags; } return result; } /// /// Determines whether the specified node archetype can be used in the surface. /// /// The nodes group archetype. /// The node archetype. /// True if can use this node archetype, otherwise false. public virtual bool CanUseNodeType(GroupArchetype groupArchetype, NodeArchetype nodeArchetype) { return (nodeArchetype.Flags & NodeFlags.NoSpawnViaPaste) == 0; } /// /// Shows the whole graph by changing the view scale and the position. /// public void ShowWholeGraph() { ShowArea(AllNodesBounds.MakeExpanded(100.0f)); } /// /// Shows the given surface area by changing the view scale and the position. /// /// The area rectangle. public void ShowArea(Rectangle areaRect) { ViewScale = (Size / areaRect.Size).MinValue * 0.95f; ViewCenterPosition = areaRect.Center; } /// /// Shows the given surface node by changing the view scale and the position and focuses the node. /// /// The node to show. public void FocusNode(SurfaceNode node) { if (node != null) { ShowArea(node.Bounds.MakeExpanded(500.0f)); Select(node); } } /// /// Mark surface as edited. /// /// True if graph has been edited (nodes structure or parameter value). public void MarkAsEdited(bool graphEdited = true) { _context.MarkAsModified(graphEdited); } private void BulkSelectUpdate(bool select = true) { bool selectionChanged = false; for (int i = 0; i < _rootControl.Children.Count; i++) { if (_rootControl.Children[i] is SurfaceControl control && control.IsSelected != select) { control.IsSelected = select; selectionChanged = true; } } if (selectionChanged) SelectionChanged?.Invoke(); } /// /// Selects all the nodes. /// public void SelectAll() { BulkSelectUpdate(true); } /// /// Deelects all the nodes. /// public void DeselectAll() { BulkSelectUpdate(false); } /// /// Clears the selection. /// public void ClearSelection() { bool selectionChanged = false; for (int i = 0; i < _rootControl.Children.Count; i++) { if (_rootControl.Children[i] is SurfaceControl control && control.IsSelected) { control.IsSelected = false; selectionChanged = true; } } if (selectionChanged) SelectionChanged?.Invoke(); } /// /// Adds the specified control to the selection. /// /// The control. public void AddToSelection(SurfaceControl control) { if (control.IsSelected) return; control.IsSelected = true; SelectionChanged?.Invoke(); } /// /// Selects the specified control. /// /// The control. public void Select(SurfaceControl control) { bool selectionChanged = false; for (int i = 0; i < _rootControl.Children.Count; i++) { if (_rootControl.Children[i] is SurfaceControl c && c != control && c.IsSelected) { c.IsSelected = false; selectionChanged = true; } } if (!control.IsSelected) { control.IsSelected = true; selectionChanged = true; } if (selectionChanged) SelectionChanged?.Invoke(); } /// /// Selects the specified controls collection. /// /// The controls. public void Select(IEnumerable controls) { var newSelection = controls.ToList(); var prevSelection = SelectedControls; if (Utils.ArraysEqual(newSelection, prevSelection)) return; ClearSelection(); foreach (var control in newSelection) control.IsSelected = true; SelectionChanged?.Invoke(); } /// /// Deselects the specified control. /// /// The control. public void Deselect(SurfaceControl control) { if (!control.IsSelected) return; control.IsSelected = false; SelectionChanged?.Invoke(); } /// /// Creates the comment around the selected nodes. /// public SurfaceComment CommentSelection(string text = "") { var selection = SelectedNodes; if (selection.Count == 0) return null; Rectangle surfaceArea = GetNodesBounds(selection).MakeExpanded(80.0f); // Order below other selected comments bool hasCommentsSelected = false; int lowestCommentOrder = int.MaxValue; for (int i = 0; i < selection.Count; i++) { if (selection[i] is not SurfaceComment || selection[i].IndexInParent >= lowestCommentOrder) continue; hasCommentsSelected = true; lowestCommentOrder = selection[i].IndexInParent; } return _context.CreateComment(ref surfaceArea, string.IsNullOrEmpty(text) ? "Comment" : text, new Color(1.0f, 1.0f, 1.0f, 0.2f), hasCommentsSelected ? lowestCommentOrder : -1); } private static Rectangle GetNodesBounds(List nodes) { if (nodes.Count == 0) return Rectangle.Empty; Rectangle surfaceArea = nodes[0].Bounds; for (int i = 1; i < nodes.Count; i++) { surfaceArea = Rectangle.Union(surfaceArea, nodes[i].Bounds); } return surfaceArea; } /// /// Deletes the specified collection of the controls. /// /// The controls. /// True if use undo/redo action for node removing. public void Delete(IEnumerable controls, bool withUndo = true) { if (!CanEdit || controls == null || !controls.Any()) return; var selectionChanged = false; List nodes = null; foreach (var control in controls) { if (control.IsSelected) { selectionChanged = true; control.IsSelected = false; } if (control is SurfaceNode node) { if ((node.Archetype.Flags & NodeFlags.NoRemove) == 0) { if (nodes == null) nodes = new List(); var sealedNodes = node.SealedNodes; if (sealedNodes != null) { foreach (var sealedNode in sealedNodes) { if (sealedNode != null) { if (sealedNode.IsSelected) { selectionChanged = true; sealedNode.IsSelected = false; } if (!nodes.Contains(sealedNode)) nodes.Add(sealedNode); } } } if (!nodes.Contains(node)) nodes.Add(node); } } else { Context.OnControlDeleted(control, SurfaceNodeActions.User); } } if (selectionChanged) SelectionChanged?.Invoke(); if (nodes != null) { if (Undo == null || !withUndo) { // Remove all nodes foreach (var node in nodes) { node.RemoveConnections(); Nodes.Remove(node); Context.OnControlDeleted(node, SurfaceNodeActions.User); } } else { var actions = new List(); // Break connections for all nodes foreach (var node in nodes) { var action = new EditNodeConnections(Context, node); node.RemoveConnections(); action.End(); actions.Add(action); } // Remove all nodes foreach (var node in nodes) { var action = new AddRemoveNodeAction(node, false); action.Do(); actions.Add(action); } AddBatchedUndoAction(new MultiUndoAction(actions, nodes.Count == 1 ? "Remove node" : "Remove nodes")); } } MarkAsEdited(); } /// /// Deletes the specified control. /// /// The control. /// True if use undo/redo action for node removing. public void Delete(SurfaceControl control, bool withUndo = true) { if (!CanEdit) return; Delete(new[] { control }, withUndo); } /// /// Deletes the selected controls. /// public void Delete() { if (!CanEdit) return; Delete(SelectedControls, true); } /// /// Finds the node of the given type. /// /// The group identifier. /// The type identifier. /// Found node or null if cannot. public SurfaceNode FindNode(ushort groupId, ushort typeId) { return _context.FindNode(groupId, typeId); } /// /// Finds the node with the given ID. /// /// The identifier. /// Found node or null if cannot. public SurfaceNode FindNode(int id) { return _context.FindNode(id); } /// /// Finds the node with the given ID. /// /// The identifier. /// Found node or null if cannot. public SurfaceNode FindNode(uint id) { return _context.FindNode(id); } /// /// Adds the undo action to be batched (eg. if multiple undo actions is performed in a sequence during single update). /// /// The action. public void AddBatchedUndoAction(IUndoAction action) { if (Undo == null || !Undo.Enabled) return; if (_batchedUndoActions == null) _batchedUndoActions = new List(); _batchedUndoActions.Add(action); } /// /// Called when node gets spawned in the surface (via UI). /// public virtual void OnNodeSpawned(SurfaceNode node) { NodeSpawned?.Invoke(node); } /// /// Called when node values gets modified in the surface (via UI). /// public virtual void OnNodeValuesEdited(SurfaceNode node) { NodeValuesEdited?.Invoke(node); } /// /// Called when node breakpoint gets modified. /// public virtual void OnNodeBreakpointEdited(SurfaceNode node) { NodeBreakpointEdited?.Invoke(node); } /// /// Called when node gets removed from the surface (via UI). /// public virtual void OnNodeDeleted(SurfaceNode node) { NodeDeleted?.Invoke(node); } /// /// Called when two nodes gets connected (via UI). /// public virtual void OnNodesConnected(IConnectionInstigator a, IConnectionInstigator b) { NodesConnected?.Invoke(a, b); MarkAsEdited(); } /// /// Called when two nodes gets disconnected (via UI). /// public virtual void OnNodesDisconnected(IConnectionInstigator a, IConnectionInstigator b) { NodesDisconnected?.Invoke(a, b); } /// public override void OnDestroy() { if (IsDisposing) return; _isReleasing = true; // Cleanup context cache _root = null; _context = null; _onSave = null; ContextStack.Clear(); foreach (var context in _contextCache.Values) { context.Clear(); } _contextCache.Clear(); // Cleanup _activeVisjectCM = null; _cmPrimaryMenu?.Dispose(); ScriptsBuilder.ScriptsReloadBegin -= OnScriptsReloadBegin; base.OnDestroy(); } } }