diff --git a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.BoundaryFirst.GatewayApproachAdjustment.cs b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.BoundaryFirst.GatewayApproachAdjustment.cs
index e0615f77f..3d2ead72d 100644
--- a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.BoundaryFirst.GatewayApproachAdjustment.cs
+++ b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.BoundaryFirst.GatewayApproachAdjustment.cs
@@ -3,56 +3,250 @@ 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.
+ /// 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)
{
- if (originalScore.TargetApproachBacktrackingViolations == 0)
- {
- return originalScore;
- }
-
var nodesById = nodes.ToDictionary(node => node.Id, StringComparer.Ordinal);
- var validApproachCount = 0;
+ var adjustedBacktracking = originalScore.TargetApproachBacktrackingViolations;
+ var adjustedUnderNode = originalScore.UnderNodeViolations;
+ var adjustedTargetJoin = originalScore.TargetApproachJoinViolations;
+ var adjustedSharedLane = originalScore.SharedLaneViolations;
- foreach (var edge in edges)
+ // 1. Gateway face approach exclusions (backtracking).
+ if (adjustedBacktracking > 0)
{
- if (!nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode)
- || !ElkShapeBoundaries.IsGatewayShape(targetNode))
+ foreach (var edge in edges)
{
- continue;
- }
+ if (!nodesById.TryGetValue(edge.TargetNodeId ?? string.Empty, out var targetNode)
+ || !ElkShapeBoundaries.IsGatewayShape(targetNode))
+ {
+ continue;
+ }
- var path = ExtractPath(edge);
- if (path.Count < 4)
- {
- continue;
+ var path = ExtractPath(edge);
+ if (path.Count >= 4 && IsValidGatewayFaceApproach(path, targetNode))
+ {
+ adjustedBacktracking = Math.Max(0, adjustedBacktracking - 1);
+ }
}
-
- // Check if this edge has a short gateway hook that's actually a valid approach.
- if (!IsValidGatewayFaceApproach(path, targetNode))
- {
- continue;
- }
-
- validApproachCount++;
}
- if (validApproachCount == 0)
+ // 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;
+ if (gap > 0.5d && gap < minClearance)
+ {
+ 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 adjustedBacktracking = Math.Max(0, originalScore.TargetApproachBacktrackingViolations - validApproachCount);
- var scoreDelta = (originalScore.TargetApproachBacktrackingViolations - adjustedBacktracking) * 50_000d;
+ 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,
@@ -61,28 +255,37 @@ internal static partial class ElkEdgeRouterIterative
originalScore.TargetCongestion,
originalScore.DiagonalCount,
originalScore.BelowGraphViolations,
- originalScore.UnderNodeViolations,
+ adjustedUnderNode,
originalScore.LongDiagonalViolations,
originalScore.EntryAngleViolations,
originalScore.GatewaySourceExitViolations,
originalScore.LabelProximityViolations,
originalScore.RepeatCollectorCorridorViolations,
originalScore.RepeatCollectorNodeClearanceViolations,
- originalScore.TargetApproachJoinViolations,
+ adjustedTargetJoin,
adjustedBacktracking,
originalScore.ExcessiveDetourViolations,
- originalScore.SharedLaneViolations,
+ 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.
- /// 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,
@@ -109,7 +312,7 @@ internal static partial class ElkEdgeRouterIterative
var requiredDepth = Math.Min(targetNode.Width, targetNode.Height);
if (finalStubLength + tolerance >= requiredDepth)
{
- return false; // Stub is long enough — not flagged as a hook
+ return false;
}
var predecessor = path[^3];
@@ -121,11 +324,9 @@ internal static partial class ElkEdgeRouterIterative
: predecessorDx >= minimumApproachSpan && predecessorDx > predecessorDy * 3d;
if (!isLongPerpendicularApproach)
{
- return false; // Not the short hook pattern
+ return false;
}
- // 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);