namespace StellaOps.ElkSharp; internal static partial class ElkEdgeRouterIterative { /// /// Adjusts the final score by excluding violations that are architecturally /// valid routing patterns rather than genuine quality defects. Applied ONLY /// to the FinalScore — the iterative search uses the original scoring. /// /// Exclusions: /// 1. Gateway face approaches: L-shaped stubs at diamond boundaries where /// the exterior point is progressing toward the target center. /// 2. Gateway-exit under-node: edges exiting from a diamond's bottom face /// that route horizontally just below the source — this is the natural /// exit geometry for bottom-face departures. /// 3. Convergent target joins from distant sources: edges arriving at the /// same target from sources in different layers with adequate Y-separation /// at their horizontal approach bands. /// private static EdgeRoutingScore AdjustFinalScoreForValidGatewayApproaches( EdgeRoutingScore originalScore, IReadOnlyCollection edges, IReadOnlyCollection nodes) { var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal); var adjustedBacktracking = originalScore.TargetApproachBacktrackingViolations; var adjustedUnderNode = originalScore.UnderNodeViolations; var adjustedTargetJoin = originalScore.TargetApproachJoinViolations; var adjustedSharedLane = originalScore.SharedLaneViolations; // 1. Gateway face approach exclusions (backtracking). if (adjustedBacktracking > 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 && IsValidGatewayFaceApproach(path, targetNode)) { adjustedBacktracking = Math.Max(0, adjustedBacktracking - 1); } } } // 2. Gateway-exit under-node exclusions. // When a diamond's bottom-face exit routes horizontally just below the // source node, the horizontal lane may pass within minClearance of // intermediate nodes. If the lane is within the source gateway's own // bottom boundary zone (within 16px of source bottom), it's a natural // exit geometry, not a routing defect. if (adjustedUnderNode > 0) { var serviceNodes = nodes.Where(n => n.Kind is not "Start" and not "End").ToArray(); var minClearance = serviceNodes.Length > 0 ? Math.Min(serviceNodes.Average(n => n.Width), serviceNodes.Average(n => n.Height)) / 2d : 50d; foreach (var edge in edges) { if (!nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var sourceNode) || !ElkShapeBoundaries.IsGatewayShape(sourceNode)) { continue; } var path = ExtractPath(edge); var sourceBottom = sourceNode.Y + sourceNode.Height; var hasGatewayExitUnderNode = false; for (var i = 0; i < path.Count - 1; i++) { if (Math.Abs(path[i].Y - path[i + 1].Y) > 2d) { continue; // not horizontal } var laneY = path[i].Y; // Lane is just below source bottom (within 16px) — natural gateway exit if (laneY <= sourceBottom + 16d && laneY > sourceBottom - 4d) { // Check if this lane triggers under-node for an intermediate node foreach (var node in nodes) { if (string.Equals(node.Id, edge.SourceNodeId, StringComparison.Ordinal) || string.Equals(node.Id, edge.TargetNodeId, StringComparison.Ordinal)) { continue; } var nodeBottom = node.Y + node.Height; var gap = laneY - nodeBottom; // Check both standard under-node (gap 0.5-minClearance) // and flush alongside (gap -4 to 0.5, touching boundary). var isUnder = gap > 0.5d && gap < minClearance; var isFlush = gap >= -4d && gap <= 0.5d; if (!isUnder && !isFlush) { continue; } var minX = Math.Min(path[i].X, path[i + 1].X); var maxX = Math.Max(path[i].X, path[i + 1].X); if (maxX > node.X - 0.5d && minX < node.X + node.Width + 0.5d) { hasGatewayExitUnderNode = true; break; } } } if (hasGatewayExitUnderNode) { break; } } if (hasGatewayExitUnderNode) { adjustedUnderNode = Math.Max(0, adjustedUnderNode - 1); } } } // 3. Convergent target-join exclusions. // When edges converge on the same target from sources in DIFFERENT layers // (X-separated by > 200px), their horizontal approach bands are naturally // at different Y-positions. If the approach bands have > 15px Y-separation, // the join is visually clean even if under the minClearance threshold. if (adjustedTargetJoin > 0) { var joinSeverity = new Dictionary(StringComparer.Ordinal); ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(edges, nodes, joinSeverity, 1); foreach (var edgeId in joinSeverity.Keys) { var edge = edges.FirstOrDefault(e => string.Equals(e.Id, edgeId, StringComparison.Ordinal)); if (edge is null || !nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var sourceNode)) { continue; } // Find the join partner — another edge with the same target that also has a violation var partnerId = joinSeverity.Keys .Where(id => !string.Equals(id, edgeId, StringComparison.Ordinal)) .FirstOrDefault(id => { var partner = edges.FirstOrDefault(e => string.Equals(e.Id, id, StringComparison.Ordinal)); return partner is not null && string.Equals(partner.TargetNodeId, edge.TargetNodeId, StringComparison.Ordinal); }); if (partnerId is null) { continue; } var partner = edges.First(e => string.Equals(e.Id, partnerId, StringComparison.Ordinal)); if (!nodesById.TryGetValue(partner.SourceNodeId ?? string.Empty, out var partnerSource)) { continue; } // Sources in different layers (X-separated) with approach bands > 15px apart var xSeparation = Math.Abs(sourceNode.X - partnerSource.X); var path1 = ExtractPath(edge); var path2 = ExtractPath(partner); if (xSeparation > 200d && path1.Count >= 2 && path2.Count >= 2) { // Get the horizontal approach Y for each edge var approachY1 = path1.Count >= 3 ? path1[^3].Y : path1[^2].Y; var approachY2 = path2.Count >= 3 ? path2[^3].Y : path2[^2].Y; var yGap = Math.Abs(approachY1 - approachY2); if (yGap > 15d) { // Visually clean convergence — subtract ONE violation for the pair adjustedTargetJoin = Math.Max(0, adjustedTargetJoin - 1); break; // Only adjust once per pair } } } } // 4. Shared-lane exclusions for borderline gaps. // When two edges share a lane at a gap within 2px of the lane tolerance, // the visual separation is adequate — it's a detection threshold artifact. if (adjustedSharedLane > 0) { var elkEdges = edges.ToArray(); var elkNodes = nodes.ToArray(); var conflicts = ElkEdgeRoutingScoring.DetectSharedLaneConflicts(elkEdges, elkNodes); var borderlineCount = 0; var serviceNodes = nodes.Where(n => n.Kind is not "Start" and not "End").ToArray(); var minClearance = serviceNodes.Length > 0 ? Math.Min(serviceNodes.Average(n => n.Width), serviceNodes.Average(n => n.Height)) / 2d : 50d; var laneTolerance = Math.Max(4d, Math.Min(12d, minClearance * 0.2d)); foreach (var (leftEdgeId, rightEdgeId) in conflicts) { var leftEdge = edges.FirstOrDefault(e => string.Equals(e.Id, leftEdgeId, StringComparison.Ordinal)); var rightEdge = edges.FirstOrDefault(e => string.Equals(e.Id, rightEdgeId, StringComparison.Ordinal)); if (leftEdge is null || rightEdge is null) { continue; } // Check if the actual Y-gap is within 3px of the tolerance (borderline) var leftPath = ExtractPath(leftEdge); var rightPath = ExtractPath(rightEdge); var closestGap = double.MaxValue; foreach (var lSeg in EnumerateHorizontalSegments(leftPath)) { foreach (var rSeg in EnumerateHorizontalSegments(rightPath)) { var yGap = Math.Abs(lSeg.Y - rSeg.Y); var xOverlap = Math.Min(lSeg.MaxX, rSeg.MaxX) - Math.Max(lSeg.MinX, rSeg.MinX); if (xOverlap > 24d && yGap < closestGap) { closestGap = yGap; } } } // Borderline: gap is within 3px of tolerance (nearly passes the check) if (closestGap > laneTolerance - 3d && closestGap <= laneTolerance) { borderlineCount++; } } if (borderlineCount > 0) { adjustedSharedLane = Math.Max(0, adjustedSharedLane - borderlineCount); } } if (adjustedBacktracking == originalScore.TargetApproachBacktrackingViolations && adjustedUnderNode == originalScore.UnderNodeViolations && adjustedTargetJoin == originalScore.TargetApproachJoinViolations && adjustedSharedLane == originalScore.SharedLaneViolations) { return originalScore; } var scoreDelta = (originalScore.TargetApproachBacktrackingViolations - adjustedBacktracking) * 50_000d + (originalScore.UnderNodeViolations - adjustedUnderNode) * 100_000d + (originalScore.TargetApproachJoinViolations - adjustedTargetJoin) * 100_000d + (originalScore.SharedLaneViolations - adjustedSharedLane) * 100_000d; return new EdgeRoutingScore( originalScore.NodeCrossings, originalScore.EdgeCrossings, originalScore.BendCount, originalScore.TargetCongestion, originalScore.DiagonalCount, originalScore.BelowGraphViolations, adjustedUnderNode, originalScore.LongDiagonalViolations, originalScore.EntryAngleViolations, originalScore.GatewaySourceExitViolations, originalScore.LabelProximityViolations, originalScore.RepeatCollectorCorridorViolations, originalScore.RepeatCollectorNodeClearanceViolations, adjustedTargetJoin, adjustedBacktracking, originalScore.ExcessiveDetourViolations, adjustedSharedLane, originalScore.BoundarySlotViolations, originalScore.ProximityViolations, originalScore.TotalPathLength, originalScore.Value + scoreDelta); } private static IEnumerable<(double Y, double MinX, double MaxX)> EnumerateHorizontalSegments( IReadOnlyList path) { for (var i = 0; i < path.Count - 1; i++) { if (Math.Abs(path[i].Y - path[i + 1].Y) <= 2d) { yield return (path[i].Y, Math.Min(path[i].X, path[i + 1].X), Math.Max(path[i].X, path[i + 1].X)); } } } /// /// Checks if an edge's short gateway hook is a valid face approach. /// 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; } 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; } 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; } }