namespace StellaOps.ElkSharp; internal static partial class ElkEdgePostProcessor { private static bool TryPolishGatewayUnderNodeTargetPeerConflicts( ElkRoutedEdge candidateEdge, IReadOnlyList currentEdges, int candidateIndex, ElkPositionedNode[] nodes, double minLineClearance, out ElkRoutedEdge polishedEdge) { polishedEdge = candidateEdge; if (string.IsNullOrWhiteSpace(candidateEdge.TargetNodeId)) { return false; } var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal); if (!nodesById.TryGetValue(candidateEdge.TargetNodeId, out var targetNode) || !ElkShapeBoundaries.IsGatewayShape(targetNode)) { return false; } nodesById.TryGetValue(candidateEdge.SourceNodeId ?? string.Empty, out var sourceNode); var peerEdges = currentEdges .Where((edge, index) => index != candidateIndex && string.Equals(edge.TargetNodeId, candidateEdge.TargetNodeId, StringComparison.Ordinal)) .ToArray(); if (peerEdges.Length == 0) { return false; } var path = ExtractFullPath(candidateEdge); if (path.Count < 2) { return false; } var sourceNodeId = candidateEdge.SourceNodeId; var targetNodeId = candidateEdge.TargetNodeId; var currentBundle = peerEdges .Append(candidateEdge) .ToArray(); var currentTargetJoinViolations = ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(currentBundle, nodes); var currentSharedLaneViolations = ElkEdgeRoutingScoring.CountSharedLaneViolations(currentBundle, nodes); var currentUnderNodeSegments = CountUnderNodeSegments(path, nodes, sourceNodeId, targetNodeId, minLineClearance); var currentUnderNodeViolations = ElkEdgeRoutingScoring.CountUnderNodeViolations([candidateEdge], nodes); var currentLocalHardPressure = ComputeUnderNodeRepairLocalHardPressure(candidateEdge, nodes); var currentPathLength = ComputePathLength(path); if (currentTargetJoinViolations == 0 && currentSharedLaneViolations == 0 && currentUnderNodeSegments == 0 && currentUnderNodeViolations == 0) { return false; } var bestEdge = default(ElkRoutedEdge); var bestTargetJoinViolations = currentTargetJoinViolations; var bestSharedLaneViolations = currentSharedLaneViolations; var bestUnderNodeSegments = currentUnderNodeSegments; var bestUnderNodeViolations = currentUnderNodeViolations; var bestLocalHardPressure = currentLocalHardPressure; var bestPathLength = currentPathLength; foreach (var candidatePath in EnumerateGatewayUnderNodePeerConflictCandidates( path, targetNode, sourceNode, peerEdges, nodes, sourceNodeId, targetNodeId, minLineClearance)) { if (!PathChanged(path, candidatePath) || candidatePath.Count < 2 || HasNodeObstacleCrossing(candidatePath, nodes, sourceNodeId, targetNodeId) || !CanAcceptGatewayTargetRepair(candidatePath, targetNode) || !HasAcceptableGatewayBoundaryPath(candidatePath, nodes, sourceNodeId, targetNodeId, targetNode, fromStart: false)) { continue; } var localCandidateEdge = BuildSingleSectionEdge(candidateEdge, candidatePath); var localBundle = peerEdges .Append(localCandidateEdge) .ToArray(); var candidateTargetJoinViolations = ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(localBundle, nodes); var candidateSharedLaneViolations = ElkEdgeRoutingScoring.CountSharedLaneViolations(localBundle, nodes); var candidateUnderNodeSegments = CountUnderNodeSegments(candidatePath, nodes, sourceNodeId, targetNodeId, minLineClearance); var candidateUnderNodeViolations = ElkEdgeRoutingScoring.CountUnderNodeViolations([localCandidateEdge], nodes); var candidateLocalHardPressure = ComputeUnderNodeRepairLocalHardPressure(localCandidateEdge, nodes); var candidatePathLength = ComputePathLength(candidatePath); if (!IsBetterGatewayUnderNodePeerConflictCandidate( candidateTargetJoinViolations, candidateSharedLaneViolations, candidateUnderNodeSegments, candidateUnderNodeViolations, candidateLocalHardPressure, candidatePathLength, bestTargetJoinViolations, bestSharedLaneViolations, bestUnderNodeSegments, bestUnderNodeViolations, bestLocalHardPressure, bestPathLength)) { continue; } bestEdge = localCandidateEdge; bestTargetJoinViolations = candidateTargetJoinViolations; bestSharedLaneViolations = candidateSharedLaneViolations; bestUnderNodeSegments = candidateUnderNodeSegments; bestUnderNodeViolations = candidateUnderNodeViolations; bestLocalHardPressure = candidateLocalHardPressure; bestPathLength = candidatePathLength; } if (bestEdge is null) { return false; } polishedEdge = bestEdge; return true; } private static IEnumerable> EnumerateGatewayUnderNodePeerConflictCandidates( IReadOnlyList path, ElkPositionedNode targetNode, ElkPositionedNode? sourceNode, IReadOnlyCollection peerEdges, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId, double minLineClearance) { foreach (var side in EnumerateGatewayUnderNodePeerConflictSides(path, targetNode, peerEdges)) { var slotCoordinates = EnumerateGatewayUnderNodePeerConflictSlotCoordinates( path, targetNode, sourceNode, peerEdges, side, minLineClearance) .ToArray(); if (slotCoordinates.Length == 0) { continue; } foreach (var slotCoordinate in slotCoordinates) { if (sourceNode is not null && ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, side, slotCoordinate, out var bandBoundary) && TryBuildSafeHorizontalBandCandidate( sourceNode, targetNode, nodes, sourceNodeId, targetNodeId, path[0], bandBoundary, minLineClearance, preferredSourceExterior: null, out var bandCandidate)) { yield return bandCandidate; } foreach (var axis in EnumerateGatewayUnderNodePeerConflictAxes( path, targetNode, side, nodes, sourceNodeId, targetNodeId, minLineClearance)) { yield return BuildMixedTargetFaceCandidate(path, targetNode, side, slotCoordinate, axis); } } } } private static IEnumerable EnumerateGatewayUnderNodePeerConflictSides( IReadOnlyList path, ElkPositionedNode targetNode, IReadOnlyCollection peerEdges) { var seen = new HashSet(StringComparer.Ordinal); var currentSide = ResolveTargetApproachSide(path, targetNode); var peerSides = peerEdges .Select(edge => ExtractFullPath(edge)) .Where(peerPath => peerPath.Count >= 2) .Select(peerPath => ResolveTargetApproachSide(peerPath, targetNode)) .ToHashSet(StringComparer.Ordinal); foreach (var side in new[] { "top", "bottom", "right", "left" }) { if (!string.Equals(side, currentSide, StringComparison.Ordinal) && !peerSides.Contains(side) && seen.Add(side)) { yield return side; } } if (seen.Add(currentSide)) { yield return currentSide; } foreach (var side in new[] { "top", "bottom", "right", "left" }) { if (seen.Add(side)) { yield return side; } } } private static IEnumerable EnumerateGatewayUnderNodePeerConflictSlotCoordinates( IReadOnlyList path, ElkPositionedNode targetNode, ElkPositionedNode? sourceNode, IReadOnlyCollection peerEdges, string side, double minLineClearance) { var coordinates = new List(); var inset = 10d; var spacing = Math.Max(14d, minLineClearance + 6d); var centerX = targetNode.X + (targetNode.Width / 2d); var centerY = targetNode.Y + (targetNode.Height / 2d); var slotMinimum = side is "left" or "right" ? targetNode.Y + inset : targetNode.X + inset; var slotMaximum = side is "left" or "right" ? targetNode.Y + targetNode.Height - inset : targetNode.X + targetNode.Width - inset; void AddClamped(double value) { AddUniqueCoordinate(coordinates, Math.Max(slotMinimum, Math.Min(slotMaximum, value))); } if (side is "left" or "right") { AddClamped(path[^1].Y); foreach (var peer in peerEdges) { var peerPath = ExtractFullPath(peer); if (peerPath.Count > 0) { AddClamped(peerPath[^1].Y - spacing); AddClamped(peerPath[^1].Y + spacing); AddClamped(peerPath[^1].Y); } } if (sourceNode is not null) { AddClamped(sourceNode.Y + (sourceNode.Height / 2d)); } AddClamped(centerY - spacing); AddClamped(centerY); AddClamped(centerY + spacing); } else { AddClamped(path[^1].X); foreach (var peer in peerEdges) { var peerPath = ExtractFullPath(peer); if (peerPath.Count > 0) { AddClamped(peerPath[^1].X - spacing); AddClamped(peerPath[^1].X + spacing); AddClamped(peerPath[^1].X); } } if (sourceNode is not null) { AddClamped(sourceNode.X + (sourceNode.Width / 2d)); } AddClamped(centerX - spacing); AddClamped(centerX); AddClamped(centerX + spacing); } foreach (var coordinate in coordinates.Take(8)) { yield return coordinate; } } private static IEnumerable EnumerateGatewayUnderNodePeerConflictAxes( IReadOnlyList path, ElkPositionedNode targetNode, string side, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId, double minLineClearance) { var coordinates = new List(); var currentAxis = ResolveTargetApproachAxisValue(path, side); if (!double.IsNaN(currentAxis)) { AddUniqueCoordinate(coordinates, currentAxis); } AddUniqueCoordinate(coordinates, ResolveDefaultTargetApproachAxis(targetNode, side)); var clearance = Math.Max(24d, minLineClearance * 0.6d); if (side is "top" or "bottom") { var minX = Math.Min(path[0].X, targetNode.X); var maxX = Math.Max(path[0].X, targetNode.X + targetNode.Width); var blockers = nodes .Where(node => !string.Equals(node.Id, sourceNodeId, StringComparison.Ordinal) && !string.Equals(node.Id, targetNodeId, StringComparison.Ordinal) && maxX > node.X + 0.5d && minX < node.X + node.Width - 0.5d) .ToArray(); if (side == "top") { var highestBlockerY = blockers.Length > 0 ? blockers.Min(node => node.Y) : Math.Min(path[0].Y, targetNode.Y); AddUniqueCoordinate(coordinates, Math.Min(targetNode.Y - 8d, highestBlockerY - clearance)); } else { var lowestBlockerY = blockers.Length > 0 ? blockers.Max(node => node.Y + node.Height) : Math.Max(path[0].Y, targetNode.Y + targetNode.Height); AddUniqueCoordinate(coordinates, Math.Max(targetNode.Y + targetNode.Height + 8d, lowestBlockerY + clearance)); } } else { var minY = Math.Min(path[0].Y, targetNode.Y); var maxY = Math.Max(path[0].Y, targetNode.Y + targetNode.Height); var blockers = nodes .Where(node => !string.Equals(node.Id, sourceNodeId, StringComparison.Ordinal) && !string.Equals(node.Id, targetNodeId, StringComparison.Ordinal) && maxY > node.Y + 0.5d && minY < node.Y + node.Height - 0.5d) .ToArray(); if (side == "left") { var leftmostBlockerX = blockers.Length > 0 ? blockers.Min(node => node.X) : Math.Min(path[0].X, targetNode.X); AddUniqueCoordinate(coordinates, Math.Min(targetNode.X - 8d, leftmostBlockerX - clearance)); } else { var rightmostBlockerX = blockers.Length > 0 ? blockers.Max(node => node.X + node.Width) : Math.Max(path[0].X, targetNode.X + targetNode.Width); AddUniqueCoordinate(coordinates, Math.Max(targetNode.X + targetNode.Width + 8d, rightmostBlockerX + clearance)); } } foreach (var coordinate in coordinates.Take(6)) { yield return coordinate; } } }