diff --git a/Examples/Nodify.Calculator/CalculatorViewModel.cs b/Examples/Nodify.Calculator/CalculatorViewModel.cs
index 49dfb82..eec8739 100644
--- a/Examples/Nodify.Calculator/CalculatorViewModel.cs
+++ b/Examples/Nodify.Calculator/CalculatorViewModel.cs
@@ -42,6 +42,9 @@ namespace Nodify.Calculator
// Dynamic ForEach node: adapt Current Item output based on list type
if (!IsLoading) HandleForEachNodeConnected(c);
+
+ // Dynamic Knot node: adapt shape/color to match connected wire
+ if (!IsLoading) HandleKnotNodeConnected(c);
})
.WhenRemoved(c =>
{
@@ -74,6 +77,9 @@ namespace Nodify.Calculator
// Dynamic ForEach node: reset Current Item output on disconnect
HandleForEachNodeDisconnected(c);
+
+ // Dynamic Knot node: reset on disconnect
+ HandleKnotNodeDisconnected(c);
});
Operations.WhenAdded(x =>
@@ -137,6 +143,12 @@ namespace Nodify.Calculator
public PendingConnectionViewModel PendingConnection { get; set; } = new PendingConnectionViewModel();
public OperationsMenuViewModel OperationsMenu { get; set; }
+ public void CreateKnotNodeAt(Point location)
+ {
+ var knot = new KnotOperationViewModel { Location = location };
+ Operations.Add(knot);
+ }
+
public INodifyCommand StartConnectionCommand { get; }
public INodifyCommand CreateConnectionCommand { get; }
public INodifyCommand DisconnectConnectorCommand { get; }
@@ -152,6 +164,7 @@ namespace Nodify.Calculator
internal bool CanCreateConnection(ConnectorViewModel source, ConnectorViewModel? target)
=> target == null || (source != target &&
(source.Shape == target.Shape ||
+ source.IsKnotConnector || target.IsKnotConnector ||
((source.IsCopyConnector || target.IsCopyConnector) && source.Shape != ConnectorShape.Triangle && target.Shape != ConnectorShape.Triangle)) &&
!source.IsConnected &&
!target.IsConnected &&
@@ -714,6 +727,69 @@ namespace Nodify.Calculator
}
}
+ private void HandleKnotNodeConnected(ConnectionViewModel c)
+ {
+ ConnectorViewModel knotConn = null;
+ ConnectorViewModel sourceConn = null;
+
+ if (c.Input.IsKnotConnector)
+ {
+ knotConn = c.Input;
+ sourceConn = c.Output;
+ }
+ else if (c.Output.IsKnotConnector)
+ {
+ knotConn = c.Output;
+ sourceConn = c.Input;
+ }
+
+ if (knotConn == null) return;
+
+ var knotOp = knotConn.Operation as KnotOperationViewModel;
+ if (knotOp == null) return;
+
+ // Adapt both input and output connectors to match the connected wire's shape/color/type
+ foreach (var conn in knotOp.Input.Concat(knotOp.Output))
+ {
+ if (conn.Shape != ConnectorShape.Triangle)
+ {
+ conn.Shape = sourceConn.Shape;
+ conn.ConnectorColor = sourceConn.RawColor;
+ conn.DataType = sourceConn.DataType;
+ }
+ }
+ }
+
+ private void HandleKnotNodeDisconnected(ConnectionViewModel c)
+ {
+ ConnectorViewModel knotConn = null;
+
+ if (c.Input.IsKnotConnector)
+ knotConn = c.Input;
+ else if (c.Output.IsKnotConnector)
+ knotConn = c.Output;
+
+ if (knotConn == null) return;
+
+ var knotOp = knotConn.Operation as KnotOperationViewModel;
+ if (knotOp == null) return;
+
+ // Check if any connection still exists on the knot
+ var stillConnected = Connections.Any(con =>
+ (con.Input.IsKnotConnector && con.Input.Operation == knotOp) ||
+ (con.Output.IsKnotConnector && con.Output.Operation == knotOp));
+
+ if (stillConnected) return;
+
+ // Reset to default
+ foreach (var conn in knotOp.Input.Concat(knotOp.Output))
+ {
+ conn.Shape = ConnectorShape.Circle;
+ conn.ConnectorColor = System.Drawing.Color.DodgerBlue;
+ conn.DataType = null;
+ }
+ }
+
private void HandleForEachNodeDisconnected(ConnectionViewModel c)
{
ConnectorViewModel forEachListInput = null;
diff --git a/Examples/Nodify.Calculator/ConnectorViewModel.cs b/Examples/Nodify.Calculator/ConnectorViewModel.cs
index c707d97..a56173f 100644
--- a/Examples/Nodify.Calculator/ConnectorViewModel.cs
+++ b/Examples/Nodify.Calculator/ConnectorViewModel.cs
@@ -121,6 +121,16 @@ namespace Nodify.Calculator
set => SetProperty(ref _isCopyConnector, value);
}
+ private bool _isKnotConnector;
+ ///
+ /// When true, this connector belongs to a Knot/Reroute node and accepts any shape for connections.
+ ///
+ public bool IsKnotConnector
+ {
+ get => _isKnotConnector;
+ set => SetProperty(ref _isKnotConnector, value);
+ }
+
private bool _isTakeListConnector;
///
/// When true, this connector belongs to a TAKE node and only accepts list/array types.
diff --git a/Examples/Nodify.Calculator/EditorView.xaml b/Examples/Nodify.Calculator/EditorView.xaml
index 558596f..079e741 100644
--- a/Examples/Nodify.Calculator/EditorView.xaml
+++ b/Examples/Nodify.Calculator/EditorView.xaml
@@ -646,6 +646,20 @@
+
+
+
+
+
+
+ {
+ private static InputGesture CreateKnotGesture { get; } = new Interactivity.MouseGesture(MouseAction.LeftClick, ModifierKeys.Control);
+
+ public KnotNodeHandler(NodifyEditor element) : base(element)
+ {
+ ProcessHandledEvents = true;
+ }
+
+ protected override void OnMouseUp(MouseButtonEventArgs e)
+ {
+ if (CreateKnotGesture.Matches(e.Source, e) && Element.DataContext is CalculatorViewModel calculator)
+ {
+ var location = Element.MouseLocation;
+ calculator.CreateKnotNodeAt(location);
+ e.Handled = true;
+ }
+ }
+ }
+
public partial class EditorView : UserControl
{
public EditorView()
@@ -44,6 +64,7 @@ namespace Nodify.Calculator
static EditorView()
{
InputProcessor.Shared.RegisterHandlerFactory(editor => new OperationsMenuHandler(editor));
+ InputProcessor.Shared.RegisterHandlerFactory(editor => new KnotNodeHandler(editor));
}
private void OnDropNode(object sender, DragEventArgs e)
diff --git a/Examples/Nodify.Calculator/Executor.cs b/Examples/Nodify.Calculator/Executor.cs
index d8ffcbf..21567c4 100644
--- a/Examples/Nodify.Calculator/Executor.cs
+++ b/Examples/Nodify.Calculator/Executor.cs
@@ -428,6 +428,20 @@ namespace Nodify.Calculator
OnLogMe?.Invoke($"Auth node resolved. Base URL: {_authBaseUrl}, Auth Type: {_authType}");
return;
}
+ // Knot/Reroute node: passthrough - forward input data to output
+ if (op is KnotOperationViewModel)
+ {
+ var knotInputCon = connections.Where(c => c.Input.Operation == op && c.Input.Shape != ConnectorShape.Triangle).FirstOrDefault();
+ if (knotInputCon != null)
+ {
+ var srcNodeId = knotInputCon.Output?.Operation?.NodeId;
+ if (srcNodeId != null && outputs.TryGetValue(srcNodeId, out var srcVal))
+ {
+ outputs[op.NodeId] = srcVal;
+ }
+ }
+ return;
+ }
if (op is SystemOperationViewModel debugSysOp && debugSysOp.SystemOperationType == SystemOperations.DEBUG)
{
// Find the data input connection (non-triangle)
diff --git a/Examples/Nodify.Calculator/KnotOperationViewModel.cs b/Examples/Nodify.Calculator/KnotOperationViewModel.cs
new file mode 100644
index 0000000..315ee23
--- /dev/null
+++ b/Examples/Nodify.Calculator/KnotOperationViewModel.cs
@@ -0,0 +1,15 @@
+namespace Nodify.Calculator
+{
+ public class KnotOperationViewModel : SystemOperationViewModel
+ {
+ public KnotOperationViewModel()
+ {
+ Title = "⬥";
+ SystemOperationType = SystemOperations.KNOT;
+
+ // Single flow-through input and output (will adapt shape when connected)
+ Input.Add(new ConnectorViewModel { Title = "", Shape = ConnectorShape.Circle, IsKnotConnector = true });
+ Output.Add(new ConnectorViewModel { Title = "", Shape = ConnectorShape.Circle, IsKnotConnector = true });
+ }
+ }
+}
diff --git a/Examples/Nodify.Calculator/SystemOperationViewModel.cs b/Examples/Nodify.Calculator/SystemOperationViewModel.cs
index fa75c5e..9c322d0 100644
--- a/Examples/Nodify.Calculator/SystemOperationViewModel.cs
+++ b/Examples/Nodify.Calculator/SystemOperationViewModel.cs
@@ -22,7 +22,8 @@ namespace Nodify.Calculator
DEBUG,
NEW_OBJECT,
FOREACH,
- ASSERT
+ ASSERT,
+ KNOT
}
public class SystemOperationViewModel : OperationViewModel