192 lines
6.2 KiB
C#
192 lines
6.2 KiB
C#
namespace StellaOps.ElkSharp;
|
|
|
|
internal static class ElkShapeBoundaries
|
|
{
|
|
internal static ElkPoint ProjectOntoShapeBoundary(ElkPositionedNode node, ElkPoint toward)
|
|
{
|
|
if (node.Kind is "Decision" or "Fork" or "Join")
|
|
{
|
|
var cx = node.X + node.Width / 2d;
|
|
var cy = node.Y + node.Height / 2d;
|
|
var dx = toward.X - cx;
|
|
var dy = toward.Y - cy;
|
|
return ResolveGatewayBoundaryPoint(node, toward, dx, dy);
|
|
}
|
|
|
|
return ProjectOntoRectBoundary(node, toward);
|
|
}
|
|
|
|
internal static ElkPoint ProjectOntoRectBoundary(ElkPositionedNode node, ElkPoint toward)
|
|
{
|
|
var cx = node.X + node.Width / 2d;
|
|
var cy = node.Y + node.Height / 2d;
|
|
var hw = node.Width / 2d;
|
|
var hh = node.Height / 2d;
|
|
var dx = toward.X - cx;
|
|
var dy = toward.Y - cy;
|
|
|
|
if (Math.Abs(dx) < 0.1d && Math.Abs(dy) < 0.1d)
|
|
{
|
|
return new ElkPoint { X = cx + hw, Y = cy };
|
|
}
|
|
|
|
var tMin = double.MaxValue;
|
|
if (dx > 0.1d) { var t = hw / dx; if (Math.Abs(dy * t) <= hh + 0.1d && t < tMin) tMin = t; }
|
|
if (dx < -0.1d) { var t = -hw / dx; if (Math.Abs(dy * t) <= hh + 0.1d && t < tMin) tMin = t; }
|
|
if (dy > 0.1d) { var t = hh / dy; if (Math.Abs(dx * t) <= hw + 0.1d && t < tMin) tMin = t; }
|
|
if (dy < -0.1d) { var t = -hh / dy; if (Math.Abs(dx * t) <= hw + 0.1d && t < tMin) tMin = t; }
|
|
|
|
return tMin < double.MaxValue
|
|
? new ElkPoint { X = cx + dx * tMin, Y = cy + dy * tMin }
|
|
: new ElkPoint { X = cx + hw, Y = cy };
|
|
}
|
|
|
|
internal static ElkPoint IntersectDiamondBoundary(
|
|
double centerX,
|
|
double centerY,
|
|
double halfWidth,
|
|
double halfHeight,
|
|
double deltaX,
|
|
double deltaY)
|
|
{
|
|
if (Math.Abs(deltaX) < 0.001d && Math.Abs(deltaY) < 0.001d)
|
|
{
|
|
return new ElkPoint
|
|
{
|
|
X = centerX,
|
|
Y = centerY,
|
|
};
|
|
}
|
|
|
|
var scale = 1d / ((Math.Abs(deltaX) / Math.Max(halfWidth, 0.001d)) + (Math.Abs(deltaY) / Math.Max(halfHeight, 0.001d)));
|
|
return new ElkPoint
|
|
{
|
|
X = centerX + (deltaX * scale),
|
|
Y = centerY + (deltaY * scale),
|
|
};
|
|
}
|
|
|
|
internal static ElkPoint ResolveGatewayBoundaryPoint(
|
|
ElkPositionedNode node,
|
|
ElkPoint candidate,
|
|
double deltaX,
|
|
double deltaY)
|
|
{
|
|
if (node.Kind is not ("Decision" or "Fork" or "Join"))
|
|
{
|
|
return candidate;
|
|
}
|
|
|
|
var centerX = node.X + (node.Width / 2d);
|
|
var centerY = node.Y + (node.Height / 2d);
|
|
if (Math.Abs(deltaX) < 0.001d && Math.Abs(deltaY) < 0.001d)
|
|
{
|
|
deltaX = candidate.X - centerX;
|
|
deltaY = candidate.Y - centerY;
|
|
}
|
|
|
|
if (node.Kind == "Decision")
|
|
{
|
|
return IntersectDiamondBoundary(centerX, centerY, node.Width / 2d, node.Height / 2d, deltaX, deltaY);
|
|
}
|
|
|
|
return IntersectPolygonBoundary(
|
|
centerX,
|
|
centerY,
|
|
deltaX,
|
|
deltaY,
|
|
BuildForkBoundaryPoints(node));
|
|
}
|
|
|
|
internal static IReadOnlyList<ElkPoint> BuildForkBoundaryPoints(ElkPositionedNode node)
|
|
{
|
|
var cornerInset = Math.Min(22d, Math.Max(6d, node.Width * 0.125d));
|
|
var verticalInset = Math.Min(8d, Math.Max(4d, node.Height * 0.065d));
|
|
return
|
|
[
|
|
new ElkPoint { X = node.X + cornerInset, Y = node.Y + verticalInset },
|
|
new ElkPoint { X = node.X + node.Width - cornerInset, Y = node.Y + verticalInset },
|
|
new ElkPoint { X = node.X + node.Width, Y = node.Y + (node.Height / 2d) },
|
|
new ElkPoint { X = node.X + node.Width - cornerInset, Y = node.Y + node.Height - verticalInset },
|
|
new ElkPoint { X = node.X + cornerInset, Y = node.Y + node.Height - verticalInset },
|
|
new ElkPoint { X = node.X, Y = node.Y + (node.Height / 2d) },
|
|
];
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|