202 lines
6.5 KiB
C#
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);
|
|
}
|
|
}
|