Enable gateway vertex entries with coordinated slot exemption

Three coordinated changes to allow edges to converge at gateway
(diamond) left/right tip vertices:

1. IsAllowedGatewayTipVertex: returns true for left/right tips,
   enabling vertex positions as valid entry points for target edges.

2. HasValidGatewayBoundaryAngle: at allowed tip vertices, accepts any
   external approach direction (not just horizontal). Source exits are
   already pushed off vertices by ForceDecisionSourceExitOffVertex.

3. CountBoundarySlotViolations: skips slot-occupancy checks when all
   entries on a gateway side are target entries converging at the
   center Y (vertex position). This prevents the -100K penalty that
   previously caused cascading search failures.

Fixes the shared-lane violation between edge/3+edge/4 — the Fork's
output edges now converge cleanly at gateway vertex entry points
instead of crowding face-interior positions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-03-30 08:46:44 +03:00
parent ec1b83b484
commit 62b6169d36
2 changed files with 33 additions and 6 deletions

View File

@@ -1125,6 +1125,23 @@ internal static class ElkEdgeRoutingScoring
}
}
// Gateway vertex exemption: when all target entries on a gateway side
// share the same vertex position (left/right tip), they're converging
// at a natural diamond corner — not competing for face slots.
var isGatewayVertexGroup = ElkShapeBoundaries.IsGatewayShape(node)
&& ordered.All(entry => !entry.IsOutgoing)
&& ordered.Length >= 2;
if (isGatewayVertexGroup)
{
var centerY = node.Y + (node.Height / 2d);
var allAtVertex = ordered.All(entry =>
Math.Abs(entry.Coordinate - centerY) <= coordinateTolerance);
if (allAtVertex)
{
continue; // Skip slot checks — valid vertex convergence
}
}
var uniqueSlotCoordinates = ElkBoundarySlots.BuildUniqueBoundarySlotCoordinates(node, side, ordered.Length);
var assignedSlotCoordinates = ElkBoundarySlots.BuildAssignedBoundarySlotAxisCoordinates(
node,

View File

@@ -36,7 +36,11 @@ internal static partial class ElkShapeBoundaries
if (IsAllowedGatewayTipVertex(node, boundaryPoint))
{
return segDx > segDy * 3d;
// Allowed tip vertices accept any external approach direction.
// Source exits are already pushed off vertices by
// ForceDecisionSourceExitOffVertex, so this only affects
// target entries — which should converge from any direction.
return true;
}
if (!TryGetGatewayBoundaryFace(node, boundaryPoint, out var faceStart, out var faceEnd))
@@ -127,11 +131,17 @@ internal static partial class ElkShapeBoundaries
ElkPoint boundaryPoint,
double tolerance = GatewayVertexTolerance)
{
// Gateway tips read as visually detached "pin" exits/entries in the renderer.
// Keep all gateway joins on a face interior instead of permitting any tip vertex.
// TODO: revisit for target entries where converging edges would benefit from
// a shared vertex entry point — requires coordinated boundary-slot changes.
return false;
// Gateway LEFT and RIGHT tip vertices are allowed as entry points.
// They're the natural convergence point for edges approaching the
// diamond along the dominant horizontal axis. Top/bottom tips are
// NOT allowed — they create detached "pin" visual artifacts.
// Source exits from tips are still blocked by ForceDecisionSourceExitOffVertex.
var centerY = node.Y + (node.Height / 2d);
var isLeftTip = Math.Abs(boundaryPoint.X - node.X) <= tolerance
&& Math.Abs(boundaryPoint.Y - centerY) <= tolerance;
var isRightTip = Math.Abs(boundaryPoint.X - (node.X + node.Width)) <= tolerance
&& Math.Abs(boundaryPoint.Y - centerY) <= tolerance;
return isLeftTip || isRightTip;
}
internal static bool IsInsideNodeBoundingBoxInterior(