Files
git.stella-ops.org/src/__Libraries/StellaOps.ElkSharp/ElkEdgeRouterIterative.LocalRepair.ObstacleSkirt.cs
master 55a8d2ff51 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>
2026-04-01 16:59:18 +03:00

174 lines
6.0 KiB
C#

using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static List<ElkPoint>? TryBuildLocalObstacleSkirtPath(
ElkPoint start,
ElkPoint end,
IReadOnlyCollection<ElkPositionedNode> nodes,
string sourceId,
string targetId,
ElkPositionedNode? targetNode,
double obstaclePadding)
{
var obstacles = nodes.Select(node => (
Left: node.X - obstaclePadding,
Top: node.Y - obstaclePadding,
Right: node.X + node.Width + obstaclePadding,
Bottom: node.Y + node.Height + obstaclePadding,
Id: node.Id)).ToArray();
List<ElkPoint>? bestPath = null;
var bestScore = double.MaxValue;
bool SegmentIsClear(ElkPoint from, ElkPoint to) =>
!ElkEdgePostProcessor.SegmentCrossesObstacle(from, to, obstacles, sourceId, targetId);
void ConsiderCandidate(IReadOnlyList<ElkPoint> rawCandidate)
{
var candidate = NormalizePolyline(rawCandidate);
if (candidate.Count < 2)
{
return;
}
for (var i = 1; i < candidate.Count; i++)
{
if (!SegmentIsClear(candidate[i - 1], candidate[i]))
{
return;
}
}
if (targetNode is not null
&& !ElkShapeBoundaries.IsGatewayShape(targetNode)
&& HasTargetApproachBacktracking(candidate, targetNode))
{
return;
}
var score = ComputePolylineLength(candidate) + (Math.Max(0, candidate.Count - 2) * 4d);
if (score >= bestScore - 0.5d)
{
return;
}
bestScore = score;
bestPath = candidate;
}
var horizontalDominant = Math.Abs(end.X - start.X) >= Math.Abs(end.Y - start.Y);
if (horizontalDominant)
{
var targetBridgeX = end.X;
if (targetNode is not null && ElkShapeBoundaries.IsGatewayShape(targetNode))
{
targetBridgeX = ResolveGatewayRoutingApproachPoint(targetNode, start, end).X;
}
var minX = Math.Min(start.X, end.X) + 0.5d;
var maxX = Math.Max(start.X, end.X) - 0.5d;
var corridorTop = Math.Min(start.Y, end.Y) - obstaclePadding;
var corridorBottom = Math.Max(start.Y, end.Y) + obstaclePadding;
var bypassYCandidates = new List<double> { start.Y, end.Y };
foreach (var obstacle in obstacles)
{
if (string.Equals(obstacle.Id, sourceId, StringComparison.Ordinal)
|| string.Equals(obstacle.Id, targetId, StringComparison.Ordinal)
|| obstacle.Right <= minX
|| obstacle.Left >= maxX
|| obstacle.Bottom <= corridorTop
|| obstacle.Top >= corridorBottom)
{
continue;
}
AddUniqueCoordinate(bypassYCandidates, obstacle.Top);
AddUniqueCoordinate(bypassYCandidates, obstacle.Bottom);
}
foreach (var bypassY in bypassYCandidates)
{
ConsiderCandidate(
[
start,
new ElkPoint { X = start.X, Y = bypassY },
new ElkPoint { X = targetBridgeX, Y = bypassY },
end,
]);
}
}
else
{
var targetBridgeY = end.Y;
if (targetNode is not null && ElkShapeBoundaries.IsGatewayShape(targetNode))
{
targetBridgeY = ResolveGatewayRoutingApproachPoint(targetNode, start, end).Y;
}
var minY = Math.Min(start.Y, end.Y) + 0.5d;
var maxY = Math.Max(start.Y, end.Y) - 0.5d;
var corridorLeft = Math.Min(start.X, end.X) - obstaclePadding;
var corridorRight = Math.Max(start.X, end.X) + obstaclePadding;
var bypassXCandidates = new List<double> { start.X, end.X };
foreach (var obstacle in obstacles)
{
if (string.Equals(obstacle.Id, sourceId, StringComparison.Ordinal)
|| string.Equals(obstacle.Id, targetId, StringComparison.Ordinal)
|| obstacle.Bottom <= minY
|| obstacle.Top >= maxY
|| obstacle.Right <= corridorLeft
|| obstacle.Left >= corridorRight)
{
continue;
}
AddUniqueCoordinate(bypassXCandidates, obstacle.Left);
AddUniqueCoordinate(bypassXCandidates, obstacle.Right);
}
foreach (var bypassX in bypassXCandidates)
{
ConsiderCandidate(
[
start,
new ElkPoint { X = bypassX, Y = start.Y },
new ElkPoint { X = bypassX, Y = targetBridgeY },
end,
]);
}
}
return bestPath;
}
private static double ComputePolylineLength(IReadOnlyList<ElkPoint> points)
{
var length = 0d;
for (var i = 1; i < points.Count; i++)
{
length += ElkEdgeRoutingGeometry.ComputeSegmentLength(points[i - 1], points[i]);
}
return length;
}
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
: 50d;
}
}