From ab88a6339a111be64226b6f036f8b1317911a393 Mon Sep 17 00:00:00 2001 From: Saas Date: Tue, 3 Feb 2026 21:36:32 +0100 Subject: [PATCH 1/6] add ripping apart connection with reconnect and auto connect when node is dropped over existing connection --- Source/Editor/Surface/VisjectSurface.Input.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/Source/Editor/Surface/VisjectSurface.Input.cs b/Source/Editor/Surface/VisjectSurface.Input.cs index 056987e52..e3c443a74 100644 --- a/Source/Editor/Surface/VisjectSurface.Input.cs +++ b/Source/Editor/Surface/VisjectSurface.Input.cs @@ -321,6 +321,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; @@ -602,6 +629,24 @@ namespace FlaxEditor.Surface { if (_movingNodes != null && _movingNodes.Count > 0) { + // Allow dropping the 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)) + { + 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); + + Debug.Log($"INPUT OUTPUT: {intersectedConnectionNodesXDistance}, NODE: {node.Width}"); + } + } + if (Undo != null && !_movingNodesDelta.IsZero && CanEdit) Undo.AddAction(new MoveNodesAction(Context, _movingNodes.Select(x => x.ID).ToArray(), _movingNodesDelta)); _movingNodes.Clear(); From 8d8bf87c6957090d0aae451848ee504669681d54 Mon Sep 17 00:00:00 2001 From: Saas Date: Wed, 4 Feb 2026 13:52:37 +0100 Subject: [PATCH 2/6] move nodes if needed and add undo support --- Source/Editor/Surface/VisjectSurface.Input.cs | 51 +++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/Source/Editor/Surface/VisjectSurface.Input.cs b/Source/Editor/Surface/VisjectSurface.Input.cs index e3c443a74..c2c5fac68 100644 --- a/Source/Editor/Surface/VisjectSurface.Input.cs +++ b/Source/Editor/Surface/VisjectSurface.Input.cs @@ -629,7 +629,7 @@ namespace FlaxEditor.Surface { if (_movingNodes != null && _movingNodes.Count > 0) { - // Allow dropping the node onto an existing connection and connect it + // Allow dropping a single node onto an existing connection and connect it if (_movingNodes.Count == 1) { var mousePos = _rootControl.PointFromParent(ref _mousePos); @@ -643,12 +643,57 @@ namespace FlaxEditor.Surface TryConnect(intersectedConnectionOutputBox, nodeInputBox); TryConnect(nodeOutputBox, intersectedConnectionInputBox); - Debug.Log($"INPUT OUTPUT: {intersectedConnectionNodesXDistance}, NODE: {node.Width}"); + 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.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; From 29b043342af0a73fd1241797edb627ba679a5c56 Mon Sep 17 00:00:00 2001 From: Saas Date: Wed, 4 Feb 2026 14:11:23 +0100 Subject: [PATCH 3/6] don't fall into the trap of moving nodes with NoMove flag again --- Source/Editor/Surface/VisjectSurface.Input.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Editor/Surface/VisjectSurface.Input.cs b/Source/Editor/Surface/VisjectSurface.Input.cs index c2c5fac68..388df0b9c 100644 --- a/Source/Editor/Surface/VisjectSurface.Input.cs +++ b/Source/Editor/Surface/VisjectSurface.Input.cs @@ -656,7 +656,7 @@ namespace FlaxEditor.Surface 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) + if (node.Right > intersectedConnectionInputBox.ParentNode.Left + 15f && !node.Archetype.Flags.HasFlag(NodeFlags.NoMove)) { node.Location += locationDelta; movedNodes.Add(node); From a57fe6c04df80cdfe663849f514ad7640ab7006b Mon Sep 17 00:00:00 2001 From: Saas Date: Wed, 4 Feb 2026 21:31:03 +0100 Subject: [PATCH 4/6] add lazy connect feature to visject --- Source/Editor/Surface/VisjectSurface.Draw.cs | 41 ++++++++++++ Source/Editor/Surface/VisjectSurface.Input.cs | 65 ++++++++++++++++++- 2 files changed, 104 insertions(+), 2 deletions(-) diff --git a/Source/Editor/Surface/VisjectSurface.Draw.cs b/Source/Editor/Surface/VisjectSurface.Draw.cs index af5893907..d79b21e9c 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 388df0b9c..f7d90512e 100644 --- a/Source/Editor/Surface/VisjectSurface.Input.cs +++ b/Source/Editor/Surface/VisjectSurface.Input.cs @@ -29,6 +29,9 @@ namespace FlaxEditor.Surface private HashSet _movingNodes; private HashSet _temporarySelectedNodes; private readonly Stack _inputBrackets = new Stack(); + private bool _isLazyConnecting; + private SurfaceNode _lazyConnectStartNode; + private SurfaceNode _lazyConnectEndNode; private class InputBracket { @@ -250,8 +253,13 @@ namespace FlaxEditor.Surface // Cache mouse location _mousePos = location; + if (_isLazyConnecting && GetControlUnderMouse() is SurfaceNode nodeUnderMouse) + _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; @@ -542,11 +550,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) + _lazyConnectStartNode = node; + // Check if mouse is over header and user is pressing mouse left button if (_leftMouseDown && controlUnderMouse.CanSelect(ref cLocation)) { @@ -581,6 +595,9 @@ namespace FlaxEditor.Surface } else { + if (_isLazyConnecting && Nodes.Count > 0) + _lazyConnectStartNode = GetClosestNodeAtLocation(location); + // Cache flags and state if (_leftMouseDown) { @@ -720,12 +737,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) { @@ -927,6 +968,26 @@ namespace FlaxEditor.Surface return false; } + private SurfaceNode GetClosestNodeAtLocation(Float2 location) + { + SurfaceNode currentClosestNode = null; + float currentClosestDistanceSquared = float.MaxValue; + + foreach (var node in Nodes) + { + 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 = ""; From 3adda3629e5cdf42b6d8dd5d6a8a5731c28eafb6 Mon Sep 17 00:00:00 2001 From: Saas Date: Wed, 4 Feb 2026 23:29:41 +0100 Subject: [PATCH 5/6] don't try to auto connect to comments and particle emitter nodes --- Source/Editor/Surface/VisjectSurface.Input.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Source/Editor/Surface/VisjectSurface.Input.cs b/Source/Editor/Surface/VisjectSurface.Input.cs index f7d90512e..74eaa3e04 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; @@ -253,7 +254,7 @@ namespace FlaxEditor.Surface // Cache mouse location _mousePos = location; - if (_isLazyConnecting && GetControlUnderMouse() is SurfaceNode nodeUnderMouse) + if (_isLazyConnecting && GetControlUnderMouse() is SurfaceNode nodeUnderMouse && !(nodeUnderMouse is SurfaceComment || nodeUnderMouse is ParticleEmitterNode)) _lazyConnectEndNode = nodeUnderMouse; else if (_isLazyConnecting && Nodes.Count > 0) _lazyConnectEndNode = GetClosestNodeAtLocation(location); @@ -558,7 +559,7 @@ namespace FlaxEditor.Surface var cLocation = _rootControl.PointFromParent(ref location); if (controlUnderMouse != null) { - if (controlUnderMouse is SurfaceNode node && _isLazyConnecting) + 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 @@ -975,6 +976,9 @@ namespace FlaxEditor.Surface 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); From 64c2d64d84575e62bf1b0a70093d45c90a5acb96 Mon Sep 17 00:00:00 2001 From: Saas Date: Thu, 5 Mar 2026 20:14:48 +0100 Subject: [PATCH 6/6] increase distante for slot into connection feature --- Source/Editor/Surface/Elements/OutputBox.cs | 12 ++++------- Source/Editor/Surface/VisjectSurface.Input.cs | 20 ++++++++++++++----- 2 files changed, 19 insertions(+), 13 deletions(-) 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.Input.cs b/Source/Editor/Surface/VisjectSurface.Input.cs index 74eaa3e04..01c78ccb1 100644 --- a/Source/Editor/Surface/VisjectSurface.Input.cs +++ b/Source/Editor/Surface/VisjectSurface.Input.cs @@ -24,6 +24,16 @@ 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; @@ -456,7 +466,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) { @@ -653,7 +663,7 @@ namespace FlaxEditor.Surface var mousePos = _rootControl.PointFromParent(ref _mousePos); InputBox intersectedConnectionInputBox; OutputBox intersectedConnectionOutputBox; - if (IntersectsConnection(mousePos, out intersectedConnectionInputBox, out intersectedConnectionOutputBox)) + if (IntersectsConnection(mousePos, out intersectedConnectionInputBox, out intersectedConnectionOutputBox, SlotNodeIntoConnectionDistance)) { SurfaceNode node = _movingNodes.First(); InputBox nodeInputBox = (InputBox)node.GetBoxes().First(b => !b.IsOutput); @@ -783,7 +793,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); @@ -1180,7 +1190,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++) { @@ -1190,7 +1200,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;