Files
git.stella-ops.org/src/__Libraries/StellaOps.ElkSharp/ElkEdgeChannels.cs
master 66d84fb17a Fix Create Deployment wizard: add missing SlicePipe import
Root cause: the | slice pipe was used in the template but SlicePipe
was not in the standalone component's imports array. This caused
Angular's resolveDirective to throw 'Cannot read factory' on every
change detection cycle, preventing mock version cards from rendering
and breaking the Continue button validation.

Also: removed unused RouterModule import, converted computed signals
to methods for PlatformContextStore-dependent values, added
platformCtx.initialize() in constructor.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 14:05:30 +02:00

257 lines
11 KiB
C#

namespace StellaOps.ElkSharp;
internal static class ElkEdgeChannels
{
internal static Dictionary<string, EdgeChannel> ComputeEdgeChannels(
IReadOnlyCollection<ElkEdge> edges,
IReadOnlyDictionary<string, ElkPositionedNode> positionedNodes,
ElkLayoutDirection direction,
IReadOnlyDictionary<string, LayerBoundary> layerBoundariesByNodeId)
{
var channels = new Dictionary<string, EdgeChannel>(edges.Count, StringComparer.Ordinal);
var backwardEdges = new List<ElkEdge>();
var forwardEdgesBySource = new Dictionary<string, List<ElkEdge>>(StringComparer.Ordinal);
var outgoingCounts = edges
.GroupBy(edge => edge.SourceNodeId, StringComparer.Ordinal)
.ToDictionary(group => group.Key, group => group.Count(), StringComparer.Ordinal);
foreach (var edge in edges)
{
var source = positionedNodes[edge.SourceNodeId];
var target = positionedNodes[edge.TargetNodeId];
var isBackward = direction == ElkLayoutDirection.LeftToRight
? (target.X + (target.Width / 2d)) < (source.X + (source.Width / 2d))
: (target.Y + (target.Height / 2d)) < (source.Y + (source.Height / 2d));
if (isBackward)
{
backwardEdges.Add(edge);
}
else
{
if (!forwardEdgesBySource.TryGetValue(edge.SourceNodeId, out var list))
{
list = [];
forwardEdgesBySource[edge.SourceNodeId] = list;
}
list.Add(edge);
}
}
var backwardGroups = backwardEdges
.GroupBy(edge => edge.TargetNodeId, StringComparer.Ordinal)
.SelectMany(targetGroup =>
{
var families = targetGroup
.GroupBy(edge => ElkEdgeChannelBands.ResolveLaneFamilyKey(edge.Label), StringComparer.Ordinal)
.OrderBy(group => ElkEdgeChannelBands.ResolveLaneFamilyPriority(group.First().Label))
.ThenBy(group => group.Key, StringComparer.Ordinal)
.ToArray();
return families.Select((familyGroup, familyIndex) => new
{
TargetNodeId = targetGroup.Key,
FamilyKey = familyGroup.Key,
FamilyIndex = familyIndex,
FamilyCount = families.Length,
Edges = familyGroup.ToArray(),
SharedOuterX = familyGroup.Max(edge =>
{
var s = positionedNodes[edge.SourceNodeId];
return s.X + s.Width;
}) + 56d,
Priority = ElkEdgeChannelBands.ResolveLaneFamilyPriority(familyGroup.First().Label),
Span = familyGroup.Max(edge =>
{
var s = positionedNodes[edge.SourceNodeId];
var t = positionedNodes[edge.TargetNodeId];
return direction == ElkLayoutDirection.LeftToRight
? Math.Abs((s.X + (s.Width / 2d)) - (t.X + (t.Width / 2d)))
: Math.Abs((s.Y + (s.Height / 2d)) - (t.Y + (t.Height / 2d)));
}),
});
})
.OrderByDescending(group => group.Span)
.ThenBy(group => group.Priority)
.ThenBy(group => group.TargetNodeId, StringComparer.Ordinal)
.ToArray();
var reservedHorizontalBands = new List<double>();
for (var laneIndex = 0; laneIndex < backwardGroups.Length; laneIndex++)
{
var group = backwardGroups[laneIndex];
var useSourceCollector = string.Equals(group.FamilyKey, "repeat", StringComparison.Ordinal);
var preferredOuterY = double.NaN;
if (direction == ElkLayoutDirection.LeftToRight && useSourceCollector)
{
var lowerCorridorY = ElkEdgeChannelCorridors.ResolveBackwardLowerCorridorY(group.Edges, positionedNodes);
preferredOuterY = !double.IsNaN(lowerCorridorY)
? lowerCorridorY
: ElkEdgeChannelCorridors.ResolveBackwardCorridorY(group.Edges, positionedNodes);
}
if (!double.IsNaN(preferredOuterY))
{
reservedHorizontalBands.Add(preferredOuterY);
}
foreach (var edge in group.Edges)
{
channels[edge.Id] = new EdgeChannel(
EdgeRouteMode.BackwardOuter,
laneIndex,
group.FamilyIndex,
group.FamilyCount,
0,
1,
0,
1,
-1,
0,
group.SharedOuterX,
preferredOuterY,
useSourceCollector,
double.NaN);
}
}
var forwardEdgesByTarget = new Dictionary<string, List<ElkEdge>>(StringComparer.Ordinal);
foreach (var sourceEdges in forwardEdgesBySource.Values)
{
foreach (var edge in sourceEdges)
{
if (!forwardEdgesByTarget.TryGetValue(edge.TargetNodeId, out var list))
{
list = [];
forwardEdgesByTarget[edge.TargetNodeId] = list;
}
list.Add(edge);
}
}
var sinkBandsByEdgeId = new Dictionary<string, (int BandIndex, int BandCount, double SharedOuterX, double PreferredOuterY)>(StringComparer.Ordinal);
if (direction == ElkLayoutDirection.LeftToRight)
{
var reservedSinkBands = new List<double>(reservedHorizontalBands);
foreach (var targetEdges in forwardEdgesByTarget)
{
var targetNode = positionedNodes[targetEdges.Key];
var isSinkTarget = string.Equals(targetNode.Kind, "End", StringComparison.OrdinalIgnoreCase)
|| !outgoingCounts.ContainsKey(targetEdges.Key);
if (!isSinkTarget || targetEdges.Value.Count < 2)
{
continue;
}
var sinkBands = targetEdges.Value
.GroupBy(edge => ElkEdgeChannelBands.ResolveLaneFamilyKey(edge.Label), StringComparer.Ordinal)
.OrderBy(group => ElkEdgeChannelBands.ResolveSinkLanePriority(group.First().Label))
.ThenBy(group => group.Key, StringComparer.Ordinal)
.ToArray();
for (var bandIndex = 0; bandIndex < sinkBands.Length; bandIndex++)
{
var sinkBandEdges = sinkBands[bandIndex].ToArray();
var sharedOuterX = sinkBands[bandIndex].Max(edge =>
{
var s = positionedNodes[edge.SourceNodeId];
return s.X + s.Width;
}) + 56d;
var familyKey = ElkEdgeChannelBands.ResolveLaneFamilyKey(sinkBands[bandIndex].First().Label);
var preferredOuterY = familyKey is "failure" or "timeout"
? ElkEdgeChannelSinkCorridors.ResolveSinkCorridorY(sinkBandEdges, positionedNodes, reservedSinkBands)
: double.NaN;
if (!double.IsNaN(preferredOuterY))
{
reservedSinkBands.Add(preferredOuterY + (bandIndex * 24d));
}
foreach (var edge in sinkBandEdges)
{
sinkBandsByEdgeId[edge.Id] = (bandIndex, sinkBands.Length, sharedOuterX, preferredOuterY);
}
}
}
}
foreach (var sourceEdges in forwardEdgesBySource.Values)
{
var sorted = sourceEdges
.OrderBy(e =>
{
var t = positionedNodes[e.TargetNodeId];
return direction == ElkLayoutDirection.LeftToRight
? t.Y + (t.Height / 2d)
: t.X + (t.Width / 2d);
})
.ToArray();
for (var index = 0; index < sorted.Length; index++)
{
var targetEdges = forwardEdgesByTarget.GetValueOrDefault(sorted[index].TargetNodeId);
var targetIncomingIndex = 0;
var targetIncomingCount = 1;
if (targetEdges is not null && targetEdges.Count > 1)
{
var sortedBySourceY = targetEdges
.OrderBy(e =>
{
var s = positionedNodes[e.SourceNodeId];
return direction == ElkLayoutDirection.LeftToRight
? s.Y + (s.Height / 2d)
: s.X + (s.Width / 2d);
})
.ToList();
targetIncomingIndex = sortedBySourceY.FindIndex(e => string.Equals(e.Id, sorted[index].Id, StringComparison.Ordinal));
targetIncomingCount = sortedBySourceY.Count;
}
var sinkBand = sinkBandsByEdgeId.GetValueOrDefault(sorted[index].Id, (-1, 0, 0d, double.NaN));
var routeMode = EdgeRouteMode.Direct;
if (sinkBandsByEdgeId.ContainsKey(sorted[index].Id))
{
var sourceNode = positionedNodes[sorted[index].SourceNodeId];
var isGatewaySource = string.Equals(sourceNode.Kind, "Decision", StringComparison.OrdinalIgnoreCase)
|| string.Equals(sourceNode.Kind, "Fork", StringComparison.OrdinalIgnoreCase);
if (isGatewaySource)
{
sinkBand = (-1, 0, 0d, double.NaN);
}
else
{
var familyKey = ElkEdgeChannelBands.ResolveLaneFamilyKey(sorted[index].Label);
if (familyKey is "failure" or "timeout")
{
routeMode = EdgeRouteMode.SinkOuterTop;
}
else
{
routeMode = EdgeRouteMode.SinkOuter;
}
}
}
channels[sorted[index].Id] = new EdgeChannel(
routeMode,
-1,
0,
1,
index,
sorted.Length,
targetIncomingIndex,
targetIncomingCount,
sinkBand.Item1,
sinkBand.Item2,
sinkBand.Item3,
sinkBand.Item4,
false,
double.NaN);
}
}
ElkEdgeChannelBands.AllocateDirectForwardChannelBands(edges, positionedNodes, layerBoundariesByNodeId, channels, direction);
return channels;
}
}