using System.Collections.Concurrent; using System.Diagnostics; using System.Globalization; namespace StellaOps.ElkSharp; internal static partial class ElkEdgeRouterIterative { private static List? TryBuildPreferredSideShortcut( ElkPositionedNode sourceNode, ElkPositionedNode targetNode, IReadOnlyCollection nodes, string sourceId, string targetId) { var sourceCenterX = sourceNode.X + (sourceNode.Width / 2d); var sourceCenterY = sourceNode.Y + (sourceNode.Height / 2d); var targetCenterX = targetNode.X + (targetNode.Width / 2d); var targetCenterY = targetNode.Y + (targetNode.Height / 2d); var deltaX = targetCenterX - sourceCenterX; var deltaY = targetCenterY - sourceCenterY; var absDx = Math.Abs(deltaX); var absDy = Math.Abs(deltaY); if (absDx < 16d && absDy < 16d) { return null; } var horizontalDominant = absDx >= absDy; var preferredSourceSide = horizontalDominant ? deltaX >= 0d ? "right" : "left" : deltaY >= 0d ? "bottom" : "top"; var preferredTargetSide = horizontalDominant ? deltaX >= 0d ? "left" : "right" : deltaY >= 0d ? "top" : "bottom"; var start = BuildPreferredBoundaryPoint(sourceNode, preferredSourceSide, targetNode); var end = BuildPreferredBoundaryPoint(targetNode, preferredTargetSide, sourceNode); return TryBuildShortestOrthogonalPath(start, end, nodes, sourceId, targetId, targetNode, 0d); } private static ElkPoint BuildPreferredBoundaryPoint( ElkPositionedNode node, string side, ElkPositionedNode otherNode) { var horizontalInset = Math.Min(24d, Math.Max(12d, node.Width / 4d)); var verticalInset = Math.Min(24d, Math.Max(12d, node.Height / 4d)); var otherCenterX = otherNode.X + (otherNode.Width / 2d); var otherCenterY = otherNode.Y + (otherNode.Height / 2d); var boundary = side switch { "left" => new ElkPoint { X = node.X, Y = Math.Clamp(otherCenterY, node.Y + verticalInset, (node.Y + node.Height) - verticalInset), }, "right" => new ElkPoint { X = node.X + node.Width, Y = Math.Clamp(otherCenterY, node.Y + verticalInset, (node.Y + node.Height) - verticalInset), }, "top" => new ElkPoint { X = Math.Clamp(otherCenterX, node.X + horizontalInset, (node.X + node.Width) - horizontalInset), Y = node.Y, }, _ => new ElkPoint { X = Math.Clamp(otherCenterX, node.X + horizontalInset, (node.X + node.Width) - horizontalInset), Y = node.Y + node.Height, }, }; if (!ElkShapeBoundaries.IsGatewayShape(node)) { return boundary; } var referencePoint = side switch { "left" => new ElkPoint { X = node.X - Math.Max(24d, node.Width / 3d), Y = boundary.Y }, "right" => new ElkPoint { X = node.X + node.Width + Math.Max(24d, node.Width / 3d), Y = boundary.Y }, "top" => new ElkPoint { X = boundary.X, Y = node.Y - Math.Max(24d, node.Height / 3d) }, _ => new ElkPoint { X = boundary.X, Y = node.Y + node.Height + Math.Max(24d, node.Height / 3d) }, }; var projected = ElkShapeBoundaries.ProjectOntoShapeBoundary(node, referencePoint); return ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(node, projected, referencePoint); } private static List? TryBuildShortestOrthogonalPath( ElkPoint start, ElkPoint end, IReadOnlyCollection nodes, string sourceId, string targetId, ElkPositionedNode? targetNode, double obstaclePadding) { var rawObstacles = nodes.Select(node => ( Left: node.X - obstaclePadding, Top: node.Y - obstaclePadding, Right: node.X + node.Width + obstaclePadding, Bottom: node.Y + node.Height + obstaclePadding, Id: node.Id)).ToArray(); bool SegmentIsClear(ElkPoint from, ElkPoint to) => !ElkEdgePostProcessor.SegmentCrossesObstacle(from, to, rawObstacles, sourceId, targetId); if (Math.Abs(start.X - end.X) < 0.5d || Math.Abs(start.Y - end.Y) < 0.5d) { return SegmentIsClear(start, end) ? [start, end] : null; } foreach (var pivot in EnumerateOrthogonalShortcutPivots(start, end, targetNode)) { if (!SegmentIsClear(start, pivot) || !SegmentIsClear(pivot, end)) { continue; } return NormalizePolyline([start, pivot, end]); } return null; } private static IEnumerable EnumerateOrthogonalShortcutPivots( ElkPoint start, ElkPoint end, ElkPositionedNode? targetNode) { var targetSide = targetNode is null ? string.Empty : ElkEdgeRoutingGeometry.ResolveBoundarySide(end, targetNode); var preferred = targetSide is "left" or "right" ? new ElkPoint { X = start.X, Y = end.Y } : new ElkPoint { X = end.X, Y = start.Y }; var alternate = targetSide is "left" or "right" ? new ElkPoint { X = end.X, Y = start.Y } : new ElkPoint { X = start.X, Y = end.Y }; yield return preferred; if (!ElkEdgeRoutingGeometry.PointsEqual(preferred, alternate)) { yield return alternate; } } private static IEnumerable EnumerateShortestRepairEndpoints( ElkPoint start, ElkPoint currentEnd, ElkPositionedNode? targetNode) { var endpoints = new List(); void AddCandidate(ElkPoint candidate) { if (!endpoints.Any(existing => ElkEdgeRoutingGeometry.PointsEqual(existing, candidate))) { endpoints.Add(candidate); } } AddCandidate(currentEnd); if (targetNode is null) { return endpoints; } if (ElkShapeBoundaries.IsGatewayShape(targetNode)) { if (ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, "left", start.Y, out var left)) { AddCandidate(left); } if (ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, "right", start.Y, out var right)) { AddCandidate(right); } if (ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, "top", start.X, out var top)) { AddCandidate(top); } if (ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, "bottom", start.X, out var bottom)) { AddCandidate(bottom); } return endpoints; } var horizontalInset = Math.Min(24d, Math.Max(12d, targetNode.Width / 4d)); var verticalInset = Math.Min(24d, Math.Max(12d, targetNode.Height / 4d)); var candidateEndpoints = new[] { new ElkPoint { X = targetNode.X, Y = Math.Clamp(start.Y, targetNode.Y + verticalInset, (targetNode.Y + targetNode.Height) - verticalInset), }, new ElkPoint { X = targetNode.X + targetNode.Width, Y = Math.Clamp(start.Y, targetNode.Y + verticalInset, (targetNode.Y + targetNode.Height) - verticalInset), }, new ElkPoint { X = Math.Clamp(start.X, targetNode.X + horizontalInset, (targetNode.X + targetNode.Width) - horizontalInset), Y = targetNode.Y, }, new ElkPoint { X = Math.Clamp(start.X, targetNode.X + horizontalInset, (targetNode.X + targetNode.Width) - horizontalInset), Y = targetNode.Y + targetNode.Height, }, }; foreach (var candidate in candidateEndpoints) { AddCandidate(candidate); } return endpoints; } }