namespace StellaOps.ElkSharp; internal static partial class ElkEdgeRouterIterative { /// /// Adjusts the final score by excluding backtracking violations that are /// actually valid orthogonal approaches to gateway (diamond) target faces. /// These L-shaped stubs (long perpendicular approach → short parallel entry) /// are the CORRECT routing pattern for orthogonal edges reaching diamond /// boundaries. The iterative search uses the original scoring (without this /// adjustment) to keep the search trajectory stable. /// private static EdgeRoutingScore AdjustFinalScoreForValidGatewayApproaches( EdgeRoutingScore originalScore, IReadOnlyCollection edges, IReadOnlyCollection nodes) { if (originalScore.TargetApproachBacktrackingViolations == 0) { return originalScore; } var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal); var validApproachCount = 0; foreach (var edge in edges) { if (!nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode) || !ElkShapeBoundaries.IsGatewayShape(targetNode)) { continue; } var path = ExtractPath(edge); if (path.Count < 4) { continue; } // Check if this edge has a short gateway hook that's actually a valid approach. if (!IsValidGatewayFaceApproach(path, targetNode)) { continue; } validApproachCount++; } if (validApproachCount == 0) { return originalScore; } var adjustedBacktracking = Math.Max(0, originalScore.TargetApproachBacktrackingViolations - validApproachCount); var scoreDelta = (originalScore.TargetApproachBacktrackingViolations - adjustedBacktracking) * 50_000d; return new EdgeRoutingScore( originalScore.NodeCrossings, originalScore.EdgeCrossings, originalScore.BendCount, originalScore.TargetCongestion, originalScore.DiagonalCount, originalScore.BelowGraphViolations, originalScore.UnderNodeViolations, originalScore.LongDiagonalViolations, originalScore.EntryAngleViolations, originalScore.GatewaySourceExitViolations, originalScore.LabelProximityViolations, originalScore.RepeatCollectorCorridorViolations, originalScore.RepeatCollectorNodeClearanceViolations, originalScore.TargetApproachJoinViolations, adjustedBacktracking, originalScore.ExcessiveDetourViolations, originalScore.SharedLaneViolations, originalScore.BoundarySlotViolations, originalScore.ProximityViolations, originalScore.TotalPathLength, originalScore.Value + scoreDelta); } /// /// Checks if an edge's short gateway hook is a valid face approach. /// A valid approach has the exterior point (penultimate) CLOSER to the /// target center than the predecessor — meaning the path is progressing /// toward the diamond, not overshooting and curling back. /// private static bool IsValidGatewayFaceApproach( IReadOnlyList path, ElkPositionedNode targetNode) { if (path.Count < 3) { return false; } const double tolerance = 0.5d; var boundaryPoint = path[^1]; var exteriorPoint = path[^2]; var finalDx = Math.Abs(boundaryPoint.X - exteriorPoint.X); var finalDy = Math.Abs(boundaryPoint.Y - exteriorPoint.Y); var finalIsHorizontal = finalDx > tolerance && finalDy <= tolerance; var finalIsVertical = finalDy > tolerance && finalDx <= tolerance; if (!finalIsHorizontal && !finalIsVertical) { return false; } var finalStubLength = finalIsHorizontal ? finalDx : finalDy; var requiredDepth = Math.Min(targetNode.Width, targetNode.Height); if (finalStubLength + tolerance >= requiredDepth) { return false; // Stub is long enough — not flagged as a hook } var predecessor = path[^3]; var predecessorDx = Math.Abs(exteriorPoint.X - predecessor.X); var predecessorDy = Math.Abs(exteriorPoint.Y - predecessor.Y); const double minimumApproachSpan = 24d; var isLongPerpendicularApproach = finalIsHorizontal ? predecessorDy >= minimumApproachSpan && predecessorDy > predecessorDx * 3d : predecessorDx >= minimumApproachSpan && predecessorDx > predecessorDy * 3d; if (!isLongPerpendicularApproach) { return false; // Not the short hook pattern } // The exterior point is closer to target center than the predecessor → // the path is approaching the gateway, not overshooting and hooking back. var targetCenterX = targetNode.X + (targetNode.Width / 2d); var targetCenterY = targetNode.Y + (targetNode.Height / 2d); var exteriorDist = Math.Abs(exteriorPoint.X - targetCenterX) + Math.Abs(exteriorPoint.Y - targetCenterY); var predecessorDist = Math.Abs(predecessor.X - targetCenterX) + Math.Abs(predecessor.Y - targetCenterY); return exteriorDist < predecessorDist; } }