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,264 @@
namespace StellaOps.ElkSharp;
internal static class ElkNodePlacement
{
internal static int ResolveOrderingIterationCount(
ElkLayoutOptions options,
int edgeCount,
int nodeCount)
{
if (options.OrderingIterations is int explicitIterations)
{
return Math.Max(2, explicitIterations);
}
var baseline = Math.Max(6, Math.Max(edgeCount / 4, nodeCount / 3));
return options.Effort switch
{
ElkLayoutEffort.Draft => Math.Min(8, baseline),
ElkLayoutEffort.Balanced => Math.Min(14, Math.Max(8, baseline)),
_ => Math.Min(24, Math.Max(12, baseline + 4)),
};
}
internal static int ResolvePlacementIterationCount(
ElkLayoutOptions options,
int nodeCount,
int layerCount)
{
if (options.PlacementIterations is int explicitIterations)
{
return Math.Max(1, explicitIterations);
}
var baseline = Math.Max(2, Math.Max(nodeCount / 8, layerCount / 2));
return options.Effort switch
{
ElkLayoutEffort.Draft => Math.Min(3, baseline),
ElkLayoutEffort.Balanced => Math.Min(6, Math.Max(3, baseline)),
_ => Math.Min(10, Math.Max(5, baseline + 2)),
};
}
internal static void RefineHorizontalPlacement(
Dictionary<string, ElkPositionedNode> positionedNodes,
IReadOnlyList<ElkNode[]> layers,
IReadOnlyDictionary<string, List<string>> incomingNodeIds,
IReadOnlyDictionary<string, List<string>> outgoingNodeIds,
IReadOnlyDictionary<string, ElkNode> nodesById,
double nodeSpacing,
int iterationCount,
ElkLayoutDirection direction)
{
if (iterationCount <= 0)
{
return;
}
for (var iteration = 0; iteration < iterationCount; iteration++)
{
var layerIndices = iteration % 2 == 0
? Enumerable.Range(0, layers.Count)
: Enumerable.Range(0, layers.Count).Reverse();
foreach (var layerIndex in layerIndices)
{
var layer = layers[layerIndex];
if (layer.Length == 0)
{
continue;
}
var desiredY = new double[layer.Length];
for (var nodeIndex = 0; nodeIndex < layer.Length; nodeIndex++)
{
var node = layer[nodeIndex];
var preferredCenter = ElkNodePlacementPreferredCenter.ResolvePreferredCenter(
node.Id,
incomingNodeIds,
outgoingNodeIds,
positionedNodes,
horizontal: true);
desiredY[nodeIndex] = preferredCenter.HasValue
? preferredCenter.Value - (node.Height / 2d)
: positionedNodes[node.Id].Y;
}
for (var nodeIndex = 1; nodeIndex < layer.Length; nodeIndex++)
{
var minY = desiredY[nodeIndex - 1] + layer[nodeIndex - 1].Height + nodeSpacing;
if (desiredY[nodeIndex] < minY)
{
desiredY[nodeIndex] = minY;
}
}
for (var nodeIndex = 0; nodeIndex < layer.Length; nodeIndex++)
{
var current = positionedNodes[layer[nodeIndex].Id];
positionedNodes[layer[nodeIndex].Id] = ElkLayoutHelpers.CreatePositionedNode(
nodesById[layer[nodeIndex].Id],
current.X,
desiredY[nodeIndex],
direction);
}
}
}
}
internal static void RefineVerticalPlacement(
Dictionary<string, ElkPositionedNode> positionedNodes,
IReadOnlyList<ElkNode[]> layers,
IReadOnlyDictionary<string, List<string>> incomingNodeIds,
IReadOnlyDictionary<string, List<string>> outgoingNodeIds,
IReadOnlyDictionary<string, ElkNode> nodesById,
double nodeSpacing,
int iterationCount,
ElkLayoutDirection direction)
{
if (iterationCount <= 0)
{
return;
}
for (var iteration = 0; iteration < iterationCount; iteration++)
{
var layerIndices = iteration % 2 == 0
? Enumerable.Range(0, layers.Count)
: Enumerable.Range(0, layers.Count).Reverse();
foreach (var layerIndex in layerIndices)
{
var layer = layers[layerIndex];
if (layer.Length == 0)
{
continue;
}
var desiredX = new double[layer.Length];
for (var nodeIndex = 0; nodeIndex < layer.Length; nodeIndex++)
{
var node = layer[nodeIndex];
var preferredCenter = ElkNodePlacementPreferredCenter.ResolvePreferredCenter(
node.Id,
incomingNodeIds,
outgoingNodeIds,
positionedNodes,
horizontal: false);
desiredX[nodeIndex] = preferredCenter.HasValue
? preferredCenter.Value - (node.Width / 2d)
: positionedNodes[node.Id].X;
}
for (var nodeIndex = 1; nodeIndex < layer.Length; nodeIndex++)
{
var minX = desiredX[nodeIndex - 1] + layer[nodeIndex - 1].Width + nodeSpacing;
if (desiredX[nodeIndex] < minX)
{
desiredX[nodeIndex] = minX;
}
}
for (var nodeIndex = 0; nodeIndex < layer.Length; nodeIndex++)
{
var current = positionedNodes[layer[nodeIndex].Id];
positionedNodes[layer[nodeIndex].Id] = ElkLayoutHelpers.CreatePositionedNode(
nodesById[layer[nodeIndex].Id],
desiredX[nodeIndex],
current.Y,
direction);
}
}
}
}
internal static void SnapOriginalPrimaryAxes(
Dictionary<string, ElkPositionedNode> positionedNodes,
IReadOnlyList<ElkNode[]> layers,
IReadOnlySet<string> dummyNodeIds,
IReadOnlyDictionary<string, List<string>> incomingNodeIds,
IReadOnlyDictionary<string, List<string>> outgoingNodeIds,
IReadOnlyDictionary<string, ElkNode> originalNodesById,
double nodeSpacing,
ElkLayoutDirection direction)
{
for (var iteration = 0; iteration < 3; iteration++)
{
foreach (var layer in layers)
{
var actualNodes = layer
.Where(node => !dummyNodeIds.Contains(node.Id) && originalNodesById.ContainsKey(node.Id))
.Select(node => originalNodesById[node.Id])
.ToArray();
if (actualNodes.Length == 0)
{
continue;
}
var nodeDesiredPairs = new (ElkNode Node, double Desired)[actualNodes.Length];
for (var nodeIndex = 0; nodeIndex < actualNodes.Length; nodeIndex++)
{
var positioned = positionedNodes[actualNodes[nodeIndex].Id];
var preferredCenter = ElkNodePlacementPreferredCenter.ResolveOriginalPreferredCenter(
actualNodes[nodeIndex].Id,
incomingNodeIds,
outgoingNodeIds,
positionedNodes,
horizontal: direction == ElkLayoutDirection.LeftToRight);
nodeDesiredPairs[nodeIndex] = (actualNodes[nodeIndex], preferredCenter.HasValue
? preferredCenter.Value - ((direction == ElkLayoutDirection.LeftToRight ? positioned.Height : positioned.Width) / 2d)
: (direction == ElkLayoutDirection.LeftToRight ? positioned.Y : positioned.X));
}
Array.Sort(nodeDesiredPairs, (a, b) => a.Desired.CompareTo(b.Desired));
var sortedNodes = nodeDesiredPairs.Select(p => p.Node).ToArray();
var desiredCoordinates = nodeDesiredPairs.Select(p => p.Desired).ToArray();
EnforceLinearSpacing(
sortedNodes,
desiredCoordinates,
nodeSpacing,
horizontal: direction == ElkLayoutDirection.LeftToRight);
for (var nodeIndex = 0; nodeIndex < sortedNodes.Length; nodeIndex++)
{
var current = positionedNodes[sortedNodes[nodeIndex].Id];
positionedNodes[sortedNodes[nodeIndex].Id] = direction == ElkLayoutDirection.LeftToRight
? ElkLayoutHelpers.CreatePositionedNode(sortedNodes[nodeIndex], current.X, desiredCoordinates[nodeIndex], direction)
: ElkLayoutHelpers.CreatePositionedNode(sortedNodes[nodeIndex], desiredCoordinates[nodeIndex], current.Y, direction);
}
}
}
}
internal static void EnforceLinearSpacing(
IReadOnlyList<ElkNode> layer,
double[] desiredCoordinates,
double spacing,
bool horizontal)
{
for (var index = 1; index < layer.Count; index++)
{
var extent = horizontal ? layer[index - 1].Height : layer[index - 1].Width;
desiredCoordinates[index] = Math.Max(
desiredCoordinates[index],
desiredCoordinates[index - 1] + extent + spacing);
}
for (var index = layer.Count - 2; index >= 0; index--)
{
var extent = horizontal ? layer[index].Height : layer[index].Width;
desiredCoordinates[index] = Math.Min(
desiredCoordinates[index],
desiredCoordinates[index + 1] - extent - spacing);
}
for (var index = 1; index < layer.Count; index++)
{
var extent = horizontal ? layer[index - 1].Height : layer[index - 1].Width;
desiredCoordinates[index] = Math.Max(
desiredCoordinates[index],
desiredCoordinates[index - 1] + extent + spacing);
}
}
}