Improve rendering

This commit is contained in:
master
2026-03-21 01:03:20 +02:00
parent d2e542f77e
commit eb27a69778
28 changed files with 9802 additions and 4490 deletions

View File

@@ -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);
}
}
}
}