// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using FlaxEditor.Scripting;
using FlaxEditor.Surface.Undo;
using FlaxEngine;
using FlaxEngine.Assertions;
namespace FlaxEditor.Surface.Elements
{
///
/// Surface boxes base class (for input and output boxes). Boxes can be connected.
///
///
///
[HideInEditor]
public abstract class Box : SurfaceNodeElementControl, IConnectionInstigator
{
private bool _isMouseDown, _isSingle;
private DateTime _lastHighlightConnectionsTime = DateTime.MinValue;
private string _originalTooltipText;
///
/// The current connection type. It's subset or equal to .
///
protected ScriptType _currentType;
///
/// The collection of the attributes used by the box. Assigned externally. Can be used to control the default value editing for the or to provide more metadata for the surface UI.
///
protected object[] _attributes;
///
/// The cached color for the current box type.
///
protected Color _currentTypeColor;
///
/// The is selected flag for the box.
///
protected bool _isSelected;
///
/// Unique box ID within single node.
///
public int ID => Archetype.BoxID;
///
/// Allowed connections type.
///
public ScriptType DefaultType => Archetype.ConnectionsType;
///
/// List with all connections to other boxes.
///
public readonly List Connections = new List();
///
/// The box text.
///
public string Text;
///
/// Gets a value indicating whether this box has any connection.
///
public bool HasAnyConnection => Connections.Count > 0;
///
/// Gets a value indicating whether this box has single connection.
///
public bool HasSingleConnection => Connections.Count == 1;
///
/// Gets a value indicating whether this instance is output box.
///
public abstract bool IsOutput { get; }
///
/// Gets or sets the current type of the box connections.
///
public ScriptType CurrentType
{
get => _currentType;
set
{
if (_currentType != value)
{
// Set new value
var prev = _currentType;
_currentType = value;
// Check if will need to update box connections due to type change
if ((Surface == null || Surface._isUpdatingBoxTypes == 0) && HasAnyConnection && !prev.CanCastTo(_currentType))
{
// Remove all invalid connections and update those which still can be valid
var connections = Connections.ToArray();
for (int i = 0; i < connections.Length; i++)
{
var targetBox = connections[i];
// Break connection
Connections.Remove(targetBox);
targetBox.Connections.Remove(this);
// Check if can connect them
if (CanConnectWith(targetBox))
{
// Connect again
Connections.Add(targetBox);
targetBox.Connections.Add(this);
}
else
{
Surface?.OnNodesDisconnected(this, targetBox);
}
targetBox.OnConnectionsChanged();
}
OnConnectionsChanged();
// Update
for (int i = 0; i < connections.Length; i++)
{
connections[i].ConnectionTick();
}
ConnectionTick();
}
// Fire event
OnCurrentTypeChanged();
}
}
}
///
/// The collection of the attributes used by the box. Assigned externally. Can be used to control the default value editing for the or to provide more metadata for the surface UI.
///
public object[] Attributes
{
get => _attributes;
set
{
if (ReferenceEquals(_attributes, value))
return;
if (_attributes == null && value != null && value.Length == 0)
return;
if (_attributes != null && value != null && Utils.ArraysEqual(_attributes, value))
return;
_attributes = value;
OnAttributesChanged();
}
}
///
/// Event called when the current type of the box gets changed.
///
public Action CurrentTypeChanged;
///
/// The box connections highlight strength (normalized to range 0-1).
///
public float ConnectionsHighlightIntensity => Mathf.Saturate(1.0f - (float)(DateTime.Now - _lastHighlightConnectionsTime).TotalSeconds);
///
/// Gets a value indicating whether this box is selected.
///
public bool IsSelected
{
get => _isSelected;
internal set
{
_isSelected = value;
OnSelectionChanged();
}
}
///
protected Box(SurfaceNode parentNode, NodeElementArchetype archetype, Float2 location)
: base(parentNode, archetype, location, new Float2(Constants.BoxSize), false)
{
_currentType = DefaultType;
_isSingle = Archetype.Single;
Text = Archetype.Text;
if (Surface != null)
{
var hints = parentNode.Archetype.ConnectionsHints;
Surface.Style.GetConnectionColor(_currentType, hints, out _currentTypeColor);
TooltipText = Surface.GetTypeName(CurrentType) ?? GetConnectionHintTypeName(hints);
}
}
private static string GetConnectionHintTypeName(ConnectionsHint hint)
{
if ((hint & ConnectionsHint.Anything) == ConnectionsHint.Anything)
return "Anything";
if ((hint & ConnectionsHint.Value) == ConnectionsHint.Value)
return "Value";
if ((hint & ConnectionsHint.Enum) == ConnectionsHint.Enum)
return "Enum";
if ((hint & ConnectionsHint.Numeric) == ConnectionsHint.Numeric)
return "Numeric";
if ((hint & ConnectionsHint.Vector) == ConnectionsHint.Vector)
return "Vector";
if ((hint & ConnectionsHint.Scalar) == ConnectionsHint.Scalar)
return "Scalar";
if ((hint & ConnectionsHint.Array) == ConnectionsHint.Array)
return "Array";
if ((hint & ConnectionsHint.Dictionary) == ConnectionsHint.Dictionary)
return "Dictionary";
return null;
}
///
/// Determines whether this box can use the specified type as a connection.
///
/// The type.
/// true if this box can use the specified type; otherwise, false.
public bool CanUseType(ScriptType type)
{
// Check direct connection
if (Surface != null)
{
if (Surface.CanUseDirectCast(type, _currentType))
return true;
}
else
{
if (VisjectSurface.CanUseDirectCastStatic(type, _currentType))
return true;
}
// Check using connection hints
if (VisjectSurface.IsTypeCompatible(Archetype.ConnectionsType, type, ParentNode.Archetype.ConnectionsHints))
return true;
// Check independent and if there is box with bigger potential because it may block current one from changing type
var parentArch = ParentNode.Archetype;
var boxes = parentArch.IndependentBoxes;
if (boxes != null)
{
for (int i = 0; i < boxes.Length; i++)
{
if (boxes[i] == -1)
break;
var b = ParentNode.GetBox(boxes[i]);
// Check if its the same and tested type matches the default value type
if (b == this && parentArch.DefaultType.CanCastTo(type))
{
// Can
return true;
}
// Check if box exists and has any connection
if (b != null && b.HasAnyConnection)
{
// Cannot
return false;
}
}
}
// Cannot
return false;
}
///
/// Removes all existing connections of that box.
///
/// Amount of connection to skip from removing.
public void RemoveConnections(int skipCount = 0)
{
if (Connections.Count > skipCount)
{
// Remove all connections
var toUpdate = new List(1 + skipCount)
{
this
};
for (int i = skipCount; i < Connections.Count; i++)
{
var targetBox = Connections[i];
targetBox.Connections.Remove(this);
toUpdate.Add(targetBox);
targetBox.OnConnectionsChanged();
Surface?.OnNodesDisconnected(this, targetBox);
}
Connections.Clear();
OnConnectionsChanged();
// Update
for (int i = 0; i < toUpdate.Count; i++)
{
toUpdate[i].ConnectionTick();
}
}
}
///
/// Updates state on connection data changed.
///
public void ConnectionTick()
{
// Update node boxes types management
ParentNode.ConnectionTick(this);
}
///
/// Checks if box is connected with the other one.
///
/// The other box.
/// True if both boxes are connected, otherwise false.
public bool AreConnected(Box box)
{
bool result = Connections.Contains(box);
Assert.IsTrue(box == null || result == box.Connections.Contains(this));
return result;
}
///
/// Break connection to the other box (works in a both ways).
///
/// The other box.
public void BreakConnection(Box box)
{
// Break link
bool r1 = box.Connections.Remove(this);
bool r2 = Connections.Remove(box);
// Ensure data was fine and connection was valid
Assert.AreEqual(r1, r2);
// Update
ConnectionTick();
box.ConnectionTick();
OnConnectionsChanged();
box.OnConnectionsChanged();
Surface?.OnNodesDisconnected(this, box);
}
///
/// Create connection to the other box (works in a both ways).
///
/// The other box.
public void CreateConnection(Box box)
{
// Check if any box can have only single connection
if (box.IsSingle)
box.RemoveConnections();
if (IsSingle)
RemoveConnections();
// Add link
box.Connections.Add(this);
Connections.Add(box);
// Ensure data is fine and connection is valid
Assert.IsTrue(AreConnected(box));
// Update
ConnectionTick();
box.ConnectionTick();
OnConnectionsChanged();
box.OnConnectionsChanged();
Surface?.OnNodesConnected(this, box);
}
///
/// True if box can use only single connection.
///
public bool IsSingle
{
get => _isSingle;
set
{
if (_isSingle != value)
{
_isSingle = value;
// Limit connections COUNT
if (_isSingle && Connections.Count > 0)
{
if (Surface.Undo != null)
{
var action = new EditNodeConnections(ParentNode.Context, ParentNode);
RemoveConnections(1);
action.End();
Surface.AddBatchedUndoAction(action);
}
else
{
RemoveConnections(1);
}
Surface.MarkAsEdited();
}
}
}
}
///
/// True if box type depends on other boxes types of the node.
///
public bool IsDependentBox
{
get
{
var boxes = ParentNode.Archetype.DependentBoxes;
if (boxes != null)
{
for (int i = 0; i < boxes.Length; i++)
{
int index = boxes[i];
if (index == -1)
break;
if (index == ID)
return true;
}
}
return false;
}
}
///
/// True if box type doesn't depend on other boxes types of the node.
///
public bool IsIndependentBox
{
get
{
var boxes = ParentNode.Archetype.IndependentBoxes;
if (boxes != null)
{
for (int i = 0; i < boxes.Length; i++)
{
int index = boxes[i];
if (index == -1)
break;
if (index == ID)
return true;
}
}
return false;
}
}
///
/// Highlights this box connections. Used to visualize signals and data transfer on the graph at runtime during debugging.
///
public void HighlightConnections()
{
_lastHighlightConnectionsTime = DateTime.Now;
}
///
/// Called when current box type changed.
///
protected virtual void OnCurrentTypeChanged()
{
if (Surface != null)
{
var hints = ParentNode.Archetype.ConnectionsHints;
Surface.Style.GetConnectionColor(_currentType, hints, out _currentTypeColor);
TooltipText = Surface.GetTypeName(CurrentType) ?? GetConnectionHintTypeName(hints);
}
CurrentTypeChanged?.Invoke(this);
}
///
/// Called when attributes collection gets changed.
///
protected virtual void OnAttributesChanged()
{
}
///
/// Called when connections array gets changed (also called after surface deserialization)
///
public virtual void OnConnectionsChanged()
{
}
///
/// Called when box gets selected or deselected.
///
protected virtual void OnSelectionChanged()
{
if (IsSelected && !ParentNode.IsSelected)
{
ParentNode.Surface.AddToSelection(ParentNode);
}
}
///
/// Draws the box GUI using .
///
protected void DrawBox()
{
var rect = new Rectangle(Float2.Zero, Size);
// Size culling
const float minBoxSize = 5.0f;
if (rect.Size.LengthSquared < minBoxSize * minBoxSize)
return;
// Debugging boxes size
//Render2D.DrawRectangle(rect, Color.Orange); return;
// Draw icon
bool hasConnections = HasAnyConnection;
float alpha = Enabled ? 1.0f : 0.6f;
Color color = _currentTypeColor * alpha;
var style = Surface.Style;
SpriteHandle icon;
if (_currentType.Type == typeof(void))
icon = hasConnections ? style.Icons.ArrowClose : style.Icons.ArrowOpen;
else
icon = hasConnections ? style.Icons.BoxClose : style.Icons.BoxOpen;
color *= ConnectionsHighlightIntensity + 1;
Render2D.DrawSprite(icon, rect, color);
// Draw selection hint
if (_isSelected)
{
float outlineAlpha = Mathf.Sin(Time.TimeSinceStartup * 4.0f) * 0.5f + 0.5f;
float outlineWidth = Mathf.Lerp(1.5f, 4.0f, outlineAlpha);
var outlineRect = new Rectangle(rect.X - outlineWidth, rect.Y - outlineWidth, rect.Width + outlineWidth * 2, rect.Height + outlineWidth * 2);
Render2D.DrawSprite(icon, outlineRect, FlaxEngine.GUI.Style.Current.BorderSelected.RGBMultiplied(1.0f + outlineAlpha * 0.4f));
}
}
///
public override void OnMouseEnter(Float2 location)
{
if (Surface.GetBoxDebuggerTooltip(this, out var debuggerTooltip))
{
_originalTooltipText = TooltipText;
TooltipText = debuggerTooltip;
}
base.OnMouseEnter(location);
}
///
public override bool OnMouseDown(Float2 location, MouseButton button)
{
if (base.OnMouseDown(location, button))
return true;
if (button == MouseButton.Left)
{
_isMouseDown = true;
Focus();
return true;
}
return false;
}
///
public override void OnMouseLeave()
{
if (_originalTooltipText != null)
{
TooltipText = _originalTooltipText;
}
if (_isMouseDown)
{
_isMouseDown = false;
if (Surface.CanEdit)
{
if (!IsOutput && HasSingleConnection)
{
var connectedBox = Connections[0];
if (Surface.Undo != null && Surface.Undo.Enabled)
{
var action = new ConnectBoxesAction((InputBox)this, (OutputBox)connectedBox, false);
BreakConnection(connectedBox);
action.End();
Surface.AddBatchedUndoAction(action);
Surface.MarkAsEdited();
}
else
{
BreakConnection(connectedBox);
}
Surface.ConnectingStart(connectedBox);
}
else
{
Surface.ConnectingStart(this);
}
}
}
base.OnMouseLeave();
}
///
public override void OnMouseMove(Float2 location)
{
Surface.ConnectingOver(this);
base.OnMouseMove(location);
}
///
public override bool OnMouseUp(Float2 location, MouseButton button)
{
if (base.OnMouseUp(location, button))
return true;
if (button == MouseButton.Left)
{
_isMouseDown = false;
if (Surface.IsConnecting)
{
Surface.ConnectingEnd(this);
}
else if (Surface.CanEdit)
{
// Click
if (Root.GetKey(KeyboardKeys.Alt))
{
// Break connections
if (Surface.Undo != null)
{
var action = new EditNodeConnections(ParentNode.Context, ParentNode);
RemoveConnections();
action.End();
Surface.AddBatchedUndoAction(action);
}
else
{
RemoveConnections();
}
Surface.MarkAsEdited();
}
else if (Root.GetKey(KeyboardKeys.Control))
{
// Add to selection
if (!ParentNode.IsSelected)
{
ParentNode.Surface.AddToSelection(ParentNode);
}
ParentNode.AddBoxToSelection(this);
}
else
{
// Forcibly select the node
ParentNode.Surface.Select(ParentNode);
ParentNode.SelectBox(this);
}
}
return true;
}
return false;
}
///
public Float2 ConnectionOrigin
{
get
{
var center = Center;
return Parent.PointToParent(ref center);
}
}
///
public bool AreConnected(IConnectionInstigator other)
{
return Connections.Contains(other as Box);
}
///
public bool CanConnectWith(IConnectionInstigator other)
{
if (other is Archetypes.Tools.RerouteNode reroute)
return reroute.CanConnectWith(this);
var start = this;
var end = other as Box;
// Allow only box with box connection
if (end == null)
{
// Cannot
return false;
}
// Disable for the same box
if (start == end)
{
// Cannot
return false;
}
// Check if boxes are connected
bool areConnected = start.AreConnected(end);
// Check if boxes are different or (one of them is disabled and both are disconnected)
if (end.IsOutput == start.IsOutput || !((end.Enabled && start.Enabled) || areConnected))
{
// Cannot
return false;
}
// Cache Input and Output box (since connection may be made in a different way)
InputBox iB;
OutputBox oB;
if (start.IsOutput)
{
iB = (InputBox)end;
oB = (OutputBox)start;
}
else
{
iB = (InputBox)start;
oB = (OutputBox)end;
}
// Validate connection type (also check if any of boxes parent can manage that connections types)
if (oB.CurrentType != ScriptType.Null)
{
if (!iB.CanUseType(oB.CurrentType))
{
if (!oB.CurrentType.CanCastTo(iB.CurrentType))
{
// Cannot
return false;
}
}
}
else
{
if (!oB.CanUseType(iB.CurrentType))
{
if (!oB.CurrentType.CanCastTo(iB.CurrentType))
{
// Cannot
return false;
}
}
}
// Can
return true;
}
///
public void DrawConnectingLine(ref Float2 startPos, ref Float2 endPos, ref Color color)
{
OutputBox.DrawConnection(ref startPos, ref endPos, ref color, 2);
}
///
public void Connect(IConnectionInstigator other)
{
if (other is Archetypes.Tools.RerouteNode reroute)
{
reroute.Connect(this);
return;
}
var start = this;
var end = (Box)other;
var areConnected = start.AreConnected(end);
// Check if boxes are different or (one of them is disabled and both are disconnected)
if (end.IsOutput == start.IsOutput || !((end.Enabled && start.Enabled) || areConnected))
return;
// Cache Input and Output box (since connection may be made in a different way)
InputBox iB;
OutputBox oB;
if (start.IsOutput)
{
iB = (InputBox)end;
oB = (OutputBox)start;
}
else
{
iB = (InputBox)start;
oB = (OutputBox)end;
}
// Check if they are already connected
if (areConnected)
{
// Break link
if (Surface.Undo != null && Surface.Undo.Enabled)
{
var action = new ConnectBoxesAction(iB, oB, false);
start.BreakConnection(end);
action.End();
Surface.AddBatchedUndoAction(action);
}
else
{
start.BreakConnection(end);
}
Surface.MarkAsEdited();
Surface?.OnNodesDisconnected(this, other);
return;
}
// Validate connection type (also check if any of boxes parent can manage that connections types)
bool useCaster = false;
if (!iB.CanUseType(oB.CurrentType))
{
if (oB.CurrentType.CanCastTo(iB.CurrentType))
useCaster = true;
else
return;
}
// Connect boxes
if (useCaster)
{
// Connect via Caster
const float casterXOffset = 250;
if (Surface.Undo != null && Surface.Undo.Enabled)
{
bool undoEnabled = Surface.Undo.Enabled;
Surface.Undo.Enabled = false;
SurfaceNode node = Surface.Context.SpawnNode(7, 22, Float2.Zero); // 22 AsNode, 25 CastNode
Surface.Undo.Enabled = undoEnabled;
if (node is not Archetypes.Tools.AsNode castNode)
throw new Exception("Node is not a casting node!");
// Set the type of the casting node
undoEnabled = castNode.Surface.Undo.Enabled;
castNode.Surface.Undo.Enabled = false;
castNode.SetPickerValue(iB.CurrentType);
castNode.Surface.Undo.Enabled = undoEnabled;
if (node.GetBox(0) is not OutputBox castOutputBox || node.GetBox(1) is not InputBox castInputBox)
throw new NullReferenceException("Casting failed. Cast node is invalid!");
// We set the position of the cast node here to set it relative to the target nodes input box
undoEnabled = castNode.Surface.Undo.Enabled;
castNode.Surface.Undo.Enabled = false;
var wantedOffset = iB.ParentNode.Location - new Float2(casterXOffset, -(iB.LocalY - castOutputBox.LocalY));
castNode.Location = Surface.Root.PointFromParent(ref wantedOffset);
castNode.Surface.Undo.Enabled = undoEnabled;
var spawnNodeAction = new AddRemoveNodeAction(castNode, true);
var connectToCastNodeAction = new ConnectBoxesAction(castInputBox, oB, true);
castInputBox.CreateConnection(oB);
connectToCastNodeAction.End();
var connectCastToTargetNodeAction = new ConnectBoxesAction(iB, castOutputBox, true);
iB.CreateConnection(castOutputBox);
connectCastToTargetNodeAction.End();
Surface.AddBatchedUndoAction(new MultiUndoAction(spawnNodeAction, connectToCastNodeAction, connectCastToTargetNodeAction));
}
else
{
SurfaceNode node = Surface.Context.SpawnNode(7, 22, Float2.Zero); // 22 AsNode, 25 CastNode
if (node is not Archetypes.Tools.AsNode castNode)
throw new Exception("Node is not a casting node!");
// Set the type of the casting node
castNode.SetPickerValue(iB.CurrentType);
if (node.GetBox(0) is not OutputBox castOutputBox || node.GetBox(1) is not InputBox castInputBox)
throw new NullReferenceException("Casting failed. Cast node is invalid!");
// We set the position of the cast node here to set it relative to the target nodes input box
var wantedOffset = iB.ParentNode.Location - new Float2(casterXOffset, -(iB.LocalY - castOutputBox.LocalY));
castNode.Location = Surface.Root.PointFromParent(ref wantedOffset);
castInputBox.CreateConnection(oB);
iB.CreateConnection(castOutputBox);
}
Surface.MarkAsEdited();
}
else
{
// Connect directly
if (Surface.Undo != null && Surface.Undo.Enabled)
{
var action = new ConnectBoxesAction(iB, oB, true);
iB.CreateConnection(oB);
action.End();
Surface.AddBatchedUndoAction(action);
}
else
{
iB.CreateConnection(oB);
}
Surface.MarkAsEdited();
}
}
///
public override void OnDestroy()
{
_currentType = ScriptType.Null;
CurrentTypeChanged = null;
Attributes = null;
base.OnDestroy();
}
}
}