diff --git a/Source/Editor/Surface/Elements/OutputBox.cs b/Source/Editor/Surface/Elements/OutputBox.cs
index 7e271fef0..8836dc0dc 100644
--- a/Source/Editor/Surface/Elements/OutputBox.cs
+++ b/Source/Editor/Surface/Elements/OutputBox.cs
@@ -34,11 +34,6 @@ namespace FlaxEditor.Surface.Elements
///
public const float DefaultConnectionOffset = 24f;
- ///
- /// Distance for the mouse to be considered above the connection
- ///
- public float MouseOverConnectionDistance => 100f / Surface.ViewScale;
-
///
public OutputBox(SurfaceNode parentNode, NodeElementArchetype archetype)
: base(parentNode, archetype, archetype.Position + new Float2(parentNode.Archetype.Size.X, 0))
@@ -109,12 +104,13 @@ namespace FlaxEditor.Surface.Elements
///
/// The other box.
/// The mouse position
- public bool IntersectsConnection(Box targetBox, ref Float2 mousePosition)
+ /// Distance at which its an intersection
+ public bool IntersectsConnection(Box targetBox, ref Float2 mousePosition, float distance)
{
float connectionOffset = Mathf.Max(0f, DefaultConnectionOffset * (1 - Editor.Instance.Options.Options.Interface.ConnectionCurvature));
Float2 start = new Float2(ConnectionOrigin.X + connectionOffset, ConnectionOrigin.Y);
Float2 end = new Float2(targetBox.ConnectionOrigin.X - connectionOffset, targetBox.ConnectionOrigin.Y);
- return IntersectsConnection(ref start, ref end, ref mousePosition, MouseOverConnectionDistance);
+ return IntersectsConnection(ref start, ref end, ref mousePosition, distance);
}
///
@@ -182,7 +178,7 @@ namespace FlaxEditor.Surface.Elements
{
// Draw all the connections
var style = Surface.Style;
- var mouseOverDistance = MouseOverConnectionDistance;
+ var mouseOverDistance = Surface.MouseOverConnectionDistance;
var startPos = ConnectionOrigin;
var startHighlight = ConnectionsHighlightIntensity;
for (int i = 0; i < Connections.Count; i++)
diff --git a/Source/Editor/Surface/VisjectSurface.Draw.cs b/Source/Editor/Surface/VisjectSurface.Draw.cs
index af5893907..a7d390132 100644
--- a/Source/Editor/Surface/VisjectSurface.Draw.cs
+++ b/Source/Editor/Surface/VisjectSurface.Draw.cs
@@ -213,6 +213,44 @@ namespace FlaxEditor.Surface
}
}
+ ///
+ /// Draw connection hints for lazy connect feature.
+ ///
+ protected virtual void DrawLazyConnect()
+ {
+ var style = FlaxEngine.GUI.Style.Current;
+
+ if (_lazyConnectStartNode != null)
+ {
+ Float2 upperLeft = _rootControl.PointToParent(_lazyConnectStartNode.UpperLeft);
+ Rectangle startNodeOutline = new Rectangle(upperLeft + 1f, _lazyConnectStartNode.Size - 1f);
+ startNodeOutline.Size *= ViewScale;
+ Render2D.DrawRectangle(startNodeOutline.MakeExpanded(4f), style.BackgroundSelected, 4f);
+ }
+
+ if (_lazyConnectEndNode != null)
+ {
+ Float2 upperLeft = _rootControl.PointToParent(_lazyConnectEndNode.UpperLeft);
+ Rectangle startNodeOutline = new Rectangle(upperLeft + 1f, _lazyConnectEndNode.Size - 1f);
+ startNodeOutline.Size *= ViewScale;
+ Render2D.DrawRectangle(startNodeOutline.MakeExpanded(4f), style.BackgroundSelected, 4f);
+ }
+
+ Rectangle startRect = new Rectangle(_rightMouseDownPos - 6f, new Float2(12f));
+ Rectangle endRect = new Rectangle(_mousePos - 6f, new Float2(12f));
+
+ // Start and end shadows/ outlines
+ Render2D.FillRectangle(startRect.MakeExpanded(2.5f), Color.Black);
+ Render2D.FillRectangle(endRect.MakeExpanded(2.5f), Color.Black);
+
+ Render2D.DrawLine(_rightMouseDownPos, _mousePos, Color.Black, 7.5f);
+ Render2D.DrawLine(_rightMouseDownPos, _mousePos, style.ForegroundGrey, 5f);
+
+ // Draw start and end boxes over the lines to hide ugly artifacts at the ends
+ Render2D.FillRectangle(startRect, style.ForegroundGrey);
+ Render2D.FillRectangle(endRect, style.ForegroundGrey);
+ }
+
///
/// Draws the contents of the surface (nodes, connections, comments, etc.).
///
@@ -260,6 +298,9 @@ namespace FlaxEditor.Surface
DrawContents();
+ if (_isLazyConnecting)
+ DrawLazyConnect();
+
//Render2D.DrawText(style.FontTitle, string.Format("Scale: {0}", _rootControl.Scale), rect, Enabled ? Color.Red : Color.Black);
// Draw border
diff --git a/Source/Editor/Surface/VisjectSurface.Input.cs b/Source/Editor/Surface/VisjectSurface.Input.cs
index 8e0d3c3d4..c2f474865 100644
--- a/Source/Editor/Surface/VisjectSurface.Input.cs
+++ b/Source/Editor/Surface/VisjectSurface.Input.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using static FlaxEditor.Surface.Archetypes.Particles;
using FlaxEditor.Options;
using FlaxEditor.Surface.Elements;
using FlaxEditor.Surface.Undo;
@@ -23,12 +24,25 @@ namespace FlaxEditor.Surface
///
public bool PanWithMiddleMouse = false;
+ ///
+ /// Distance for the mouse to be considered above the connection.
+ ///
+ public float MouseOverConnectionDistance => 100f / ViewScale;
+
+ ///
+ /// Distance of a node from which it is able to be slotted into an existing connection.
+ ///
+ public float SlotNodeIntoConnectionDistance => 250f / ViewScale;
+
private string _currentInputText = string.Empty;
private Float2 _movingNodesDelta;
private Float2 _gridRoundingDelta;
private HashSet _movingNodes;
private HashSet _temporarySelectedNodes;
private readonly Stack _inputBrackets = new Stack();
+ private bool _isLazyConnecting;
+ private SurfaceNode _lazyConnectStartNode;
+ private SurfaceNode _lazyConnectEndNode;
private InputBinding _focusSelectedNodeBinding;
private class InputBracket
@@ -251,8 +265,13 @@ namespace FlaxEditor.Surface
// Cache mouse location
_mousePos = location;
+ if (_isLazyConnecting && GetControlUnderMouse() is SurfaceNode nodeUnderMouse && !(nodeUnderMouse is SurfaceComment || nodeUnderMouse is ParticleEmitterNode))
+ _lazyConnectEndNode = nodeUnderMouse;
+ else if (_isLazyConnecting && Nodes.Count > 0)
+ _lazyConnectEndNode = GetClosestNodeAtLocation(location);
+
// Moving around surface with mouse
- if (_rightMouseDown)
+ if (_rightMouseDown && !_isLazyConnecting)
{
// Calculate delta
var delta = location - _rightMouseDownPos;
@@ -322,6 +341,33 @@ namespace FlaxEditor.Surface
foreach (var node in _movingNodes)
{
+ // Allow ripping the node from its current connection
+ if (RootWindow.GetKey(KeyboardKeys.Alt))
+ {
+ InputBox nodeConnectedInput = null;
+ OutputBox nodeConnectedOuput = null;
+
+ var boxes = node.GetBoxes();
+ foreach (var box in boxes)
+ {
+ if (!box.IsOutput && box.Connections.Count > 0)
+ {
+ nodeConnectedInput = (InputBox)box;
+ continue;
+ }
+ if (box.IsOutput && box.Connections.Count > 0)
+ {
+ nodeConnectedOuput = (OutputBox)box;
+ continue;
+ }
+ }
+
+ if (nodeConnectedInput != null && nodeConnectedOuput != null)
+ TryConnect(nodeConnectedOuput.Connections[0], nodeConnectedInput.Connections[0]);
+
+ node.RemoveConnections();
+ }
+
if (gridSnap)
{
Float2 unroundedLocation = node.Location;
@@ -421,7 +467,7 @@ namespace FlaxEditor.Surface
if (!handled && CanEdit && CanUseNodeType(7, 29))
{
var mousePos = _rootControl.PointFromParent(ref _mousePos);
- if (IntersectsConnection(mousePos, out InputBox inputBox, out OutputBox outputBox) && GetControlUnderMouse() == null)
+ if (IntersectsConnection(mousePos, out InputBox inputBox, out OutputBox outputBox, MouseOverConnectionDistance) && GetControlUnderMouse() == null)
{
if (Undo != null)
{
@@ -516,11 +562,17 @@ namespace FlaxEditor.Surface
_middleMouseDownPos = location;
}
+ if (root.GetKey(KeyboardKeys.Alt) && button == MouseButton.Right)
+ _isLazyConnecting = true;
+
// Check if any node is under the mouse
SurfaceControl controlUnderMouse = GetControlUnderMouse();
var cLocation = _rootControl.PointFromParent(ref location);
if (controlUnderMouse != null)
{
+ if (controlUnderMouse is SurfaceNode node && _isLazyConnecting && !(controlUnderMouse is SurfaceComment || controlUnderMouse is ParticleEmitterNode))
+ _lazyConnectStartNode = node;
+
// Check if mouse is over header and user is pressing mouse left button
if (_leftMouseDown && controlUnderMouse.CanSelect(ref cLocation))
{
@@ -555,6 +607,9 @@ namespace FlaxEditor.Surface
}
else
{
+ if (_isLazyConnecting && Nodes.Count > 0)
+ _lazyConnectStartNode = GetClosestNodeAtLocation(location);
+
// Cache flags and state
if (_leftMouseDown)
{
@@ -603,8 +658,71 @@ namespace FlaxEditor.Surface
{
if (_movingNodes != null && _movingNodes.Count > 0)
{
+ // Allow dropping a single node onto an existing connection and connect it
+ if (_movingNodes.Count == 1)
+ {
+ var mousePos = _rootControl.PointFromParent(ref _mousePos);
+ InputBox intersectedConnectionInputBox;
+ OutputBox intersectedConnectionOutputBox;
+ if (IntersectsConnection(mousePos, out intersectedConnectionInputBox, out intersectedConnectionOutputBox, SlotNodeIntoConnectionDistance))
+ {
+ SurfaceNode node = _movingNodes.First();
+ InputBox nodeInputBox = (InputBox)node.GetBoxes().First(b => !b.IsOutput);
+ OutputBox nodeOutputBox = (OutputBox)node.GetBoxes().First(b => b.IsOutput);
+ TryConnect(intersectedConnectionOutputBox, nodeInputBox);
+ TryConnect(nodeOutputBox, intersectedConnectionInputBox);
+
+ float intersectedConnectionNodesXDistance = intersectedConnectionInputBox.ParentNode.Left - intersectedConnectionOutputBox.ParentNode.Right;
+ float paddedNodeWidth = node.Width + 2f;
+ if (intersectedConnectionNodesXDistance < paddedNodeWidth)
+ {
+ List visitedNodes = new List{ node };
+ List movedNodes = new List();
+ Float2 locationDelta = new Float2(paddedNodeWidth, 0f);
+
+ MoveConnectedNodes(intersectedConnectionInputBox.ParentNode);
+
+ void MoveConnectedNodes(SurfaceNode node)
+ {
+ // Only move node if it is to the right of the node we have connected the moved node to
+ if (node.Right > intersectedConnectionInputBox.ParentNode.Left + 15f && !node.Archetype.Flags.HasFlag(NodeFlags.NoMove))
+ {
+ node.Location += locationDelta;
+ movedNodes.Add(node);
+ }
+
+ visitedNodes.Add(node);
+
+ foreach (var box in node.GetBoxes())
+ {
+ if (!box.HasAnyConnection || box == intersectedConnectionInputBox)
+ continue;
+
+ foreach (var connectedBox in box.Connections)
+ {
+ SurfaceNode nextNode = connectedBox.ParentNode;
+ if (visitedNodes.Contains(nextNode))
+ continue;
+
+ MoveConnectedNodes(nextNode);
+ }
+ }
+ }
+
+ Float2 nodeMoveOffset = new Float2(node.Width * 0.5f, 0f);
+ node.Location += nodeMoveOffset;
+
+ var moveNodesAction = new MoveNodesAction(Context, movedNodes.Select(n => n.ID).ToArray(), locationDelta);
+ var moveNodeAction = new MoveNodesAction(Context, [node.ID], nodeMoveOffset);
+ var multiAction = new MultiUndoAction(moveNodeAction, moveNodesAction);
+
+ AddBatchedUndoAction(multiAction);
+ }
+ }
+ }
+
if (Undo != null && !_movingNodesDelta.IsZero && CanEdit)
- Undo.AddAction(new MoveNodesAction(Context, _movingNodes.Select(x => x.ID).ToArray(), _movingNodesDelta));
+ AddBatchedUndoAction(new MoveNodesAction(Context, _movingNodes.Select(x => x.ID).ToArray(), _movingNodesDelta));
_movingNodes.Clear();
}
_movingNodesDelta = Float2.Zero;
@@ -631,12 +749,36 @@ namespace FlaxEditor.Surface
{
// Check if any control is under the mouse
_cmStartPos = location;
- if (controlUnderMouse == null)
+ if (controlUnderMouse == null && !_isLazyConnecting)
{
showPrimaryMenu = true;
}
}
_mouseMoveAmount = 0;
+
+ if (_isLazyConnecting)
+ {
+ if (_lazyConnectStartNode != null && _lazyConnectEndNode != null && _lazyConnectStartNode != _lazyConnectEndNode)
+ {
+ // First check if there is a type matching input and output where input
+ OutputBox startNodeOutput = (OutputBox)_lazyConnectStartNode.GetBoxes().FirstOrDefault(b => b.IsOutput, null);
+ InputBox endNodeInput = null;
+
+ if (startNodeOutput != null)
+ endNodeInput = (InputBox)_lazyConnectEndNode.GetBoxes().FirstOrDefault(b => !b.IsOutput && b.CurrentType == startNodeOutput.CurrentType && !b.HasAnyConnection && b.IsActive && b.CanConnectWith(startNodeOutput), null);
+
+ // Perform less strict checks (less ideal conditions for connection but still good) if the first checks failed
+ if (endNodeInput == null)
+ endNodeInput = (InputBox)_lazyConnectEndNode.GetBoxes().FirstOrDefault(b => !b.IsOutput && !b.HasAnyConnection && b.CanConnectWith(startNodeOutput), null);
+
+ if (startNodeOutput != null && endNodeInput != null)
+ TryConnect(startNodeOutput, endNodeInput);
+ }
+
+ _isLazyConnecting = false;
+ _lazyConnectStartNode = null;
+ _lazyConnectEndNode = null;
+ }
}
if (_middleMouseDown && button == MouseButton.Middle)
{
@@ -652,7 +794,7 @@ namespace FlaxEditor.Surface
{
// Surface was not moved with MMB so try to remove connection underneath
var mousePos = _rootControl.PointFromParent(ref location);
- if (IntersectsConnection(mousePos, out InputBox inputBox, out OutputBox outputBox))
+ if (IntersectsConnection(mousePos, out InputBox inputBox, out OutputBox outputBox, MouseOverConnectionDistance))
{
var action = new EditNodeConnections(inputBox.ParentNode.Context, inputBox.ParentNode);
inputBox.BreakConnection(outputBox);
@@ -846,6 +988,29 @@ namespace FlaxEditor.Surface
return false;
}
+ private SurfaceNode GetClosestNodeAtLocation(Float2 location)
+ {
+ SurfaceNode currentClosestNode = null;
+ float currentClosestDistanceSquared = float.MaxValue;
+
+ foreach (var node in Nodes)
+ {
+ if (node is SurfaceComment || node is ParticleEmitterNode)
+ continue;
+
+ Float2 nodeSurfaceLocation = _rootControl.PointToParent(node.Center);
+
+ float distanceSquared = Float2.DistanceSquared(location, nodeSurfaceLocation);
+ if (distanceSquared < currentClosestDistanceSquared)
+ {
+ currentClosestNode = node;
+ currentClosestDistanceSquared = distanceSquared;
+ }
+ }
+
+ return currentClosestNode;
+ }
+
private void ResetInput()
{
InputText = "";
@@ -1035,7 +1200,7 @@ namespace FlaxEditor.Surface
return new Float2(xLocation, yLocation);
}
- private bool IntersectsConnection(Float2 mousePosition, out InputBox inputBox, out OutputBox outputBox)
+ private bool IntersectsConnection(Float2 mousePosition, out InputBox inputBox, out OutputBox outputBox, float distance)
{
for (int i = 0; i < Nodes.Count; i++)
{
@@ -1045,7 +1210,7 @@ namespace FlaxEditor.Surface
{
for (int k = 0; k < ob.Connections.Count; k++)
{
- if (ob.IntersectsConnection(ob.Connections[k], ref mousePosition))
+ if (ob.IntersectsConnection(ob.Connections[k], ref mousePosition, distance))
{
outputBox = ob;
inputBox = ob.Connections[k] as InputBox;