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;
}
}