namespace StellaOps.ElkSharp; internal static partial class ElkCompoundLayout { private static ElkRoutedEdge[] InsertCompoundBoundaryCrossings( IReadOnlyCollection routedEdges, IReadOnlyDictionary positionedNodes, ElkCompoundHierarchy hierarchy) { return routedEdges .Select(edge => { var path = ExtractPath(edge); if (path.Count < 2) { return edge; } var lcaNodeId = hierarchy.GetLowestCommonAncestor(edge.SourceNodeId, edge.TargetNodeId); var sourceAncestorIds = hierarchy.GetAncestorIdsNearestFirst(edge.SourceNodeId) .TakeWhile(ancestorNodeId => !string.Equals(ancestorNodeId, lcaNodeId, StringComparison.Ordinal)) .ToArray(); var targetAncestorIds = hierarchy.GetAncestorIdsNearestFirst(edge.TargetNodeId) .TakeWhile(ancestorNodeId => !string.Equals(ancestorNodeId, lcaNodeId, StringComparison.Ordinal)) .ToArray(); if (sourceAncestorIds.Length == 0 && targetAncestorIds.Length == 0) { return edge; } var rebuiltPath = path.ToList(); var startSegmentIndex = 0; foreach (var ancestorNodeId in sourceAncestorIds) { if (!positionedNodes.TryGetValue(ancestorNodeId, out var ancestorNode)) { continue; } if (!TryFindBoundaryTransitionFromStart(rebuiltPath, ancestorNode, startSegmentIndex, out var insertIndex, out var boundaryPoint)) { continue; } rebuiltPath.Insert(insertIndex, boundaryPoint); startSegmentIndex = insertIndex; } var endSegmentIndex = rebuiltPath.Count - 2; foreach (var ancestorNodeId in targetAncestorIds) { if (!positionedNodes.TryGetValue(ancestorNodeId, out var ancestorNode)) { continue; } if (!TryFindBoundaryTransitionFromEnd(rebuiltPath, ancestorNode, endSegmentIndex, out var insertIndex, out var boundaryPoint)) { continue; } rebuiltPath.Insert(insertIndex, boundaryPoint); endSegmentIndex = insertIndex - 2; } return BuildEdgeWithPath(edge, rebuiltPath); }) .ToArray(); } private static List ExtractPath(ElkRoutedEdge edge) { var path = new List(); foreach (var section in edge.Sections) { AppendPoint(path, section.StartPoint); foreach (var bendPoint in section.BendPoints) { AppendPoint(path, bendPoint); } AppendPoint(path, section.EndPoint); } return path; } private static ElkRoutedEdge BuildEdgeWithPath(ElkRoutedEdge edge, IReadOnlyList path) { var normalizedPath = NormalizePath(path); if (normalizedPath.Count < 2) { return edge; } return edge with { Sections = [ new ElkEdgeSection { StartPoint = normalizedPath[0], EndPoint = normalizedPath[^1], BendPoints = normalizedPath.Count > 2 ? normalizedPath.Skip(1).Take(normalizedPath.Count - 2).ToArray() : [], }, ], }; } private static bool TryFindBoundaryTransitionFromStart( IReadOnlyList path, ElkPositionedNode boundaryNode, int startSegmentIndex, out int insertIndex, out ElkPoint boundaryPoint) { insertIndex = -1; boundaryPoint = default!; for (var segmentIndex = Math.Max(0, startSegmentIndex); segmentIndex < path.Count - 1; segmentIndex++) { var from = path[segmentIndex]; var to = path[segmentIndex + 1]; if (!IsInsideOrOn(boundaryNode, from) || IsInsideOrOn(boundaryNode, to)) { continue; } if (!TryResolveBoundaryTransition(boundaryNode, from, to, exitBoundary: true, out boundaryPoint)) { continue; } insertIndex = segmentIndex + 1; return true; } return false; } private static bool TryFindBoundaryTransitionFromEnd( IReadOnlyList path, ElkPositionedNode boundaryNode, int startSegmentIndex, out int insertIndex, out ElkPoint boundaryPoint) { insertIndex = -1; boundaryPoint = default!; for (var segmentIndex = Math.Min(startSegmentIndex, path.Count - 2); segmentIndex >= 0; segmentIndex--) { var from = path[segmentIndex]; var to = path[segmentIndex + 1]; if (IsInsideOrOn(boundaryNode, from) || !IsInsideOrOn(boundaryNode, to)) { continue; } if (!TryResolveBoundaryTransition(boundaryNode, from, to, exitBoundary: false, out boundaryPoint)) { continue; } insertIndex = segmentIndex + 1; return true; } return false; } private static bool TryResolveBoundaryTransition( ElkPositionedNode boundaryNode, ElkPoint from, ElkPoint to, bool exitBoundary, out ElkPoint boundaryPoint) { boundaryPoint = default!; if (!Clip(boundaryNode, from, to, out var enterScale, out var exitScale)) { return false; } var scale = exitBoundary ? exitScale : enterScale; scale = Math.Clamp(scale, 0d, 1d); boundaryPoint = new ElkPoint { X = from.X + ((to.X - from.X) * scale), Y = from.Y + ((to.Y - from.Y) * scale), }; return true; } private static bool Clip( ElkPositionedNode node, ElkPoint from, ElkPoint to, out double enterScale, out double exitScale) { enterScale = 0d; exitScale = 1d; var deltaX = to.X - from.X; var deltaY = to.Y - from.Y; return ClipTest(-deltaX, from.X - node.X, ref enterScale, ref exitScale) && ClipTest(deltaX, (node.X + node.Width) - from.X, ref enterScale, ref exitScale) && ClipTest(-deltaY, from.Y - node.Y, ref enterScale, ref exitScale) && ClipTest(deltaY, (node.Y + node.Height) - from.Y, ref enterScale, ref exitScale); static bool ClipTest(double p, double q, ref double enter, ref double exit) { if (Math.Abs(p) <= 0.0001d) { return q >= -0.0001d; } var ratio = q / p; if (p < 0d) { if (ratio > exit) { return false; } if (ratio > enter) { enter = ratio; } } else { if (ratio < enter) { return false; } if (ratio < exit) { exit = ratio; } } return true; } } private static bool IsInsideOrOn(ElkPositionedNode node, ElkPoint point) { const double tolerance = 0.01d; return point.X >= node.X - tolerance && point.X <= node.X + node.Width + tolerance && point.Y >= node.Y - tolerance && point.Y <= node.Y + node.Height + tolerance; } private static List NormalizePath(IReadOnlyList path) { var normalized = new List(path.Count); foreach (var point in path) { AppendPoint(normalized, point); } return normalized; } private static void AppendPoint(ICollection path, ElkPoint point) { if (path.Count > 0 && path.Last() is { } previousPoint && AreSamePoint(previousPoint, point)) { return; } path.Add(new ElkPoint { X = point.X, Y = point.Y }); } private static bool AreSamePoint(ElkPoint left, ElkPoint right) => Math.Abs(left.X - right.X) <= 0.01d && Math.Abs(left.Y - right.Y) <= 0.01d; }