// 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 readonly 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; Size = CalculateNodeSize(width, height); // Update boxes on width change //if (!Mathf.NearEqual(prevSize.X, Size.X)) { for (int i = 0; i < Elements.Count; i++) { if (Elements[i] is OutputBox box) { box.Location = box.Archetype.Position + new Float2(width, 0); } } } } /// /// Automatically resizes the node to match the title size and all the elements for best fit of the node dimensions. /// public 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 { 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(); } /// /// 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() { base.OnSurfaceLoaded(); UpdateBoxesTypes(); for (int i = 0; i < Elements.Count; i++) { if (Elements[i] is Box box) box.OnConnectionsChanged(); } } /// public override void OnDeleted() { RemoveConnections(); base.OnDeleted(); } /// /// 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?.Undo != 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?.Undo != 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) { Render2D.DrawSprite(style.Cross, _closeButtonRect, _closeButtonRect.Contains(_mousePosition) && Surface.CanEdit ? 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; } } }