diff --git a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRoutingScoring.cs b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRoutingScoring.cs index 3d57dd259..5edbdb2ad 100644 --- a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRoutingScoring.cs +++ b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRoutingScoring.cs @@ -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, diff --git a/src/__Libraries/StellaOps.ElkSharp/ElkShapeBoundaries.GatewayInterior.cs b/src/__Libraries/StellaOps.ElkSharp/ElkShapeBoundaries.GatewayInterior.cs index fbb27b440..11f88f67c 100644 --- a/src/__Libraries/StellaOps.ElkSharp/ElkShapeBoundaries.GatewayInterior.cs +++ b/src/__Libraries/StellaOps.ElkSharp/ElkShapeBoundaries.GatewayInterior.cs @@ -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(