Files
git.stella-ops.org/src/__Libraries/StellaOps.ElkSharp/ElkEdgeChannelGutters.cs
2026-03-23 13:23:19 +02:00

236 lines
8.3 KiB
C#

namespace StellaOps.ElkSharp;
internal static class ElkEdgeChannelGutters
{
internal static bool ExpandVerticalCorridorGutters(
Dictionary<string, ElkPositionedNode> positionedNodes,
IReadOnlyCollection<ElkRoutedEdge> routedEdges,
IReadOnlyDictionary<string, int> layersByNodeId,
IReadOnlyDictionary<string, ElkNode> nodesById,
double baseLayerSpacing,
ElkLayoutDirection direction)
{
if (direction != ElkLayoutDirection.LeftToRight)
{
return false;
}
var boundariesByLayer = layersByNodeId
.Where(entry => positionedNodes.ContainsKey(entry.Key))
.GroupBy(entry => entry.Value)
.OrderBy(group => group.Key)
.Select(group =>
{
var nodes = group.Select(entry => positionedNodes[entry.Key]).ToArray();
return new
{
Layer = group.Key,
Boundary = new LayerBoundary(
nodes.Min(node => node.X),
nodes.Max(node => node.X + node.Width),
nodes.Min(node => node.Y),
nodes.Max(node => node.Y + node.Height)),
};
})
.ToArray();
if (boundariesByLayer.Length < 2)
{
return false;
}
var requiredBoundaryDeltas = new Dictionary<int, double>();
for (var boundaryIndex = 0; boundaryIndex < boundariesByLayer.Length - 1; boundaryIndex++)
{
var current = boundariesByLayer[boundaryIndex];
var next = boundariesByLayer[boundaryIndex + 1];
var gap = next.Boundary.MinX - current.Boundary.MaxX;
if (gap <= 0d)
{
continue;
}
var verticalSegments = routedEdges
.SelectMany(edge => edge.Sections.SelectMany(section =>
{
var points = new List<ElkPoint> { section.StartPoint };
points.AddRange(section.BendPoints);
points.Add(section.EndPoint);
return points.Zip(points.Skip(1), (start, end) => new
{
Edge = edge,
Start = start,
End = end,
});
}))
.Where(segment =>
Math.Abs(segment.Start.X - segment.End.X) <= 0.01d
&& Math.Abs(segment.End.Y - segment.Start.Y) >= 36d
&& segment.Start.X > current.Boundary.MaxX + 8d
&& segment.Start.X < next.Boundary.MinX - 8d)
.ToArray();
var laneCount = verticalSegments
.Select(segment => Math.Round(segment.Start.X / 12d) * 12d)
.Distinct()
.Count();
if (laneCount == 0)
{
continue;
}
var familyCount = verticalSegments
.Select(segment => ElkEdgeChannelBands.ResolveLaneFamilyKey(segment.Edge.Label))
.Distinct(StringComparer.Ordinal)
.Count();
var desiredGap = Math.Max(
baseLayerSpacing + 88d,
136d + (laneCount * 28d) + (Math.Max(0, familyCount - 1) * 24d));
if (gap >= desiredGap)
{
continue;
}
requiredBoundaryDeltas[current.Layer] = desiredGap - gap;
}
if (requiredBoundaryDeltas.Count == 0)
{
return false;
}
foreach (var nodeId in positionedNodes.Keys.ToArray())
{
if (!layersByNodeId.TryGetValue(nodeId, out var nodeLayer))
{
continue;
}
var shiftX = requiredBoundaryDeltas
.Where(entry => nodeLayer > entry.Key)
.Sum(entry => entry.Value);
if (shiftX <= 0.01d)
{
continue;
}
var current = positionedNodes[nodeId];
positionedNodes[nodeId] = ElkLayoutHelpers.CreatePositionedNode(nodesById[nodeId], current.X + shiftX, current.Y, direction);
}
return true;
}
internal static bool CompactSparseVerticalCorridorGutters(
Dictionary<string, ElkPositionedNode> positionedNodes,
IReadOnlyCollection<ElkRoutedEdge> routedEdges,
IReadOnlyDictionary<string, int> layersByNodeId,
IReadOnlyDictionary<string, ElkNode> nodesById,
double baseLayerSpacing,
ElkLayoutDirection direction)
{
if (direction != ElkLayoutDirection.LeftToRight)
{
return false;
}
var boundariesByLayer = layersByNodeId
.Where(entry => positionedNodes.ContainsKey(entry.Key))
.GroupBy(entry => entry.Value)
.OrderBy(group => group.Key)
.Select(group =>
{
var nodes = group.Select(entry => positionedNodes[entry.Key]).ToArray();
return new
{
Layer = group.Key,
Boundary = new LayerBoundary(
nodes.Min(node => node.X),
nodes.Max(node => node.X + node.Width),
nodes.Min(node => node.Y),
nodes.Max(node => node.Y + node.Height)),
};
})
.ToArray();
if (boundariesByLayer.Length < 2)
{
return false;
}
var boundaryShifts = new Dictionary<int, double>();
for (var boundaryIndex = 0; boundaryIndex < boundariesByLayer.Length - 1; boundaryIndex++)
{
var current = boundariesByLayer[boundaryIndex];
var next = boundariesByLayer[boundaryIndex + 1];
var gap = next.Boundary.MinX - current.Boundary.MaxX;
if (gap <= 0d)
{
continue;
}
var verticalSegments = routedEdges
.SelectMany(edge => edge.Sections.SelectMany(section =>
{
var points = new List<ElkPoint> { section.StartPoint };
points.AddRange(section.BendPoints);
points.Add(section.EndPoint);
return points.Zip(points.Skip(1), (start, end) => new
{
Edge = edge,
Start = start,
End = end,
});
}))
.Where(segment =>
Math.Abs(segment.Start.X - segment.End.X) <= 0.01d
&& Math.Abs(segment.End.Y - segment.Start.Y) >= 36d
&& segment.Start.X > current.Boundary.MaxX + 8d
&& segment.Start.X < next.Boundary.MinX - 8d)
.ToArray();
var laneCount = verticalSegments
.Select(segment => Math.Round(segment.Start.X / 12d) * 12d)
.Distinct()
.Count();
var familyCount = verticalSegments
.Select(segment => ElkEdgeChannelBands.ResolveLaneFamilyKey(segment.Edge.Label))
.Distinct(StringComparer.Ordinal)
.Count();
var desiredGap = Math.Max(
baseLayerSpacing * 0.72d,
120d + (laneCount * 20d) + (Math.Max(0, familyCount - 1) * 16d));
var maxGap = desiredGap + 28d;
if (gap <= maxGap)
{
continue;
}
boundaryShifts[current.Layer] = desiredGap - gap;
}
if (boundaryShifts.Count == 0)
{
return false;
}
foreach (var nodeId in positionedNodes.Keys.ToArray())
{
if (!layersByNodeId.TryGetValue(nodeId, out var nodeLayer))
{
continue;
}
var shiftX = boundaryShifts
.Where(entry => nodeLayer > entry.Key)
.Sum(entry => entry.Value);
if (Math.Abs(shiftX) <= 0.01d)
{
continue;
}
var current = positionedNodes[nodeId];
positionedNodes[nodeId] = ElkLayoutHelpers.CreatePositionedNode(nodesById[nodeId], current.X + shiftX, current.Y, direction);
}
return true;
}
}