Skip to content

Commit 8daa046

Browse files
committed
cleanup
1 parent acb1da0 commit 8daa046

12 files changed

Lines changed: 318 additions & 15 deletions

File tree

src/NodeDev.Blazor/Components/GraphCanvas.razor.cs

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -192,11 +192,22 @@ private void UpdateNodes()
192192

193193
private GraphPortModel? FindPort(Connection connection)
194194
{
195-
return FindNodeModel(connection.Parent)?.Ports
195+
var nodePort = FindNodeModel(connection.Parent)?.Ports
196+
.OfType<GraphPortModel>()
197+
.FirstOrDefault(x => x.Connection == connection);
198+
if (nodePort != null)
199+
return nodePort;
200+
201+
return Diagram.Groups
202+
.SelectMany(x => x.Ports)
196203
.OfType<GraphPortModel>()
197204
.FirstOrDefault(x => x.Connection == connection);
198205
}
199206

207+
private LambdaGroupModel? FindLambdaGroup(string? bodyScopeId) => Diagram.Groups
208+
.OfType<LambdaGroupModel>()
209+
.FirstOrDefault(x => x.DelegateNode.BodyScopeId == bodyScopeId);
210+
200211
#endregion
201212

202213
#region Events from client
@@ -418,6 +429,9 @@ private static void OnLambdaGroupMoved(MovableModel movableModel)
418429
if (movableModel is not LambdaGroupModel group)
419430
return;
420431

432+
var groupDecoration = group.DelegateNode.GetOrAddDecoration<NodeDecorationPosition>(() => new(Vector2.Zero));
433+
groupDecoration.Position = new((float)group.Position.X, (float)group.Position.Y);
434+
421435
foreach (var nodeModel in group.GetDescendantNodeModels())
422436
{
423437
var decoration = nodeModel.Node.GetOrAddDecoration<NodeDecorationPosition>(() => new(Vector2.Zero));
@@ -754,6 +768,23 @@ public void ToggleBreakpointOnSelectedNode()
754768

755769
public void RemoveNode(Node node)
756770
{
771+
if (node is LambdaReturnNode { IsImplicit: true } boundaryReturn)
772+
{
773+
var group = FindLambdaGroup(boundaryReturn.CallableScopeId);
774+
if (group != null)
775+
{
776+
foreach (var port in group.Ports
777+
.OfType<GraphPortModel>()
778+
.Where(x => x.Connection.Parent == boundaryReturn)
779+
.ToList())
780+
{
781+
group.RemovePort(port);
782+
}
783+
group.Refresh();
784+
}
785+
return;
786+
}
787+
757788
var nodeModel = FindNodeModel(node);
758789
if (nodeModel == null)
759790
return;
@@ -832,6 +863,8 @@ public void AddNode(Node node)
832863
{
833864
if (node is CreateDelegateNode delegateNode)
834865
AddLambdaGroupModel(delegateNode);
866+
else if (node is LambdaReturnNode { IsImplicit: true } boundaryReturn)
867+
AddBoundaryReturnToGroup(boundaryReturn);
835868
else
836869
AddGraphNodeModel(node);
837870

@@ -859,10 +892,11 @@ private void EnsureInitialScopedPosition(Node node)
859892
return;
860893

861894
var ownerPosition = owner.GetOrAddDecoration<NodeDecorationPosition>(() => new(Vector2.Zero)).Position;
895+
var groupPadding = FindLambdaGroup(owner.BodyScopeId)?.Padding ?? LambdaGroupModel.MinimumPadding;
862896
var existingNodesInScope = Diagram.Nodes
863897
.OfType<GraphNodeModel>()
864898
.Count(x => x.Node.CallableScopeId == node.CallableScopeId);
865-
var offset = new Vector2(80 + existingNodesInScope * 220, 100);
899+
var offset = new Vector2(groupPadding + existingNodesInScope * 220, groupPadding);
866900
node.AddDecoration(new NodeDecorationPosition(ownerPosition + offset));
867901
}
868902

@@ -872,11 +906,32 @@ private LambdaGroupModel AddLambdaGroupModel(CreateDelegateNode node)
872906
foreach (var capture in node.CaptureInputs)
873907
group.AddPort(new GraphPortModel(group, capture, true));
874908
group.AddPort(new GraphPortModel(group, node.DelegateOutput, false));
909+
if (group.BoundaryReturn is { } boundaryReturn)
910+
AddBoundaryReturnPorts(group, boundaryReturn);
875911

876912
group.Moved += OnLambdaGroupMoved;
877913
return group;
878914
}
879915

916+
private void AddBoundaryReturnToGroup(LambdaReturnNode boundaryReturn)
917+
{
918+
var group = FindLambdaGroup(boundaryReturn.CallableScopeId);
919+
if (group == null)
920+
return;
921+
922+
AddBoundaryReturnPorts(group, boundaryReturn);
923+
group.Refresh();
924+
}
925+
926+
private static void AddBoundaryReturnPorts(LambdaGroupModel group, LambdaReturnNode boundaryReturn)
927+
{
928+
foreach (var connection in boundaryReturn.Inputs)
929+
{
930+
if (group.Ports.OfType<GraphPortModel>().All(x => x.Connection != connection))
931+
group.AddPort(new GraphPortModel(group, connection, true));
932+
}
933+
}
934+
880935
private void ReparentScopedModels()
881936
{
882937
var groupsByScope = Diagram.Groups
@@ -982,6 +1037,12 @@ public void Refresh(Node node)
9821037
UpdateNodes();
9831038
return;
9841039
}
1040+
if (node is LambdaReturnNode { IsImplicit: true } boundaryReturn)
1041+
{
1042+
var group = FindLambdaGroup(boundaryReturn.CallableScopeId);
1043+
group?.Refresh();
1044+
return;
1045+
}
9851046

9861047
var nodeModel = FindNodeModel(node) as GraphNodeModel;
9871048
if (nodeModel == null)
@@ -1037,12 +1098,12 @@ public void Refresh(Node node)
10371098

10381099
private void InitializeCanvasWithGraphNodes()
10391100
{
1040-
foreach (var node in Graph.Nodes.Values.Where(x => x is not CreateDelegateNode))
1041-
AddGraphNodeModel(node);
1042-
10431101
foreach (var delegateNode in Graph.Nodes.Values.OfType<CreateDelegateNode>())
10441102
AddLambdaGroupModel(delegateNode);
10451103

1104+
foreach (var node in Graph.Nodes.Values.Where(x => x is not CreateDelegateNode and not LambdaReturnNode { IsImplicit: true }))
1105+
AddGraphNodeModel(node);
1106+
10461107
ReparentScopedModels();
10471108

10481109
// add links

src/NodeDev.Blazor/DiagramsModels/LambdaGroupModel.cs

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
using Blazor.Diagrams.Core.Geometry;
12
using Blazor.Diagrams.Core.Models;
3+
using NodeDev.Blazor.NodeAttributes;
24
using NodeDev.Core.Connections;
35
using NodeDev.Core.Nodes;
46
using NodeDev.Core.Nodes.Delegates;
@@ -12,14 +14,21 @@ namespace NodeDev.Blazor.DiagramsModels;
1214
public sealed class LambdaGroupModel : GroupModel
1315
{
1416
public const byte MinimumPadding = 60;
17+
public const byte FuncPadding = 90;
18+
public const double MinimumWidth = 600;
19+
public const double MinimumHeight = 420;
1520

1621
public LambdaGroupModel(CreateDelegateNode delegateNode, IEnumerable<NodeModel>? children = null)
17-
: base(children ?? [], CalculatePadding(delegateNode))
22+
: base(CreateChildren(delegateNode, children), CalculatePadding(delegateNode))
1823
{
1924
DelegateNode = delegateNode;
2025
}
2126

2227
public CreateDelegateNode DelegateNode { get; }
28+
public LambdaReturnNode? BoundaryReturn => DelegateNode.Graph
29+
.GetNodesInScope(DelegateNode.BodyScopeId)
30+
.OfType<LambdaReturnNode>()
31+
.SingleOrDefault(x => x.IsImplicit);
2332

2433
public GraphPortModel GetPort(Connection connection) =>
2534
Ports.OfType<GraphPortModel>().First(x => x.Connection == connection);
@@ -42,6 +51,55 @@ public IEnumerable<GraphNodeModel> GetDescendantNodeModels()
4251

4352
private static byte CalculatePadding(CreateDelegateNode node)
4453
{
45-
return (byte)Math.Min(byte.MaxValue, MinimumPadding + node.CaptureInputs.Count * 15);
54+
var minimum = node.Kind == DelegateKind.Func ? FuncPadding : MinimumPadding;
55+
return (byte)Math.Min(byte.MaxValue, minimum + node.CaptureInputs.Count * 15);
56+
}
57+
58+
private static Point GetInitialPosition(CreateDelegateNode node, byte padding)
59+
{
60+
var childPositions = node.Graph.GetNodesInScope(node.BodyScopeId)
61+
.Where(x => x is not LambdaReturnNode { IsImplicit: true })
62+
.Where(x => x.Decorations.TryGetValue(typeof(NodeDecorationPosition), out _))
63+
.Select(x => (NodeDecorationPosition)x.Decorations[typeof(NodeDecorationPosition)])
64+
.ToList();
65+
if (childPositions.Count != 0)
66+
{
67+
return new Point(
68+
childPositions.Min(x => x.X) - padding,
69+
childPositions.Min(x => x.Y) - padding);
70+
}
71+
72+
var position = node.GetOrAddDecoration<NodeDecorationPosition>(() => new(System.Numerics.Vector2.Zero));
73+
return new Point(position.X, position.Y);
74+
}
75+
76+
private static IEnumerable<NodeModel> CreateChildren(CreateDelegateNode node, IEnumerable<NodeModel>? children)
77+
{
78+
var padding = CalculatePadding(node);
79+
var position = GetInitialPosition(node, padding);
80+
var layoutSize = new Size(
81+
Math.Max(1, MinimumWidth - padding * 2),
82+
Math.Max(1, MinimumHeight - padding * 2));
83+
yield return new LambdaLayoutModel(
84+
new Point(position.X + padding, position.Y + padding),
85+
layoutSize);
86+
87+
foreach (var child in children ?? [])
88+
yield return child;
89+
}
90+
91+
/// <summary>
92+
/// An invisible child that gives the auto-sized group a stable minimum workspace.
93+
/// Real children can still grow the group when they move outside these bounds.
94+
/// </summary>
95+
private sealed class LambdaLayoutModel : NodeModel
96+
{
97+
public LambdaLayoutModel(Point position, Size size) : base(position)
98+
{
99+
Size = size;
100+
ControlledSize = true;
101+
Locked = true;
102+
Visible = false;
103+
}
46104
}
47105
}

src/NodeDev.Blazor/DiagramsModels/LambdaGroupWidget.razor

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,31 @@
6060
<PortRenderer @key="delegatePort" Port="delegatePort" Class="lambda-boundary-port" Style="@($"background-color: {delegatePort.PortColor}")" />
6161
</div>
6262

63+
@if (Group.BoundaryReturn is { } boundaryReturn)
64+
{
65+
var execPort = Group.GetPort(boundaryReturn.ExecInput);
66+
var resultPort = Group.GetPort(boundaryReturn.ResultInput);
67+
<div class="lambda-return-boundary" data-test-id="lambda-boundary-return">
68+
<span class="lambda-return-title">Return</span>
69+
<div class="lambda-return-port" data-test-id="lambda-return-exec-port" data-port-name="@boundaryReturn.ExecInput.Name">
70+
<PortRenderer @key="execPort" Port="execPort" Class="lambda-boundary-port" Style="@($"background-color: {execPort.PortColor}")" />
71+
<span>@boundaryReturn.ExecInput.Name</span>
72+
</div>
73+
<div class="lambda-return-port lambda-return-result" data-test-id="lambda-return-result-port" data-port-name="@boundaryReturn.ResultInput.Name">
74+
<PortRenderer @key="resultPort" Port="resultPort" Class="lambda-boundary-port" Style="@($"background-color: {resultPort.PortColor}")" />
75+
<span>@boundaryReturn.ResultInput.Name</span>
76+
@if (boundaryReturn.ResultInput.Type.AllowTextboxEdit && resultPort.Links.Count == 0)
77+
{
78+
<input class="lambda-return-editor"
79+
aria-label="Lambda return result"
80+
value="@boundaryReturn.ResultInput.TextboxValue"
81+
@onchange="x => GraphCanvas.OnTextboxValueChanged(resultPort, x.Value?.ToString())"
82+
@onpointerdown:stopPropagation="true" />
83+
}
84+
</div>
85+
</div>
86+
}
87+
6388
<GroupNodes Group="Group" />
6489
</div>
6590

src/NodeDev.Blazor/wwwroot/styles.css

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,64 @@ g.diagram-link path:not(.selection-helper) {
311311
transform: translate(1px, -50%);
312312
}
313313

314+
.lambda-return-boundary {
315+
position: absolute;
316+
right: -1px;
317+
bottom: 18px;
318+
width: 190px;
319+
box-sizing: border-box;
320+
overflow: visible;
321+
padding: 0 10px 7px 14px;
322+
border: 1px solid rgba(185, 28, 28, 0.45);
323+
border-right: 0;
324+
border-radius: 8px 0 0 8px;
325+
background: rgba(254, 242, 242, 0.94);
326+
color: #7f1d1d;
327+
font-size: 0.75rem;
328+
pointer-events: all;
329+
}
330+
331+
.lambda-return-title {
332+
display: block;
333+
margin: -1px -10px 5px -14px;
334+
padding: 4px 10px 4px 14px;
335+
border-radius: 8px 0 0 0;
336+
background: #dc2626;
337+
color: white;
338+
font-weight: 700;
339+
}
340+
341+
.lambda-return-port {
342+
position: relative;
343+
min-height: 24px;
344+
display: flex;
345+
align-items: center;
346+
gap: 7px;
347+
}
348+
349+
.lambda-return-port .diagram-port {
350+
width: 18px;
351+
height: 18px;
352+
flex: none;
353+
border: 1px solid #d4d4d4;
354+
border-radius: 9999px;
355+
cursor: pointer;
356+
position: relative;
357+
margin: 0;
358+
pointer-events: all;
359+
}
360+
361+
.lambda-return-editor {
362+
width: 72px;
363+
min-width: 0;
364+
padding: 3px 6px;
365+
border: 1px solid rgba(127, 29, 29, 0.35);
366+
border-radius: 4px;
367+
background: white;
368+
color: #111827;
369+
font: inherit;
370+
}
371+
314372
.lambda-boundary-row .diagram-port {
315373
width: 18px;
316374
height: 18px;

src/NodeDev.Core/Graph.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,8 @@ public void ValidateCallableScopes()
554554
{
555555
if (bodyNodes.OfType<LambdaCompleteNode>().Any())
556556
throw new InvalidOperationException($"Func delegate '{owner.SignatureDisplayName}' contains an Action completion node.");
557+
if (bodyNodes.OfType<LambdaReturnNode>().Count(x => x.IsImplicit) > 1)
558+
throw new InvalidOperationException($"Func delegate '{owner.SignatureDisplayName}' contains more than one implicit return node.");
557559
if (!bodyNodes.OfType<LambdaReturnNode>().Any())
558560
throw new InvalidOperationException($"Func delegate '{owner.SignatureDisplayName}' has no return node.");
559561
if (bodyNodes.OfType<LambdaReturnNode>().Any(x => x.ResultInput.Type != owner.ResultType))

src/NodeDev.Core/ManagerServices/GraphManagerService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ private void CreateDefaultDelegateBody(CreateDelegateNode owner)
136136
{
137137
var entry = new LambdaEntryNode(Graph) { CallableScopeId = owner.BodyScopeId };
138138
Node terminal = owner.Kind == DelegateKind.Func
139-
? new LambdaReturnNode(Graph) { CallableScopeId = owner.BodyScopeId }
139+
? new LambdaReturnNode(Graph) { CallableScopeId = owner.BodyScopeId, IsImplicit = true }
140140
: new LambdaCompleteNode(Graph) { CallableScopeId = owner.BodyScopeId };
141141

142142
entry.RefreshFromOwner(owner);

src/NodeDev.Core/Nodes/Delegates/LambdaReturnNode.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
using Microsoft.CodeAnalysis.CSharp.Syntax;
22
using NodeDev.Core.CodeGeneration;
33
using NodeDev.Core.Connections;
4+
using System.Text.Json;
45
using SF = Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
56

67
namespace NodeDev.Core.Nodes.Delegates;
78

89
public sealed class LambdaReturnNode : Flow.FlowNode
910
{
11+
private sealed record SerializedLambdaReturnPayload(bool IsImplicit);
12+
1013
public LambdaReturnNode(Graph graph, string? id = null) : base(graph, id)
1114
{
1215
Name = "Lambda Return";
@@ -17,6 +20,11 @@ public LambdaReturnNode(Graph graph, string? id = null) : base(graph, id)
1720
public override string TitleColor => "red";
1821
public override bool IsFlowNode => true;
1922
public override bool BreaksDeadEnd => true;
23+
/// <summary>
24+
/// Identifies the terminal created with the Func body. The canvas projects this
25+
/// terminal onto the lambda boundary; user-added returns remain ordinary nodes.
26+
/// </summary>
27+
public bool IsImplicit { get; internal set; }
2028
public Connection ExecInput => Inputs[0];
2129
public Connection ResultInput => Inputs[1];
2230

@@ -36,6 +44,16 @@ internal override void FinalizeDeserialization()
3644
RefreshFromOwner(owner);
3745
}
3846

47+
protected override string? SerializePayload() =>
48+
JsonSerializer.Serialize(new SerializedLambdaReturnPayload(IsImplicit));
49+
50+
protected override void DeserializePayload(string? payload)
51+
{
52+
IsImplicit = JsonSerializer.Deserialize<SerializedLambdaReturnPayload>(
53+
payload ?? throw new InvalidOperationException("Lambda return payload is missing."))?.IsImplicit
54+
?? throw new InvalidOperationException("Unable to deserialize lambda return payload.");
55+
}
56+
3957
internal override StatementSyntax GenerateRoslynStatement(Dictionary<Connection, Graph.NodePathChunks>? subChunks, GenerationContext context)
4058
{
4159
var resultName = context.GetVariableName(ResultInput)

0 commit comments

Comments
 (0)