283 lines
8.8 KiB
C#
283 lines
8.8 KiB
C#
namespace StellaOps.ElkSharp;
|
|
|
|
internal static partial class ElkCompoundLayout
|
|
{
|
|
private static ElkRoutedEdge[] InsertCompoundBoundaryCrossings(
|
|
IReadOnlyCollection<ElkRoutedEdge> routedEdges,
|
|
IReadOnlyDictionary<string, ElkPositionedNode> 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<ElkPoint> ExtractPath(ElkRoutedEdge edge)
|
|
{
|
|
var path = new List<ElkPoint>();
|
|
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<ElkPoint> 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<ElkPoint> 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<ElkPoint> 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<ElkPoint> NormalizePath(IReadOnlyList<ElkPoint> path)
|
|
{
|
|
var normalized = new List<ElkPoint>(path.Count);
|
|
foreach (var point in path)
|
|
{
|
|
AppendPoint(normalized, point);
|
|
}
|
|
|
|
return normalized;
|
|
}
|
|
|
|
private static void AppendPoint(ICollection<ElkPoint> 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;
|
|
}
|