Unify minLineClearance across pipeline via ElkLayoutClearance

Add ElkLayoutClearance (thread-static scoped holder) so all 15+
ResolveMinLineClearance call sites in scoring/post-processing use the
same NodeSpacing-aware clearance as the iterative optimizer.

Formula: max(avgNodeSize/2, nodeSpacing * 1.2)
At NodeSpacing=40: max(52.7, 48) = 52.7 (unchanged)
At NodeSpacing=60: max(52.7, 72) = 72 (wider corridors)

The infrastructure is in place. Wider spacing (50+) still needs
routing-level tuning for the different edge convergence patterns
that arise from different node arrangements.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-04-01 16:59:18 +03:00
parent abbf004948
commit 55a8d2ff51
6 changed files with 62 additions and 2 deletions

View File

@@ -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<double, double>(); // gutterY → requiredShift

View File

@@ -318,6 +318,13 @@ internal static partial class ElkEdgePostProcessor
private static double ResolveMinLineClearance(IReadOnlyCollection<ElkPositionedNode> 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

View File

@@ -158,6 +158,12 @@ internal static partial class ElkEdgeRouterIterative
private static double ResolveMinLineClearance(IReadOnlyCollection<ElkPositionedNode> 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

View File

@@ -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<CandidateSolution>();

View File

@@ -233,6 +233,12 @@ internal static partial class ElkEdgeRoutingScoring
private static double ResolveMinLineClearance(IReadOnlyCollection<ElkPositionedNode> 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

View File

@@ -0,0 +1,35 @@
namespace StellaOps.ElkSharp;
/// <summary>
/// Layout-wide minimum line clearance, computed once from NodeSpacing at
/// the optimization entry point and available to all scoring/post-processing
/// helpers via <see cref="Current"/>. This avoids threading a parameter
/// through 15+ static call sites.
/// </summary>
internal static class ElkLayoutClearance
{
[ThreadStatic]
private static double _current;
/// <summary>
/// Gets the layout-wide minimum line clearance. Returns 0 if not set
/// (callers fall back to node-size-based clearance).
/// </summary>
internal static double Current => _current;
/// <summary>
/// Sets the layout-wide clearance for the duration of a layout operation.
/// Call at the start of optimization with the NodeSpacing-aware value.
/// </summary>
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;
}
}