namespace StellaOps.ElkSharp; internal static partial class ElkEdgePostProcessor { private static List? TryBuildGatewaySourceBoundarySlotSkirtCandidate( IReadOnlyList currentPath, ElkPositionedNode sourceNode, ElkPoint boundaryPoint, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId, double obstaclePadding) { if (!ElkShapeBoundaries.IsGatewayShape(sourceNode) || currentPath.Count < 3) { return null; } var firstExteriorIndex = FindFirstGatewayExteriorPointIndex(currentPath, sourceNode); var preferredContinuationIndex = FindGatewaySourceCurlRecoveryIndex(currentPath, firstExteriorIndex) ?? FindPreferredGatewayExitContinuationIndex(currentPath, sourceNode, firstExteriorIndex); var candidateIndices = new HashSet { Math.Clamp(currentPath.Count - 2, 1, currentPath.Count - 1), Math.Clamp(preferredContinuationIndex, 1, currentPath.Count - 1), }; List? bestCandidate = null; List? bestLaneRejoinCandidate = null; var bestScore = double.PositiveInfinity; var bestLaneRejoinScore = double.PositiveInfinity; foreach (var continuationIndex in candidateIndices.OrderBy(index => index)) { var continuationPoint = currentPath[continuationIndex]; var repairCandidates = new List>(); var laneRejoinCandidate = TryBuildGatewaySourceBoundarySlotLaneRejoinCandidate( currentPath, sourceNode, boundaryPoint, continuationPoint, continuationIndex, nodes, sourceNodeId, targetNodeId); if (laneRejoinCandidate is not null && PathChanged(currentPath, laneRejoinCandidate)) { var laneRejoinScore = ComputePathLength(laneRejoinCandidate) + (Math.Max(0, laneRejoinCandidate.Count - 2) * 4d); if (laneRejoinScore < bestLaneRejoinScore) { bestLaneRejoinScore = laneRejoinScore; bestLaneRejoinCandidate = laneRejoinCandidate; } } var continuationAnchoredCandidate = BuildGatewaySourceRepairPath( currentPath, sourceNode, boundaryPoint, continuationPoint, continuationIndex, continuationPoint, nodes, sourceNodeId, targetNodeId); if (PathChanged(currentPath, continuationAnchoredCandidate)) { repairCandidates.Add(continuationAnchoredCandidate); } var prefixCandidate = TryBuildLocalObstacleSkirtBoundaryShortcut( currentPath, boundaryPoint, continuationPoint, nodes, sourceNodeId, targetNodeId, targetNode: null, obstaclePadding); if (prefixCandidate is not null && prefixCandidate.Count >= 2) { var rebuilt = new List(prefixCandidate); for (var i = continuationIndex + 1; i < currentPath.Count; i++) { var point = currentPath[i]; if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], point)) { rebuilt.Add(new ElkPoint { X = point.X, Y = point.Y }); } } var skirtCandidate = NormalizePathPoints(rebuilt); if (PathChanged(currentPath, skirtCandidate)) { repairCandidates.Add(skirtCandidate); } } foreach (var candidate in repairCandidates) { var score = ComputePathLength(candidate) + (Math.Max(0, candidate.Count - 2) * 4d); if (score >= bestScore) { continue; } bestScore = score; bestCandidate = candidate; } } return bestLaneRejoinCandidate ?? bestCandidate; } private static List? TryBuildGatewaySourceBoundarySlotLaneRejoinCandidate( IReadOnlyList currentPath, ElkPositionedNode sourceNode, ElkPoint boundaryPoint, ElkPoint continuationPoint, int continuationIndex, IReadOnlyCollection nodes, string? sourceNodeId, string? targetNodeId) { if (!ElkShapeBoundaries.IsGatewayShape(sourceNode) || currentPath.Count < 3) { return null; } var boundarySide = ElkEdgeRoutingGeometry.ResolveBoundarySide(boundaryPoint, sourceNode); var padding = 8d; ElkPoint rejoinExterior; switch (boundarySide) { case "left": rejoinExterior = new ElkPoint { X = sourceNode.X - padding, Y = continuationPoint.Y }; break; case "right": rejoinExterior = new ElkPoint { X = sourceNode.X + sourceNode.Width + padding, Y = continuationPoint.Y }; break; case "top": rejoinExterior = new ElkPoint { X = continuationPoint.X, Y = sourceNode.Y - padding }; break; case "bottom": rejoinExterior = new ElkPoint { X = continuationPoint.X, Y = sourceNode.Y + sourceNode.Height + padding }; break; default: return null; } if (ElkShapeBoundaries.IsInsideNodeBoundingBoxInterior(sourceNode, rejoinExterior) || !ElkShapeBoundaries.HasValidGatewayBoundaryAngle(sourceNode, boundaryPoint, rejoinExterior)) { return null; } var rebuilt = new List { boundaryPoint }; if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], rejoinExterior)) { rebuilt.Add(rejoinExterior); } if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], continuationPoint)) { rebuilt.Add(continuationPoint); } for (var i = continuationIndex + 1; i < currentPath.Count; i++) { var point = currentPath[i]; if (!ElkEdgeRoutingGeometry.PointsEqual(rebuilt[^1], point)) { rebuilt.Add(new ElkPoint { X = point.X, Y = point.Y }); } } var candidate = NormalizePathPoints(rebuilt); if (!PathChanged(currentPath, candidate) || HasNodeObstacleCrossing(candidate, nodes, sourceNodeId, targetNodeId) || !HasAcceptableGatewayBoundaryPath(candidate, nodes, sourceNodeId, targetNodeId, sourceNode, fromStart: true) || !HasCleanGatewaySourceBandPath(candidate, sourceNode)) { return null; } return candidate; } private static bool TrySelectImprovedBoundarySlotSourceCandidate( ElkRoutedEdge[] edges, ElkRoutedEdge[] processedEdges, int edgeIndex, ElkRoutedEdge edge, IReadOnlyList currentPath, IReadOnlyCollection> candidates, IReadOnlyCollection nodes, out List selectedPath) { selectedPath = []; if (candidates.Count == 0) { return false; } var baselineLayout = BuildBoundarySlotEvaluationLayout( edges, processedEdges, edgeIndex, BuildSingleSectionEdge(edge, currentPath)); var baselineScore = ElkEdgeRoutingScoring.ComputeScore(baselineLayout, nodes); var bestScore = baselineScore; List? bestPath = null; var seenSignatures = new HashSet(StringComparer.Ordinal); foreach (var candidate in candidates) { var normalizedCandidate = NormalizePathPoints(candidate); if (!PathChanged(currentPath, normalizedCandidate) || !seenSignatures.Add(CreatePathSignature(normalizedCandidate))) { continue; } var candidateLayout = (ElkRoutedEdge[])baselineLayout.Clone(); candidateLayout[edgeIndex] = BuildSingleSectionEdge(edge, normalizedCandidate); var candidateScore = ElkEdgeRoutingScoring.ComputeScore(candidateLayout, nodes); if (!IsBetterBoundarySlotSourceCandidate(baselineScore, bestScore, candidateScore)) { continue; } bestScore = candidateScore; bestPath = normalizedCandidate; } if (bestPath is null) { return false; } selectedPath = bestPath; return true; } private static ElkRoutedEdge[] BuildBoundarySlotEvaluationLayout( ElkRoutedEdge[] edges, ElkRoutedEdge[] processedEdges, int edgeIndex, ElkRoutedEdge currentEdge) { var layout = new ElkRoutedEdge[edges.Length]; for (var i = 0; i < edges.Length; i++) { layout[i] = i < edgeIndex ? processedEdges[i] : edges[i]; } layout[edgeIndex] = currentEdge; return layout; } private static bool IsBetterBoundarySlotSourceCandidate( EdgeRoutingScore baselineScore, EdgeRoutingScore currentBestScore, EdgeRoutingScore candidateScore) { if (candidateScore.BoundarySlotViolations >= baselineScore.BoundarySlotViolations || HasBlockingBoundarySlotSourceCandidateRegression( baselineScore, candidateScore, allowTemporarySoftTrade: candidateScore.BoundarySlotViolations < baselineScore.BoundarySlotViolations)) { return false; } if (currentBestScore.BoundarySlotViolations >= baselineScore.BoundarySlotViolations) { return true; } if (candidateScore.BoundarySlotViolations != currentBestScore.BoundarySlotViolations) { return candidateScore.BoundarySlotViolations < currentBestScore.BoundarySlotViolations; } if (candidateScore.Value > currentBestScore.Value + 0.5d) { return true; } if (candidateScore.Value + 0.5d < currentBestScore.Value) { return false; } return candidateScore.TotalPathLength < currentBestScore.TotalPathLength - 0.5d; } private static bool HasBlockingBoundarySlotSourceCandidateRegression( EdgeRoutingScore baselineScore, EdgeRoutingScore candidateScore, bool allowTemporarySoftTrade) { return candidateScore.NodeCrossings > baselineScore.NodeCrossings || candidateScore.BelowGraphViolations > baselineScore.BelowGraphViolations || candidateScore.UnderNodeViolations > baselineScore.UnderNodeViolations || candidateScore.LongDiagonalViolations > baselineScore.LongDiagonalViolations || candidateScore.EntryAngleViolations > baselineScore.EntryAngleViolations || candidateScore.GatewaySourceExitViolations > baselineScore.GatewaySourceExitViolations || candidateScore.RepeatCollectorCorridorViolations > baselineScore.RepeatCollectorCorridorViolations || candidateScore.RepeatCollectorNodeClearanceViolations > baselineScore.RepeatCollectorNodeClearanceViolations || (!allowTemporarySoftTrade && candidateScore.TargetApproachJoinViolations > baselineScore.TargetApproachJoinViolations) || candidateScore.TargetApproachBacktrackingViolations > baselineScore.TargetApproachBacktrackingViolations || (!allowTemporarySoftTrade && candidateScore.SharedLaneViolations > baselineScore.SharedLaneViolations); } }