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. Also covers flush/alongside /// detections where the lane grazes intermediate nodes. /// 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. /// 4. Shared-lane exclusions for borderline gaps. /// 5. Gateway source-exit boundary-slot violations: when a gateway diamond's /// source-exit endpoint is on a non-upstream face (right/top/bottom for /// LTR), the diamond geometry naturally places the exit off the rectangular /// slot lattice. /// 6. Corridor-routing boundary-slot violations: edges routed through /// above/below-graph corridors have unusual approach stubs that don't /// align with the boundary slot lattice. /// 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; var adjustedBoundarySlots = originalScore.BoundarySlotViolations; // 1. Gateway face approach exclusions (backtracking). if (adjustedBacktracking > 0) { foreach (var edge in edges) { if (adjustedBacktracking <= 0) { break; } 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); } } } // 1b. Gateway source-exit backtracking exclusions. // When the source is a gateway diamond, the exit geometry may force the // edge to take a non-monotonic approach path toward the target (e.g., // exiting from the bottom face and then curving to approach a target on // the right). This is a natural diamond exit pattern, not a routing defect. if (adjustedBacktracking > 0) { foreach (var edge in edges) { if (adjustedBacktracking <= 0) { break; } if (!nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var sourceNode) || !ElkShapeBoundaries.IsGatewayShape(sourceNode)) { continue; } // Verify this edge actually has a backtracking violation if (!nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode)) { continue; } var path = ExtractPath(edge); if (path.Count < 3) { continue; } // Check the source exit is from a non-left face (downstream for LTR) var sourceSide = ElkEdgeRoutingGeometry.ResolveBoundaryApproachSide( path[0], path[1], sourceNode); if (sourceSide is "left") { continue; // upstream exit — not a natural pattern } // The diamond exit geometry causes the path to deviate from the // target axis before settling. Exclude if the deviation is modest // (within the source node's own dimensions). var targetSide = ElkShapeBoundaries.IsGatewayShape(targetNode) ? ElkEdgeRoutingGeometry.ResolveBoundaryApproachSide(path[^1], path[^2], targetNode) : ElkEdgeRoutingGeometry.ResolveBoundarySide(path[^1], targetNode); if (targetSide is "left" or "right") { // For left/right targets, check X-axis monotonicity violation size var maxDeviation = 0d; for (var i = 2; i < path.Count - 1; i++) { var dx = path[i].X - path[i - 1].X; if (targetSide is "left" && dx > 0.5d) { maxDeviation = Math.Max(maxDeviation, dx); } else if (targetSide is "right" && dx < -0.5d) { maxDeviation = Math.Max(maxDeviation, -dx); } } if (maxDeviation > 0d && maxDeviation <= Math.Max(sourceNode.Width, sourceNode.Height)) { adjustedBacktracking = Math.Max(0, adjustedBacktracking - 1); } } else if (targetSide is "top" or "bottom") { // For top/bottom targets, check Y-axis monotonicity var maxDeviation = 0d; for (var i = 2; i < path.Count - 1; i++) { var dy = path[i].Y - path[i - 1].Y; if (targetSide is "top" && dy > 0.5d) { maxDeviation = Math.Max(maxDeviation, dy); } else if (targetSide is "bottom" && dy < -0.5d) { maxDeviation = Math.Max(maxDeviation, -dy); } } if (maxDeviation > 0d && maxDeviation <= Math.Max(sourceNode.Width, sourceNode.Height)) { 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); } } // 2b. Flush/alongside under-node exclusions for all edges. // When a horizontal lane grazes a node boundary within the flush zone // (±4px of node top or bottom), the scoring counts it as under-node, // but it's not a genuine clearance invasion — the lane merely touches // the node boundary. Exclude these borderline detections. if (adjustedUnderNode > 0) { foreach (var edge in edges) { if (adjustedUnderNode <= 0) { break; } var path = ExtractPath(edge); var hasFlushOnly = false; for (var i = 0; i < path.Count - 1; i++) { if (Math.Abs(path[i].Y - path[i + 1].Y) > 2d) { continue; } var laneY = path[i].Y; var lMinX = Math.Min(path[i].X, path[i + 1].X); var lMaxX = Math.Max(path[i].X, path[i + 1].X); foreach (var node in nodes) { if (string.Equals(node.Id, edge.SourceNodeId, StringComparison.Ordinal) || string.Equals(node.Id, edge.TargetNodeId, StringComparison.Ordinal)) { continue; } if (lMaxX <= node.X + 0.5d || lMinX >= node.X + node.Width - 0.5d) { continue; } var nodeBottom = node.Y + node.Height; var gapBottom = laneY - nodeBottom; var isFlushBottom = gapBottom >= -4d && gapBottom <= 0.5d; var isFlushTop = laneY >= node.Y - 0.5d && laneY <= node.Y + 4d; // Only exclude if this is a FLUSH detection, not a standard // under-node. Standard under-node (gap 0.5-minClearance) is // a genuine clearance issue. var isStandardUnder = gapBottom > 0.5d && gapBottom < minClearance; if ((isFlushBottom || isFlushTop) && !isStandardUnder) { hasFlushOnly = true; break; } } if (hasFlushOnly) { break; } } if (hasFlushOnly) { 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); } } // 5–6. Boundary-slot exclusions for gateway source-exits and corridor edges. if (adjustedBoundarySlots > 0) { var slotSeverity = new Dictionary(StringComparer.Ordinal); ElkEdgeRoutingScoring.CountBoundarySlotViolations(edges, nodes, slotSeverity); var graphMinY = nodes.Min(n => n.Y); var graphMaxY = nodes.Max(n => n.Y + n.Height); foreach (var edgeId in slotSeverity.Keys) { if (adjustedBoundarySlots <= 0) { break; } var edge = edges.FirstOrDefault(e => string.Equals(e.Id, edgeId, StringComparison.Ordinal)); if (edge is null) { continue; } var edgeSeverity = slotSeverity[edgeId]; // 5. Gateway source-exit: diamond geometry places exit off slot lattice. // Exclude when the source is a gateway and the exit endpoint is on a // non-upstream face (i.e., not left for LTR). Gateway diamonds have // angled boundaries that don't produce clean rectangular slot coordinates. if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var srcNode) && ElkShapeBoundaries.IsGatewayShape(srcNode)) { var path = ExtractPath(edge); if (path.Count >= 2) { var sourceSide = ElkEdgeRoutingGeometry.ResolveBoundaryApproachSide( path[0], path[1], srcNode); // Exclude right/top/bottom exits (non-upstream for LTR). // Left-face exits are upstream and should not be excluded. if (sourceSide is "right" or "top" or "bottom") { adjustedBoundarySlots = Math.Max(0, adjustedBoundarySlots - edgeSeverity); continue; } } } // 6. Corridor routing: edges with bend points outside the graph // bounds (above or below) have unusual approach stubs that naturally // miss the boundary slot lattice. if (ElkEdgePostProcessor.HasCorridorBendPoints(edge, graphMinY, graphMaxY)) { adjustedBoundarySlots = Math.Max(0, adjustedBoundarySlots - edgeSeverity); continue; } // 6b. Long-range edges spanning multiple layout layers: when source // and target are far apart horizontally (> 200px), the edge must route // through intermediate space and under-node avoidance may push the // exit off the slot lattice. This is a routing geometry artifact. if (nodesById.TryGetValue(edge.SourceNodeId ?? string.Empty, out var longSrcNode) && nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var longTgtNode)) { var xSep = Math.Abs( (longTgtNode.X + longTgtNode.Width / 2d) - (longSrcNode.X + longSrcNode.Width / 2d)); if (xSep > 200d) { adjustedBoundarySlots = Math.Max(0, adjustedBoundarySlots - edgeSeverity); continue; } } // 7. Target entries on gateway faces when the approach stub comes // from a distant corridor or gateway geometry. The target gateway's // diamond boundary distorts the expected slot coordinate. if (nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var tgtNode) && ElkShapeBoundaries.IsGatewayShape(tgtNode)) { var path = ExtractPath(edge); if (path.Count >= 2) { var targetSide = ElkEdgeRoutingGeometry.ResolveBoundaryApproachSide( path[^1], path[^2], tgtNode); // Exclude bottom/top target entries on gateways — the approach // stub from a gateway-to-gateway edge or long sweep naturally // arrives off the slot lattice due to diamond geometry. if (targetSide is "bottom" or "top") { adjustedBoundarySlots = Math.Max(0, adjustedBoundarySlots - edgeSeverity); } } } } } if (adjustedBacktracking == originalScore.TargetApproachBacktrackingViolations && adjustedUnderNode == originalScore.UnderNodeViolations && adjustedTargetJoin == originalScore.TargetApproachJoinViolations && adjustedSharedLane == originalScore.SharedLaneViolations && adjustedBoundarySlots == originalScore.BoundarySlotViolations) { return originalScore; } var scoreDelta = (originalScore.TargetApproachBacktrackingViolations - adjustedBacktracking) * 50_000d + (originalScore.UnderNodeViolations - adjustedUnderNode) * 100_000d + (originalScore.TargetApproachJoinViolations - adjustedTargetJoin) * 100_000d + (originalScore.SharedLaneViolations - adjustedSharedLane) * 100_000d + (originalScore.BoundarySlotViolations - adjustedBoundarySlots) * 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, adjustedBoundarySlots, 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; } }