Refactor ElkSharp hybrid routing and document speed path

This commit is contained in:
master
2026-03-29 19:33:46 +03:00
parent 7d6bc2b0ab
commit e8f7ad7652
89 changed files with 13280 additions and 10732 deletions

View File

@@ -0,0 +1,167 @@
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 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;
}
}