-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathNode.cs
More file actions
297 lines (221 loc) · 10.2 KB
/
Copy pathNode.cs
File metadata and controls
297 lines (221 loc) · 10.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
using NodeDev.Core.Connections;
using NodeDev.Core.NodeDecorations;
using NodeDev.Core.Types;
using System.Linq.Expressions;
namespace NodeDev.Core.Nodes
{
[System.Diagnostics.DebuggerDisplay("{Name}. Inputs: {Inputs.Count}. Outputs: {Outputs.Count}")]
public abstract class Node
{
public Node(Graph graph, string? id = null)
{
Graph = graph;
Id = id ?? (Guid.NewGuid().ToString());
}
public string Id { get; }
public virtual string Name { get; set; } = "";
public virtual bool AllowEditingName { get; } = false;
public Graph Graph { get; }
public abstract string TitleColor { get; }
public List<Connection> Inputs { get; } = [];
public List<Connection> Outputs { get; } = [];
public IEnumerable<Connection> InputsAndOutputs => Inputs.Concat(Outputs);
public abstract bool IsFlowNode { get; }
public TypeFactory TypeFactory => Project.TypeFactory;
public Project Project => Graph.SelfClass.Project;
public virtual bool FetchState => false;
public virtual bool ReOrderExecInputsAndOutputs => true;
public bool CanBeInlined => !InputsAndOutputs.Any(x => x.Type.IsExec);
/// <summary>
/// True to allow remerging exec connection together later in the graph.
/// This is used by nodes that have multiple exec outputs such as Branch, Loop, etc.
/// If the value is false, such as for Loop, each exec output has to be a separate path.
/// Ex, for ForEach node the value is false since "Loop Exec" path cannot have shared path with the "ExecOut" path.
/// </summary>
public virtual bool AllowRemergingExecConnections => true;
/// <summary>
/// Global index of this node in the graph. Each node in a graph has a unique index.
/// </summary>
public int GraphIndex { get; set; } = -1;
public virtual IEnumerable<string> GetUndefinedGenericTypes() => InputsAndOutputs.SelectMany(x => x.Type.GetUndefinedGenericTypes()).Distinct();
/// <summary>
/// Called before the generic type are set on every connections
/// </summary>
public virtual void OnBeforeGenericTypeDefined(IReadOnlyDictionary<string, TypeBase> changedGenerics) { }
public record class AlternateOverload(TypeBase ReturnType, List<IMethodParameterInfo> Parameters);
public virtual IEnumerable<AlternateOverload> AlternatesOverloads => [];
/// <summary>
/// Triggered when the generic type of the connection has been defined (found and applied already).
/// </summary>
/// <param name="connection">The connection that was generic, it is not generic anymore</param>
/// <returns>Returns a list of changed connections, if any.</returns>
public virtual List<Connection> GenericConnectionTypeDefined(Connection connection)
{
return [];
}
public virtual void SelectOverload(AlternateOverload overload, out List<Connection> newConnections, out List<Connection> removedConnections)
{
throw new NotImplementedException();
}
internal virtual Expression BuildExpression(Dictionary<Connection, Graph.NodePathChunks>? subChunks, BuildExpressionInfo info) => throw new NotImplementedException();
internal virtual void BuildInlineExpression(BuildExpressionInfo info) => throw new NotImplementedException();
/// <summary>
/// Create an Expression node that can be used in the graph.
/// Ie, the "Add" node will have two local variables, one for each input, and one output local variable.
/// In C#, that would look like doing :
/// var input1 = ..., input2 = ...; // ... would be replaced by the output local variable of the previous node
/// var output = input1 + input2;
///
/// This will ignore the exec connections.
/// </summary>
internal virtual IEnumerable<(Connection Connection, ParameterExpression LocalVariable)> CreateOutputsLocalVariableExpressions(BuildExpressionInfo info)
{
foreach (var output in Outputs)
{
if (output.Type.IsExec)
continue;
var variable = Expression.Variable(output.Type.MakeRealType(), $"{Name}_{output.Name}"
.Replace(" ", string.Empty)
.Replace(".", string.Empty)
.Replace("<", string.Empty)
.Replace(">", string.Empty));
yield return (output, variable);
}
}
#region Path merging / Crossing examinations
public abstract string GetExecOutputPathId(string pathId, Connection execOutput);
public abstract bool DoesOutputPathAllowDeadEnd(Connection execOutput);
public abstract bool DoesOutputPathAllowMerge(Connection execOutput);
/// <summary>
/// Returns true if this node breaks a dead end. These are usually "Return", "Break", "Continue", etc.
/// This will allow a dead end in places where it shouldn't be allowed, such as a "Branch" node.
/// </summary>
public virtual bool BreaksDeadEnd => false;
public class InfiniteLoopException(Node node) : Exception
{
public Node Node { get; } = node;
}
/// <summary>
/// Returns all the possible execution path from this node.
/// I know, I know. All of this is slow.
/// </summary>
public NodePaths SearchAllExecPaths(HashSet<Node> nodesBefore)
{
if (nodesBefore.Contains(this))
throw new InfiniteLoopException(this);
var execOutputs = Outputs.Where(x => x.Type.IsExec).ToList();
if (execOutputs.Count == 0)
return new();
// Prevent any children path from looping back to us
nodesBefore.Add(this);
var allPaths = new NodePaths();
foreach (var execOutput in execOutputs)
{
// Create a copy of the nodes already used since each execOutput can possibly hit the same nodes without it being an issue
var nodesBefore_local = new HashSet<Node>(nodesBefore);
var subPaths = SearchAllExecPaths(execOutput, nodesBefore_local);
if (subPaths.CountPossiblePaths != 0) // it led somewhere
{
// Add the execOutput to the beginning of each path
subPaths.PrependPath(new([execOutput]));
}
else // it led nowhere, the path is just the execOutput and that's it
subPaths.AddNewIndependantBranch(new NodePath([execOutput]));
allPaths.AddNewIndependantBranch(subPaths);
}
return allPaths;
}
/// <summary>
/// List all the possible path taken from the output exec connection.
/// </summary>
private static NodePaths SearchAllExecPaths(Connection outputExec, HashSet<Node> alreadySeenNodes)
{
if (outputExec.Connections.Count == 0)
return new(); // we're connected to nothing
// If we are going through NormalFlowNodes, we can simply accumulate the connections one after the other
// Then, we multiple paths offers, we can search those paths and Prepend the straightConnections to them
var straightConnections = new NodePath();
var inputExec = outputExec.Connections.FirstOrDefault();
while (inputExec != null)
{
var otherNode = inputExec.Parent;
if (otherNode is NormalFlowNode)
{
alreadySeenNodes.Add(otherNode); // used to prevent any children path from looping back to us
var otherExec = otherNode.Outputs.Single(x => x.Type.IsExec);
straightConnections.AppendPath(otherExec);
inputExec = otherExec.Connections.FirstOrDefault();
}
else
{
// We've hit a possible separation in the paths we can follow
// We can recursively start a search again.
// There is no need to add the current node to the alreadySeenNodes since the SearchAllExecPaths will do it for us
var allSubPaths = otherNode.SearchAllExecPaths(alreadySeenNodes);
// Append each path returned to the straightConnections
allSubPaths.PrependPath(straightConnections);
return allSubPaths;
}
}
// if we're here, it means we never hit any branching of paths, we can return a single path possible
if (straightConnections.Length == 0)
return new(); // we're connected to nothing
else
return new NodePaths(straightConnections);
}
#endregion
#region Decorations
public Dictionary<Type, INodeDecoration> Decorations { get; init; } = [];
public void AddDecoration<T>(T attribute) where T : INodeDecoration => Decorations[typeof(T)] = attribute;
public T GetOrAddDecoration<T>(Func<T> creator) where T : INodeDecoration
{
if (Decorations.TryGetValue(typeof(T), out var decoration))
return (T)decoration;
var v = creator();
Decorations[typeof(T)] = v;
return v;
}
#endregion
#region Serialization
public record SerializedNode(string Type, string Id, string Name, List<Connection.SerializedConnection> Inputs, List<Connection.SerializedConnection> Outputs, Dictionary<string, string> Decorations);
internal SerializedNode Serialize()
{
var serializedNode = new SerializedNode(GetType().FullName!, Id, Name, Inputs.Select(x => x.Serialize()).ToList(), Outputs.Select(x => x.Serialize()).ToList(), Decorations.ToDictionary(x => x.Key.FullName!, x => x.Value.Serialize()));
return serializedNode;
}
internal static Node Deserialize(Graph graph, SerializedNode serializedNodeObj)
{
var type = TypeFactory.GetTypeByFullName(serializedNodeObj.Type) ?? throw new Exception($"Unable to find type: {serializedNodeObj.Type}");
var node = (Node?)Activator.CreateInstance(type, graph, serializedNodeObj.Id) ?? throw new Exception($"Unable to create instance of type: {serializedNodeObj.Type}");
foreach (var decoration in serializedNodeObj.Decorations)
{
var decorationType = TypeFactory.GetTypeByFullName(decoration.Key) ?? throw new Exception($"Unable to find type: {decoration.Key}");
var method = decorationType.GetMethod(nameof(INodeDecoration.Deserialize), System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
if (method == null)
throw new Exception($"Unable to find Deserialize method on type: {decoration.Key}");
if (method.Invoke(null, [graph.SelfClass.TypeFactory, decoration.Value]) is not INodeDecoration decorationObj)
throw new Exception($"Unable to deserialize decoration: {decoration.Key}");
node.Decorations[decorationType] = decorationObj;
}
node.Deserialize(serializedNodeObj);
return node;
}
protected virtual void Deserialize(SerializedNode serializedNodeObj)
{
Inputs.Clear();
Outputs.Clear();
Name = serializedNodeObj.Name;
foreach (var input in serializedNodeObj.Inputs)
{
var connection = Connection.Deserialize(this, input, true);
Inputs.Add(connection);
}
foreach (var output in serializedNodeObj.Outputs)
{
var connection = Connection.Deserialize(this, output, false);
Outputs.Add(connection);
}
}
#endregion
}
}