Files
git.stella-ops.org/src/__Libraries/StellaOps.ElkSharp/ElkShapeBoundaries.cs
2026-03-23 13:23:19 +02:00

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);
}
}