namespace StellaOps.ElkSharp; internal static class ElkEdgeChannelGutters { internal static bool ExpandVerticalCorridorGutters( Dictionary positionedNodes, IReadOnlyCollection routedEdges, IReadOnlyDictionary layersByNodeId, IReadOnlyDictionary 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(); 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 { 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 positionedNodes, IReadOnlyCollection routedEdges, IReadOnlyDictionary layersByNodeId, IReadOnlyDictionary 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(); 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 { 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; } }