244 lines
7.7 KiB
C#
244 lines
7.7 KiB
C#
namespace StellaOps.ElkSharp;
|
|
|
|
internal static partial class ElkShapeBoundaries
|
|
{
|
|
internal static bool TryProjectGatewayDiagonalBoundary(
|
|
ElkPositionedNode node,
|
|
ElkPoint anchor,
|
|
ElkPoint fallbackBoundary,
|
|
out ElkPoint boundaryPoint)
|
|
{
|
|
boundaryPoint = default!;
|
|
if (!IsGatewayShape(node))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var candidates = new List<ElkPoint>();
|
|
var projectedAnchor = ProjectOntoShapeBoundary(node, anchor);
|
|
AddGatewayCandidate(node, candidates, projectedAnchor);
|
|
AddGatewayCandidate(node, candidates, fallbackBoundary);
|
|
AddGatewayCandidate(node, candidates, ProjectOntoShapeBoundary(node, fallbackBoundary));
|
|
|
|
foreach (var vertex in BuildGatewayBoundaryPoints(node))
|
|
{
|
|
AddGatewayCandidate(node, candidates, vertex);
|
|
}
|
|
|
|
var centerX = node.X + (node.Width / 2d);
|
|
var centerY = node.Y + (node.Height / 2d);
|
|
var directionX = Math.Sign(centerX - anchor.X);
|
|
var directionY = Math.Sign(centerY - anchor.Y);
|
|
var diagonalDirections = new HashSet<(int X, int Y)>();
|
|
if (directionX != 0 && directionY != 0)
|
|
{
|
|
diagonalDirections.Add((directionX, directionY));
|
|
}
|
|
|
|
var fallbackDirectionX = Math.Sign(fallbackBoundary.X - anchor.X);
|
|
var fallbackDirectionY = Math.Sign(fallbackBoundary.Y - anchor.Y);
|
|
if (fallbackDirectionX != 0 && fallbackDirectionY != 0)
|
|
{
|
|
diagonalDirections.Add((fallbackDirectionX, fallbackDirectionY));
|
|
}
|
|
|
|
foreach (var diagonalDirection in diagonalDirections)
|
|
{
|
|
if (TryIntersectGatewayRay(
|
|
node,
|
|
anchor.X,
|
|
anchor.Y,
|
|
diagonalDirection.X,
|
|
diagonalDirection.Y,
|
|
out var rayBoundary))
|
|
{
|
|
AddGatewayCandidate(node, candidates, rayBoundary);
|
|
}
|
|
}
|
|
|
|
var bestCandidate = default(ElkPoint?);
|
|
var bestScore = double.PositiveInfinity;
|
|
foreach (var candidate in candidates)
|
|
{
|
|
var score = ScoreGatewayBoundaryCandidate(node, anchor, projectedAnchor, candidate);
|
|
if (score >= bestScore)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
bestScore = score;
|
|
bestCandidate = candidate;
|
|
}
|
|
|
|
if (bestCandidate is null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
boundaryPoint = PreferGatewayEdgeInteriorBoundary(node, bestCandidate, anchor);
|
|
return true;
|
|
}
|
|
|
|
internal static ElkPoint IntersectPolygonBoundary(
|
|
double originX,
|
|
double originY,
|
|
double deltaX,
|
|
double deltaY,
|
|
IReadOnlyList<ElkPoint> polygon)
|
|
{
|
|
var bestScale = double.PositiveInfinity;
|
|
ElkPoint? bestPoint = null;
|
|
for (var index = 0; index < polygon.Count; index++)
|
|
{
|
|
var start = polygon[index];
|
|
var end = polygon[(index + 1) % polygon.Count];
|
|
if (!TryIntersectRayWithSegment(originX, originY, deltaX, deltaY, start, end, out var scale, out var point))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (scale < bestScale)
|
|
{
|
|
bestScale = scale;
|
|
bestPoint = point;
|
|
}
|
|
}
|
|
|
|
return bestPoint ?? new ElkPoint
|
|
{
|
|
X = originX + deltaX,
|
|
Y = originY + deltaY,
|
|
};
|
|
}
|
|
|
|
internal static bool TryIntersectRayWithSegment(
|
|
double originX,
|
|
double originY,
|
|
double deltaX,
|
|
double deltaY,
|
|
ElkPoint segmentStart,
|
|
ElkPoint segmentEnd,
|
|
out double scale,
|
|
out ElkPoint point)
|
|
{
|
|
scale = double.PositiveInfinity;
|
|
point = default!;
|
|
|
|
var segmentDeltaX = segmentEnd.X - segmentStart.X;
|
|
var segmentDeltaY = segmentEnd.Y - segmentStart.Y;
|
|
var denominator = Cross(deltaX, deltaY, segmentDeltaX, segmentDeltaY);
|
|
if (Math.Abs(denominator) <= 0.001d)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var relativeX = segmentStart.X - originX;
|
|
var relativeY = segmentStart.Y - originY;
|
|
var rayScale = Cross(relativeX, relativeY, segmentDeltaX, segmentDeltaY) / denominator;
|
|
var segmentScale = Cross(relativeX, relativeY, deltaX, deltaY) / denominator;
|
|
if (rayScale < 0d || segmentScale < 0d || segmentScale > 1d)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
scale = rayScale;
|
|
point = new ElkPoint
|
|
{
|
|
X = originX + (deltaX * rayScale),
|
|
Y = originY + (deltaY * rayScale),
|
|
};
|
|
return true;
|
|
}
|
|
|
|
internal static double Cross(double ax, double ay, double bx, double by)
|
|
{
|
|
return (ax * by) - (ay * bx);
|
|
}
|
|
|
|
private static bool TryIntersectGatewayRay(
|
|
ElkPositionedNode node,
|
|
double originX,
|
|
double originY,
|
|
double deltaX,
|
|
double deltaY,
|
|
out ElkPoint boundaryPoint)
|
|
{
|
|
var polygon = BuildGatewayBoundaryPoints(node);
|
|
var bestScale = double.PositiveInfinity;
|
|
ElkPoint? bestPoint = null;
|
|
for (var index = 0; index < polygon.Count; index++)
|
|
{
|
|
var start = polygon[index];
|
|
var end = polygon[(index + 1) % polygon.Count];
|
|
if (!TryIntersectRayWithSegment(originX, originY, deltaX, deltaY, start, end, out var scale, out var point))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (scale < bestScale)
|
|
{
|
|
bestScale = scale;
|
|
bestPoint = point;
|
|
}
|
|
}
|
|
|
|
boundaryPoint = bestPoint ?? default!;
|
|
return bestPoint is not null;
|
|
}
|
|
|
|
private static void AddGatewayCandidate(
|
|
ElkPositionedNode node,
|
|
ICollection<ElkPoint> candidates,
|
|
ElkPoint candidate)
|
|
{
|
|
if (!IsPointOnGatewayBoundary(node, candidate, 2d))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (candidates.Any(existing =>
|
|
Math.Abs(existing.X - candidate.X) <= CoordinateTolerance
|
|
&& Math.Abs(existing.Y - candidate.Y) <= CoordinateTolerance))
|
|
{
|
|
return;
|
|
}
|
|
|
|
candidates.Add(candidate);
|
|
}
|
|
|
|
private static double ScoreGatewayBoundaryCandidate(
|
|
ElkPositionedNode node,
|
|
ElkPoint anchor,
|
|
ElkPoint projectedAnchor,
|
|
ElkPoint candidate)
|
|
{
|
|
var towardCenterX = (node.X + (node.Width / 2d)) - anchor.X;
|
|
var towardCenterY = (node.Y + (node.Height / 2d)) - anchor.Y;
|
|
var candidateDeltaX = candidate.X - anchor.X;
|
|
var candidateDeltaY = candidate.Y - anchor.Y;
|
|
var towardDot = (candidateDeltaX * towardCenterX) + (candidateDeltaY * towardCenterY);
|
|
if (towardDot <= 0d)
|
|
{
|
|
return double.PositiveInfinity;
|
|
}
|
|
|
|
var absDx = Math.Abs(candidateDeltaX);
|
|
var absDy = Math.Abs(candidateDeltaY);
|
|
var isDiagonal = absDx >= 3d && absDy >= 3d;
|
|
var diagonalPenalty = isDiagonal
|
|
? Math.Abs(absDx - absDy)
|
|
: 10_000d;
|
|
var projectedDistance = Math.Abs(candidate.X - projectedAnchor.X) + Math.Abs(candidate.Y - projectedAnchor.Y);
|
|
var segmentLength = Math.Sqrt((candidateDeltaX * candidateDeltaX) + (candidateDeltaY * candidateDeltaY));
|
|
var candidateNearVertex = IsNearGatewayVertex(node, candidate, GatewayVertexTolerance);
|
|
var projectedNearVertex = IsNearGatewayVertex(node, projectedAnchor, GatewayVertexTolerance);
|
|
var vertexPenalty = candidateNearVertex
|
|
? projectedNearVertex
|
|
? 4d
|
|
: 24d
|
|
: 0d;
|
|
|
|
return diagonalPenalty + (segmentLength * 0.05d) + (projectedDistance * 0.1d) + vertexPenalty;
|
|
}
|
|
}
|