namespace StellaOps.ElkSharp; internal static class ElkEdgeChannelBands { internal static void AllocateDirectForwardChannelBands( IReadOnlyCollection edges, IReadOnlyDictionary positionedNodes, IReadOnlyDictionary layerBoundariesByNodeId, Dictionary 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 positionedNodes, IReadOnlyDictionary layerBoundariesByNodeId, IReadOnlyDictionary 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); } }