234 lines
8.3 KiB
C#
234 lines
8.3 KiB
C#
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;
|
|
}
|
|
|
|
}
|