125 lines
4.1 KiB
C#
125 lines
4.1 KiB
C#
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);
|
|
}
|
|
|
|
}
|