Improve rendering
This commit is contained in:
295
src/__Libraries/StellaOps.ElkSharp/ElkEdgePostProcessor.cs
Normal file
295
src/__Libraries/StellaOps.ElkSharp/ElkEdgePostProcessor.cs
Normal file
@@ -0,0 +1,295 @@
|
||||
namespace StellaOps.ElkSharp;
|
||||
|
||||
internal static class ElkEdgePostProcessor
|
||||
{
|
||||
internal static ElkRoutedEdge[] SnapAnchorsToNodeBoundary(
|
||||
ElkRoutedEdge[] edges,
|
||||
ElkPositionedNode[] nodes)
|
||||
{
|
||||
var nodesById = nodes.ToDictionary(n => n.Id, StringComparer.Ordinal);
|
||||
var result = new ElkRoutedEdge[edges.Length];
|
||||
for (var i = 0; i < edges.Length; i++)
|
||||
{
|
||||
var edge = edges[i];
|
||||
var anyChanged = false;
|
||||
var newSections = edge.Sections.ToList();
|
||||
|
||||
for (var s = 0; s < newSections.Count; s++)
|
||||
{
|
||||
var section = newSections[s];
|
||||
var startFixed = false;
|
||||
var endFixed = false;
|
||||
var newStart = section.StartPoint;
|
||||
var newEnd = section.EndPoint;
|
||||
|
||||
if (edge.SourceNodeId is not null && nodesById.TryGetValue(edge.SourceNodeId, out var srcNode) && s == 0)
|
||||
{
|
||||
if (newStart.X > srcNode.X + 1d && newStart.X < srcNode.X + srcNode.Width - 1d
|
||||
&& newStart.Y > srcNode.Y + 1d && newStart.Y < srcNode.Y + srcNode.Height - 1d)
|
||||
{
|
||||
var target = section.BendPoints.Count > 0 ? section.BendPoints.First() : section.EndPoint;
|
||||
newStart = ElkShapeBoundaries.ProjectOntoShapeBoundary(srcNode, target);
|
||||
startFixed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (edge.TargetNodeId is not null && nodesById.TryGetValue(edge.TargetNodeId, out var tgtNode) && s == newSections.Count - 1)
|
||||
{
|
||||
var source = section.BendPoints.Count > 0 ? section.BendPoints.Last() : section.StartPoint;
|
||||
var projected = ElkShapeBoundaries.ProjectOntoShapeBoundary(tgtNode, source);
|
||||
if (Math.Abs(projected.X - newEnd.X) > 3d || Math.Abs(projected.Y - newEnd.Y) > 3d)
|
||||
{
|
||||
newEnd = projected;
|
||||
endFixed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (startFixed || endFixed)
|
||||
{
|
||||
anyChanged = true;
|
||||
newSections[s] = new ElkEdgeSection
|
||||
{
|
||||
StartPoint = newStart,
|
||||
EndPoint = newEnd,
|
||||
BendPoints = section.BendPoints,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
result[i] = anyChanged
|
||||
? new ElkRoutedEdge { Id = edge.Id, SourceNodeId = edge.SourceNodeId, TargetNodeId = edge.TargetNodeId, Label = edge.Label, Sections = newSections }
|
||||
: edge;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
internal static ElkRoutedEdge[] AvoidNodeCrossings(
|
||||
ElkRoutedEdge[] edges,
|
||||
ElkPositionedNode[] nodes,
|
||||
ElkLayoutDirection direction)
|
||||
{
|
||||
if (direction != ElkLayoutDirection.LeftToRight || nodes.Length == 0)
|
||||
{
|
||||
return edges;
|
||||
}
|
||||
|
||||
const double margin = 18d;
|
||||
var obstacles = nodes.Select(n => (
|
||||
Left: n.X - margin, Top: n.Y - margin,
|
||||
Right: n.X + n.Width + margin, Bottom: n.Y + n.Height + margin,
|
||||
Id: n.Id
|
||||
)).ToArray();
|
||||
var graphMinY = nodes.Min(n => n.Y);
|
||||
var graphMaxY = nodes.Max(n => n.Y + n.Height);
|
||||
|
||||
var result = new ElkRoutedEdge[edges.Length];
|
||||
for (var edgeIndex = 0; edgeIndex < edges.Length; edgeIndex++)
|
||||
{
|
||||
var edge = edges[edgeIndex];
|
||||
var sourceId = edge.SourceNodeId ?? "";
|
||||
var targetId = edge.TargetNodeId ?? "";
|
||||
|
||||
var hasCorridorPts = HasCorridorBendPoints(edge, graphMinY, graphMaxY);
|
||||
if (hasCorridorPts && IsRepeatCollectorLabel(edge.Label))
|
||||
{
|
||||
result[edgeIndex] = edge;
|
||||
continue;
|
||||
}
|
||||
|
||||
var hasCrossing = false;
|
||||
foreach (var section in edge.Sections)
|
||||
{
|
||||
var pts = new List<ElkPoint> { section.StartPoint };
|
||||
pts.AddRange(section.BendPoints);
|
||||
pts.Add(section.EndPoint);
|
||||
for (var i = 0; i < pts.Count - 1 && !hasCrossing; i++)
|
||||
{
|
||||
if (hasCorridorPts && IsCorridorSegment(pts[i], pts[i + 1], graphMinY, graphMaxY))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
hasCrossing = SegmentCrossesObstacle(pts[i], pts[i + 1], obstacles, sourceId, targetId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasCrossing)
|
||||
{
|
||||
result[edgeIndex] = edge;
|
||||
continue;
|
||||
}
|
||||
|
||||
var hasCorridorPoints = hasCorridorPts;
|
||||
var newSections = new List<ElkEdgeSection>(edge.Sections.Count);
|
||||
foreach (var section in edge.Sections)
|
||||
{
|
||||
if (hasCorridorPoints)
|
||||
{
|
||||
var corridorRerouted = ElkEdgePostProcessorCorridor.ReroutePreservingCorridor(
|
||||
section, obstacles, sourceId, targetId, margin, graphMinY, graphMaxY);
|
||||
if (corridorRerouted is not null)
|
||||
{
|
||||
newSections.Add(corridorRerouted);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var rerouted = ElkEdgePostProcessorAStar.RerouteWithGridAStar(
|
||||
section.StartPoint, section.EndPoint,
|
||||
obstacles, sourceId, targetId, margin);
|
||||
|
||||
if (rerouted is not null && rerouted.Count >= 2)
|
||||
{
|
||||
newSections.Add(new ElkEdgeSection
|
||||
{
|
||||
StartPoint = rerouted[0],
|
||||
EndPoint = rerouted[^1],
|
||||
BendPoints = rerouted.Skip(1).Take(rerouted.Count - 2).ToArray(),
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
newSections.Add(section);
|
||||
}
|
||||
}
|
||||
|
||||
result[edgeIndex] = new ElkRoutedEdge
|
||||
{
|
||||
Id = edge.Id,
|
||||
SourceNodeId = edge.SourceNodeId,
|
||||
TargetNodeId = edge.TargetNodeId,
|
||||
Label = edge.Label,
|
||||
Sections = newSections,
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
internal static ElkRoutedEdge[] EliminateDiagonalSegments(ElkRoutedEdge[] edges, ElkPositionedNode[] nodes)
|
||||
{
|
||||
var graphMinY = nodes.Length > 0 ? nodes.Min(n => n.Y) : 0d;
|
||||
var graphMaxY = nodes.Length > 0 ? nodes.Max(n => n.Y + n.Height) : 0d;
|
||||
var obstacles = nodes.Select(n => (L: n.X - 4d, T: n.Y - 4d, R: n.X + n.Width + 4d, B: n.Y + n.Height + 4d, Id: n.Id)).ToArray();
|
||||
var result = new ElkRoutedEdge[edges.Length];
|
||||
for (var i = 0; i < edges.Length; i++)
|
||||
{
|
||||
var edge = edges[i];
|
||||
var anyFixed = false;
|
||||
var newSections = new List<ElkEdgeSection>();
|
||||
|
||||
foreach (var section in edge.Sections)
|
||||
{
|
||||
var pts = new List<ElkPoint> { section.StartPoint };
|
||||
pts.AddRange(section.BendPoints);
|
||||
pts.Add(section.EndPoint);
|
||||
|
||||
var fixedPts = new List<ElkPoint> { pts[0] };
|
||||
for (var j = 1; j < pts.Count; j++)
|
||||
{
|
||||
var prev = fixedPts[^1];
|
||||
var curr = pts[j];
|
||||
var dx = Math.Abs(curr.X - prev.X);
|
||||
var dy = Math.Abs(curr.Y - prev.Y);
|
||||
if (dx > 3d && dy > 3d)
|
||||
{
|
||||
var prevIsCorridor = prev.Y < graphMinY - 8d || prev.Y > graphMaxY + 8d;
|
||||
var currIsCorridor = curr.Y < graphMinY - 8d || curr.Y > graphMaxY + 8d;
|
||||
var isBackwardSection = section.EndPoint.X < section.StartPoint.X - 1d;
|
||||
if (prevIsCorridor)
|
||||
{
|
||||
fixedPts.Add(new ElkPoint { X = curr.X, Y = prev.Y });
|
||||
anyFixed = true;
|
||||
}
|
||||
else if (currIsCorridor && isBackwardSection)
|
||||
{
|
||||
// Preserve diagonal for backward collector edges
|
||||
}
|
||||
else
|
||||
{
|
||||
fixedPts.Add(new ElkPoint { X = prev.X, Y = curr.Y });
|
||||
anyFixed = true;
|
||||
}
|
||||
}
|
||||
fixedPts.Add(curr);
|
||||
}
|
||||
|
||||
newSections.Add(new ElkEdgeSection
|
||||
{
|
||||
StartPoint = fixedPts[0],
|
||||
EndPoint = fixedPts[^1],
|
||||
BendPoints = fixedPts.Skip(1).Take(fixedPts.Count - 2).ToArray(),
|
||||
});
|
||||
}
|
||||
|
||||
result[i] = anyFixed
|
||||
? new ElkRoutedEdge { Id = edge.Id, SourceNodeId = edge.SourceNodeId, TargetNodeId = edge.TargetNodeId, Label = edge.Label, Sections = newSections }
|
||||
: edge;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
internal static bool IsRepeatCollectorLabel(string? label)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(label))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var normalized = label.Trim().ToLowerInvariant();
|
||||
return normalized.StartsWith("repeat ", StringComparison.Ordinal)
|
||||
|| normalized.Equals("body", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
internal static bool IsCorridorSegment(ElkPoint p1, ElkPoint p2, double graphMinY, double graphMaxY)
|
||||
{
|
||||
return p1.Y < graphMinY - 8d || p1.Y > graphMaxY + 8d
|
||||
|| p2.Y < graphMinY - 8d || p2.Y > graphMaxY + 8d;
|
||||
}
|
||||
|
||||
internal static bool HasCorridorBendPoints(ElkRoutedEdge edge, double graphMinY, double graphMaxY)
|
||||
{
|
||||
foreach (var section in edge.Sections)
|
||||
{
|
||||
foreach (var bp in section.BendPoints)
|
||||
{
|
||||
if (bp.Y < graphMinY - 8d || bp.Y > graphMaxY + 8d)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static bool SegmentCrossesObstacle(
|
||||
ElkPoint p1, ElkPoint p2,
|
||||
(double Left, double Top, double Right, double Bottom, string Id)[] obstacles,
|
||||
string sourceId, string targetId)
|
||||
{
|
||||
var segLen = Math.Sqrt(Math.Pow(p1.X - p2.X, 2) + Math.Pow(p1.Y - p2.Y, 2));
|
||||
var isH = Math.Abs(p1.Y - p2.Y) < 2d;
|
||||
var isV = Math.Abs(p1.X - p2.X) < 2d;
|
||||
if (!isH && !isV) return segLen > 15d;
|
||||
|
||||
foreach (var ob in obstacles)
|
||||
{
|
||||
if (ob.Id == sourceId || ob.Id == targetId) continue;
|
||||
if (isH && p1.Y > ob.Top && p1.Y < ob.Bottom)
|
||||
{
|
||||
var minX = Math.Min(p1.X, p2.X);
|
||||
var maxX = Math.Max(p1.X, p2.X);
|
||||
if (maxX > ob.Left && minX < ob.Right) return true;
|
||||
}
|
||||
else if (isV && p1.X > ob.Left && p1.X < ob.Right)
|
||||
{
|
||||
var minY = Math.Min(p1.Y, p2.Y);
|
||||
var maxY = Math.Max(p1.Y, p2.Y);
|
||||
if (maxY > ob.Top && minY < ob.Bottom) return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user