// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using FlaxEditor.Scripting; using FlaxEditor.Surface.Elements; using FlaxEditor.Surface.Undo; using FlaxEngine; using FlaxEngine.GUI; namespace FlaxEditor.Surface { /// /// The surface breakpoint data for debugger. /// [HideInEditor] public struct SurfaceBreakpoint { /// /// True if breakpoint is active, otherwise it's not set. /// public bool Set; /// /// True if breakpoint is enabled, otherwise false. /// public bool Enabled; /// /// True if breakpoint is currently hit. /// public bool Hit; } /// /// Visject Surface node control. /// /// [HideInEditor] public class SurfaceNode : SurfaceControl { /// /// Flag used to discard node values setting during event sending for node UI flushing. /// protected bool _isDuringValuesEditing; /// /// The header rectangle (local space). /// protected Rectangle _headerRect; /// /// The close button rectangle (local space). /// protected Rectangle _closeButtonRect; /// /// The footer rectangle (local space). /// protected Rectangle _footerRect; /// /// The node archetype. /// public NodeArchetype Archetype; /// /// The group archetype. /// public readonly GroupArchetype GroupArchetype; /// /// The elements collection. /// public readonly List Elements = new List(); /// /// The values (node parameters in layout based on ). /// public object[] Values; /// /// Gets or sets the node title text. /// public string Title { get; set; } /// /// The identifier of the node. /// public readonly uint ID; /// /// Gets the type (packed GroupID (higher 16 bits) and TypeID (lower 16 bits)). /// public uint Type => ((uint)GroupArchetype.GroupID << 16) | Archetype.TypeID; /// /// The metadata. /// public readonly SurfaceMeta Meta = new SurfaceMeta(); /// /// Occurs when node values collection gets changed. /// public event Action ValuesChanged; /// /// The breakpoint on the node. /// public SurfaceBreakpoint Breakpoint = new SurfaceBreakpoint(); /// /// Initializes a new instance of the class. /// /// The node id. /// The surface context. /// The node archetype. /// The group archetype. public SurfaceNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) : base(context, nodeArch.Size.X + Constants.NodeMarginX * 2, nodeArch.Size.Y + Constants.NodeMarginY * 2 + Constants.NodeHeaderSize + Constants.NodeFooterSize) { Title = nodeArch.Title; ID = id; Archetype = nodeArch; GroupArchetype = groupArch; AutoFocus = false; TooltipText = nodeArch.Description; CullChildren = false; BackgroundColor = Style.Current.BackgroundNormal; if (Archetype.DefaultValues != null) { Values = new object[Archetype.DefaultValues.Length]; Array.Copy(Archetype.DefaultValues, Values, Values.Length); } } /// /// Gets the custom text for Content Search. Can be used to include node in search results for a specific text query. /// public virtual string ContentSearchText => null; /// /// Gets the color of the footer of the node. /// protected virtual Color FooterColor => GroupArchetype.Color; /// /// Calculates the size of the node including header, footer, and margins. /// /// The node area width. /// The node area height. /// The node control total size. protected virtual Float2 CalculateNodeSize(float width, float height) { return new Float2(width + Constants.NodeMarginX * 2, height + Constants.NodeMarginY * 2 + Constants.NodeHeaderSize + Constants.NodeFooterSize); } /// /// Resizes the node area. /// /// The width. /// The height. public void Resize(float width, float height) { if (Surface == null) return; for (int i = 0; i < Elements.Count; i++) { if (Elements[i] is OutputBox box) { box.Location = box.Archetype.Position + new Float2(width, 0); } } Size = CalculateNodeSize(width, height); } /// /// Automatically resizes the node to match the title size and all the elements for best fit of the node dimensions. /// public virtual void ResizeAuto() { if (Surface == null) return; var width = 0.0f; var height = 0.0f; var leftHeight = 0.0f; var rightHeight = 0.0f; var leftWidth = 40.0f; var rightWidth = 40.0f; var boxLabelFont = Style.Current.FontSmall; var titleLabelFont = Style.Current.FontLarge; for (int i = 0; i < Children.Count; i++) { var child = Children[i]; if (!child.Visible) continue; if (child is InputBox inputBox) { var boxWidth = boxLabelFont.MeasureText(inputBox.Text).X + 20; if (inputBox.DefaultValueEditor != null) boxWidth += inputBox.DefaultValueEditor.Width + 4; leftWidth = Mathf.Max(leftWidth, boxWidth); leftHeight = Mathf.Max(leftHeight, inputBox.Archetype.Position.Y - Constants.NodeMarginY - Constants.NodeHeaderSize + 20.0f); } else if (child is OutputBox outputBox) { rightWidth = Mathf.Max(rightWidth, boxLabelFont.MeasureText(outputBox.Text).X + 20); rightHeight = Mathf.Max(rightHeight, outputBox.Archetype.Position.Y - Constants.NodeMarginY - Constants.NodeHeaderSize + 20.0f); } else if (child is Control control) { if (control.AnchorPreset == AnchorPresets.TopLeft) { width = Mathf.Max(width, control.Right + 4 - Constants.NodeMarginX); height = Mathf.Max(height, control.Bottom + 4 - Constants.NodeMarginY - Constants.NodeHeaderSize); } else if (!_headerRect.Intersects(control.Bounds)) { width = Mathf.Max(width, control.Width + 4); height = Mathf.Max(height, control.Height + 4); } } } width = Mathf.Max(width, leftWidth + rightWidth + 10); width = Mathf.Max(width, titleLabelFont.MeasureText(Title).X + 30); height = Mathf.Max(height, Mathf.Max(leftHeight, rightHeight)); Resize(width, height); } /// /// Creates an element from the archetype and adds the element to the node. /// /// The element archetype. /// The created element. Null if the archetype is invalid. public ISurfaceNodeElement AddElement(NodeElementArchetype arch) { ISurfaceNodeElement element = null; switch (arch.Type) { case NodeElementType.Input: element = new InputBox(this, arch); break; case NodeElementType.Output: element = new OutputBox(this, arch); break; case NodeElementType.BoolValue: element = new BoolValue(this, arch); break; case NodeElementType.FloatValue: element = new FloatValue(this, arch); break; case NodeElementType.IntegerValue: element = new IntegerValue(this, arch); break; case NodeElementType.ColorValue: element = new ColorValue(this, arch); break; case NodeElementType.ComboBox: element = new ComboBoxElement(this, arch); break; case NodeElementType.Asset: element = new AssetSelect(this, arch); break; case NodeElementType.Text: element = new TextView(this, arch); break; case NodeElementType.TextBox: element = new TextBoxView(this, arch); break; case NodeElementType.SkeletonBoneIndexSelect: element = new SkeletonBoneIndexSelectElement(this, arch); break; case NodeElementType.BoxValue: element = new BoxValue(this, arch); break; case NodeElementType.EnumValue: element = new EnumValue(this, arch); break; case NodeElementType.SkeletonNodeNameSelect: element = new SkeletonNodeNameSelectElement(this, arch); break; case NodeElementType.Actor: element = new ActorSelect(this, arch); break; case NodeElementType.UnsignedIntegerValue: element = new UnsignedIntegerValue(this, arch); break; //default: throw new NotImplementedException("Unknown node element type: " + arch.Type); } if (element != null) { AddElement(element); } return element; } /// /// Adds the element to the node. /// /// The element. public void AddElement(ISurfaceNodeElement element) { Elements.Add(element); if (element is Control control) AddChild(control); } /// /// Adds the box to the node. /// /// True if add output box, otherwise it will be an input. /// The box Id. /// The y-level in the UI. /// The box text. /// The box type. /// If true the box will be able to have just a single connection, otherwise false. /// Index of the default value in the node values array. /// The box. public Box AddBox(bool isOut, int id, int yLevel, string text, ScriptType type, bool single, int valueIndex = -1) { if (type == ScriptType.Null) type = ScriptType.Object; // Try to reuse box var box = GetBox(id); if ((isOut && box is InputBox) || (!isOut && box is OutputBox)) { box.Dispose(); box = null; } // Create new if missing if (box == null) { if (isOut) box = new OutputBox(this, NodeElementArchetype.Factory.Output(yLevel, text, type, id, single)); else box = new InputBox(this, NodeElementArchetype.Factory.Input(yLevel, text, single, type, id, valueIndex)); AddElement(box); } else { // Sync properties for exiting box box.Text = text; box.CurrentType = type; box.Y = Constants.NodeMarginY + Constants.NodeHeaderSize + yLevel * Constants.LayoutOffsetY; } // Update box box.OnConnectionsChanged(); return box; } /// /// Removes the element from the node. /// /// The element. /// if set to true dispose control after removing, otherwise false. public void RemoveElement(ISurfaceNodeElement element, bool dispose = true) { if (element == null) return; if (element is Box box) box.RemoveConnections(); Elements.Remove(element); if (element is Control control) { RemoveChild(control); if (dispose) control.Dispose(); } } /// /// Removes all connections from and to that node. /// public virtual void RemoveConnections() { for (int i = 0; i < Elements.Count; i++) { if (Elements[i] is Box box) box.RemoveConnections(); } UpdateBoxesTypes(); } /// /// Array of nodes that are sealed to this node - sealed nodes are duplicated/copied/pasted/removed in a batch. Null if unused. /// public virtual SurfaceNode[] SealedNodes => null; /// /// Called after adding the control to the surface after paste. /// /// The nodes IDs mapping (original node ID to pasted node ID). Can be sued to update internal node's data after paste operation from the original data. public virtual void OnPasted(System.Collections.Generic.Dictionary idsMapping) { } /// /// Gets a value indicating whether this node uses dependent boxes. /// public bool HasDependentBoxes => Archetype.DependentBoxes != null; /// /// Gets a value indicating whether this node uses independent boxes. /// public bool HasIndependentBoxes => Archetype.IndependentBoxes != null; /// /// Gets a value indicating whether this node has dependent boxes with assigned valid types. Otherwise any box has no dependent type assigned. /// public bool HasDependentBoxesSetup { get { if (Archetype.DependentBoxes == null || Archetype.IndependentBoxes == null) return true; for (int i = 0; i < Archetype.DependentBoxes.Length; i++) { var b = GetBox(Archetype.DependentBoxes[i]); if (b != null && b.CurrentType == b.DefaultType) return false; } return true; } } private static readonly List UpdateStack = new List(); /// /// Updates dependant/independent boxes types. /// public void UpdateBoxesTypes() { // Check there is no need to use box types dependency feature if (Archetype.DependentBoxes == null && Archetype.IndependentBoxes == null) { // Check if need to update update current type of the typeless boxes that use connection hints if ((Archetype.ConnectionsHints & ConnectionsHint.All) != 0) { for (int j = 0; j < Elements.Count; j++) { if (Elements[j] is Box box && box.DefaultType == ScriptType.Null) { box.CurrentType = box.HasAnyConnection ? box.Connections[0].CurrentType : ScriptType.Null; } } } return; } // Prevent recursive loop call that might happen if (UpdateStack.Contains(this)) return; UpdateStack.Add(this); var independentBoxesLength = Archetype.IndependentBoxes?.Length; var dependentBoxesLength = Archetype.DependentBoxes?.Length; // Get type to assign to all dependent boxes var type = Archetype.DefaultType; for (int i = 0; i < independentBoxesLength; i++) { var b = GetBox(Archetype.IndependentBoxes[i]); if (b != null && b.HasAnyConnection) { // Check if the current type is set based on connection hints (eg. null box got vector connection) if (type == ScriptType.Null && b.Connections[0].CurrentType != ScriptType.Null) { type = b.Connections[0].CurrentType; break; } // Check if that type if part of default type if (Surface != null) { if (Surface.CanUseDirectCast(type, b.Connections[0].DefaultType)) { type = b.Connections[0].CurrentType; break; } } else { if (VisjectSurface.CanUseDirectCastStatic(type, b.Connections[0].DefaultType)) { type = b.Connections[0].CurrentType; break; } } } } // Assign connection type for (int i = 0; i < dependentBoxesLength; i++) { var b = GetBox(Archetype.DependentBoxes[i]); if (b != null) { b.CurrentType = Archetype.DependentBoxFilter != null ? Archetype.DependentBoxFilter(b, type) : type; } } // Validate minor independent boxes to fit main one for (int i = 0; i < independentBoxesLength; i++) { var b = GetBox(Archetype.IndependentBoxes[i]); if (b != null) { b.CurrentType = type; } } UpdateStack.Remove(this); } /// /// Tries to get box with given ID. /// /// The box id. /// Box or null if cannot find. public Box GetBox(int id) { // TODO: maybe create local cache for boxes? but not a dictionary, use lookup table because ids are usually small (less than 20) for (int i = 0; i < Elements.Count; i++) { if (Elements[i] is Box box && box.ID == id) return box; } return null; } /// /// Tries to get box with given ID. /// /// The box id. /// Box or null if cannot find. /// True fi box has been found, otherwise false. public bool TryGetBox(int id, out Box result) { // TODO: maybe create local cache for boxes? but not a dictionary, use lookup table because ids are usually small (less than 20) for (int i = 0; i < Elements.Count; i++) { if (Elements[i] is Box box && box.ID == id) { result = box; return true; } } result = null; return false; } internal List GetBoxes() { var result = new List(); for (int i = 0; i < Elements.Count; i++) { if (Elements[i] is Box box) result.Add(box); } return result; } internal void GetBoxes(List result) { result.Clear(); for (int i = 0; i < Elements.Count; i++) { if (Elements[i] is Box box) result.Add(box); } } internal Box GetNextBox(Box box) { // Get the one after it for (int i = box.IndexInParent + 1; i < Elements.Count; i++) { if (Elements[i] is Box b) { return b; } } return null; } internal Box GetPreviousBox(Box box) { // Get the one before it for (int i = box.IndexInParent - 1; i >= 0; i--) { if (Elements[i] is Box b) { return b; } } return null; } /// /// Returns true if any box is selected by the user (one or more). /// public bool HasBoxesSelection { get { if (!IsSelected) return false; for (int i = 0; i < Elements.Count; i++) { if (Elements[i] is Box box && box.IsSelected) return true; } return false; } } /// /// Selects all the boxes. /// public void SelectAllBoxes() { for (int i = 0; i < Elements.Count; i++) { if (Elements[i] is Box box) box.IsSelected = true; } } /// /// Clears the box selection. /// public void ClearBoxSelection() { for (int i = 0; i < Elements.Count; i++) { if (Elements[i] is Box box) box.IsSelected = false; } } /// /// Adds the specified box to the selection. /// /// The box. public void AddBoxToSelection(Box box) { box.IsSelected = true; } /// /// Selects the specified control. /// /// The box. public void SelectBox(Box box) { ClearBoxSelection(); box.IsSelected = true; } /// /// Selects the specified controls collection. /// /// The boxes. public void SelectBoxes(IEnumerable boxes) { ClearBoxSelection(); foreach (var box in boxes) { box.IsSelected = true; } } /// /// Deselects the specified control. /// /// The box. public void DeselectBox(Box box) { box.IsSelected = false; } /// /// Called when node gets selected or deselected. /// protected override void OnSelectionChanged() { if (!IsSelected) { ClearBoxSelection(); } } /// /// Implementation of Depth-First traversal over the graph of surface nodes. /// /// True if perform traversal over node inputs. Otherwise will use output boxes. /// True if throw exception on recursive cycle detection. /// The list of nodes as a result of depth-first traversal algorithm execution. public IEnumerable DepthFirstTraversal(bool traverseInputs, bool throwOnCycle = false) { // Reference: https://github.com/stefnotch/flax-custom-visject-plugin/blob/a26a98b40f909a0b10c2259b858e058290003dce/Source/Editor/ExpressionGraphSurface.cs#L231 // The states of a node are // null Nothing (unvisited and not on the stack) // false Processing( visited and on the stack) // true Completed ( visited and not on the stack) var nodeState = new Dictionary(); var toProcess = new Stack(); var output = new List(); // Start processing the nodes (backwards) toProcess.Push(this); while (toProcess.Count > 0) { var node = toProcess.Peek(); // We have never seen this node before if (!nodeState.ContainsKey(node)) { // We are now processing it nodeState.Add(node, false); } else { // Otherwise, we are done processing it nodeState[node] = true; // Remove it from the stack toProcess.Pop(); // And add it to the output output.Add(node); } // For all parents, push them onto the stack if they haven't been visited yet var elements = node.Elements; for (int i = 0; i < elements.Count; i++) { var box = node.Elements[i] as Box; if ((traverseInputs && box is InputBox) || (!traverseInputs && box is OutputBox)) { foreach (var connection in box.Connections) { // It has been visited previously if (nodeState.TryGetValue(connection.ParentNode, out bool state)) { if (state == false && throwOnCycle) { // It's still processing, so there must be a cycle! throw new Exception("Cycle detected!"); } } else { // It hasn't been visited, add it to the stack toProcess.Push(connection.ParentNode); } } } } } return output; } /// /// Draws all the connections between surface objects related to this node. /// /// The current mouse position (in surface-space). public virtual void DrawConnections(ref Float2 mousePosition) { for (int j = 0; j < Elements.Count; j++) { if (Elements[j] is OutputBox ob && ob.HasAnyConnection) { ob.DrawConnections(ref mousePosition); } } } /// /// Draws all selected connections between surface objects related to this node. /// /// The index of the currently selected connection. public void DrawSelectedConnections(int selectedConnectionIndex) { if (_isSelected) { if (HasBoxesSelection) { for (int j = 0; j < Elements.Count; j++) { if (Elements[j] is Box box && box.IsSelected && selectedConnectionIndex < box.Connections.Count) { if (box is OutputBox ob) { ob.DrawSelectedConnection(ob.Connections[selectedConnectionIndex]); } else { if (box.Connections[selectedConnectionIndex] is OutputBox outputBox) { outputBox.DrawSelectedConnection(box); } } } } } else { for (int j = 0; j < Elements.Count; j++) { if (Elements[j] is Box box) { if (box is OutputBox ob) { for (int i = 0; i < ob.Connections.Count; i++) { ob.DrawSelectedConnection(ob.Connections[i]); } } else { for (int i = 0; i < box.Connections.Count; i++) { if (box.Connections[i] is OutputBox outputBox) { outputBox.DrawSelectedConnection(box); } } } } } } } } /// protected override bool ShowTooltip => base.ShowTooltip && _headerRect.Contains(ref _mousePosition) && !Surface.IsLeftMouseButtonDown && !Surface.IsRightMouseButtonDown && !Surface.IsPrimaryMenuOpened; /// public override bool OnShowTooltip(out string text, out Float2 location, out Rectangle area) { var result = base.OnShowTooltip(out text, out _, out area); // Change the position location = new Float2(_headerRect.Width * 0.5f, _headerRect.Bottom); return result; } /// public override void OnSurfaceCanEditChanged(bool canEdit) { base.OnSurfaceCanEditChanged(canEdit); for (int i = 0; i < Elements.Count; i++) { if (Elements[i] is SurfaceNodeElementControl element) element.OnSurfaceCanEditChanged(canEdit); else if (Elements[i] is Control control) control.Enabled = canEdit; } } /// public override bool OnTestTooltipOverControl(ref Float2 location) { return _headerRect.Contains(ref location) && ShowTooltip; } /// public override bool CanSelect(ref Float2 location) { return _headerRect.MakeOffsetted(Location).Contains(ref location); } /// public override void OnSurfaceLoaded(SurfaceNodeActions action) { base.OnSurfaceLoaded(action); UpdateBoxesTypes(); for (int i = 0; i < Elements.Count; i++) { if (Elements[i] is Box box) box.OnConnectionsChanged(); } } /// public override void OnDeleted(SurfaceNodeActions action) { RemoveConnections(); base.OnDeleted(action); } /// /// Sets the value of the node parameter. /// /// The value index. /// The value. /// True if graph has been edited (nodes structure or parameter value). public virtual void SetValue(int index, object value, bool graphEdited = true) { if (_isDuringValuesEditing || (Surface != null && !Surface.CanEdit)) return; if (FlaxEngine.Json.JsonSerializer.ValueEquals(value, Values[index])) return; if (value is byte[] && Values[index] is byte[] && Utils.ArraysEqual((byte[])value, (byte[])Values[index])) return; _isDuringValuesEditing = true; var before = Surface?.Undo != null ? (object[])Values.Clone() : null; Values[index] = value; OnValuesChanged(); Surface?.MarkAsEdited(graphEdited); if (Surface != null) Surface.AddBatchedUndoAction(new EditNodeValuesAction(this, before, graphEdited)); _isDuringValuesEditing = false; } /// /// Sets the values of the node parameters. /// /// The values. /// True if graph has been edited (nodes structure or parameter value). public virtual void SetValues(object[] values, bool graphEdited = true) { if (_isDuringValuesEditing || !Surface.CanEdit) return; if (values == null || Values == null || values.Length != Values.Length) throw new ArgumentException(); _isDuringValuesEditing = true; var before = Surface.Undo != null ? (object[])Values.Clone() : null; Array.Copy(values, Values, values.Length); OnValuesChanged(); Surface.MarkAsEdited(graphEdited); if (Surface != null) Surface.AddBatchedUndoAction(new EditNodeValuesAction(this, before, graphEdited)); _isDuringValuesEditing = false; } internal void SetIsDuringValuesEditing(bool value) { _isDuringValuesEditing = value; } /// /// Called when node values set gets changed. /// public virtual void OnValuesChanged() { ValuesChanged?.Invoke(); Surface?.OnNodeValuesEdited(this); } /// /// Updates the given box connection. /// /// The box. public virtual void ConnectionTick(Box box) { UpdateBoxesTypes(); } /// protected override void UpdateRectangles() { const float footerSize = Constants.NodeFooterSize; const float headerSize = Constants.NodeHeaderSize; const float closeButtonMargin = Constants.NodeCloseButtonMargin; const float closeButtonSize = Constants.NodeCloseButtonSize; _headerRect = new Rectangle(0, 0, Width, headerSize); _closeButtonRect = new Rectangle(Width - closeButtonSize - closeButtonMargin, closeButtonMargin, closeButtonSize, closeButtonSize); _footerRect = new Rectangle(0, Height - footerSize, Width, footerSize); } /// public override void Draw() { var style = Style.Current; // Background var backgroundRect = new Rectangle(Float2.Zero, Size); Render2D.FillRectangle(backgroundRect, BackgroundColor); // Breakpoint hit if (Breakpoint.Hit) { var colorTop = Color.OrangeRed; var colorBottom = Color.Red; var time = DateTime.Now - Engine.StartupTime; Render2D.DrawRectangle(backgroundRect.MakeExpanded(Mathf.Lerp(3.0f, 12.0f, Mathf.Sin((float)time.TotalSeconds * 10.0f) * 0.5f + 0.5f)), colorTop, colorTop, colorBottom, colorBottom, 2.0f); } // Header var headerColor = style.BackgroundHighlighted; if (_headerRect.Contains(ref _mousePosition)) headerColor *= 1.07f; Render2D.FillRectangle(_headerRect, headerColor); Render2D.DrawText(style.FontLarge, Title, _headerRect, style.Foreground, TextAlignment.Center, TextAlignment.Center); // Close button if ((Archetype.Flags & NodeFlags.NoCloseButton) == 0 && Surface.CanEdit) { Render2D.DrawSprite(style.Cross, _closeButtonRect, _closeButtonRect.Contains(_mousePosition) ? style.Foreground : style.ForegroundGrey); } // Footer Render2D.FillRectangle(_footerRect, FooterColor); DrawChildren(); // Selection outline if (_isSelected) { var colorTop = Color.Orange; var colorBottom = Color.OrangeRed; Render2D.DrawRectangle(backgroundRect, colorTop, colorTop, colorBottom, colorBottom); } // Breakpoint dot if (Breakpoint.Set) { var icon = Breakpoint.Enabled ? Surface.Style.Icons.BoxClose : Surface.Style.Icons.BoxOpen; Render2D.DrawSprite(icon, new Rectangle(-7, -7, 16, 16), new Color(0.9f, 0.9f, 0.9f)); Render2D.DrawSprite(icon, new Rectangle(-6, -6, 14, 14), new Color(0.894117647f, 0.0784313725f, 0.0f)); } } /// public override bool OnMouseUp(Float2 location, MouseButton button) { if (base.OnMouseUp(location, button)) return true; // Close if (button == MouseButton.Left && (Archetype.Flags & NodeFlags.NoCloseButton) == 0) { if (_closeButtonRect.Contains(ref location)) { Surface.Delete(this); return true; } } // Secondary Context Menu if (button == MouseButton.Right) { if (!IsSelected) Surface.Select(this); var tmp = PointToParent(ref location); Surface.ShowSecondaryCM(Parent.PointToParent(ref tmp), this); return true; } return false; } } }