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,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;
}
}