Improve rendering
This commit is contained in:
@@ -0,0 +1,334 @@
|
||||
namespace StellaOps.ElkSharp;
|
||||
|
||||
internal static class ElkEdgePostProcessorSimplify
|
||||
{
|
||||
internal static ElkRoutedEdge[] SimplifyEdgePaths(
|
||||
ElkRoutedEdge[] edges,
|
||||
ElkPositionedNode[] nodes)
|
||||
{
|
||||
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 graphMinY = nodes.Length > 0 ? nodes.Min(n => n.Y) : 0d;
|
||||
var graphMaxY = nodes.Length > 0 ? nodes.Max(n => n.Y + n.Height) : 0d;
|
||||
var result = new ElkRoutedEdge[edges.Length];
|
||||
for (var i = 0; i < edges.Length; i++)
|
||||
{
|
||||
var edge = edges[i];
|
||||
var excludeIds = new HashSet<string>(StringComparer.Ordinal) { edge.SourceNodeId ?? "", edge.TargetNodeId ?? "" };
|
||||
var anyChanged = false;
|
||||
var newSections = new List<ElkEdgeSection>(edge.Sections.Count);
|
||||
var hasCorridor = ElkEdgePostProcessor.HasCorridorBendPoints(edge, graphMinY, graphMaxY);
|
||||
|
||||
foreach (var section in edge.Sections)
|
||||
{
|
||||
var pts = new List<ElkPoint> { section.StartPoint };
|
||||
pts.AddRange(section.BendPoints);
|
||||
pts.Add(section.EndPoint);
|
||||
|
||||
// Pass 1: Remove collinear points
|
||||
var cleaned = new List<ElkPoint> { pts[0] };
|
||||
for (var j = 1; j < pts.Count - 1; j++)
|
||||
{
|
||||
var prev = cleaned[^1];
|
||||
var curr = pts[j];
|
||||
var next = pts[j + 1];
|
||||
var sameX = Math.Abs(prev.X - curr.X) < 1d && Math.Abs(curr.X - next.X) < 1d;
|
||||
var sameY = Math.Abs(prev.Y - curr.Y) < 1d && Math.Abs(curr.Y - next.Y) < 1d;
|
||||
if (sameX || sameY)
|
||||
{
|
||||
anyChanged = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
cleaned.Add(curr);
|
||||
}
|
||||
}
|
||||
cleaned.Add(pts[^1]);
|
||||
|
||||
// Pass 2: Try L-shape shortcuts for each triple (skip for corridor-routed edges)
|
||||
var changed = !hasCorridor;
|
||||
var simplifyPass = 0;
|
||||
while (changed && simplifyPass++ < 20)
|
||||
{
|
||||
changed = false;
|
||||
for (var j = 0; j + 2 < cleaned.Count; j++)
|
||||
{
|
||||
var a = cleaned[j];
|
||||
var c = cleaned[j + 2];
|
||||
var corner1 = new ElkPoint { X = a.X, Y = c.Y };
|
||||
var corner2 = new ElkPoint { X = c.X, Y = a.Y };
|
||||
|
||||
foreach (var corner in new[] { corner1, corner2 })
|
||||
{
|
||||
if (SegmentClearsObstacles(a, corner, obstacles, excludeIds)
|
||||
&& SegmentClearsObstacles(corner, c, obstacles, excludeIds))
|
||||
{
|
||||
cleaned[j + 1] = corner;
|
||||
changed = true;
|
||||
anyChanged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove trailing duplicates (bend point == endpoint)
|
||||
while (cleaned.Count > 2
|
||||
&& Math.Abs(cleaned[^1].X - cleaned[^2].X) < 1d
|
||||
&& Math.Abs(cleaned[^1].Y - cleaned[^2].Y) < 1d)
|
||||
{
|
||||
cleaned.RemoveAt(cleaned.Count - 2);
|
||||
}
|
||||
|
||||
// Remove leading duplicates (start point == first bend)
|
||||
while (cleaned.Count > 2
|
||||
&& Math.Abs(cleaned[0].X - cleaned[1].X) < 1d
|
||||
&& Math.Abs(cleaned[0].Y - cleaned[1].Y) < 1d)
|
||||
{
|
||||
cleaned.RemoveAt(1);
|
||||
}
|
||||
|
||||
newSections.Add(new ElkEdgeSection
|
||||
{
|
||||
StartPoint = cleaned[0],
|
||||
EndPoint = cleaned[^1],
|
||||
BendPoints = cleaned.Skip(1).Take(cleaned.Count - 2).ToArray(),
|
||||
});
|
||||
}
|
||||
|
||||
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[] TightenOuterCorridors(
|
||||
ElkRoutedEdge[] edges,
|
||||
ElkPositionedNode[] nodes)
|
||||
{
|
||||
if (nodes.Length == 0) return edges;
|
||||
|
||||
var graphMinY = nodes.Min(n => n.Y);
|
||||
var graphMaxY = nodes.Max(n => n.Y + n.Height);
|
||||
const double minMargin = 12d;
|
||||
const double laneGap = 8d;
|
||||
|
||||
var outerEdges = new List<(int Index, double CorridorY, bool IsAbove)>();
|
||||
for (var i = 0; i < edges.Length; i++)
|
||||
{
|
||||
var aboveYs = new List<double>();
|
||||
var belowYs = new List<double>();
|
||||
foreach (var section in edges[i].Sections)
|
||||
{
|
||||
foreach (var bp in section.BendPoints)
|
||||
{
|
||||
if (bp.Y < graphMinY - 8d)
|
||||
{
|
||||
aboveYs.Add(bp.Y);
|
||||
}
|
||||
else if (bp.Y > graphMaxY + 8d)
|
||||
{
|
||||
belowYs.Add(bp.Y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (aboveYs.Count > 0)
|
||||
{
|
||||
outerEdges.Add((i, aboveYs.Min(), true));
|
||||
}
|
||||
|
||||
if (belowYs.Count > 0)
|
||||
{
|
||||
outerEdges.Add((i, belowYs.Max(), false));
|
||||
}
|
||||
}
|
||||
|
||||
if (outerEdges.Count == 0) return edges;
|
||||
|
||||
NormalizeCorridorYValues(outerEdges, edges, graphMinY, graphMaxY);
|
||||
|
||||
var aboveLanes = outerEdges.Where(e => e.IsAbove)
|
||||
.GroupBy(e => Math.Round(e.CorridorY, 1))
|
||||
.OrderBy(g => g.Key)
|
||||
.ToArray();
|
||||
var belowLanes = outerEdges.Where(e => !e.IsAbove)
|
||||
.GroupBy(e => Math.Round(e.CorridorY, 1))
|
||||
.OrderByDescending(g => g.Key)
|
||||
.ToArray();
|
||||
|
||||
var result = edges.ToArray();
|
||||
var shifts = new Dictionary<int, double>();
|
||||
|
||||
for (var lane = 0; lane < aboveLanes.Length; lane++)
|
||||
{
|
||||
var targetY = graphMinY - minMargin - (lane * laneGap);
|
||||
var currentY = aboveLanes[lane].Key;
|
||||
var shift = targetY - currentY;
|
||||
if (Math.Abs(shift) > 2d)
|
||||
{
|
||||
foreach (var entry in aboveLanes[lane])
|
||||
{
|
||||
shifts[entry.Index] = shift;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var lane = 0; lane < belowLanes.Length; lane++)
|
||||
{
|
||||
var targetY = graphMaxY + minMargin + (lane * laneGap);
|
||||
var currentY = belowLanes[lane].Key;
|
||||
var shift = targetY - currentY;
|
||||
if (Math.Abs(shift) > 2d)
|
||||
{
|
||||
foreach (var entry in belowLanes[lane])
|
||||
{
|
||||
shifts[entry.Index] = shift;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (edgeIndex, shift) in shifts)
|
||||
{
|
||||
var edge = result[edgeIndex];
|
||||
var boundary = shift > 0 ? graphMaxY : graphMinY;
|
||||
var newSections = new List<ElkEdgeSection>();
|
||||
foreach (var section in edge.Sections)
|
||||
{
|
||||
var newBendPoints = section.BendPoints.Select(bp =>
|
||||
{
|
||||
if ((shift < 0 && bp.Y < graphMinY - 4d) || (shift > 0 && bp.Y > graphMaxY + 4d)
|
||||
|| (shift > 0 && bp.Y < graphMinY - 4d) || (shift < 0 && bp.Y > graphMaxY + 4d))
|
||||
{
|
||||
return new ElkPoint { X = bp.X, Y = bp.Y + shift };
|
||||
}
|
||||
return bp;
|
||||
}).ToArray();
|
||||
|
||||
newSections.Add(new ElkEdgeSection
|
||||
{
|
||||
StartPoint = section.StartPoint,
|
||||
EndPoint = section.EndPoint,
|
||||
BendPoints = newBendPoints,
|
||||
});
|
||||
}
|
||||
|
||||
result[edgeIndex] = new ElkRoutedEdge
|
||||
{
|
||||
Id = edge.Id,
|
||||
SourceNodeId = edge.SourceNodeId,
|
||||
TargetNodeId = edge.TargetNodeId,
|
||||
Label = edge.Label,
|
||||
Sections = newSections,
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
internal static bool SegmentClearsObstacles(
|
||||
ElkPoint p1, ElkPoint p2,
|
||||
(double L, double T, double R, double B, string Id)[] obstacles,
|
||||
HashSet<string> excludeIds)
|
||||
{
|
||||
var isH = Math.Abs(p1.Y - p2.Y) < 1d;
|
||||
var isV = Math.Abs(p1.X - p2.X) < 1d;
|
||||
if (!isH && !isV) return true;
|
||||
|
||||
foreach (var ob in obstacles)
|
||||
{
|
||||
if (excludeIds.Contains(ob.Id)) continue;
|
||||
if (isH && p1.Y > ob.T && p1.Y < ob.B)
|
||||
{
|
||||
if (Math.Max(p1.X, p2.X) > ob.L && Math.Min(p1.X, p2.X) < ob.R) return false;
|
||||
}
|
||||
else if (isV && p1.X > ob.L && p1.X < ob.R)
|
||||
{
|
||||
if (Math.Max(p1.Y, p2.Y) > ob.T && Math.Min(p1.Y, p2.Y) < ob.B) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void NormalizeCorridorYValues(
|
||||
List<(int Index, double CorridorY, bool IsAbove)> outerEdges,
|
||||
ElkRoutedEdge[] edges,
|
||||
double graphMinY, double graphMaxY)
|
||||
{
|
||||
const double mergeThreshold = 6d;
|
||||
var groups = new List<List<int>>();
|
||||
var sorted = outerEdges.OrderBy(e => e.CorridorY).ToArray();
|
||||
foreach (var entry in sorted)
|
||||
{
|
||||
var merged = false;
|
||||
foreach (var group in groups)
|
||||
{
|
||||
var groupY = outerEdges[group[0]].CorridorY;
|
||||
if (Math.Abs(entry.CorridorY - groupY) <= mergeThreshold && entry.IsAbove == outerEdges[group[0]].IsAbove)
|
||||
{
|
||||
group.Add(outerEdges.IndexOf(entry));
|
||||
merged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!merged)
|
||||
{
|
||||
groups.Add([outerEdges.IndexOf(entry)]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var group in groups)
|
||||
{
|
||||
if (group.Count <= 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var targetY = outerEdges[group[0]].CorridorY;
|
||||
for (var gi = 1; gi < group.Count; gi++)
|
||||
{
|
||||
var idx = group[gi];
|
||||
var edgeIndex = outerEdges[idx].Index;
|
||||
var currentY = outerEdges[idx].CorridorY;
|
||||
var shift = targetY - currentY;
|
||||
if (Math.Abs(shift) < 0.5d)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var edge = edges[edgeIndex];
|
||||
var newSections = new List<ElkEdgeSection>();
|
||||
foreach (var section in edge.Sections)
|
||||
{
|
||||
var newBendPoints = section.BendPoints.Select(bp =>
|
||||
{
|
||||
if (Math.Abs(bp.Y - currentY) < 2d)
|
||||
{
|
||||
return new ElkPoint { X = bp.X, Y = targetY };
|
||||
}
|
||||
|
||||
return bp;
|
||||
}).ToArray();
|
||||
|
||||
newSections.Add(new ElkEdgeSection
|
||||
{
|
||||
StartPoint = section.StartPoint,
|
||||
EndPoint = section.EndPoint,
|
||||
BendPoints = newBendPoints,
|
||||
});
|
||||
}
|
||||
|
||||
edges[edgeIndex] = new ElkRoutedEdge
|
||||
{
|
||||
Id = edge.Id,
|
||||
SourceNodeId = edge.SourceNodeId,
|
||||
TargetNodeId = edge.TargetNodeId,
|
||||
Label = edge.Label,
|
||||
Sections = newSections,
|
||||
};
|
||||
outerEdges[idx] = (edgeIndex, targetY, outerEdges[idx].IsAbove);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user