using System.Collections.Concurrent; using System.Diagnostics; using System.Globalization; namespace StellaOps.ElkSharp; internal static partial class ElkEdgeRouterIterative { private static Dictionary SpreadTargetEndpoints( ElkRoutedEdge[] edges, Dictionary nodesById, double graphMinY, double graphMaxY, double minLineClearance) { var result = new Dictionary(StringComparer.Ordinal); // Group routable edges by target + entry side var groups = new Dictionary>(StringComparer.Ordinal); foreach (var edge in edges) { if (!ShouldRouteEdge(edge, graphMinY, graphMaxY)) { continue; } var lastSection = edge.Sections.LastOrDefault(); if (lastSection is null || !nodesById.TryGetValue(edge.TargetNodeId ?? "", out var targetNode)) { continue; } var ep = lastSection.EndPoint; var adjacentPoint = lastSection.BendPoints.LastOrDefault() ?? lastSection.StartPoint; var side = ResolveEntrySide(ep, adjacentPoint, targetNode); var key = $"{edge.TargetNodeId}|{side}"; if (!groups.TryGetValue(key, out var list)) { list = []; groups[key] = list; } list.Add((edge.Id, ep)); } // For each group with 2+ edges on the same side, spread them foreach (var (key, group) in groups) { if (group.Count < 2) { continue; } var parts = key.Split('|'); if (!nodesById.TryGetValue(parts[0], out var node)) { continue; } var side = parts[1]; var sorted = side is "left" or "right" ? group.OrderBy(g => g.OrigEnd.Y).ThenBy(g => g.EdgeId, StringComparer.Ordinal).ToList() : group.OrderBy(g => g.OrigEnd.X).ThenBy(g => g.EdgeId, StringComparer.Ordinal).ToList(); var assignedSlotCoordinates = ElkBoundarySlots.BuildAssignedBoundarySlotCoordinates(node, side, sorted.Count); if (side is "left" or "right") { for (var i = 0; i < sorted.Count; i++) { result[sorted[i].EdgeId] = new ElkPoint { X = sorted[i].OrigEnd.X, Y = assignedSlotCoordinates[i], }; } } else { for (var i = 0; i < sorted.Count; i++) { result[sorted[i].EdgeId] = new ElkPoint { X = assignedSlotCoordinates[i], Y = sorted[i].OrigEnd.Y, }; } } } return result; } private static string ResolveEntrySide(ElkPoint endpoint, ElkPoint adjacentPoint, ElkPositionedNode node) { return ElkEdgeRoutingGeometry.ResolveBoundaryApproachSide(endpoint, adjacentPoint, node); } private static bool ShouldRouteEdge(ElkRoutedEdge edge, double graphMinY, double graphMaxY) { // Skip port-anchored edges (their anchors are fixed) if (!string.IsNullOrWhiteSpace(edge.SourcePortId) || !string.IsNullOrWhiteSpace(edge.TargetPortId)) { return false; } // Skip backward edges (routed through external corridors) if (!string.IsNullOrWhiteSpace(edge.Kind) && edge.Kind.StartsWith("backward|", StringComparison.OrdinalIgnoreCase)) { return false; } // Skip edges with corridor bend points (already routed outside graph bounds) if (ElkEdgePostProcessor.HasCorridorBendPoints(edge, graphMinY, graphMaxY)) { return false; } // Skip repeat collector labels return !ElkEdgePostProcessor.IsRepeatCollectorLabel(edge.Label); } }