diff --git a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeChannelGutters.HorizontalRouting.cs b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeChannelGutters.HorizontalRouting.cs index ca4b9b99b..28d298bee 100644 --- a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeChannelGutters.HorizontalRouting.cs +++ b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeChannelGutters.HorizontalRouting.cs @@ -34,6 +34,12 @@ internal static class ElkEdgeHorizontalRoutingGutters var minClearance = serviceNodes.Length > 0 ? Math.Min(serviceNodes.Average(n => n.Width), serviceNodes.Average(n => n.Height)) / 2d : 50d; + // Use layout-wide clearance if available (scales with NodeSpacing). + var overrideClearance = ElkLayoutClearance.Current; + if (overrideClearance > 0d) + { + minClearance = overrideClearance; + } // Scan routed edges for horizontal segments with under-node or alongside violations. var gutterRequirements = new Dictionary(); // gutterY → requiredShift diff --git a/src/__Libraries/StellaOps.ElkSharp/ElkEdgePostProcessor.UnderNode.cs b/src/__Libraries/StellaOps.ElkSharp/ElkEdgePostProcessor.UnderNode.cs index 4142750e2..87f2bb344 100644 --- a/src/__Libraries/StellaOps.ElkSharp/ElkEdgePostProcessor.UnderNode.cs +++ b/src/__Libraries/StellaOps.ElkSharp/ElkEdgePostProcessor.UnderNode.cs @@ -318,6 +318,13 @@ internal static partial class ElkEdgePostProcessor private static double ResolveMinLineClearance(IReadOnlyCollection nodes) { + // Use the layout-wide clearance if set (scales with NodeSpacing). + var overrideClearance = ElkLayoutClearance.Current; + if (overrideClearance > 0d) + { + return overrideClearance; + } + var serviceNodes = nodes.Where(node => node.Kind is not "Start" and not "End").ToArray(); return serviceNodes.Length > 0 ? Math.Min(serviceNodes.Average(node => node.Width), serviceNodes.Average(node => node.Height)) / 2d diff --git a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.LocalRepair.ObstacleSkirt.cs b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.LocalRepair.ObstacleSkirt.cs index 470a0000c..c93a66e49 100644 --- a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.LocalRepair.ObstacleSkirt.cs +++ b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.LocalRepair.ObstacleSkirt.cs @@ -158,6 +158,12 @@ internal static partial class ElkEdgeRouterIterative private static double ResolveMinLineClearance(IReadOnlyCollection nodes) { + var overrideClearance = ElkLayoutClearance.Current; + if (overrideClearance > 0d) + { + return overrideClearance; + } + var serviceNodes = nodes.Where(node => node.Kind is not "Start" and not "End").ToArray(); return serviceNodes.Length > 0 ? Math.Min(serviceNodes.Average(node => node.Width), serviceNodes.Average(node => node.Height)) / 2d diff --git a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.cs b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.cs index d0d5e82ac..8ae2f3d11 100644 --- a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.cs +++ b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.cs @@ -37,9 +37,9 @@ internal static partial class ElkEdgeRouterIterative ? Math.Min(serviceNodes.Average(n => n.Width), serviceNodes.Average(n => n.Height)) / 2d : 50d; // Scale clearance with NodeSpacing: wider spacing should produce wider - // routing corridors. Use the larger of node-size-based clearance and - // spacing-proportional clearance so the pipeline adapts to any spacing. + // routing corridors so edges don't visually hug node boundaries. var minLineClearance = Math.Max(nodeSizeClearance, layoutOptions.NodeSpacing * 1.2d); + using var clearanceScope = ElkLayoutClearance.Set(minLineClearance); var diagnostics = ElkLayoutDiagnostics.Current; var validSolutions = new List(); diff --git a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRoutingScoring.cs b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRoutingScoring.cs index 778b9ca91..3f06e7940 100644 --- a/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRoutingScoring.cs +++ b/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRoutingScoring.cs @@ -233,6 +233,12 @@ internal static partial class ElkEdgeRoutingScoring private static double ResolveMinLineClearance(IReadOnlyCollection nodes) { + var overrideClearance = ElkLayoutClearance.Current; + if (overrideClearance > 0d) + { + return overrideClearance; + } + var serviceNodes = nodes.Where(n => n.Kind is not "Start" and not "End").ToArray(); return serviceNodes.Length > 0 ? Math.Min(serviceNodes.Average(n => n.Width), serviceNodes.Average(n => n.Height)) / 2d diff --git a/src/__Libraries/StellaOps.ElkSharp/ElkLayoutClearance.cs b/src/__Libraries/StellaOps.ElkSharp/ElkLayoutClearance.cs new file mode 100644 index 000000000..1452aba74 --- /dev/null +++ b/src/__Libraries/StellaOps.ElkSharp/ElkLayoutClearance.cs @@ -0,0 +1,35 @@ +namespace StellaOps.ElkSharp; + +/// +/// Layout-wide minimum line clearance, computed once from NodeSpacing at +/// the optimization entry point and available to all scoring/post-processing +/// helpers via . This avoids threading a parameter +/// through 15+ static call sites. +/// +internal static class ElkLayoutClearance +{ + [ThreadStatic] + private static double _current; + + /// + /// Gets the layout-wide minimum line clearance. Returns 0 if not set + /// (callers fall back to node-size-based clearance). + /// + internal static double Current => _current; + + /// + /// Sets the layout-wide clearance for the duration of a layout operation. + /// Call at the start of optimization with the NodeSpacing-aware value. + /// + internal static IDisposable Set(double minLineClearance) + { + var previous = _current; + _current = minLineClearance; + return new ClearanceScope(previous); + } + + private sealed class ClearanceScope(double previous) : IDisposable + { + public void Dispose() => _current = previous; + } +}