Improve rendering
This commit is contained in:
191
src/__Libraries/StellaOps.ElkSharp/ElkShapeBoundaries.cs
Normal file
191
src/__Libraries/StellaOps.ElkSharp/ElkShapeBoundaries.cs
Normal file
@@ -0,0 +1,191 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user