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