Refactor ElkSharp hybrid routing and document speed path
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
|
||||
namespace StellaOps.ElkSharp;
|
||||
|
||||
internal static partial class ElkEdgeRouterIterative
|
||||
{
|
||||
private static List<ElkPoint>? TryBuildPreferredSideShortcut(
|
||||
ElkPositionedNode sourceNode,
|
||||
ElkPositionedNode targetNode,
|
||||
IReadOnlyCollection<ElkPositionedNode> nodes,
|
||||
string sourceId,
|
||||
string targetId)
|
||||
{
|
||||
var sourceCenterX = sourceNode.X + (sourceNode.Width / 2d);
|
||||
var sourceCenterY = sourceNode.Y + (sourceNode.Height / 2d);
|
||||
var targetCenterX = targetNode.X + (targetNode.Width / 2d);
|
||||
var targetCenterY = targetNode.Y + (targetNode.Height / 2d);
|
||||
var deltaX = targetCenterX - sourceCenterX;
|
||||
var deltaY = targetCenterY - sourceCenterY;
|
||||
var absDx = Math.Abs(deltaX);
|
||||
var absDy = Math.Abs(deltaY);
|
||||
if (absDx < 16d && absDy < 16d)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var horizontalDominant = absDx >= absDy;
|
||||
var preferredSourceSide = horizontalDominant
|
||||
? deltaX >= 0d ? "right" : "left"
|
||||
: deltaY >= 0d ? "bottom" : "top";
|
||||
var preferredTargetSide = horizontalDominant
|
||||
? deltaX >= 0d ? "left" : "right"
|
||||
: deltaY >= 0d ? "top" : "bottom";
|
||||
var start = BuildPreferredBoundaryPoint(sourceNode, preferredSourceSide, targetNode);
|
||||
var end = BuildPreferredBoundaryPoint(targetNode, preferredTargetSide, sourceNode);
|
||||
return TryBuildShortestOrthogonalPath(start, end, nodes, sourceId, targetId, targetNode, 0d);
|
||||
}
|
||||
|
||||
private static ElkPoint BuildPreferredBoundaryPoint(
|
||||
ElkPositionedNode node,
|
||||
string side,
|
||||
ElkPositionedNode otherNode)
|
||||
{
|
||||
var horizontalInset = Math.Min(24d, Math.Max(12d, node.Width / 4d));
|
||||
var verticalInset = Math.Min(24d, Math.Max(12d, node.Height / 4d));
|
||||
var otherCenterX = otherNode.X + (otherNode.Width / 2d);
|
||||
var otherCenterY = otherNode.Y + (otherNode.Height / 2d);
|
||||
|
||||
var boundary = side switch
|
||||
{
|
||||
"left" => new ElkPoint
|
||||
{
|
||||
X = node.X,
|
||||
Y = Math.Clamp(otherCenterY, node.Y + verticalInset, (node.Y + node.Height) - verticalInset),
|
||||
},
|
||||
"right" => new ElkPoint
|
||||
{
|
||||
X = node.X + node.Width,
|
||||
Y = Math.Clamp(otherCenterY, node.Y + verticalInset, (node.Y + node.Height) - verticalInset),
|
||||
},
|
||||
"top" => new ElkPoint
|
||||
{
|
||||
X = Math.Clamp(otherCenterX, node.X + horizontalInset, (node.X + node.Width) - horizontalInset),
|
||||
Y = node.Y,
|
||||
},
|
||||
_ => new ElkPoint
|
||||
{
|
||||
X = Math.Clamp(otherCenterX, node.X + horizontalInset, (node.X + node.Width) - horizontalInset),
|
||||
Y = node.Y + node.Height,
|
||||
},
|
||||
};
|
||||
|
||||
if (!ElkShapeBoundaries.IsGatewayShape(node))
|
||||
{
|
||||
return boundary;
|
||||
}
|
||||
|
||||
var referencePoint = side switch
|
||||
{
|
||||
"left" => new ElkPoint { X = node.X - Math.Max(24d, node.Width / 3d), Y = boundary.Y },
|
||||
"right" => new ElkPoint { X = node.X + node.Width + Math.Max(24d, node.Width / 3d), Y = boundary.Y },
|
||||
"top" => new ElkPoint { X = boundary.X, Y = node.Y - Math.Max(24d, node.Height / 3d) },
|
||||
_ => new ElkPoint { X = boundary.X, Y = node.Y + node.Height + Math.Max(24d, node.Height / 3d) },
|
||||
};
|
||||
|
||||
var projected = ElkShapeBoundaries.ProjectOntoShapeBoundary(node, referencePoint);
|
||||
return ElkShapeBoundaries.PreferGatewayEdgeInteriorBoundary(node, projected, referencePoint);
|
||||
}
|
||||
|
||||
private static List<ElkPoint>? TryBuildShortestOrthogonalPath(
|
||||
ElkPoint start,
|
||||
ElkPoint end,
|
||||
IReadOnlyCollection<ElkPositionedNode> nodes,
|
||||
string sourceId,
|
||||
string targetId,
|
||||
ElkPositionedNode? targetNode,
|
||||
double obstaclePadding)
|
||||
{
|
||||
var rawObstacles = nodes.Select(node => (
|
||||
Left: node.X - obstaclePadding,
|
||||
Top: node.Y - obstaclePadding,
|
||||
Right: node.X + node.Width + obstaclePadding,
|
||||
Bottom: node.Y + node.Height + obstaclePadding,
|
||||
Id: node.Id)).ToArray();
|
||||
|
||||
bool SegmentIsClear(ElkPoint from, ElkPoint to) =>
|
||||
!ElkEdgePostProcessor.SegmentCrossesObstacle(from, to, rawObstacles, sourceId, targetId);
|
||||
|
||||
if (Math.Abs(start.X - end.X) < 0.5d || Math.Abs(start.Y - end.Y) < 0.5d)
|
||||
{
|
||||
return SegmentIsClear(start, end)
|
||||
? [start, end]
|
||||
: null;
|
||||
}
|
||||
|
||||
foreach (var pivot in EnumerateOrthogonalShortcutPivots(start, end, targetNode))
|
||||
{
|
||||
if (!SegmentIsClear(start, pivot) || !SegmentIsClear(pivot, end))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return NormalizePolyline([start, pivot, end]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static IEnumerable<ElkPoint> EnumerateOrthogonalShortcutPivots(
|
||||
ElkPoint start,
|
||||
ElkPoint end,
|
||||
ElkPositionedNode? targetNode)
|
||||
{
|
||||
var targetSide = targetNode is null
|
||||
? string.Empty
|
||||
: ElkEdgeRoutingGeometry.ResolveBoundarySide(end, targetNode);
|
||||
var preferred = targetSide is "left" or "right"
|
||||
? new ElkPoint { X = start.X, Y = end.Y }
|
||||
: new ElkPoint { X = end.X, Y = start.Y };
|
||||
var alternate = targetSide is "left" or "right"
|
||||
? new ElkPoint { X = end.X, Y = start.Y }
|
||||
: new ElkPoint { X = start.X, Y = end.Y };
|
||||
|
||||
yield return preferred;
|
||||
if (!ElkEdgeRoutingGeometry.PointsEqual(preferred, alternate))
|
||||
{
|
||||
yield return alternate;
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<ElkPoint> EnumerateShortestRepairEndpoints(
|
||||
ElkPoint start,
|
||||
ElkPoint currentEnd,
|
||||
ElkPositionedNode? targetNode)
|
||||
{
|
||||
var endpoints = new List<ElkPoint>();
|
||||
|
||||
void AddCandidate(ElkPoint candidate)
|
||||
{
|
||||
if (!endpoints.Any(existing => ElkEdgeRoutingGeometry.PointsEqual(existing, candidate)))
|
||||
{
|
||||
endpoints.Add(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
AddCandidate(currentEnd);
|
||||
if (targetNode is null)
|
||||
{
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
if (ElkShapeBoundaries.IsGatewayShape(targetNode))
|
||||
{
|
||||
if (ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, "left", start.Y, out var left))
|
||||
{
|
||||
AddCandidate(left);
|
||||
}
|
||||
|
||||
if (ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, "right", start.Y, out var right))
|
||||
{
|
||||
AddCandidate(right);
|
||||
}
|
||||
|
||||
if (ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, "top", start.X, out var top))
|
||||
{
|
||||
AddCandidate(top);
|
||||
}
|
||||
|
||||
if (ElkShapeBoundaries.TryProjectGatewayBoundarySlot(targetNode, "bottom", start.X, out var bottom))
|
||||
{
|
||||
AddCandidate(bottom);
|
||||
}
|
||||
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
var horizontalInset = Math.Min(24d, Math.Max(12d, targetNode.Width / 4d));
|
||||
var verticalInset = Math.Min(24d, Math.Max(12d, targetNode.Height / 4d));
|
||||
var candidateEndpoints = new[]
|
||||
{
|
||||
new ElkPoint
|
||||
{
|
||||
X = targetNode.X,
|
||||
Y = Math.Clamp(start.Y, targetNode.Y + verticalInset, (targetNode.Y + targetNode.Height) - verticalInset),
|
||||
},
|
||||
new ElkPoint
|
||||
{
|
||||
X = targetNode.X + targetNode.Width,
|
||||
Y = Math.Clamp(start.Y, targetNode.Y + verticalInset, (targetNode.Y + targetNode.Height) - verticalInset),
|
||||
},
|
||||
new ElkPoint
|
||||
{
|
||||
X = Math.Clamp(start.X, targetNode.X + horizontalInset, (targetNode.X + targetNode.Width) - horizontalInset),
|
||||
Y = targetNode.Y,
|
||||
},
|
||||
new ElkPoint
|
||||
{
|
||||
X = Math.Clamp(start.X, targetNode.X + horizontalInset, (targetNode.X + targetNode.Width) - horizontalInset),
|
||||
Y = targetNode.Y + targetNode.Height,
|
||||
},
|
||||
};
|
||||
|
||||
foreach (var candidate in candidateEndpoints)
|
||||
{
|
||||
AddCandidate(candidate);
|
||||
}
|
||||
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user