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,124 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
namespace StellaOps.ElkSharp;
internal static partial class ElkEdgeRouterIterative
{
private static Dictionary<string, ElkPoint> SpreadTargetEndpoints(
ElkRoutedEdge[] edges,
Dictionary<string, ElkPositionedNode> nodesById,
double graphMinY,
double graphMaxY,
double minLineClearance)
{
var result = new Dictionary<string, ElkPoint>(StringComparer.Ordinal);
// Group routable edges by target + entry side
var groups = new Dictionary<string, List<(string EdgeId, ElkPoint OrigEnd)>>(StringComparer.Ordinal);
foreach (var edge in edges)
{
if (!ShouldRouteEdge(edge, graphMinY, graphMaxY))
{
continue;
}
var lastSection = edge.Sections.LastOrDefault();
if (lastSection is null || !nodesById.TryGetValue(edge.TargetNodeId ?? "", out var targetNode))
{
continue;
}
var ep = lastSection.EndPoint;
var adjacentPoint = lastSection.BendPoints.LastOrDefault() ?? lastSection.StartPoint;
var side = ResolveEntrySide(ep, adjacentPoint, targetNode);
var key = $"{edge.TargetNodeId}|{side}";
if (!groups.TryGetValue(key, out var list))
{
list = [];
groups[key] = list;
}
list.Add((edge.Id, ep));
}
// For each group with 2+ edges on the same side, spread them
foreach (var (key, group) in groups)
{
if (group.Count < 2)
{
continue;
}
var parts = key.Split('|');
if (!nodesById.TryGetValue(parts[0], out var node))
{
continue;
}
var side = parts[1];
var sorted = side is "left" or "right"
? group.OrderBy(g => g.OrigEnd.Y).ThenBy(g => g.EdgeId, StringComparer.Ordinal).ToList()
: group.OrderBy(g => g.OrigEnd.X).ThenBy(g => g.EdgeId, StringComparer.Ordinal).ToList();
var assignedSlotCoordinates = ElkBoundarySlots.BuildAssignedBoundarySlotCoordinates(node, side, sorted.Count);
if (side is "left" or "right")
{
for (var i = 0; i < sorted.Count; i++)
{
result[sorted[i].EdgeId] = new ElkPoint
{
X = sorted[i].OrigEnd.X,
Y = assignedSlotCoordinates[i],
};
}
}
else
{
for (var i = 0; i < sorted.Count; i++)
{
result[sorted[i].EdgeId] = new ElkPoint
{
X = assignedSlotCoordinates[i],
Y = sorted[i].OrigEnd.Y,
};
}
}
}
return result;
}
private static string ResolveEntrySide(ElkPoint endpoint, ElkPoint adjacentPoint, ElkPositionedNode node)
{
return ElkEdgeRoutingGeometry.ResolveBoundaryApproachSide(endpoint, adjacentPoint, node);
}
private static bool ShouldRouteEdge(ElkRoutedEdge edge, double graphMinY, double graphMaxY)
{
// Skip port-anchored edges (their anchors are fixed)
if (!string.IsNullOrWhiteSpace(edge.SourcePortId) || !string.IsNullOrWhiteSpace(edge.TargetPortId))
{
return false;
}
// Skip backward edges (routed through external corridors)
if (!string.IsNullOrWhiteSpace(edge.Kind)
&& edge.Kind.StartsWith("backward|", StringComparison.OrdinalIgnoreCase))
{
return false;
}
// Skip edges with corridor bend points (already routed outside graph bounds)
if (ElkEdgePostProcessor.HasCorridorBendPoints(edge, graphMinY, graphMaxY))
{
return false;
}
// Skip repeat collector labels
return !ElkEdgePostProcessor.IsRepeatCollectorLabel(edge.Label);
}
}