From 2bacf055708fbe36a19b03e1df54efde6d7b1985 Mon Sep 17 00:00:00 2001
From: snakex64 <39806655+snakex64@users.noreply.github.com>
Date: Thu, 21 Nov 2024 19:00:09 -0500
Subject: [PATCH] Apply code preferences, removed unused namespace and use tabs
everywhere (#57)
---
.../Platforms/Windows/App.xaml.cs | 4 +-
.../Components/DebuggerConsolePanel.razor.cs | 4 +-
.../Components/GraphCanvas.razor.cs | 6 +-
.../DiagramsModels/GraphNodeModel.cs | 57 +-
.../DiagramsModels/GraphPortModel.cs | 38 +-
.../SmoothPathGeneratorWithDirectVertices.cs | 4 +-
.../NodeDecorations/NodeDecorationPosition.cs | 9 +-
.../Services/DebuggedPathService.cs | 5 +-
.../GraphManager/GraphManagerService.cs | 186 ++--
.../Services/ServicesExtension.cs | 5 -
src/NodeDev.Blazor/Utility.cs | 9 +-
src/NodeDev.Core/BuildError.cs | 10 +-
src/NodeDev.Core/BuildOptions.cs | 4 +-
src/NodeDev.Core/Class/NodeClass.cs | 9 +-
src/NodeDev.Core/Class/NodeClassMethod.cs | 5 -
.../Class/NodeClassMethodParameter.cs | 276 +++--
src/NodeDev.Core/Class/NodeClassProperty.cs | 4 +-
.../Class/NodeClassTypeCreator.cs | 13 +-
src/NodeDev.Core/Connections/Connection.cs | 350 ++++---
src/NodeDev.Core/Graph.cs | 953 +++++++++---------
src/NodeDev.Core/GraphExecutor.cs | 8 +-
src/NodeDev.Core/Migrations/MigrationBase.cs | 14 +-
.../Migration_1_0_1_class_as_raw_json.cs | 136 +--
.../NodeDecorations/NodeDecoration.cs | 9 +-
src/NodeDev.Core/NodePaths.cs | 2 +-
src/NodeDev.Core/NodeProvider.cs | 246 +++--
src/NodeDev.Core/Nodes/ArrayGet.cs | 8 +-
src/NodeDev.Core/Nodes/ArraySet.cs | 11 +-
src/NodeDev.Core/Nodes/BuildExpressionInfo.cs | 1 -
.../Nodes/BuildExpressionOptions.cs | 4 +-
src/NodeDev.Core/Nodes/DeclareVariableNode.cs | 28 +-
src/NodeDev.Core/Nodes/Flow/Branch.cs | 6 +-
src/NodeDev.Core/Nodes/Flow/EntryNode.cs | 4 +-
src/NodeDev.Core/Nodes/Flow/FlowNode.cs | 8 +-
src/NodeDev.Core/Nodes/Flow/ForNode.cs | 93 +-
src/NodeDev.Core/Nodes/Flow/ForeachNode.cs | 2 +-
src/NodeDev.Core/Nodes/Flow/WhileNode.cs | 22 +-
src/NodeDev.Core/Nodes/GetPropertyOrField.cs | 2 +-
src/NodeDev.Core/Nodes/Math/Add.cs | 3 +-
src/NodeDev.Core/Nodes/Math/And.cs | 3 +-
src/NodeDev.Core/Nodes/Math/BiggerThan.cs | 3 +-
.../Nodes/Math/BiggerThanOrEqual.cs | 3 +-
.../Nodes/Math/BinaryOperationMath.cs | 7 +-
src/NodeDev.Core/Nodes/Math/Divide.cs | 3 +-
src/NodeDev.Core/Nodes/Math/Equals.cs | 3 +-
src/NodeDev.Core/Nodes/Math/IsNotNull.cs | 3 +-
src/NodeDev.Core/Nodes/Math/IsNull.cs | 3 +-
src/NodeDev.Core/Nodes/Math/Modulo.cs | 3 +-
src/NodeDev.Core/Nodes/Math/Multiply.cs | 3 +-
src/NodeDev.Core/Nodes/Math/Not.cs | 3 +-
src/NodeDev.Core/Nodes/Math/NotEquals.cs | 3 +-
src/NodeDev.Core/Nodes/Math/Or.cs | 3 +-
src/NodeDev.Core/Nodes/Math/SmallerThan.cs | 3 +-
.../Nodes/Math/SmallerThanOrEqual.cs | 3 +-
src/NodeDev.Core/Nodes/Math/Subtract.cs | 3 +-
.../Nodes/Math/TwoOperationMath.cs | 16 +-
src/NodeDev.Core/Nodes/Math/Xor.cs | 3 +-
src/NodeDev.Core/Nodes/MethodCall.cs | 9 +-
src/NodeDev.Core/Nodes/New.cs | 133 ++-
src/NodeDev.Core/Nodes/NoFlowNode.cs | 21 +-
src/NodeDev.Core/Nodes/Node.cs | 24 +-
src/NodeDev.Core/Nodes/NormalFlowNode.cs | 39 +-
src/NodeDev.Core/Nodes/Null.cs | 3 +-
src/NodeDev.Core/Nodes/Self.cs | 5 +-
src/NodeDev.Core/Nodes/SetPropertyOrField.cs | 3 +-
.../Nodes/SetVariableValueNode.cs | 22 +-
src/NodeDev.Core/Nodes/TypeOf.cs | 2 +-
src/NodeDev.Core/Project.cs | 2 +-
src/NodeDev.Core/ProjectSettings.cs | 2 +-
src/NodeDev.Core/Types/ExecType.cs | 20 +-
src/NodeDev.Core/Types/IMethodInfo.cs | 37 +-
src/NodeDev.Core/Types/NodeClassArrayType.cs | 160 ++-
src/NodeDev.Core/Types/NodeClassType.cs | 119 ++-
src/NodeDev.Core/Types/RealMethodInfo.cs | 2 +-
.../Types/RealMethodParameterInfo.cs | 8 +-
src/NodeDev.Core/Types/RealType.cs | 35 +-
src/NodeDev.Core/Types/TypeBase.cs | 7 +-
src/NodeDev.Core/Types/TypeFactory.cs | 18 +-
.../Types/UndefinedGenericType.cs | 112 +-
src/NodeDev.EndToEndTests/HelperExtensions.cs | 8 +-
src/NodeDev.EndToEndTests/Hooks/Hooks.cs | 241 +++--
src/NodeDev.EndToEndTests/ImplicitUsings.cs | 2 -
src/NodeDev.EndToEndTests/Pages/HomePage.cs | 114 +--
.../MainPageStepDefinitions.cs | 80 +-
src/NodeDev.Tests/EventsTests.cs | 1 -
src/NodeDev.Tests/GraphAnalysisTests.cs | 150 +--
src/NodeDev.Tests/GraphExecutorTests.cs | 70 +-
src/NodeDev.Tests/GraphManagerServiceTests.cs | 445 ++++----
.../NodeClassTypeCreatorTests.cs | 1 -
src/NodeDev.Tests/NodeDevTestsBase.cs | 36 +-
src/NodeDev.Tests/NodeProviderTests.cs | 8 +-
src/NodeDev.Tests/RealTypeTests.cs | 3 +-
src/NodeDev.Tests/SerializableBuildOptions.cs | 36 +-
src/NodeDev.Tests/SerializationTests.cs | 8 +-
src/NodeDev.Tests/TypeBaseTests.cs | 111 +-
src/NodeDev.Tests/TypeFactoryTests.cs | 1 -
.../UndefinedGenericTypeTests.cs | 4 +-
97 files changed, 2274 insertions(+), 2457 deletions(-)
diff --git a/src/NodeDev.Blazor.MAUI/Platforms/Windows/App.xaml.cs b/src/NodeDev.Blazor.MAUI/Platforms/Windows/App.xaml.cs
index ef0adf7..dca6d66 100644
--- a/src/NodeDev.Blazor.MAUI/Platforms/Windows/App.xaml.cs
+++ b/src/NodeDev.Blazor.MAUI/Platforms/Windows/App.xaml.cs
@@ -1,6 +1,4 @@
-using Microsoft.UI.Xaml;
-
-// To learn more about WinUI, the WinUI project structure,
+// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace NodeDev.Blazor.MAUI.WinUI
diff --git a/src/NodeDev.Blazor/Components/DebuggerConsolePanel.razor.cs b/src/NodeDev.Blazor/Components/DebuggerConsolePanel.razor.cs
index 652c685..b07f286 100644
--- a/src/NodeDev.Blazor/Components/DebuggerConsolePanel.razor.cs
+++ b/src/NodeDev.Blazor/Components/DebuggerConsolePanel.razor.cs
@@ -1,8 +1,6 @@
using Microsoft.AspNetCore.Components;
using NodeDev.Core;
-using System.Diagnostics;
using System.Reactive.Subjects;
-using System.Text;
namespace NodeDev.Blazor.Components;
@@ -29,7 +27,7 @@ protected override void OnInitialized()
base.OnInitialized();
RefreshRequiredDisposable = RefreshRequiredSubject.AcceptThenSample(TimeSpan.FromMilliseconds(100)).Subscribe(_ => InvokeAsync(StateHasChanged));
-
+
GraphExecutionChangedDisposable = Project.GraphExecutionChanged.Subscribe(OnGraphExecutionChanged);
}
diff --git a/src/NodeDev.Blazor/Components/GraphCanvas.razor.cs b/src/NodeDev.Blazor/Components/GraphCanvas.razor.cs
index d2e8ee5..26e6210 100644
--- a/src/NodeDev.Blazor/Components/GraphCanvas.razor.cs
+++ b/src/NodeDev.Blazor/Components/GraphCanvas.razor.cs
@@ -7,15 +7,15 @@
using Microsoft.AspNetCore.Components;
using NodeDev.Blazor.DiagramsModels;
using NodeDev.Blazor.NodeAttributes;
+using NodeDev.Blazor.Services;
+using NodeDev.Blazor.Services.GraphManager;
using NodeDev.Core;
+using NodeDev.Core.Class;
using NodeDev.Core.Connections;
using NodeDev.Core.Nodes;
using NodeDev.Core.Types;
using System.Numerics;
using System.Reactive.Linq;
-using NodeDev.Core.Class;
-using NodeDev.Blazor.Services;
-using NodeDev.Blazor.Services.GraphManager;
namespace NodeDev.Blazor.Components;
diff --git a/src/NodeDev.Blazor/DiagramsModels/GraphNodeModel.cs b/src/NodeDev.Blazor/DiagramsModels/GraphNodeModel.cs
index 5ccab66..c62e750 100644
--- a/src/NodeDev.Blazor/DiagramsModels/GraphNodeModel.cs
+++ b/src/NodeDev.Blazor/DiagramsModels/GraphNodeModel.cs
@@ -2,18 +2,13 @@
using NodeDev.Blazor.NodeAttributes;
using NodeDev.Core.Connections;
using NodeDev.Core.Nodes;
-using System;
-using System.Collections.Generic;
-using System.Linq;
using System.Numerics;
-using System.Text;
-using System.Threading.Tasks;
namespace NodeDev.Blazor.DiagramsModels
{
- public class GraphNodeModel : NodeModel
- {
- internal readonly Node Node;
+ public class GraphNodeModel : NodeModel
+ {
+ internal readonly Node Node;
///
/// Set to true by the canvas when the user hit "f2" to edit the node name.
@@ -23,18 +18,18 @@ public class GraphNodeModel : NodeModel
public GraphNodeModel(Node node) : base(new(node.GetOrAddDecoration(() => new(Vector2.Zero)).X, node.GetOrAddDecoration(() => new(Vector2.Zero)).Y))
- {
- Node = node;
- }
+ {
+ Node = node;
+ }
- public GraphPortModel GetPort(Connection connection) => Ports.OfType().First( x=> x.Connection == connection);
+ public GraphPortModel GetPort(Connection connection) => Ports.OfType().First(x => x.Connection == connection);
internal void OnNodeExecuted(Connection exec)
{
}
- internal void OnConnectionPathHighlighted(Connection connection)
+ internal void OnConnectionPathHighlighted(Connection connection)
{
var port = GetPort(connection);
@@ -47,7 +42,7 @@ internal void OnConnectionPathHighlighted(Connection connection)
}
}
- internal void OnConnectionPathUnhighlighted(Connection connection)
+ internal void OnConnectionPathUnhighlighted(Connection connection)
{
var port = GetPort(connection);
@@ -60,23 +55,23 @@ internal void OnConnectionPathUnhighlighted(Connection connection)
internal async Task OnNodeExecuting(Connection exec)
{
- var port = GetPort(exec);
-
- foreach (var link in port.Links.OfType())
- {
- if(!link.Classes.Contains("executing"))
- link.Classes += " executing";
-
- link.Refresh();
- }
-
- var currentCount = ++port.ExecutionCount;
- await Task.Delay(100);
- if (currentCount == port.ExecutionCount)
- {
- foreach (var link in port.Links.OfType())
- {
- link.Classes = link.Classes.Replace(" executing", "");
+ var port = GetPort(exec);
+
+ foreach (var link in port.Links.OfType())
+ {
+ if (!link.Classes.Contains("executing"))
+ link.Classes += " executing";
+
+ link.Refresh();
+ }
+
+ var currentCount = ++port.ExecutionCount;
+ await Task.Delay(100);
+ if (currentCount == port.ExecutionCount)
+ {
+ foreach (var link in port.Links.OfType())
+ {
+ link.Classes = link.Classes.Replace(" executing", "");
link.Refresh();
}
}
diff --git a/src/NodeDev.Blazor/DiagramsModels/GraphPortModel.cs b/src/NodeDev.Blazor/DiagramsModels/GraphPortModel.cs
index 57681a4..a1f2d45 100644
--- a/src/NodeDev.Blazor/DiagramsModels/GraphPortModel.cs
+++ b/src/NodeDev.Blazor/DiagramsModels/GraphPortModel.cs
@@ -5,30 +5,30 @@
namespace NodeDev.Blazor.DiagramsModels;
-public class GraphPortModel: PortModel
+public class GraphPortModel : PortModel
{
- internal readonly Connection Connection;
+ internal readonly Connection Connection;
- internal int ExecutionCount = 0;
+ internal int ExecutionCount = 0;
- internal string PortColor => GraphCanvas.GetTypeShapeColor(Connection.Type, Connection.Parent.TypeFactory);
+ internal string PortColor => GraphCanvas.GetTypeShapeColor(Connection.Type, Connection.Parent.TypeFactory);
- public GraphPortModel(GraphNodeModel parent, Connection connection, bool isInput) : base(parent, isInput ? PortAlignment.Left : PortAlignment.Right)
- {
- Connection = connection;
- }
+ public GraphPortModel(GraphNodeModel parent, Connection connection, bool isInput) : base(parent, isInput ? PortAlignment.Left : PortAlignment.Right)
+ {
+ Connection = connection;
+ }
- public override bool CanAttachTo(ILinkable other)
- {
- if(!base.CanAttachTo(other))
- return false;
+ public override bool CanAttachTo(ILinkable other)
+ {
+ if (!base.CanAttachTo(other))
+ return false;
- if (other is not GraphPortModel otherPort)
- return false;
-
- if(Alignment == otherPort.Alignment) // can't plug input to input or output to output
- return false;
+ if (other is not GraphPortModel otherPort)
+ return false;
- return Connection.Type.IsAssignableTo(otherPort.Connection.Type, out _, out _);
- }
+ if (Alignment == otherPort.Alignment) // can't plug input to input or output to output
+ return false;
+
+ return Connection.Type.IsAssignableTo(otherPort.Connection.Type, out _, out _);
+ }
}
diff --git a/src/NodeDev.Blazor/DiagramsModels/SmoothPathGeneratorWithDirectVertices.cs b/src/NodeDev.Blazor/DiagramsModels/SmoothPathGeneratorWithDirectVertices.cs
index 82a1710..2d406f2 100644
--- a/src/NodeDev.Blazor/DiagramsModels/SmoothPathGeneratorWithDirectVertices.cs
+++ b/src/NodeDev.Blazor/DiagramsModels/SmoothPathGeneratorWithDirectVertices.cs
@@ -93,9 +93,9 @@ private Point GetCurvePoint(Anchor anchor, double pX, double pY, double cX, doub
if (anchor is SinglePortAnchor spa)
{
PortAlignment portAlignment;
- if((isFirstInRoute && first) || (isLastInRoute && !first))
+ if ((isFirstInRoute && first) || (isLastInRoute && !first))
portAlignment = spa.Port.Alignment;
- else if(pX < cX)
+ else if (pX < cX)
portAlignment = PortAlignment.Right;
else
portAlignment = PortAlignment.Left;
diff --git a/src/NodeDev.Blazor/NodeDecorations/NodeDecorationPosition.cs b/src/NodeDev.Blazor/NodeDecorations/NodeDecorationPosition.cs
index cf281b9..b58ac1e 100644
--- a/src/NodeDev.Blazor/NodeDecorations/NodeDecorationPosition.cs
+++ b/src/NodeDev.Blazor/NodeDecorations/NodeDecorationPosition.cs
@@ -1,12 +1,7 @@
using NodeDev.Core.NodeDecorations;
using NodeDev.Core.Types;
-using System;
-using System.Collections.Generic;
-using System.Linq;
using System.Numerics;
-using System.Text;
using System.Text.Json;
-using System.Threading.Tasks;
namespace NodeDev.Blazor.NodeAttributes
{
@@ -24,7 +19,7 @@ public NodeDecorationPosition(Vector2 position)
private record class SerializedNodeDecoration(float X, float Y);
-
+
public string Serialize()
{
return JsonSerializer.Serialize(new SerializedNodeDecoration(X, Y));
@@ -36,5 +31,5 @@ public static INodeDecoration Deserialize(TypeFactory typeFactory, string Json)
return new NodeDecorationPosition(new(serializedNodeDecoration.X, serializedNodeDecoration.Y));
}
- }
+ }
}
diff --git a/src/NodeDev.Blazor/Services/DebuggedPathService.cs b/src/NodeDev.Blazor/Services/DebuggedPathService.cs
index da4d92d..838d423 100644
--- a/src/NodeDev.Blazor/Services/DebuggedPathService.cs
+++ b/src/NodeDev.Blazor/Services/DebuggedPathService.cs
@@ -1,9 +1,6 @@
-
-using Microsoft.AspNetCore.Components;
-using NodeDev.Core;
+using NodeDev.Core;
using NodeDev.Core.Connections;
using NodeDev.Core.Nodes;
-using static MudBlazor.Defaults;
namespace NodeDev.Blazor.Services;
diff --git a/src/NodeDev.Blazor/Services/GraphManager/GraphManagerService.cs b/src/NodeDev.Blazor/Services/GraphManager/GraphManagerService.cs
index 9422263..d97b8de 100644
--- a/src/NodeDev.Blazor/Services/GraphManager/GraphManagerService.cs
+++ b/src/NodeDev.Blazor/Services/GraphManager/GraphManagerService.cs
@@ -10,99 +10,99 @@ namespace NodeDev.Blazor.Services.GraphManager;
///
public class GraphManagerService
{
- private readonly IGraphCanvas GraphCanvas;
-
- private Graph Graph => GraphCanvas.Graph;
-
- public GraphManagerService(IGraphCanvas graphCanvas)
- {
- GraphCanvas = graphCanvas;
- }
-
- public void AddNewConnectionBetween(Connection source, Connection destination)
- {
- Graph.Connect(source, destination, false);
-
- // we're plugging something something with a generic into something without a generic
- if (source.IsAssignableTo(destination, true, true, out var newTypesLeft, out var newTypesRight, out var usedInitialTypes))
- {
- if (newTypesLeft.Count != 0)
- PropagateNewGeneric(source.Parent, newTypesLeft, usedInitialTypes, destination, false);
- if (newTypesRight.Count != 0)
- PropagateNewGeneric(destination.Parent, newTypesRight, usedInitialTypes, source, false);
- }
-
- GraphCanvas.UpdatePortColor(source);
- GraphCanvas.UpdatePortColor(destination);
-
- // we have to disconnect the previously connected exec, since exec outputs can only have one connection
- if (source.Type.IsExec && source.Connections.Count > 1)
- DisconnectConnectionBetween(source, source.Connections.First(x => x != destination));
- else if (!destination.Type.IsExec && destination.Connections.Count > 1) // non-exec inputs can only have one connection
- DisconnectConnectionBetween(destination.Connections.First(x => x != source), destination);
-
- Graph.RaiseGraphChanged(true);
- }
-
- public void DisconnectConnectionBetween(Connection source, Connection destination)
- {
- Graph.Disconnect(source, destination, false);
- GraphCanvas.RemoveLinkFromGraphCanvas(source, destination);
-
- GraphCanvas.UpdatePortColor(source);
- GraphCanvas.UpdatePortColor(destination);
- }
-
- ///
- /// Propagate the new generic type to all the connections of the node and recursively to the connected nodes.
- ///
- /// The connection that initiated the propagation. This is used to avoid reupdating back and forth, sometimes erasing information in the process.
- public void PropagateNewGeneric(Node node, IReadOnlyDictionary changedGenerics, bool useInitialTypes, Connection? initiatingConnection, bool overrideInitialTypes)
- {
+ private readonly IGraphCanvas GraphCanvas;
+
+ private Graph Graph => GraphCanvas.Graph;
+
+ public GraphManagerService(IGraphCanvas graphCanvas)
+ {
+ GraphCanvas = graphCanvas;
+ }
+
+ public void AddNewConnectionBetween(Connection source, Connection destination)
+ {
+ Graph.Connect(source, destination, false);
+
+ // we're plugging something something with a generic into something without a generic
+ if (source.IsAssignableTo(destination, true, true, out var newTypesLeft, out var newTypesRight, out var usedInitialTypes))
+ {
+ if (newTypesLeft.Count != 0)
+ PropagateNewGeneric(source.Parent, newTypesLeft, usedInitialTypes, destination, false);
+ if (newTypesRight.Count != 0)
+ PropagateNewGeneric(destination.Parent, newTypesRight, usedInitialTypes, source, false);
+ }
+
+ GraphCanvas.UpdatePortColor(source);
+ GraphCanvas.UpdatePortColor(destination);
+
+ // we have to disconnect the previously connected exec, since exec outputs can only have one connection
+ if (source.Type.IsExec && source.Connections.Count > 1)
+ DisconnectConnectionBetween(source, source.Connections.First(x => x != destination));
+ else if (!destination.Type.IsExec && destination.Connections.Count > 1) // non-exec inputs can only have one connection
+ DisconnectConnectionBetween(destination.Connections.First(x => x != source), destination);
+
+ Graph.RaiseGraphChanged(true);
+ }
+
+ public void DisconnectConnectionBetween(Connection source, Connection destination)
+ {
+ Graph.Disconnect(source, destination, false);
+ GraphCanvas.RemoveLinkFromGraphCanvas(source, destination);
+
+ GraphCanvas.UpdatePortColor(source);
+ GraphCanvas.UpdatePortColor(destination);
+ }
+
+ ///
+ /// Propagate the new generic type to all the connections of the node and recursively to the connected nodes.
+ ///
+ /// The connection that initiated the propagation. This is used to avoid reupdating back and forth, sometimes erasing information in the process.
+ public void PropagateNewGeneric(Node node, IReadOnlyDictionary changedGenerics, bool useInitialTypes, Connection? initiatingConnection, bool overrideInitialTypes)
+ {
node.OnBeforeGenericTypeDefined(changedGenerics);
- bool hadAnyChanges = false;
- foreach (var port in node.InputsAndOutputs) // check if any of the ports have the generic we just solved
- {
- var previousType = useInitialTypes ? port.InitialType : port.Type;
-
- if (!previousType.GetUndefinedGenericTypes().Any(changedGenerics.ContainsKey))
- continue;
-
- // update port.Type property as well as the textbox visibility if necessary
- port.UpdateTypeAndTextboxVisibility(previousType.ReplaceUndefinedGeneric(changedGenerics), overrideInitialType: overrideInitialTypes);
- hadAnyChanges |= node.GenericConnectionTypeDefined(port).Count != 0;
- GraphCanvas.UpdatePortColor(port);
-
- var isPortInput = port.IsInput; // cache for performance, IsInput is slow
- // check if other connections had their own generics and if we just solved them
- foreach (var other in port.Connections.ToList())
- {
- if (other == initiatingConnection)
- continue;
-
- var source = isPortInput ? other : port;
- var target = isPortInput ? port : other;
- if (source.IsAssignableTo(target, isPortInput, !isPortInput, out var changedGenericsLeft2, out var changedGenericsRight2, out var usedInitialTypes) && (changedGenericsLeft2.Count != 0 || changedGenericsRight2.Count != 0))
- {
- if (changedGenericsLeft2.Count != 0)
- PropagateNewGeneric(port.Parent, changedGenericsLeft2, usedInitialTypes, other, false);
- if (changedGenericsRight2.Count != 0)
- PropagateNewGeneric(other.Parent, changedGenericsRight2, usedInitialTypes, port, false);
- }
- else if ((changedGenericsLeft2?.Count ?? 0) != 0)// looks like changing the generic made it so we can't link to this connection anymore
- DisconnectConnectionBetween(port, other);
- }
- }
-
- if (hadAnyChanges)
- Graph.RaiseGraphChanged(false);
- }
-
- public void SelectNodeOverload(Node popupNode, Node.AlternateOverload overload)
- {
- popupNode.SelectOverload(overload, out var newConnections, out var removedConnections);
-
- Graph.MergeRemovedConnectionsWithNewConnections(newConnections, removedConnections);
- }
+ bool hadAnyChanges = false;
+ foreach (var port in node.InputsAndOutputs) // check if any of the ports have the generic we just solved
+ {
+ var previousType = useInitialTypes ? port.InitialType : port.Type;
+
+ if (!previousType.GetUndefinedGenericTypes().Any(changedGenerics.ContainsKey))
+ continue;
+
+ // update port.Type property as well as the textbox visibility if necessary
+ port.UpdateTypeAndTextboxVisibility(previousType.ReplaceUndefinedGeneric(changedGenerics), overrideInitialType: overrideInitialTypes);
+ hadAnyChanges |= node.GenericConnectionTypeDefined(port).Count != 0;
+ GraphCanvas.UpdatePortColor(port);
+
+ var isPortInput = port.IsInput; // cache for performance, IsInput is slow
+ // check if other connections had their own generics and if we just solved them
+ foreach (var other in port.Connections.ToList())
+ {
+ if (other == initiatingConnection)
+ continue;
+
+ var source = isPortInput ? other : port;
+ var target = isPortInput ? port : other;
+ if (source.IsAssignableTo(target, isPortInput, !isPortInput, out var changedGenericsLeft2, out var changedGenericsRight2, out var usedInitialTypes) && (changedGenericsLeft2.Count != 0 || changedGenericsRight2.Count != 0))
+ {
+ if (changedGenericsLeft2.Count != 0)
+ PropagateNewGeneric(port.Parent, changedGenericsLeft2, usedInitialTypes, other, false);
+ if (changedGenericsRight2.Count != 0)
+ PropagateNewGeneric(other.Parent, changedGenericsRight2, usedInitialTypes, port, false);
+ }
+ else if ((changedGenericsLeft2?.Count ?? 0) != 0)// looks like changing the generic made it so we can't link to this connection anymore
+ DisconnectConnectionBetween(port, other);
+ }
+ }
+
+ if (hadAnyChanges)
+ Graph.RaiseGraphChanged(false);
+ }
+
+ public void SelectNodeOverload(Node popupNode, Node.AlternateOverload overload)
+ {
+ popupNode.SelectOverload(overload, out var newConnections, out var removedConnections);
+
+ Graph.MergeRemovedConnectionsWithNewConnections(newConnections, removedConnections);
+ }
}
diff --git a/src/NodeDev.Blazor/Services/ServicesExtension.cs b/src/NodeDev.Blazor/Services/ServicesExtension.cs
index ed460ca..407d087 100644
--- a/src/NodeDev.Blazor/Services/ServicesExtension.cs
+++ b/src/NodeDev.Blazor/Services/ServicesExtension.cs
@@ -1,10 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using MudBlazor.Services;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
namespace NodeDev.Blazor.Services
{
diff --git a/src/NodeDev.Blazor/Utility.cs b/src/NodeDev.Blazor/Utility.cs
index 20db2af..d575dcf 100644
--- a/src/NodeDev.Blazor/Utility.cs
+++ b/src/NodeDev.Blazor/Utility.cs
@@ -1,13 +1,6 @@
-using NodeDev.Core;
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
-using System.Reactive;
+using System.Diagnostics;
using System.Reactive.Disposables;
using System.Reactive.Linq;
-using System.Text;
-using System.Threading.Tasks;
namespace NodeDev.Blazor;
diff --git a/src/NodeDev.Core/BuildError.cs b/src/NodeDev.Core/BuildError.cs
index d1ced45..859e43f 100644
--- a/src/NodeDev.Core/BuildError.cs
+++ b/src/NodeDev.Core/BuildError.cs
@@ -4,10 +4,10 @@ namespace NodeDev.Core;
public class BuildError : Exception
{
- public readonly Node Node;
+ public readonly Node Node;
- public BuildError(string message, Node node, Exception? inner) : base(message, inner)
- {
- Node = node;
- }
+ public BuildError(string message, Node node, Exception? inner) : base(message, inner)
+ {
+ Node = node;
+ }
}
diff --git a/src/NodeDev.Core/BuildOptions.cs b/src/NodeDev.Core/BuildOptions.cs
index dffc34c..f0b6941 100644
--- a/src/NodeDev.Core/BuildOptions.cs
+++ b/src/NodeDev.Core/BuildOptions.cs
@@ -4,7 +4,7 @@ namespace NodeDev.Core;
public record class BuildOptions(BuildExpressionOptions BuildExpressionOptions, bool PreBuildOnly, string OutputPath)
{
- public static readonly BuildOptions Debug = new(BuildExpressionOptions.Debug, false, "bin/Debug");
+ public static readonly BuildOptions Debug = new(BuildExpressionOptions.Debug, false, "bin/Debug");
- public static readonly BuildOptions Release = new(BuildExpressionOptions.Release, false, "bin/Release");
+ public static readonly BuildOptions Release = new(BuildExpressionOptions.Release, false, "bin/Release");
}
diff --git a/src/NodeDev.Core/Class/NodeClass.cs b/src/NodeDev.Core/Class/NodeClass.cs
index 280c8a1..61e0ccc 100644
--- a/src/NodeDev.Core/Class/NodeClass.cs
+++ b/src/NodeDev.Core/Class/NodeClass.cs
@@ -1,9 +1,4 @@
using NodeDev.Core.Types;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
namespace NodeDev.Core.Class
{
@@ -49,13 +44,13 @@ internal void Deserialize_Step2(SerializedNodeClass serializedNodeClass)
Methods.Add(NodeClassMethod.Deserialize(this, method));
}
- internal void Deserialize_Step3()
+ internal void Deserialize_Step3()
{
foreach (var method in Methods)
method.Deserialize_Step3();
}
- internal SerializedNodeClass Serialize()
+ internal SerializedNodeClass Serialize()
{
var serializedNodeClass = new SerializedNodeClass(Name, Namespace, Methods.Select(x => x.Serialize()).ToList(), Properties.Select(x => x.Serialize()).ToList());
diff --git a/src/NodeDev.Core/Class/NodeClassMethod.cs b/src/NodeDev.Core/Class/NodeClassMethod.cs
index 8a6b76c..dea43ef 100644
--- a/src/NodeDev.Core/Class/NodeClassMethod.cs
+++ b/src/NodeDev.Core/Class/NodeClassMethod.cs
@@ -1,12 +1,7 @@
using NodeDev.Core.Nodes;
using NodeDev.Core.Nodes.Flow;
using NodeDev.Core.Types;
-using System;
-using System.Collections.Generic;
-using System.Linq;
using System.Reflection;
-using System.Text;
-using System.Threading.Tasks;
namespace NodeDev.Core.Class
{
diff --git a/src/NodeDev.Core/Class/NodeClassMethodParameter.cs b/src/NodeDev.Core/Class/NodeClassMethodParameter.cs
index d0208a4..cb7c6d7 100644
--- a/src/NodeDev.Core/Class/NodeClassMethodParameter.cs
+++ b/src/NodeDev.Core/Class/NodeClassMethodParameter.cs
@@ -1,147 +1,141 @@
-using NodeDev.Core.Connections;
-using NodeDev.Core.Nodes;
+using NodeDev.Core.Nodes;
using NodeDev.Core.Nodes.Flow;
using NodeDev.Core.Types;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
namespace NodeDev.Core.Class
{
- public class NodeClassMethodParameter : IMethodParameterInfo
- {
- internal record class SerializedNodeClassMethodParameter(string Name, TypeBase.SerializedType ParameterType, bool? IsOut);
-
- public string Name { get; private set; }
-
- public TypeBase ParameterType { get; private set; }
-
- public NodeClassMethod Method { get; }
-
- public bool IsOut { get; set; }
-
- public NodeClassMethodParameter(string name, TypeBase parameterType, NodeClassMethod method)
- {
- Name = name;
- ParameterType = parameterType;
- Method = method;
- }
-
- internal SerializedNodeClassMethodParameter Serialize()
- {
- return new SerializedNodeClassMethodParameter(Name, ParameterType.SerializeWithFullTypeName(), IsOut);
- }
-
- internal static NodeClassMethodParameter Deserialize(TypeFactory typeFactory, SerializedNodeClassMethodParameter serializedNodeClassMethodParameter, NodeClassMethod nodeClassMethod)
- {
- return new NodeClassMethodParameter(serializedNodeClassMethodParameter.Name, TypeBase.Deserialize(typeFactory, serializedNodeClassMethodParameter.ParameterType), nodeClassMethod)
- {
- IsOut = serializedNodeClassMethodParameter.IsOut ?? false
- };
- }
-
- #region Actions from UI
-
- public void SetIsOut(bool value)
- {
- if (value == IsOut)
- return;
-
- IsOut = value;
-
- RefreshAllMethodCalls();
- RefreshEntryAndReturnNodes();
- }
-
- private void RefreshEntryAndReturnNodes()
- {
- var entry = Method.Graph.Nodes.Values.OfType().First();
- var returnNodes = Method.Graph.Nodes.Values.OfType().ToList();
-
- entry.Refresh();
- foreach (var returnNode in returnNodes)
- returnNode.Refresh();
- }
-
- private void RefreshAllMethodCalls()
- {
- foreach (var methodCall in Method.Class.Project.GetNodes())
- {
- if (methodCall.TargetMethod == Method)
- {
- methodCall.SelectOverload(((IMethodInfo)Method).AlternateOverload(), out var newConnections, out var removedConnections);
-
- methodCall.Graph.MergeRemovedConnectionsWithNewConnections(newConnections, removedConnections);
- }
- }
- }
-
- public void MoveUp()
- {
- var index = Method.Parameters.IndexOf(this);
- if (index <= 0)
- return;
-
- SwapParameter(index, index - 1);
- }
-
- public void MoveDown()
- {
- var index = Method.Parameters.IndexOf(this);
- if (index == Method.Parameters.Count - 1 || index == -1)
- return;
-
- SwapParameter(index, index + 1);
- }
-
- public void Remove()
- {
- var index = Method.Parameters.IndexOf(this);
- if (index == -1)
- return;
-
- Method.Parameters.RemoveAt(index);
-
- RefreshAllMethodCalls();
- RefreshEntryAndReturnNodes();
- }
-
- private void SwapParameter(int index, int newIndex)
- {
- var previous = Method.Parameters[newIndex];
- Method.Parameters[newIndex] = this;
- Method.Parameters[index] = previous;
-
- RefreshAllMethodCalls();
- RefreshEntryAndReturnNodes();
- }
-
- public void Rename(string name)
- {
- var oldName = Name;
- Name = name;
-
- foreach (var methodCall in Method.Graph.Project.GetNodes())
- {
- if (methodCall.TargetMethod == Method)
- methodCall.OnMethodParameterRenamed(oldName, this);
- }
-
- var entry = Method.Graph.Nodes.Values.OfType().FirstOrDefault();
- if (entry != null)
- entry.RenameParameter(this, Method.Parameters.IndexOf(this));
- }
-
- public void ChangeType(TypeBase type)
- {
- ParameterType = type;
-
- RefreshAllMethodCalls();
- RefreshEntryAndReturnNodes();
- }
-
- #endregion
- }
+ public class NodeClassMethodParameter : IMethodParameterInfo
+ {
+ internal record class SerializedNodeClassMethodParameter(string Name, TypeBase.SerializedType ParameterType, bool? IsOut);
+
+ public string Name { get; private set; }
+
+ public TypeBase ParameterType { get; private set; }
+
+ public NodeClassMethod Method { get; }
+
+ public bool IsOut { get; set; }
+
+ public NodeClassMethodParameter(string name, TypeBase parameterType, NodeClassMethod method)
+ {
+ Name = name;
+ ParameterType = parameterType;
+ Method = method;
+ }
+
+ internal SerializedNodeClassMethodParameter Serialize()
+ {
+ return new SerializedNodeClassMethodParameter(Name, ParameterType.SerializeWithFullTypeName(), IsOut);
+ }
+
+ internal static NodeClassMethodParameter Deserialize(TypeFactory typeFactory, SerializedNodeClassMethodParameter serializedNodeClassMethodParameter, NodeClassMethod nodeClassMethod)
+ {
+ return new NodeClassMethodParameter(serializedNodeClassMethodParameter.Name, TypeBase.Deserialize(typeFactory, serializedNodeClassMethodParameter.ParameterType), nodeClassMethod)
+ {
+ IsOut = serializedNodeClassMethodParameter.IsOut ?? false
+ };
+ }
+
+ #region Actions from UI
+
+ public void SetIsOut(bool value)
+ {
+ if (value == IsOut)
+ return;
+
+ IsOut = value;
+
+ RefreshAllMethodCalls();
+ RefreshEntryAndReturnNodes();
+ }
+
+ private void RefreshEntryAndReturnNodes()
+ {
+ var entry = Method.Graph.Nodes.Values.OfType().First();
+ var returnNodes = Method.Graph.Nodes.Values.OfType().ToList();
+
+ entry.Refresh();
+ foreach (var returnNode in returnNodes)
+ returnNode.Refresh();
+ }
+
+ private void RefreshAllMethodCalls()
+ {
+ foreach (var methodCall in Method.Class.Project.GetNodes())
+ {
+ if (methodCall.TargetMethod == Method)
+ {
+ methodCall.SelectOverload(((IMethodInfo)Method).AlternateOverload(), out var newConnections, out var removedConnections);
+
+ methodCall.Graph.MergeRemovedConnectionsWithNewConnections(newConnections, removedConnections);
+ }
+ }
+ }
+
+ public void MoveUp()
+ {
+ var index = Method.Parameters.IndexOf(this);
+ if (index <= 0)
+ return;
+
+ SwapParameter(index, index - 1);
+ }
+
+ public void MoveDown()
+ {
+ var index = Method.Parameters.IndexOf(this);
+ if (index == Method.Parameters.Count - 1 || index == -1)
+ return;
+
+ SwapParameter(index, index + 1);
+ }
+
+ public void Remove()
+ {
+ var index = Method.Parameters.IndexOf(this);
+ if (index == -1)
+ return;
+
+ Method.Parameters.RemoveAt(index);
+
+ RefreshAllMethodCalls();
+ RefreshEntryAndReturnNodes();
+ }
+
+ private void SwapParameter(int index, int newIndex)
+ {
+ var previous = Method.Parameters[newIndex];
+ Method.Parameters[newIndex] = this;
+ Method.Parameters[index] = previous;
+
+ RefreshAllMethodCalls();
+ RefreshEntryAndReturnNodes();
+ }
+
+ public void Rename(string name)
+ {
+ var oldName = Name;
+ Name = name;
+
+ foreach (var methodCall in Method.Graph.Project.GetNodes())
+ {
+ if (methodCall.TargetMethod == Method)
+ methodCall.OnMethodParameterRenamed(oldName, this);
+ }
+
+ var entry = Method.Graph.Nodes.Values.OfType().FirstOrDefault();
+ if (entry != null)
+ entry.RenameParameter(this, Method.Parameters.IndexOf(this));
+ }
+
+ public void ChangeType(TypeBase type)
+ {
+ ParameterType = type;
+
+ RefreshAllMethodCalls();
+ RefreshEntryAndReturnNodes();
+ }
+
+ #endregion
+ }
}
diff --git a/src/NodeDev.Core/Class/NodeClassProperty.cs b/src/NodeDev.Core/Class/NodeClassProperty.cs
index e7cc5f1..bf74306 100644
--- a/src/NodeDev.Core/Class/NodeClassProperty.cs
+++ b/src/NodeDev.Core/Class/NodeClassProperty.cs
@@ -81,8 +81,8 @@ internal SerializedNodeClassProperty Serialize()
{
var serializedNodeClassProperty = new SerializedNodeClassProperty(Name, PropertyType.SerializeWithFullTypeName());
- return serializedNodeClassProperty;
- }
+ return serializedNodeClassProperty;
+ }
#endregion
}
diff --git a/src/NodeDev.Core/Class/NodeClassTypeCreator.cs b/src/NodeDev.Core/Class/NodeClassTypeCreator.cs
index 2a847bf..fe354b4 100644
--- a/src/NodeDev.Core/Class/NodeClassTypeCreator.cs
+++ b/src/NodeDev.Core/Class/NodeClassTypeCreator.cs
@@ -1,11 +1,10 @@
-using System.Reflection.Emit;
-using System.Reflection;
-using NodeDev.Core.Types;
+using Dis2Msil;
using FastExpressionCompiler;
-using Dis2Msil;
-using System.Text;
+using NodeDev.Core.Types;
+using System.Reflection;
+using System.Reflection.Emit;
using System.Runtime.Loader;
-using System.Linq.Expressions;
+using System.Text;
namespace NodeDev.Core.Class;
@@ -150,7 +149,7 @@ public void GetBodyAsCsAndMsilCode(string assemblyPath, NodeClassMethod method,
var assembly = context.LoadFromAssemblyPath(assemblyPath);
var type = assembly.GetType(HiddenName(method.Class.Name));
- if(type == null)
+ if (type == null)
{
msil = "type not found";
return;
diff --git a/src/NodeDev.Core/Connections/Connection.cs b/src/NodeDev.Core/Connections/Connection.cs
index 692b918..d9bf44d 100644
--- a/src/NodeDev.Core/Connections/Connection.cs
+++ b/src/NodeDev.Core/Connections/Connection.cs
@@ -1,188 +1,182 @@
using NodeDev.Core.Nodes;
using NodeDev.Core.Types;
-using System;
-using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
-using System.Linq;
using System.Numerics;
-using System.Text;
-using System.Text.Json;
-using System.Threading.Tasks;
namespace NodeDev.Core.Connections
{
- [System.Diagnostics.DebuggerDisplay("{Parent.Name}:{Name} - {Type}, {Connections.Count}")]
- public class Connection
- {
- public string Id { get; }
+ [System.Diagnostics.DebuggerDisplay("{Parent.Name}:{Name} - {Type}, {Connections.Count}")]
+ public class Connection
+ {
+ public string Id { get; }
- public string Name { get; set; }
+ public string Name { get; set; }
- public Node Parent { get; }
-
- public TypeBase Type { get; private set; }
-
- ///
- /// Initial type of the connection. Used to remember the type before generics were resolved in case we need to re-resolve them differently.
- ///
- public TypeBase InitialType { get; private set; }
-
- internal readonly List _Connections = [];
-
- public IReadOnlyList Connections => _Connections;
-
- ///
- /// Vertices of the connection. Used for drawing connections with multiple segments.
- /// This is defined either if the connection is an input AND not exec, or if the connection is an output AND exec.
- /// This is because for every possible types of inputs there is always only one output connected to it. Except for execs since multiple output paths can be connected to it.
- ///
- public readonly List Vertices = [];
-
- public string? TextboxValue { get; private set; }
-
- public object? ParsedTextboxValue { get; set; }
-
- ///
- /// Global index of this connection in the graph. Each connection of each node in a graph has a unique index.
- ///
- public int GraphIndex { get; set; } = -1;
-
- ///
- /// LinkedExec can be used to make sure a connection can only ever be used while on a path inside the linked exec connection.
- /// Ex. during a 'foreach' loop, the 'Item' connection can only be used inside the 'Loop Exec' connection.
- ///
- public Connection? LinkedExec { get; }
-
- public bool IsInput => Parent.Inputs.Contains(this);
- public bool IsOutput => Parent.Outputs.Contains(this);
-
- public Connection(string name, Node parent, TypeBase type, string? id = null, Connection? linkedExec = null)
- {
- Id = id ?? Guid.NewGuid().ToString();
- Name = name;
- Parent = parent;
- Type = type;
- InitialType = type;
- LinkedExec = linkedExec;
- }
-
- #region Serialization
-
- public record class SerializedConnectionVertex(float X, float Y);
- public record SerializedConnection(string Id, string Name, TypeBase.SerializedType SerializedType, List Connections, string? TextboxValue, List? Vertices, string? LinkedExec);
- internal SerializedConnection Serialize()
- {
- var connections = Connections.ToList();
-
- var serializedConnection = new SerializedConnection(Id, Name, Type.SerializeWithFullTypeName(), Connections.Select(x => x.Id).ToList(), TextboxValue, Vertices.Select(x => new SerializedConnectionVertex(x.X, x.Y)).ToList(), LinkedExec?.Id);
-
- return serializedConnection;
- }
-
- internal static Connection Deserialize(Node parent, SerializedConnection serializedConnectionObj, bool isInput)
- {
- // Find the LinkedExec connection, if any
- Connection? linkedExec = null;
- if (linkedExec != null)
- linkedExec = parent.Graph.Nodes.SelectMany(x => x.Value.InputsAndOutputs).FirstOrDefault(x => x.Id == serializedConnectionObj.LinkedExec);
-
- var type = TypeBase.Deserialize(parent.TypeFactory, serializedConnectionObj.SerializedType);
- var connection = new Connection(serializedConnectionObj.Name, parent, type, serializedConnectionObj.Id, linkedExec);
-
- connection.TextboxValue = serializedConnectionObj.TextboxValue;
- if (connection.TextboxValue != null && isInput)
- connection.ParsedTextboxValue = connection.Type.ParseTextboxEdit(connection.TextboxValue);
-
- if (serializedConnectionObj.Vertices != null)
- connection.Vertices.AddRange(serializedConnectionObj.Vertices.Select(x => new Vector2(x.X, x.Y)));
-
- foreach (var connectionId in serializedConnectionObj.Connections)
- {
- var otherConnection = parent.Graph.Nodes.SelectMany(x => x.Value.InputsAndOutputs).FirstOrDefault(x => x.Id == connectionId);
- if (otherConnection == null)
- continue;
-
- if (!connection.Connections.Contains(otherConnection))
- connection._Connections.Add(otherConnection);
- if (!otherConnection.Connections.Contains(connection))
- otherConnection._Connections.Add(connection);
- }
-
- return connection;
- }
-
- #endregion
-
- public bool IsAssignableTo(Connection other, bool alsoValidateInitialTypeSource, bool alsoValidateInitialTypeDestination, [MaybeNullWhen(false)] out Dictionary changedGenericsLeft, [MaybeNullWhen(false)] out Dictionary changedGenericsRight, out bool usedInitialTypes)
- {
- if (Type.IsAssignableTo(other.Type, out var changedGenericsLeft1, out var changedGenericsRight1, out var depth1))
- {
- if (alsoValidateInitialTypeSource || alsoValidateInitialTypeDestination)
- {
- var initialType = alsoValidateInitialTypeSource ? InitialType : Type;
- var otherInitialType = alsoValidateInitialTypeDestination ? other.InitialType : other.Type;
- if ((initialType != Type || otherInitialType != other.Type) && initialType.IsAssignableTo(otherInitialType, out var changedGenericsLeft2, out var changedGenericsRight2, out var depth2))
- {
- if ((changedGenericsLeft2.Count != 0 || changedGenericsRight2.Count != 0) && depth2 < depth1)
- {
- changedGenericsLeft = changedGenericsLeft2;
- changedGenericsRight = changedGenericsRight2;
- usedInitialTypes = true;
- return true;
- }
- }
- }
-
- changedGenericsLeft = changedGenericsLeft1;
- changedGenericsRight = changedGenericsRight1;
- usedInitialTypes = false;
- return true;
- }
-
- changedGenericsLeft = changedGenericsRight = null;
- usedInitialTypes = false;
- return false;
- }
-
- public void UpdateVertices(IEnumerable vertices)
- {
- Vertices.Clear();
- Vertices.AddRange(vertices);
- }
-
-
- public void UpdateTypeAndTextboxVisibility(TypeBase newType, bool overrideInitialType)
- {
- Type = newType;
-
- if (overrideInitialType)
- InitialType = newType;
-
- if (Type.AllowTextboxEdit)
- TextboxValue = Type.DefaultTextboxValue;
- else
- TextboxValue = null;
- }
-
- public void UpdateTextboxText(string? text)
- {
- if (Type.AllowTextboxEdit)
- {
- TextboxValue = text;
- if (text == null)
- ParsedTextboxValue = null;
- else
- {
- try
- {
- ParsedTextboxValue = Type.ParseTextboxEdit(text);
- }
- catch (Exception)
- { }
- }
- }
- }
-
-
- }
+ public Node Parent { get; }
+
+ public TypeBase Type { get; private set; }
+
+ ///
+ /// Initial type of the connection. Used to remember the type before generics were resolved in case we need to re-resolve them differently.
+ ///
+ public TypeBase InitialType { get; private set; }
+
+ internal readonly List _Connections = [];
+
+ public IReadOnlyList Connections => _Connections;
+
+ ///
+ /// Vertices of the connection. Used for drawing connections with multiple segments.
+ /// This is defined either if the connection is an input AND not exec, or if the connection is an output AND exec.
+ /// This is because for every possible types of inputs there is always only one output connected to it. Except for execs since multiple output paths can be connected to it.
+ ///
+ public readonly List Vertices = [];
+
+ public string? TextboxValue { get; private set; }
+
+ public object? ParsedTextboxValue { get; set; }
+
+ ///
+ /// Global index of this connection in the graph. Each connection of each node in a graph has a unique index.
+ ///
+ public int GraphIndex { get; set; } = -1;
+
+ ///
+ /// LinkedExec can be used to make sure a connection can only ever be used while on a path inside the linked exec connection.
+ /// Ex. during a 'foreach' loop, the 'Item' connection can only be used inside the 'Loop Exec' connection.
+ ///
+ public Connection? LinkedExec { get; }
+
+ public bool IsInput => Parent.Inputs.Contains(this);
+ public bool IsOutput => Parent.Outputs.Contains(this);
+
+ public Connection(string name, Node parent, TypeBase type, string? id = null, Connection? linkedExec = null)
+ {
+ Id = id ?? Guid.NewGuid().ToString();
+ Name = name;
+ Parent = parent;
+ Type = type;
+ InitialType = type;
+ LinkedExec = linkedExec;
+ }
+
+ #region Serialization
+
+ public record class SerializedConnectionVertex(float X, float Y);
+ public record SerializedConnection(string Id, string Name, TypeBase.SerializedType SerializedType, List Connections, string? TextboxValue, List? Vertices, string? LinkedExec);
+ internal SerializedConnection Serialize()
+ {
+ var connections = Connections.ToList();
+
+ var serializedConnection = new SerializedConnection(Id, Name, Type.SerializeWithFullTypeName(), Connections.Select(x => x.Id).ToList(), TextboxValue, Vertices.Select(x => new SerializedConnectionVertex(x.X, x.Y)).ToList(), LinkedExec?.Id);
+
+ return serializedConnection;
+ }
+
+ internal static Connection Deserialize(Node parent, SerializedConnection serializedConnectionObj, bool isInput)
+ {
+ // Find the LinkedExec connection, if any
+ Connection? linkedExec = null;
+ if (linkedExec != null)
+ linkedExec = parent.Graph.Nodes.SelectMany(x => x.Value.InputsAndOutputs).FirstOrDefault(x => x.Id == serializedConnectionObj.LinkedExec);
+
+ var type = TypeBase.Deserialize(parent.TypeFactory, serializedConnectionObj.SerializedType);
+ var connection = new Connection(serializedConnectionObj.Name, parent, type, serializedConnectionObj.Id, linkedExec);
+
+ connection.TextboxValue = serializedConnectionObj.TextboxValue;
+ if (connection.TextboxValue != null && isInput)
+ connection.ParsedTextboxValue = connection.Type.ParseTextboxEdit(connection.TextboxValue);
+
+ if (serializedConnectionObj.Vertices != null)
+ connection.Vertices.AddRange(serializedConnectionObj.Vertices.Select(x => new Vector2(x.X, x.Y)));
+
+ foreach (var connectionId in serializedConnectionObj.Connections)
+ {
+ var otherConnection = parent.Graph.Nodes.SelectMany(x => x.Value.InputsAndOutputs).FirstOrDefault(x => x.Id == connectionId);
+ if (otherConnection == null)
+ continue;
+
+ if (!connection.Connections.Contains(otherConnection))
+ connection._Connections.Add(otherConnection);
+ if (!otherConnection.Connections.Contains(connection))
+ otherConnection._Connections.Add(connection);
+ }
+
+ return connection;
+ }
+
+ #endregion
+
+ public bool IsAssignableTo(Connection other, bool alsoValidateInitialTypeSource, bool alsoValidateInitialTypeDestination, [MaybeNullWhen(false)] out Dictionary changedGenericsLeft, [MaybeNullWhen(false)] out Dictionary changedGenericsRight, out bool usedInitialTypes)
+ {
+ if (Type.IsAssignableTo(other.Type, out var changedGenericsLeft1, out var changedGenericsRight1, out var depth1))
+ {
+ if (alsoValidateInitialTypeSource || alsoValidateInitialTypeDestination)
+ {
+ var initialType = alsoValidateInitialTypeSource ? InitialType : Type;
+ var otherInitialType = alsoValidateInitialTypeDestination ? other.InitialType : other.Type;
+ if ((initialType != Type || otherInitialType != other.Type) && initialType.IsAssignableTo(otherInitialType, out var changedGenericsLeft2, out var changedGenericsRight2, out var depth2))
+ {
+ if ((changedGenericsLeft2.Count != 0 || changedGenericsRight2.Count != 0) && depth2 < depth1)
+ {
+ changedGenericsLeft = changedGenericsLeft2;
+ changedGenericsRight = changedGenericsRight2;
+ usedInitialTypes = true;
+ return true;
+ }
+ }
+ }
+
+ changedGenericsLeft = changedGenericsLeft1;
+ changedGenericsRight = changedGenericsRight1;
+ usedInitialTypes = false;
+ return true;
+ }
+
+ changedGenericsLeft = changedGenericsRight = null;
+ usedInitialTypes = false;
+ return false;
+ }
+
+ public void UpdateVertices(IEnumerable vertices)
+ {
+ Vertices.Clear();
+ Vertices.AddRange(vertices);
+ }
+
+
+ public void UpdateTypeAndTextboxVisibility(TypeBase newType, bool overrideInitialType)
+ {
+ Type = newType;
+
+ if (overrideInitialType)
+ InitialType = newType;
+
+ if (Type.AllowTextboxEdit)
+ TextboxValue = Type.DefaultTextboxValue;
+ else
+ TextboxValue = null;
+ }
+
+ public void UpdateTextboxText(string? text)
+ {
+ if (Type.AllowTextboxEdit)
+ {
+ TextboxValue = text;
+ if (text == null)
+ ParsedTextboxValue = null;
+ else
+ {
+ try
+ {
+ ParsedTextboxValue = Type.ParseTextboxEdit(text);
+ }
+ catch (Exception)
+ { }
+ }
+ }
+ }
+
+
+ }
}
diff --git a/src/NodeDev.Core/Graph.cs b/src/NodeDev.Core/Graph.cs
index c958061..6c64205 100644
--- a/src/NodeDev.Core/Graph.cs
+++ b/src/NodeDev.Core/Graph.cs
@@ -2,114 +2,111 @@
using NodeDev.Core.Connections;
using NodeDev.Core.Nodes;
using NodeDev.Core.Nodes.Flow;
-using System.Collections.Concurrent;
using System.Linq.Expressions;
-using System.Reflection;
-using System.Text.Json;
namespace NodeDev.Core;
public class Graph
{
- public IReadOnlyDictionary Nodes { get; } = new Dictionary();
-
- public NodeClass SelfClass => SelfMethod.Class;
- public NodeClassMethod SelfMethod { get; set; }
-
- public Project Project => SelfMethod.Class.Project;
-
- static Graph()
- {
- NodeProvider.Initialize();
- }
-
- public void RaiseGraphChanged(bool requireUIRefresh) => Project.GraphChangedSubject.OnNext((this, requireUIRefresh));
-
- #region GetChunks
-
- public class BadMergeException(Connection input) : Exception($"Error merging path to {input.Name} of tool {input.Parent.Name}") { }
- public class DeadEndNotAllowed(List inputs) : Exception(inputs.Count == 1 ? $"Dead end not allowed in {inputs[0].Name} of tool {inputs[0].Parent.Name}" : $"Dead end not allowed in {inputs.Count} tools") { }
-
-
- ///
- /// Contains either the straight path between two lines of connections (such as a simple a -> b -> c) or a subchunk of paths.
- /// The subchunk is used to group an entire chunk such as. It would contain "c" and "d" in the following example:
- /// -> c
- /// a -> b | -> e
- /// -> d
- /// "e" would be added in the next chunk, along with anything else after it
- /// Only one of the two values is set at once, either or
- /// Both and can be null, in that case it means that chunk part is a dead end.
- ///
- internal record class NodePathChunkPart(Connection Input, Connection? Output, Dictionary? SubChunk)
- {
- internal bool ContainOutput(Connection output)
- {
- if (Output == output)
- return true;
-
- if (SubChunk?.Count > 0)
- {
- foreach (var subChunk in SubChunk)
- {
- if (subChunk.Value.ContainOutput(output))
- return true;
- }
- }
-
- return false;
- }
- }
-
- ///
- /// Contains the starting point of the path, the chunks of the path and the merging point of the path.
- /// If both InputMergePoint and DeadEndInput are null, the path simply led nowhere at all. Should rarely be the case in a valid graph.
- ///
- /// Start point of the path.
- /// List of all the chunks inside the path.
- /// Merging point at the end of the path. Null if the path was a dead end or a breaking such as "Return".
- /// If the path was a dead end, this is the inputs that led to the dead end. Ex if both side of a branch ends in dead end, they will both indicate their last input.
- internal record class NodePathChunks(Connection OutputStartPoint, List Chunks, Connection? InputMergePoint, List? DeadEndInputs)
- {
- internal bool ContainOutput(Connection output)
- {
+ public IReadOnlyDictionary Nodes { get; } = new Dictionary();
+
+ public NodeClass SelfClass => SelfMethod.Class;
+ public NodeClassMethod SelfMethod { get; set; }
+
+ public Project Project => SelfMethod.Class.Project;
+
+ static Graph()
+ {
+ NodeProvider.Initialize();
+ }
+
+ public void RaiseGraphChanged(bool requireUIRefresh) => Project.GraphChangedSubject.OnNext((this, requireUIRefresh));
+
+ #region GetChunks
+
+ public class BadMergeException(Connection input) : Exception($"Error merging path to {input.Name} of tool {input.Parent.Name}") { }
+ public class DeadEndNotAllowed(List inputs) : Exception(inputs.Count == 1 ? $"Dead end not allowed in {inputs[0].Name} of tool {inputs[0].Parent.Name}" : $"Dead end not allowed in {inputs.Count} tools") { }
+
+
+ ///
+ /// Contains either the straight path between two lines of connections (such as a simple a -> b -> c) or a subchunk of paths.
+ /// The subchunk is used to group an entire chunk such as. It would contain "c" and "d" in the following example:
+ /// -> c
+ /// a -> b | -> e
+ /// -> d
+ /// "e" would be added in the next chunk, along with anything else after it
+ /// Only one of the two values is set at once, either or
+ /// Both and can be null, in that case it means that chunk part is a dead end.
+ ///
+ internal record class NodePathChunkPart(Connection Input, Connection? Output, Dictionary? SubChunk)
+ {
+ internal bool ContainOutput(Connection output)
+ {
+ if (Output == output)
+ return true;
+
+ if (SubChunk?.Count > 0)
+ {
+ foreach (var subChunk in SubChunk)
+ {
+ if (subChunk.Value.ContainOutput(output))
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
+ ///
+ /// Contains the starting point of the path, the chunks of the path and the merging point of the path.
+ /// If both InputMergePoint and DeadEndInput are null, the path simply led nowhere at all. Should rarely be the case in a valid graph.
+ ///
+ /// Start point of the path.
+ /// List of all the chunks inside the path.
+ /// Merging point at the end of the path. Null if the path was a dead end or a breaking such as "Return".
+ /// If the path was a dead end, this is the inputs that led to the dead end. Ex if both side of a branch ends in dead end, they will both indicate their last input.
+ internal record class NodePathChunks(Connection OutputStartPoint, List Chunks, Connection? InputMergePoint, List? DeadEndInputs)
+ {
+ internal bool ContainOutput(Connection output)
+ {
if (OutputStartPoint == output)
return true;
- foreach (var part in Chunks)
- {
- if (part.ContainOutput(output))
- return true;
- }
-
- return false;
- }
- }
-
- ///
- /// Get the chunk of a path until the next merging point. In the following example, we would chunk something like "a, (b.c, b.d), e":
- /// -> c
- /// a -> b | -> e
- /// -> d
- /// if "e" was to be merging with another path, we'd stop at "e" and return it as a merging point.
- ///
- internal NodePathChunks GetChunks(Connection execOutput, bool allowDeadEnd)
- {
- var chunks = new List();
-
- var currentInput = execOutput.Connections.FirstOrDefault();
- if (currentInput == null)
- {
- if (!allowDeadEnd)
- throw new DeadEndNotAllowed([]);
-
- return new NodePathChunks(execOutput, chunks, null, null); // the path led nowhere
- }
-
- while (true)
- {
- if (currentInput.Parent.Outputs.Count(x => x.Type.IsExec) <= 1) // we can keep adding to the straight path. It's either a dead end or the path keeps going
- {
+ foreach (var part in Chunks)
+ {
+ if (part.ContainOutput(output))
+ return true;
+ }
+
+ return false;
+ }
+ }
+
+ ///
+ /// Get the chunk of a path until the next merging point. In the following example, we would chunk something like "a, (b.c, b.d), e":
+ /// -> c
+ /// a -> b | -> e
+ /// -> d
+ /// if "e" was to be merging with another path, we'd stop at "e" and return it as a merging point.
+ ///
+ internal NodePathChunks GetChunks(Connection execOutput, bool allowDeadEnd)
+ {
+ var chunks = new List();
+
+ var currentInput = execOutput.Connections.FirstOrDefault();
+ if (currentInput == null)
+ {
+ if (!allowDeadEnd)
+ throw new DeadEndNotAllowed([]);
+
+ return new NodePathChunks(execOutput, chunks, null, null); // the path led nowhere
+ }
+
+ while (true)
+ {
+ if (currentInput.Parent.Outputs.Count(x => x.Type.IsExec) <= 1) // we can keep adding to the straight path. It's either a dead end or the path keeps going
+ {
if (currentInput.Connections.Count != 1)
{
// We reached a merging point. We need to validate if this merging point was already validated by the last chunk. It is possible that we are in a junction like so :
@@ -131,396 +128,396 @@ internal NodePathChunks GetChunks(Connection execOutput, bool allowDeadEnd)
// ... <- we are here
// If we are indeed in this scenario, everything from the first "if" should be in the "chunks"
// We can check any merge point of the last chunk. They should all either be the same of null
- if(chunks.LastOrDefault()?.SubChunk?.Values?.FirstOrDefault(x => x.InputMergePoint != null)?.InputMergePoint != currentInput)
+ if (chunks.LastOrDefault()?.SubChunk?.Values?.FirstOrDefault(x => x.InputMergePoint != null)?.InputMergePoint != currentInput)
return new NodePathChunks(execOutput, chunks, currentInput, null); // we reached a merging point
}
// find the next output node to follow
var output = currentInput.Parent.Outputs.FirstOrDefault(x => x.Type.IsExec && x != execOutput);
- var nextInput = output?.Connections.FirstOrDefault(); // get the next input, there's either 1 or none
-
- if (nextInput == null)
- {
- // We've reached a dead end, let's check if we were allowed to in the first place
- // Some nodes like "Return" are allowed to be dead ends, so we check the parent of that last node see if it's allowed
- if (!allowDeadEnd && !currentInput.Parent.BreaksDeadEnd)
- throw new DeadEndNotAllowed([currentInput]);
-
- chunks.Add(new NodePathChunkPart(currentInput, null, null));
- return new NodePathChunks(execOutput, chunks, null, [currentInput]); // we reached a dead end
- }
-
- // add the current node to the chunks, after we know we can keep going
- chunks.Add(new NodePathChunkPart(currentInput, output, null));
-
- currentInput = nextInput; // we can keep going
-
- if (currentInput.Connections.Count != 1)
- return new NodePathChunks(execOutput, chunks, currentInput, null); // we reached a merging point
- }
- else // we have a subchunk
- {
- // Get all the chunks of the node (example, both "c" and "d" in the example above)
- var subChunk = GetChunks(currentInput, currentInput.Parent, allowDeadEnd);
-
- if (subChunk.Count == 0)
- return new NodePathChunks(execOutput, chunks, null, [currentInput]); // we reached a dead end
-
- // We had some actual path, add it to the chunks
- var part = new NodePathChunkPart(currentInput, null, subChunk);
- chunks.Add(part);
-
- // get the merge point, it should be either null if it's all dead end, or all the same merge point. No need to validate, as GetChunks already did
- var mergePoint = subChunk.Values.FirstOrDefault(x => x.InputMergePoint != null)?.InputMergePoint;
-
- if (mergePoint != null)
- {
- // we are not the only one merging here, we need to check if all the other paths are sub chunks of ours.
- // That would mean we can keep going, otherwise we need to stop here and let our parent handle the merge.
- foreach (var mergeOutput in mergePoint.Connections)
- {
- if (!part.ContainOutput(mergeOutput))
- {
- // we can't keep going, we need to stop here and let our parent handle the merge
- return new NodePathChunks(execOutput, chunks, mergePoint, null);
- }
- }
-
- // It's all our stuff, we can keep going
-
- // we merged back, we can keep going with the next nodes along the path since it's still our path
- currentInput = mergePoint;
- }
- else
- {
- // we reached a dead end, we can stop the path here
- var deadEndInputs = subChunk.Values.Where(x => x.DeadEndInputs != null).ToList();
- if (deadEndInputs.Count != 0) // we have dead ends, not just "nothing connected to the outputs"
- return new NodePathChunks(execOutput, chunks, null, deadEndInputs.SelectMany(x => x.DeadEndInputs!).ToList());
- else
- return new NodePathChunks(execOutput, chunks, null, [currentInput]); // we reached a dead end
- }
- }
- }
- }
-
- ///
- /// Get all the chunks of a node. This will recursively get all the chunks of the outputs of the node.
- ///
- /// Exec input that was used to enter that node.
- /// Node to get all the chunks from.
- /// Does a parent allow dead end here.
- ///
- ///
- private Dictionary GetChunks(Connection input, Node node, bool allowDeadEnd)
- {
- var chunks = new Dictionary();
-
- foreach (var output in node.Outputs.Where(x => x.Type.IsExec))
- {
- // allowDeadEnd is prioritized over the node's own setting, since we can have a dead end if the parent allows it.
- // Cases like a "branch" inside a loop can be a dead end, even though branch doesn't allow it, because the loop does.
- var chunk = GetChunks(output, allowDeadEnd || node.DoesOutputPathAllowDeadEnd(output));
- chunks[output] = chunk;
-
- // Validate if the chunk is a merge and if it is allowed
- if (chunk.InputMergePoint != null && !node.DoesOutputPathAllowMerge(output))
- throw new BadMergeException(output);
- }
-
- if (chunks.Count == 0) // it's a dead end because the node doesn't even have an exec output
- return chunks;
-
- // Validate that the dead ends are allowed. They are either allowed if the parent path allows it or if they are breaking nodes like "Return"
- if (!allowDeadEnd)
- {
- var hasInvalidDeadEnd = chunks
- .Where(x => !node.DoesOutputPathAllowDeadEnd(x.Key) && (x.Value.DeadEndInputs != null || x.Value.InputMergePoint == null)) // x.Value.InputMergePoint == null is a dead end because the output connection was not connected to anything
- .Where(x => x.Value.DeadEndInputs?.All(y => !y!.Parent.BreaksDeadEnd) ?? true) // ?? true because any dead end by "no connection" is automatically an invalid dead end
- .ToList();
-
- if (hasInvalidDeadEnd.Count != 0)
- {
- var deadEnds = new List();
- foreach (var invalidDeadEnd in hasInvalidDeadEnd)
- {
- if (invalidDeadEnd.Value.DeadEndInputs != null)
- deadEnds.AddRange(invalidDeadEnd.Value.DeadEndInputs);
- else // it's a dead end because the output connection was not connected to anything
- deadEnds.Add(input);
- }
-
- throw new DeadEndNotAllowed(deadEnds);
- }
- }
-
- // validate that all the chunks have the same merging point. If not, the path that don't merge at the same place must be dead ends
- var nbDifferentMergePoint = chunks.Values.Where(x => x.InputMergePoint != null).Select(x => x.InputMergePoint).Distinct().Count();
- if (nbDifferentMergePoint > 1) // all the same or none is fine, but more than one is not
- throw new BadMergeException(chunks.Values.First(x => x.InputMergePoint != null).InputMergePoint!); // we can throw any of the inputs, they all have different merging points
-
- return chunks;
- }
-
- #endregion
-
- #region BuildExpression
-
- public LambdaExpression BuildExpression(BuildExpressionOptions options)
- {
- var entry = (Nodes.Values.FirstOrDefault(x => x is EntryNode)?.Outputs.FirstOrDefault()) ?? throw new Exception($"No entry node found in graph {SelfMethod.Name}");
- var returnLabelTarget = !SelfMethod.HasReturnValue ? Expression.Label("ReturnLabel") : Expression.Label(SelfMethod.ReturnType.MakeRealType(), "ReturnLabel");
-
- var info = new BuildExpressionInfo(returnLabelTarget, options, SelfMethod.IsStatic ? null : Expression.Parameter(SelfClass.ClassTypeBase.MakeRealType(), "this"));
-
- // Create a variable for each output parameter
- foreach (var parameter in SelfMethod.Parameters)
- {
- if (parameter.ParameterType.IsExec)
- continue;
-
- var type = parameter.ParameterType.MakeRealType();
- var variable = Expression.Parameter(type, parameter.Name);
- info.MethodParametersExpression[parameter.Name] = variable;
- }
-
- // Create a variable for each node's output
- foreach (var node in Nodes.Values)
- {
- if (node.CanBeInlined)
- continue; // this can be inlined, no need to create local variables for stuff such as "a + b"
-
- // normal execution nodes each have their own local variable for every output
- // Their input connections will be inlined or point to another local variable of someone else's output
- foreach ((var connection, var variable) in node.CreateOutputsLocalVariableExpressions(info))
- info.LocalVariables[connection] = variable;
- }
-
- var chunks = GetChunks(entry, false);
-
- var expressions = BuildExpression(chunks, info);
-
- // Create the return label with its default return value (if needed)
- var returnLabel = SelfMethod.HasReturnValue ? Expression.Label(returnLabelTarget, Expression.Default(returnLabelTarget.Type)) : Expression.Label(returnLabelTarget);
- // create a list of all the local variables that were used in the entire method
- var localVariables = info.LocalVariables.Values
- .OfType()
- .Distinct() // lots of inputs use the same variable as another node's output, make sure we only declare them once
- .Except(info.MethodParametersExpression.Values); // Remove the method parameters as they are declared later and not here
-
- var expressionBlock = Expression.Block(localVariables, expressions.Append(returnLabel));
-
- var parameters = SelfMethod.IsStatic ? info.MethodParametersExpression.Values : info.MethodParametersExpression.Values.Prepend(info.ThisExpression!);
- var lambdaExpression = Expression.Lambda(expressionBlock, parameters);
-
- return lambdaExpression;
- }
-
- internal static Expression[] BuildExpression(NodePathChunks chunks, BuildExpressionInfo info)
- {
- var expressions = new Expression[chunks.Chunks.Count];
-
- for (int i = 0; i < chunks.Chunks.Count; ++i)
- {
- var chunk = chunks.Chunks[i];
-
- // connect all the inputs to it's inputs
- foreach (var input in chunk.Input.Parent.Inputs)
- ConnectInputExpression(input, info);
-
- try
- {
- var expression = chunk.Input.Parent.BuildExpression(chunk.SubChunk, info);
- expressions[i] = expression;
- }
- catch (Exception ex) when (ex is not BuildError)
- {
- throw new BuildError(ex.Message, chunk.Input.Parent, ex);
- }
- }
-
- return expressions;
- }
-
- private static void BuildInlineExpression(Node node, BuildExpressionInfo info)
- {
- if (info.InlinedNodes.Contains(node))
- return;
-
- if (!node.CanBeInlined)
- throw new Exception($"{nameof(BuildInlineExpression)} can only be called on nodes that can be inlined: {node.Name}");
-
- foreach (var input in node.Inputs)
- {
- ConnectInputExpression(input, info);
- }
-
- // now that all our dependencies are built, we can build the node itself
- try
- {
- node.BuildInlineExpression(info);
- }
- catch (Exception ex) when (ex is not BuildError)
- {
- throw new BuildError(ex.Message, node, ex);
- }
-
- info.InlinedNodes.Add(node);
- }
-
- private static void ConnectInputExpression(Connection input, BuildExpressionInfo info)
- {
- if (input.Type.IsExec)
- return;
-
- if (input.Connections.Count == 0)
- {
- if (!input.Type.AllowTextboxEdit || input.ParsedTextboxValue == null)
- info.LocalVariables[input] = Expression.Default(input.Type.MakeRealType());
- else
- info.LocalVariables[input] = Expression.Constant(input.ParsedTextboxValue, input.Type.MakeRealType());
- }
- else
- {
- var otherNode = input.Connections[0].Parent;
- if (otherNode.CanBeInlined)
- BuildInlineExpression(otherNode, info);
-
- // Get the local variable or expression associated with that input and use it as that input's expression
- info.LocalVariables[input] = info.LocalVariables[input.Connections[0]];
- }
- }
-
- #endregion
-
- #region Invoke
-
- public Task Invoke(Action action)
- {
- return Invoke(() =>
- {
- action();
- return Task.CompletedTask;
- });
- }
-
- public async Task Invoke(Func action)
- {
- await action(); // temporary
- }
-
- #endregion
-
- #region Connect / Disconnect / MergedRemovedConnectionsWithNewConnections
-
- public void MergeRemovedConnectionsWithNewConnections(List newConnections, List removedConnections)
- {
- foreach (var removedConnection in removedConnections)
- {
- var newConnection = newConnections.FirstOrDefault(x => x.Parent == removedConnection.Parent && x.Name == removedConnection.Name && x.Type == removedConnection.Type);
-
- // if we found a new connection, connect them together and remove the old connection
- foreach (var oldLink in removedConnection.Connections)
- {
- oldLink._Connections.Remove(removedConnection); // cleanup the old connection
-
- if (newConnection != null)
- {
- // Before we re-connect them let's make sure both are inputs or both outputs
- if (oldLink.Parent.Outputs.Contains(oldLink) == newConnection.Parent.Inputs.Contains(newConnection))
- {
- // we can safely reconnect the new connection to the old link
- // Either newConnection is an input and removedConnection is an output or vice versa
- newConnection._Connections.Add(oldLink);
- oldLink._Connections.Add(newConnection);
- }
- }
- }
- }
-
- RaiseGraphChanged(true); // this always require refreshing the UI as this is custom behavior that needs to be replicated by the UI
- }
-
- public void Connect(Connection connection1, Connection connection2, bool requireUIRefresh)
- {
- if (!connection1._Connections.Contains(connection2))
- connection1._Connections.Add(connection2);
- if (!connection2._Connections.Contains(connection1))
- connection2._Connections.Add(connection1);
-
- RaiseGraphChanged(requireUIRefresh);
- }
-
- public void Disconnect(Connection connection1, Connection connection2, bool requireUIRefresh)
- {
- connection1._Connections.Remove(connection2);
- connection2._Connections.Remove(connection1);
-
- RaiseGraphChanged(requireUIRefresh);
- }
-
- #endregion
-
- #region AddNode
-
- public void AddNode(Node node, bool requireUIRefresh)
- {
- ((IDictionary)Nodes)[node.Id] = node;
-
- RaiseGraphChanged(requireUIRefresh);
- }
-
- public Node AddNode(NodeProvider.NodeSearchResult searchResult, bool requireUIRefresh)
- {
- var node = (Node)Activator.CreateInstance(searchResult.Type, this, null)!;
- AddNode(node, requireUIRefresh);
-
- if (searchResult is NodeProvider.MethodCallNode methodCall && node is MethodCall methodCallNode)
- methodCallNode.SetMethodTarget(methodCall.MethodInfo);
- else if (searchResult is NodeProvider.GetPropertyOrFieldNode getPropertyOrField && node is GetPropertyOrField getPropertyOrFieldNode)
- getPropertyOrFieldNode.SetMemberTarget(getPropertyOrField.MemberInfo);
- else if (searchResult is NodeProvider.SetPropertyOrFieldNode setPropertyOrField && node is SetPropertyOrField setPropertyOrFieldNode)
- setPropertyOrFieldNode.SetMemberTarget(setPropertyOrField.MemberInfo);
-
- return node;
- }
+ var nextInput = output?.Connections.FirstOrDefault(); // get the next input, there's either 1 or none
+
+ if (nextInput == null)
+ {
+ // We've reached a dead end, let's check if we were allowed to in the first place
+ // Some nodes like "Return" are allowed to be dead ends, so we check the parent of that last node see if it's allowed
+ if (!allowDeadEnd && !currentInput.Parent.BreaksDeadEnd)
+ throw new DeadEndNotAllowed([currentInput]);
+
+ chunks.Add(new NodePathChunkPart(currentInput, null, null));
+ return new NodePathChunks(execOutput, chunks, null, [currentInput]); // we reached a dead end
+ }
+
+ // add the current node to the chunks, after we know we can keep going
+ chunks.Add(new NodePathChunkPart(currentInput, output, null));
+
+ currentInput = nextInput; // we can keep going
+
+ if (currentInput.Connections.Count != 1)
+ return new NodePathChunks(execOutput, chunks, currentInput, null); // we reached a merging point
+ }
+ else // we have a subchunk
+ {
+ // Get all the chunks of the node (example, both "c" and "d" in the example above)
+ var subChunk = GetChunks(currentInput, currentInput.Parent, allowDeadEnd);
+
+ if (subChunk.Count == 0)
+ return new NodePathChunks(execOutput, chunks, null, [currentInput]); // we reached a dead end
+
+ // We had some actual path, add it to the chunks
+ var part = new NodePathChunkPart(currentInput, null, subChunk);
+ chunks.Add(part);
+
+ // get the merge point, it should be either null if it's all dead end, or all the same merge point. No need to validate, as GetChunks already did
+ var mergePoint = subChunk.Values.FirstOrDefault(x => x.InputMergePoint != null)?.InputMergePoint;
+
+ if (mergePoint != null)
+ {
+ // we are not the only one merging here, we need to check if all the other paths are sub chunks of ours.
+ // That would mean we can keep going, otherwise we need to stop here and let our parent handle the merge.
+ foreach (var mergeOutput in mergePoint.Connections)
+ {
+ if (!part.ContainOutput(mergeOutput))
+ {
+ // we can't keep going, we need to stop here and let our parent handle the merge
+ return new NodePathChunks(execOutput, chunks, mergePoint, null);
+ }
+ }
+
+ // It's all our stuff, we can keep going
+
+ // we merged back, we can keep going with the next nodes along the path since it's still our path
+ currentInput = mergePoint;
+ }
+ else
+ {
+ // we reached a dead end, we can stop the path here
+ var deadEndInputs = subChunk.Values.Where(x => x.DeadEndInputs != null).ToList();
+ if (deadEndInputs.Count != 0) // we have dead ends, not just "nothing connected to the outputs"
+ return new NodePathChunks(execOutput, chunks, null, deadEndInputs.SelectMany(x => x.DeadEndInputs!).ToList());
+ else
+ return new NodePathChunks(execOutput, chunks, null, [currentInput]); // we reached a dead end
+ }
+ }
+ }
+ }
+
+ ///
+ /// Get all the chunks of a node. This will recursively get all the chunks of the outputs of the node.
+ ///
+ /// Exec input that was used to enter that node.
+ /// Node to get all the chunks from.
+ /// Does a parent allow dead end here.
+ ///
+ ///
+ private Dictionary GetChunks(Connection input, Node node, bool allowDeadEnd)
+ {
+ var chunks = new Dictionary();
+
+ foreach (var output in node.Outputs.Where(x => x.Type.IsExec))
+ {
+ // allowDeadEnd is prioritized over the node's own setting, since we can have a dead end if the parent allows it.
+ // Cases like a "branch" inside a loop can be a dead end, even though branch doesn't allow it, because the loop does.
+ var chunk = GetChunks(output, allowDeadEnd || node.DoesOutputPathAllowDeadEnd(output));
+ chunks[output] = chunk;
+
+ // Validate if the chunk is a merge and if it is allowed
+ if (chunk.InputMergePoint != null && !node.DoesOutputPathAllowMerge(output))
+ throw new BadMergeException(output);
+ }
+
+ if (chunks.Count == 0) // it's a dead end because the node doesn't even have an exec output
+ return chunks;
+
+ // Validate that the dead ends are allowed. They are either allowed if the parent path allows it or if they are breaking nodes like "Return"
+ if (!allowDeadEnd)
+ {
+ var hasInvalidDeadEnd = chunks
+ .Where(x => !node.DoesOutputPathAllowDeadEnd(x.Key) && (x.Value.DeadEndInputs != null || x.Value.InputMergePoint == null)) // x.Value.InputMergePoint == null is a dead end because the output connection was not connected to anything
+ .Where(x => x.Value.DeadEndInputs?.All(y => !y!.Parent.BreaksDeadEnd) ?? true) // ?? true because any dead end by "no connection" is automatically an invalid dead end
+ .ToList();
+
+ if (hasInvalidDeadEnd.Count != 0)
+ {
+ var deadEnds = new List();
+ foreach (var invalidDeadEnd in hasInvalidDeadEnd)
+ {
+ if (invalidDeadEnd.Value.DeadEndInputs != null)
+ deadEnds.AddRange(invalidDeadEnd.Value.DeadEndInputs);
+ else // it's a dead end because the output connection was not connected to anything
+ deadEnds.Add(input);
+ }
+
+ throw new DeadEndNotAllowed(deadEnds);
+ }
+ }
+
+ // validate that all the chunks have the same merging point. If not, the path that don't merge at the same place must be dead ends
+ var nbDifferentMergePoint = chunks.Values.Where(x => x.InputMergePoint != null).Select(x => x.InputMergePoint).Distinct().Count();
+ if (nbDifferentMergePoint > 1) // all the same or none is fine, but more than one is not
+ throw new BadMergeException(chunks.Values.First(x => x.InputMergePoint != null).InputMergePoint!); // we can throw any of the inputs, they all have different merging points
+
+ return chunks;
+ }
+
+ #endregion
+
+ #region BuildExpression
+
+ public LambdaExpression BuildExpression(BuildExpressionOptions options)
+ {
+ var entry = (Nodes.Values.FirstOrDefault(x => x is EntryNode)?.Outputs.FirstOrDefault()) ?? throw new Exception($"No entry node found in graph {SelfMethod.Name}");
+ var returnLabelTarget = !SelfMethod.HasReturnValue ? Expression.Label("ReturnLabel") : Expression.Label(SelfMethod.ReturnType.MakeRealType(), "ReturnLabel");
+
+ var info = new BuildExpressionInfo(returnLabelTarget, options, SelfMethod.IsStatic ? null : Expression.Parameter(SelfClass.ClassTypeBase.MakeRealType(), "this"));
+
+ // Create a variable for each output parameter
+ foreach (var parameter in SelfMethod.Parameters)
+ {
+ if (parameter.ParameterType.IsExec)
+ continue;
+
+ var type = parameter.ParameterType.MakeRealType();
+ var variable = Expression.Parameter(type, parameter.Name);
+ info.MethodParametersExpression[parameter.Name] = variable;
+ }
+
+ // Create a variable for each node's output
+ foreach (var node in Nodes.Values)
+ {
+ if (node.CanBeInlined)
+ continue; // this can be inlined, no need to create local variables for stuff such as "a + b"
+
+ // normal execution nodes each have their own local variable for every output
+ // Their input connections will be inlined or point to another local variable of someone else's output
+ foreach ((var connection, var variable) in node.CreateOutputsLocalVariableExpressions(info))
+ info.LocalVariables[connection] = variable;
+ }
+
+ var chunks = GetChunks(entry, false);
+
+ var expressions = BuildExpression(chunks, info);
+
+ // Create the return label with its default return value (if needed)
+ var returnLabel = SelfMethod.HasReturnValue ? Expression.Label(returnLabelTarget, Expression.Default(returnLabelTarget.Type)) : Expression.Label(returnLabelTarget);
+ // create a list of all the local variables that were used in the entire method
+ var localVariables = info.LocalVariables.Values
+ .OfType()
+ .Distinct() // lots of inputs use the same variable as another node's output, make sure we only declare them once
+ .Except(info.MethodParametersExpression.Values); // Remove the method parameters as they are declared later and not here
+
+ var expressionBlock = Expression.Block(localVariables, expressions.Append(returnLabel));
+
+ var parameters = SelfMethod.IsStatic ? info.MethodParametersExpression.Values : info.MethodParametersExpression.Values.Prepend(info.ThisExpression!);
+ var lambdaExpression = Expression.Lambda(expressionBlock, parameters);
+
+ return lambdaExpression;
+ }
+
+ internal static Expression[] BuildExpression(NodePathChunks chunks, BuildExpressionInfo info)
+ {
+ var expressions = new Expression[chunks.Chunks.Count];
+
+ for (int i = 0; i < chunks.Chunks.Count; ++i)
+ {
+ var chunk = chunks.Chunks[i];
+
+ // connect all the inputs to it's inputs
+ foreach (var input in chunk.Input.Parent.Inputs)
+ ConnectInputExpression(input, info);
+
+ try
+ {
+ var expression = chunk.Input.Parent.BuildExpression(chunk.SubChunk, info);
+ expressions[i] = expression;
+ }
+ catch (Exception ex) when (ex is not BuildError)
+ {
+ throw new BuildError(ex.Message, chunk.Input.Parent, ex);
+ }
+ }
+
+ return expressions;
+ }
+
+ private static void BuildInlineExpression(Node node, BuildExpressionInfo info)
+ {
+ if (info.InlinedNodes.Contains(node))
+ return;
+
+ if (!node.CanBeInlined)
+ throw new Exception($"{nameof(BuildInlineExpression)} can only be called on nodes that can be inlined: {node.Name}");
+
+ foreach (var input in node.Inputs)
+ {
+ ConnectInputExpression(input, info);
+ }
+
+ // now that all our dependencies are built, we can build the node itself
+ try
+ {
+ node.BuildInlineExpression(info);
+ }
+ catch (Exception ex) when (ex is not BuildError)
+ {
+ throw new BuildError(ex.Message, node, ex);
+ }
+
+ info.InlinedNodes.Add(node);
+ }
+
+ private static void ConnectInputExpression(Connection input, BuildExpressionInfo info)
+ {
+ if (input.Type.IsExec)
+ return;
+
+ if (input.Connections.Count == 0)
+ {
+ if (!input.Type.AllowTextboxEdit || input.ParsedTextboxValue == null)
+ info.LocalVariables[input] = Expression.Default(input.Type.MakeRealType());
+ else
+ info.LocalVariables[input] = Expression.Constant(input.ParsedTextboxValue, input.Type.MakeRealType());
+ }
+ else
+ {
+ var otherNode = input.Connections[0].Parent;
+ if (otherNode.CanBeInlined)
+ BuildInlineExpression(otherNode, info);
+
+ // Get the local variable or expression associated with that input and use it as that input's expression
+ info.LocalVariables[input] = info.LocalVariables[input.Connections[0]];
+ }
+ }
+
+ #endregion
+
+ #region Invoke
+
+ public Task Invoke(Action action)
+ {
+ return Invoke(() =>
+ {
+ action();
+ return Task.CompletedTask;
+ });
+ }
+
+ public async Task Invoke(Func action)
+ {
+ await action(); // temporary
+ }
+
+ #endregion
+
+ #region Connect / Disconnect / MergedRemovedConnectionsWithNewConnections
+
+ public void MergeRemovedConnectionsWithNewConnections(List newConnections, List removedConnections)
+ {
+ foreach (var removedConnection in removedConnections)
+ {
+ var newConnection = newConnections.FirstOrDefault(x => x.Parent == removedConnection.Parent && x.Name == removedConnection.Name && x.Type == removedConnection.Type);
+
+ // if we found a new connection, connect them together and remove the old connection
+ foreach (var oldLink in removedConnection.Connections)
+ {
+ oldLink._Connections.Remove(removedConnection); // cleanup the old connection
+
+ if (newConnection != null)
+ {
+ // Before we re-connect them let's make sure both are inputs or both outputs
+ if (oldLink.Parent.Outputs.Contains(oldLink) == newConnection.Parent.Inputs.Contains(newConnection))
+ {
+ // we can safely reconnect the new connection to the old link
+ // Either newConnection is an input and removedConnection is an output or vice versa
+ newConnection._Connections.Add(oldLink);
+ oldLink._Connections.Add(newConnection);
+ }
+ }
+ }
+ }
+
+ RaiseGraphChanged(true); // this always require refreshing the UI as this is custom behavior that needs to be replicated by the UI
+ }
+
+ public void Connect(Connection connection1, Connection connection2, bool requireUIRefresh)
+ {
+ if (!connection1._Connections.Contains(connection2))
+ connection1._Connections.Add(connection2);
+ if (!connection2._Connections.Contains(connection1))
+ connection2._Connections.Add(connection1);
+
+ RaiseGraphChanged(requireUIRefresh);
+ }
+
+ public void Disconnect(Connection connection1, Connection connection2, bool requireUIRefresh)
+ {
+ connection1._Connections.Remove(connection2);
+ connection2._Connections.Remove(connection1);
+
+ RaiseGraphChanged(requireUIRefresh);
+ }
+
+ #endregion
+
+ #region AddNode
+
+ public void AddNode(Node node, bool requireUIRefresh)
+ {
+ ((IDictionary)Nodes)[node.Id] = node;
+
+ RaiseGraphChanged(requireUIRefresh);
+ }
+
+ public Node AddNode(NodeProvider.NodeSearchResult searchResult, bool requireUIRefresh)
+ {
+ var node = (Node)Activator.CreateInstance(searchResult.Type, this, null)!;
+ AddNode(node, requireUIRefresh);
+
+ if (searchResult is NodeProvider.MethodCallNode methodCall && node is MethodCall methodCallNode)
+ methodCallNode.SetMethodTarget(methodCall.MethodInfo);
+ else if (searchResult is NodeProvider.GetPropertyOrFieldNode getPropertyOrField && node is GetPropertyOrField getPropertyOrFieldNode)
+ getPropertyOrFieldNode.SetMemberTarget(getPropertyOrField.MemberInfo);
+ else if (searchResult is NodeProvider.SetPropertyOrFieldNode setPropertyOrField && node is SetPropertyOrField setPropertyOrFieldNode)
+ setPropertyOrFieldNode.SetMemberTarget(setPropertyOrField.MemberInfo);
+
+ return node;
+ }
+
+ #endregion
+
+ #region RemoveNode
+
+ public void RemoveNode(Node node, bool requireUIRefresh)
+ {
+ ((IDictionary)Nodes).Remove(node.Id);
+
+ RaiseGraphChanged(requireUIRefresh);
+ }
- #endregion
-
- #region RemoveNode
+ #endregion
- public void RemoveNode(Node node, bool requireUIRefresh)
- {
- ((IDictionary)Nodes).Remove(node.Id);
+ #region Serialization
- RaiseGraphChanged(requireUIRefresh);
- }
-
- #endregion
-
- #region Serialization
-
- internal record class SerializedGraph(List Nodes);
- internal SerializedGraph Serialize()
- {
- var nodes = new List();
+ internal record class SerializedGraph(List Nodes);
+ internal SerializedGraph Serialize()
+ {
+ var nodes = new List();
- foreach (var node in Nodes.Values)
- nodes.Add(node.Serialize());
+ foreach (var node in Nodes.Values)
+ nodes.Add(node.Serialize());
- var serializedGraph = new SerializedGraph(nodes);
+ var serializedGraph = new SerializedGraph(nodes);
- return serializedGraph;
- }
+ return serializedGraph;
+ }
- internal static void Deserialize(SerializedGraph serializedGraphObj, Graph graph)
- {
- foreach (var serializedNode in serializedGraphObj.Nodes)
- {
- var node = Node.Deserialize(graph, serializedNode);
- graph.AddNode(node, false);
- }
- }
+ internal static void Deserialize(SerializedGraph serializedGraphObj, Graph graph)
+ {
+ foreach (var serializedNode in serializedGraphObj.Nodes)
+ {
+ var node = Node.Deserialize(graph, serializedNode);
+ graph.AddNode(node, false);
+ }
+ }
- #endregion
+ #endregion
}
\ No newline at end of file
diff --git a/src/NodeDev.Core/GraphExecutor.cs b/src/NodeDev.Core/GraphExecutor.cs
index daf57c8..12c400a 100644
--- a/src/NodeDev.Core/GraphExecutor.cs
+++ b/src/NodeDev.Core/GraphExecutor.cs
@@ -1,12 +1,6 @@
using NodeDev.Core.Connections;
using NodeDev.Core.Nodes;
using NodeDev.Core.Nodes.Flow;
-using System;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
namespace NodeDev.Core
{
@@ -141,7 +135,7 @@ public void Execute(object? self, Span