Files
git.stella-ops.org/src/__Libraries/StellaOps.ElkSharp/ElkEdgeChannelBands.cs
2026-03-23 13:23:19 +02:00

202 lines
6.5 KiB
C#

namespace StellaOps.ElkSharp;
internal static class ElkEdgeChannelBands
{
internal static void AllocateDirectForwardChannelBands(
IReadOnlyCollection<ElkEdge> edges,
IReadOnlyDictionary<string, ElkPositionedNode> positionedNodes,
IReadOnlyDictionary<string, LayerBoundary> layerBoundariesByNodeId,
Dictionary<string, EdgeChannel> channels,
ElkLayoutDirection direction)
{
if (direction != ElkLayoutDirection.LeftToRight)
{
return;
}
var candidates = edges
.Select(edge => TryCreateDirectChannelCandidate(edge, positionedNodes, layerBoundariesByNodeId, channels))
.Where(candidate => candidate is not null)
.Select(candidate => candidate!.Value)
.GroupBy(candidate => candidate.GapKey, StringComparer.Ordinal);
foreach (var gapGroup in candidates)
{
var ordered = gapGroup
.OrderBy(candidate => candidate.TargetCenterY)
.ThenBy(candidate => candidate.TargetX)
.ThenBy(candidate => candidate.SourceCenterY)
.ThenBy(candidate => candidate.FamilyPriority)
.ThenBy(candidate => candidate.EdgeId, StringComparer.Ordinal)
.ToArray();
if (ordered.Length == 0)
{
continue;
}
var gapMinX = ordered.Max(candidate => candidate.GapMinX);
var gapMaxX = ordered.Min(candidate => candidate.GapMaxX);
if (gapMaxX - gapMinX < 24d)
{
continue;
}
var edgePadding = Math.Min(28d, Math.Max(16d, (gapMaxX - gapMinX) * 0.12d));
var usableMinX = gapMinX + edgePadding;
var usableMaxX = gapMaxX - edgePadding;
if (usableMaxX <= usableMinX)
{
usableMinX = gapMinX + 12d;
usableMaxX = gapMaxX - 12d;
}
for (var index = 0; index < ordered.Length; index++)
{
var preferredX = ordered.Length == 1
? (usableMinX + usableMaxX) / 2d
: usableMinX + ((usableMaxX - usableMinX) * (index / (double)(ordered.Length - 1)));
preferredX = ElkLayoutHelpers.Clamp(preferredX, ordered[index].GapMinX + 8d, ordered[index].GapMaxX - 8d);
if (!channels.TryGetValue(ordered[index].EdgeId, out var channel))
{
continue;
}
channels[ordered[index].EdgeId] = channel with
{
PreferredDirectChannelX = preferredX,
};
}
}
}
internal static DirectChannelCandidate? TryCreateDirectChannelCandidate(
ElkEdge edge,
IReadOnlyDictionary<string, ElkPositionedNode> positionedNodes,
IReadOnlyDictionary<string, LayerBoundary> layerBoundariesByNodeId,
IReadOnlyDictionary<string, EdgeChannel> channels)
{
if (!channels.TryGetValue(edge.Id, out var channel) || channel.RouteMode != EdgeRouteMode.Direct)
{
return null;
}
var source = positionedNodes[edge.SourceNodeId];
var target = positionedNodes[edge.TargetNodeId];
var sourceCenterX = source.X + (source.Width / 2d);
var targetCenterX = target.X + (target.Width / 2d);
if (targetCenterX <= sourceCenterX + 1d)
{
return null;
}
var sourceCenterY = source.Y + (source.Height / 2d);
var targetCenterY = target.Y + (target.Height / 2d);
if (Math.Abs(targetCenterY - sourceCenterY) < 56d)
{
return null;
}
var sourceBoundary = ElkLayoutHelpers.ResolveLayerBoundary(edge.SourceNodeId, layerBoundariesByNodeId, source);
var targetBoundary = ElkLayoutHelpers.ResolveLayerBoundary(edge.TargetNodeId, layerBoundariesByNodeId, target);
var gapMinX = sourceBoundary.MaxX + 12d;
var gapMaxX = targetBoundary.MinX - 12d;
if (gapMaxX - gapMinX < 48d)
{
return null;
}
var gapKey = $"{Math.Round(gapMinX, 2):0.##}|{Math.Round(gapMaxX, 2):0.##}";
return new DirectChannelCandidate(
edge.Id,
gapKey,
gapMinX,
gapMaxX,
ResolveLaneFamilyPriority(edge.Label),
sourceCenterY,
targetCenterY,
target.X);
}
internal static string ResolveLaneFamilyKey(string? label)
{
if (string.IsNullOrWhiteSpace(label))
{
return "default";
}
var normalized = label.Trim().ToLowerInvariant();
if (normalized.Contains("failure", StringComparison.Ordinal))
{
return "failure";
}
if (normalized.Contains("timeout", StringComparison.Ordinal))
{
return "timeout";
}
if (normalized.StartsWith("repeat ", StringComparison.Ordinal)
|| normalized.Equals("body", StringComparison.Ordinal))
{
return "repeat";
}
if (normalized.StartsWith("when ", StringComparison.Ordinal))
{
return "success";
}
if (normalized.Contains("otherwise", StringComparison.Ordinal)
|| normalized.Contains("default", StringComparison.Ordinal))
{
return "default";
}
if (normalized.Contains("missing condition", StringComparison.Ordinal))
{
return "missing-condition";
}
return "default";
}
internal static int ResolveLaneFamilyPriority(string? label)
{
return ResolveLaneFamilyKey(label) switch
{
"failure" => 0,
"timeout" => 1,
"repeat" => 2,
"default" => 3,
"success" => 4,
"missing-condition" => 5,
_ => 6,
};
}
internal static int ResolveSinkLanePriority(string? label)
{
return ResolveLaneFamilyKey(label) switch
{
"default" => 0,
"success" => 1,
"repeat" => 2,
"timeout" => 3,
"failure" => 4,
"missing-condition" => 5,
_ => 6,
};
}
internal static double ResolveSinkBandOffset(int bandIndex, double firstSpacing = 36d, double subsequentSpacing = 28d)
{
if (bandIndex <= 0)
{
return 0d;
}
return firstSpacing + ((bandIndex - 1) * subsequentSpacing);
}
}