namespace OpenNest.Geometry
{
///
/// Conservative perpendicular-extent tests for slides along an exact axis direction.
/// A ray parallel to an axis can only meet an entity whose extent across that axis holds
/// the ray's perpendicular coordinate. Each test answers "may emit" whenever the matching
/// kernel could still report a hit: margins cover the kernel's own tolerances and its
/// floating-point rounding. A pair with any coordinate or radius that is NaN, infinite
/// or beyond is always kept, because the kernels' products
/// can then become NaN and still emit.
///
internal static class SlideBand
{
internal enum Axis
{
None,
/// Push along ±X; the perpendicular coordinate is Y.
PerpendicularY,
/// Push along ±Y; the perpendicular coordinate is X.
PerpendicularX,
}
///
/// Largest coordinate or radius magnitude the tests reason about. Below it every
/// square and product in the kernels stays finite (the squares stay below 1e301).
///
internal const double CoordinateLimit = 1e150;
// RayLine accepts a segment parameter in [-Eps, 1 + Eps], which reaches Eps of the
// segment's perpendicular span past either end. Twice that is kept: the computed
// parameter differs from the exact one by a few ulps, far less than the extra Eps.
// No absolute slack is needed: the kernels translate entities with the same
// expressions as these tests, and a circle point c + r·sinθ rounds monotonically, so
// it never lies outside fl(c ± r).
internal const double SpanSlack = 2 * Math.Tolerance.Epsilon;
// SolveRayCircle accepts a nonnegative discriminant b² − 4ac. For an axis ray that is
// 4(r² − oy²) up to rounding of terms of size ox² + oy² + r², so a hit is possible when
// oy² ≤ r² + ε(1 + ox² + oy² + r²). This ε is hundreds of ulps; the absolute 1 is a
// floor for squares that underflow.
internal const double QuadraticSlack = 1e-13;
public static Axis For(double dirX, double dirY)
{
if (dirY == 0 && (dirX == 1 || dirX == -1))
return Axis.PerpendicularY;
if (dirX == 0 && (dirY == 1 || dirY == -1))
return Axis.PerpendicularX;
return Axis.None;
}
///
/// False only when from (vx, vy) along the axis cannot
/// emit against translated by (dx, dy).
///
public static bool RayMayHit(
Axis axis,
double vx,
double vy,
Entity entity,
double dx,
double dy
)
{
if (!Bounded(vx) || !Bounded(vy))
return true;
switch (entity)
{
case Line line:
{
var p1x = line.pt1.X + dx;
var p1y = line.pt1.Y + dy;
var p2x = line.pt2.X + dx;
var p2y = line.pt2.Y + dy;
if (!Bounded(p1x) || !Bounded(p1y) || !Bounded(p2x) || !Bounded(p2y))
return true;
return PointMayReachSegment(
Perp(axis, vx, vy),
Perp(axis, p1x, p1y),
Perp(axis, p2x, p2y)
);
}
case Arc arc:
return PointMayReachCircle(
axis,
vx,
vy,
arc.Center.X + dx,
arc.Center.Y + dy,
arc.Radius
);
case Circle circle:
return PointMayReachCircle(
axis,
vx,
vy,
circle.Center.X + dx,
circle.Center.Y + dy,
circle.Radius
);
default:
return true;
}
}
///
/// False only when no point of the circle (cx, cy, r) can reach the segment along the
/// axis, which is what tests for each arc/line pair.
///
public static bool CircleMayReachLine(
Axis axis,
double cx,
double cy,
double r,
double p1x,
double p1y,
double p2x,
double p2y
)
{
if (
!Bounded(cx) || !Bounded(cy) || !Bounded(r)
|| !Bounded(p1x) || !Bounded(p1y) || !Bounded(p2x) || !Bounded(p2y)
)
return true;
var c = Perp(axis, cx, cy);
var a = Perp(axis, p1x, p1y);
var b = Perp(axis, p2x, p2y);
var lo = System.Math.Min(a, b);
var hi = System.Math.Max(a, b);
var radius = System.Math.Abs(r);
var margin = SpanSlack * (hi - lo);
if (c + radius < lo - margin || c - radius > hi + margin)
return false;
return true;
}
///
/// False only when two circles sliding along the axis can never touch, the condition
/// solves for both external and internal contact.
///
public static bool CirclesMayTouch(
Axis axis,
double movingCx,
double movingCy,
double movingR,
double stationaryCx,
double stationaryCy,
double stationaryR
)
{
if (
!Bounded(movingCx) || !Bounded(movingCy) || !Bounded(movingR)
|| !Bounded(stationaryCx) || !Bounded(stationaryCy) || !Bounded(stationaryR)
)
return true;
var radius = System.Math.Abs(movingR) + System.Math.Abs(stationaryR);
return CenterMayReach(
Para(axis, movingCx, movingCy) - Para(axis, stationaryCx, stationaryCy),
Perp(axis, movingCx, movingCy) - Perp(axis, stationaryCx, stationaryCy),
radius
);
}
private static bool PointMayReachSegment(double v, double a, double b)
{
var lo = System.Math.Min(a, b);
var hi = System.Math.Max(a, b);
var margin = SpanSlack * (hi - lo);
if (v < lo - margin || v > hi + margin)
return false;
return true;
}
private static bool PointMayReachCircle(
Axis axis,
double vx,
double vy,
double cx,
double cy,
double r
)
{
if (!Bounded(cx) || !Bounded(cy) || !Bounded(r))
return true;
return CenterMayReach(
Para(axis, vx, vy) - Para(axis, cx, cy),
Perp(axis, vx, vy) - Perp(axis, cx, cy),
r
);
}
private static bool CenterMayReach(double ox, double oy, double r)
{
var oy2 = oy * oy;
var r2 = r * r;
var slack = QuadraticSlack * (1 + ox * ox + oy2 + r2);
if (oy2 > r2 + slack)
return false;
return true;
}
/// False for NaN, infinities and magnitudes beyond the coordinate limit.
private static bool Bounded(double value) => System.Math.Abs(value) <= CoordinateLimit;
internal static double Perp(Axis axis, double x, double y) =>
axis == Axis.PerpendicularY ? y : x;
internal static double Para(Axis axis, double x, double y) =>
axis == Axis.PerpendicularY ? x : y;
}
}