namespace StellaOps.ElkSharp; internal static class ElkEdgeRouterHighway { private const double MinHighwayRatio = 2d / 5d; private const double BoundaryInset = 4d; private const double MinimumSpreadSpacing = 12d; private const double CoordinateTolerance = 0.5d; internal static ElkRoutedEdge[] BreakShortHighways( ElkRoutedEdge[] edges, ElkPositionedNode[] nodes) { if (edges.Length < 2 || nodes.Length == 0) { return edges; } var serviceNodes = nodes.Where(n => n.Kind is not "Start" and not "End").ToArray(); var minLineClearance = serviceNodes.Length > 0 ? Math.Min(serviceNodes.Average(n => n.Width), serviceNodes.Average(n => n.Height)) / 2d : 50d; var nodesById = nodes.ToDictionary(n => n.Id, StringComparer.Ordinal); var graphMinY = nodes.Min(n => n.Y); var graphMaxY = nodes.Max(n => n.Y + n.Height); var result = edges.ToArray(); foreach (var (key, edgeIndices) in BuildTargetSideGroups(result, nodesById, graphMinY, graphMaxY) .OrderBy(pair => pair.Key, StringComparer.Ordinal)) { if (edgeIndices.Count < 2) { continue; } var separator = key.IndexOf('|', StringComparison.Ordinal); var targetId = key[..separator]; var side = key[(separator + 1)..]; if (!nodesById.TryGetValue(targetId, out var targetNode)) { continue; } ProcessTargetSideGroup(result, edgeIndices, targetNode, side, minLineClearance); } return result; } internal static IReadOnlyList DetectRemainingBrokenHighways( ElkRoutedEdge[] edges, ElkPositionedNode[] nodes) { if (edges.Length < 2 || nodes.Length == 0) { return []; } var serviceNodes = nodes.Where(n => n.Kind is not "Start" and not "End").ToArray(); var minLineClearance = serviceNodes.Length > 0 ? Math.Min(serviceNodes.Average(n => n.Width), serviceNodes.Average(n => n.Height)) / 2d : 50d; var nodesById = nodes.ToDictionary(n => n.Id, StringComparer.Ordinal); var graphMinY = nodes.Min(n => n.Y); var graphMaxY = nodes.Max(n => n.Y + n.Height); var detections = new List(); foreach (var (key, edgeIndices) in BuildTargetSideGroups(edges, nodesById, graphMinY, graphMaxY) .OrderBy(pair => pair.Key, StringComparer.Ordinal)) { if (edgeIndices.Count < 2) { continue; } var separator = key.IndexOf('|', StringComparison.Ordinal); var targetId = key[..separator]; var side = key[(separator + 1)..]; if (!nodesById.TryGetValue(targetId, out var targetNode)) { continue; } var detection = EvaluateTargetSideGroup(edges, edgeIndices, targetNode, side, minLineClearance); if (detection is not null && detection.Value.Diagnostic.WasBroken) { detections.Add(detection.Value.Diagnostic); } } return detections; } private static bool ShouldProcessEdge(ElkRoutedEdge edge, double graphMinY, double graphMaxY) { if (!string.IsNullOrWhiteSpace(edge.Kind) && edge.Kind.StartsWith("backward|", StringComparison.OrdinalIgnoreCase)) { return false; } if (ElkEdgePostProcessor.HasCorridorBendPoints(edge, graphMinY, graphMaxY)) { return false; } if (ElkEdgePostProcessor.IsRepeatCollectorLabel(edge.Label)) { return false; } return true; } private static void ProcessTargetSideGroup( ElkRoutedEdge[] result, List edgeIndices, ElkPositionedNode targetNode, string side, double minLineClearance) { var evaluation = EvaluateTargetSideGroup(result, edgeIndices, targetNode, side, minLineClearance); if (evaluation is null) { return; } ElkLayoutDiagnostics.AddDetectedHighway(evaluation.Value.Diagnostic); if (!evaluation.Value.Diagnostic.WasBroken) { return; } var ordered = evaluation.Value.Members .OrderBy(member => member.EndpointCoord) .ThenBy(member => member.EdgeId, StringComparer.Ordinal) .ToList(); var slotCoords = BuildSlotCoordinates(targetNode, side, ordered.Count, minLineClearance); for (var i = 0; i < ordered.Count; i++) { var adjustedPath = AdjustPathToTargetSlot(ordered[i].Path, targetNode, side, slotCoords[i], minLineClearance); WriteBackPath(result, ordered[i].Index, adjustedPath); } } private static Dictionary> BuildTargetSideGroups( IReadOnlyList edges, IReadOnlyDictionary nodesById, double graphMinY, double graphMaxY) { var edgesByTargetSide = new Dictionary>(StringComparer.Ordinal); for (var i = 0; i < edges.Count; i++) { var edge = edges[i]; if (!ShouldProcessEdge(edge, graphMinY, graphMaxY)) { continue; } if (!nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode)) { continue; } var path = ExtractFullPath(edge); if (path.Count < 2) { continue; } var side = ElkEdgeRoutingGeometry.ResolveBoundarySide(path[^1], targetNode); var key = $"{targetNode.Id}|{side}"; if (!edgesByTargetSide.TryGetValue(key, out var list)) { list = []; edgesByTargetSide[key] = list; } list.Add(i); } return edgesByTargetSide; } private static GroupEvaluation? EvaluateTargetSideGroup( IReadOnlyList edges, IReadOnlyList edgeIndices, ElkPositionedNode targetNode, string side, double minLineClearance) { var members = edgeIndices .Select(index => CreateMember(edges[index], index, targetNode, side)) .Where(member => member.Path.Count >= 2) .OrderBy(member => member.EdgeId, StringComparer.Ordinal) .ToList(); if (members.Count < 2) { return null; } var pairMetrics = ComputePairMetrics(members); var actualGap = ComputeMinEndpointGap(members, side); var requiresSpread = (actualGap + CoordinateTolerance) < minLineClearance && !pairMetrics.AllPairsApplicable; if (!requiresSpread && pairMetrics.ShortestSharedRatio < MinHighwayRatio) { requiresSpread = pairMetrics.HasSharedSegment; } var diagnostic = new ElkHighwayDiagnostics { TargetNodeId = targetNode.Id, SharedAxis = side, SharedCoord = Math.Round(members.Average(member => member.EndpointCoord), 1), EdgeIds = members.Select(member => member.EdgeId).ToArray(), MinRatio = pairMetrics.HasSharedSegment ? Math.Round(pairMetrics.ShortestSharedRatio, 3) : 0d, WasBroken = requiresSpread, Reason = requiresSpread ? pairMetrics.HasSharedSegment && pairMetrics.ShortestSharedRatio < MinHighwayRatio ? $"shared ratio {pairMetrics.ShortestSharedRatio:F2} < {MinHighwayRatio:F2}" : $"gap {actualGap:F0}px < clearance {minLineClearance:F0}px" : pairMetrics.AllPairsApplicable ? $"shared ratio {pairMetrics.ShortestSharedRatio:F2} >= {MinHighwayRatio:F2}" : $"gap {actualGap:F0}px >= clearance {minLineClearance:F0}px", }; return new GroupEvaluation(members, diagnostic); } private static HighwayPairMetrics ComputePairMetrics(IReadOnlyList members) { var shortestSharedRatio = double.MaxValue; var hasSharedSegment = false; var allPairsApplicable = true; for (var i = 0; i < members.Count; i++) { for (var j = i + 1; j < members.Count; j++) { var sharedLength = ElkEdgeRoutingGeometry.ComputeLongestSharedApproachSegmentLength( members[i].Path, members[j].Path); if (sharedLength <= 1d) { allPairsApplicable = false; continue; } hasSharedSegment = true; var shortestPath = Math.Min(members[i].PathLength, members[j].PathLength); if (shortestPath <= 1d) { allPairsApplicable = false; continue; } var ratio = sharedLength / shortestPath; shortestSharedRatio = Math.Min(shortestSharedRatio, ratio); if (ratio < MinHighwayRatio) { allPairsApplicable = false; } } } return new HighwayPairMetrics( HasSharedSegment: hasSharedSegment, AllPairsApplicable: allPairsApplicable && hasSharedSegment, ShortestSharedRatio: hasSharedSegment ? shortestSharedRatio : 0d); } private static double ComputeMinEndpointGap(IReadOnlyList members, string side) { var coords = members .Select(member => member.EndpointCoord) .OrderBy(value => value) .ToArray(); if (coords.Length < 2) { return double.MaxValue; } var minGap = double.MaxValue; for (var i = 1; i < coords.Length; i++) { minGap = Math.Min(minGap, coords[i] - coords[i - 1]); } return minGap; } private static double[] BuildSlotCoordinates( ElkPositionedNode targetNode, string side, int count, double minLineClearance) { if (count <= 1) { return [ side is "left" or "right" ? targetNode.Y + (targetNode.Height / 2d) : targetNode.X + (targetNode.Width / 2d), ]; } var axisMin = side is "left" or "right" ? targetNode.Y + BoundaryInset : targetNode.X + BoundaryInset; var axisMax = side is "left" or "right" ? targetNode.Y + targetNode.Height - BoundaryInset : targetNode.X + targetNode.Width - BoundaryInset; var axisLength = Math.Max(8d, axisMax - axisMin); var spacing = Math.Max( MinimumSpreadSpacing, Math.Min(minLineClearance, axisLength / (count - 1))); var totalSpan = (count - 1) * spacing; var center = (axisMin + axisMax) / 2d; var start = Math.Max(axisMin, Math.Min(center - (totalSpan / 2d), axisMax - totalSpan)); return Enumerable.Range(0, count) .Select(index => Math.Min(axisMax, start + (index * spacing))) .ToArray(); } private static List AdjustPathToTargetSlot( IReadOnlyList path, ElkPositionedNode targetNode, string side, double slotCoord, double minLineClearance) { var adjusted = path .Select(point => new ElkPoint { X = point.X, Y = point.Y }) .ToList(); if (adjusted.Count < 2) { return adjusted; } if (side is "left" or "right") { var targetX = side == "left" ? targetNode.X : targetNode.X + targetNode.Width; while (adjusted.Count >= 3 && Math.Abs(adjusted[^2].X - targetX) <= CoordinateTolerance) { adjusted.RemoveAt(adjusted.Count - 2); } var anchor = adjusted[^2]; var rebuilt = adjusted.Take(adjusted.Count - 1).ToList(); if (Math.Abs(anchor.X - targetX) <= CoordinateTolerance) { var offset = Math.Max(24d, minLineClearance / 2d); rebuilt.Add(new ElkPoint { X = side == "left" ? targetX - offset : targetX + offset, Y = anchor.Y, }); anchor = rebuilt[^1]; } if (Math.Abs(anchor.Y - slotCoord) > CoordinateTolerance) { rebuilt.Add(new ElkPoint { X = anchor.X, Y = slotCoord }); } rebuilt.Add(new ElkPoint { X = targetX, Y = slotCoord }); return NormalizePath(rebuilt); } var targetY = side == "top" ? targetNode.Y : targetNode.Y + targetNode.Height; while (adjusted.Count >= 3 && Math.Abs(adjusted[^2].Y - targetY) <= CoordinateTolerance) { adjusted.RemoveAt(adjusted.Count - 2); } var verticalAnchor = adjusted[^2]; var verticalRebuilt = adjusted.Take(adjusted.Count - 1).ToList(); if (Math.Abs(verticalAnchor.Y - targetY) <= CoordinateTolerance) { var offset = Math.Max(24d, minLineClearance / 2d); verticalRebuilt.Add(new ElkPoint { X = verticalAnchor.X, Y = side == "top" ? targetY - offset : targetY + offset, }); verticalAnchor = verticalRebuilt[^1]; } if (Math.Abs(verticalAnchor.X - slotCoord) > CoordinateTolerance) { verticalRebuilt.Add(new ElkPoint { X = slotCoord, Y = verticalAnchor.Y }); } verticalRebuilt.Add(new ElkPoint { X = slotCoord, Y = targetY }); return NormalizePath(verticalRebuilt); } private static List NormalizePath(IReadOnlyList path) { var deduped = new List(); foreach (var point in path) { if (deduped.Count == 0 || !ElkEdgeRoutingGeometry.PointsEqual(deduped[^1], point)) { deduped.Add(point); } } if (deduped.Count <= 2) { return deduped; } var simplified = new List { deduped[0] }; for (var i = 1; i < deduped.Count - 1; i++) { var previous = simplified[^1]; var current = deduped[i]; var next = deduped[i + 1]; var sameX = Math.Abs(previous.X - current.X) <= CoordinateTolerance && Math.Abs(current.X - next.X) <= CoordinateTolerance; var sameY = Math.Abs(previous.Y - current.Y) <= CoordinateTolerance && Math.Abs(current.Y - next.Y) <= CoordinateTolerance; if (!sameX && !sameY) { simplified.Add(current); } } simplified.Add(deduped[^1]); return simplified; } private static HighwayMember CreateMember( ElkRoutedEdge edge, int index, ElkPositionedNode targetNode, string side) { var path = ExtractFullPath(edge); var endpointCoord = side is "left" or "right" ? path[^1].Y : path[^1].X; return new HighwayMember( Index: index, EdgeId: edge.Id, Edge: edge, Path: path, PathLength: ElkEdgeRoutingGeometry.ComputePathLength(edge), EndpointCoord: endpointCoord); } private static void WriteBackPath(ElkRoutedEdge[] result, int edgeIndex, IReadOnlyList path) { var edge = result[edgeIndex]; result[edgeIndex] = new ElkRoutedEdge { Id = edge.Id, SourceNodeId = edge.SourceNodeId, TargetNodeId = edge.TargetNodeId, SourcePortId = edge.SourcePortId, TargetPortId = edge.TargetPortId, Kind = edge.Kind, Label = edge.Label, Sections = [ new ElkEdgeSection { StartPoint = path[0], EndPoint = path[^1], BendPoints = path.Count > 2 ? path.Skip(1).Take(path.Count - 2).ToArray() : [], }, ], }; } private static List ExtractFullPath(ElkRoutedEdge edge) { var path = new List(); foreach (var section in edge.Sections) { if (path.Count == 0) { path.Add(section.StartPoint); } path.AddRange(section.BendPoints); path.Add(section.EndPoint); } return path; } private readonly record struct HighwayMember( int Index, string EdgeId, ElkRoutedEdge Edge, List Path, double PathLength, double EndpointCoord); private readonly record struct HighwayPairMetrics( bool HasSharedSegment, bool AllPairsApplicable, double ShortestSharedRatio); private readonly record struct GroupEvaluation( IReadOnlyList Members, ElkHighwayDiagnostics Diagnostic); }