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