From ec1b83b4849e729bc6afb8f0ecf1bcc1f59ca630 Mon Sep 17 00:00:00 2001 From: master <> Date: Mon, 30 Mar 2026 08:05:33 +0300 Subject: [PATCH] Extend under-node detection for edges flush with node boundaries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Edges running alongside a node's top or bottom boundary (within 4px) are now flagged as under-node violations — they're visually "glued" to the node edge. Previously, only edges BELOW the node bottom were detected (gap > 0.5px). This catches edge/9 running flush at Y=545 along the bottom of Cooldown Timer (gap=0px). Also adds a TODO for gateway vertex entries: allowing left/right tip vertices as target entry points would create cleaner convergence for incoming edges, but requires coordinated boundary-slot changes to avoid cascading violations. The approach is validated but not yet safe to enable. Co-Authored-By: Claude Opus 4.6 (1M context) --- ...BoundaryFirst.GatewayApproachAdjustment.cs | 22 ++++++++++++------- .../ElkEdgeRoutingScoring.cs | 12 ++++++++-- .../ElkShapeBoundaries.GatewayInterior.cs | 2 ++ 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.BoundaryFirst.GatewayApproachAdjustment.cs b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.BoundaryFirst.GatewayApproachAdjustment.cs index 3d2ead72d..1edcac3e6 100644 --- a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.BoundaryFirst.GatewayApproachAdjustment.cs +++ b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.BoundaryFirst.GatewayApproachAdjustment.cs @@ -94,15 +94,21 @@ internal static partial class ElkEdgeRouterIterative var nodeBottom = node.Y + node.Height; var gap = laneY - nodeBottom; - if (gap > 0.5d && gap < minClearance) + // 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) { - 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; - } + 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; } } } diff --git a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRoutingScoring.cs b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRoutingScoring.cs index 42c8656cd..3d57dd259 100644 --- a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRoutingScoring.cs +++ b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRoutingScoring.cs @@ -323,8 +323,16 @@ internal static class ElkEdgeRoutingScoring continue; } - var distanceBelowNode = laneY - (node.Y + node.Height); - if (distanceBelowNode <= 0.5d || distanceBelowNode >= minClearance) + var nodeBottom = node.Y + node.Height; + var distanceBelowNode = laneY - nodeBottom; + + // Standard under-node: lane runs below node within clearance. + var isBelow = distanceBelowNode > 0.5d && distanceBelowNode < minClearance; + // Extended alongside: lane runs flush with (within 4px of) the + // node's top or bottom boundary — visually "glued" to the edge. + var isFlushBottom = distanceBelowNode >= -4d && distanceBelowNode <= 0.5d; + var isFlushTop = laneY >= node.Y - 0.5d && laneY <= node.Y + 4d; + if (!isBelow && !isFlushBottom && !isFlushTop) { continue; } diff --git a/src/__Libraries/StellaOps.ElkSharp/ElkShapeBoundaries.GatewayInterior.cs b/src/__Libraries/StellaOps.ElkSharp/ElkShapeBoundaries.GatewayInterior.cs index 7b87406f8..fbb27b440 100644 --- a/src/__Libraries/StellaOps.ElkSharp/ElkShapeBoundaries.GatewayInterior.cs +++ b/src/__Libraries/StellaOps.ElkSharp/ElkShapeBoundaries.GatewayInterior.cs @@ -129,6 +129,8 @@ internal static partial class ElkShapeBoundaries { // 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; }