Refactor ElkSharp hybrid routing and document speed path
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user