using OpenNest.Geometry;
using OpenNest.Math;
namespace OpenNest.Tests.Geometry;
///
/// The axis band filter in must skip only pairs whose kernels
/// cannot emit: the emitted event sequence (order, distances and contact points, bit for bit)
/// stays identical to the frozen pre-filter enumeration in .
///
public class SlideBandTests
{
private static readonly (double X, double Y)[] AxisDirections = { (1, 0), (-1, 0), (0, 1), (0, -1) };
[Theory]
[InlineData(1.0, 3, 12)]
[InlineData(250.0, 3, 12)]
[InlineData(100000.0, 3, 12)]
[InlineData(30000000.0, 3, 12)]
[InlineData(1.0, 24, 160)]
[InlineData(250.0, 24, 160)]
[InlineData(100000.0, 24, 160)]
[InlineData(30000000.0, 24, 160)]
public void RandomShapes_EmitTheSameEventsAsTheUnfilteredEnumeration(double scale, int minSides, int maxSides)
{
// Boundaries of 24 or more entities go through the bucket index; smaller ones scan.
var random = new Random(20261031 + (int)scale + maxSides);
var compared = 0;
var nonEmpty = 0;
var trials = maxSides > SlideBandIndex.MinimumEntities ? 25 : 150;
for (var trial = 0; trial < trials; trial++)
{
var stationary = RandomBoundary(random, scale, minSides, maxSides);
var moving = RandomBoundary(random, scale, minSides, maxSides);
foreach (var (dirX, dirY) in AxisDirections)
{
foreach (var (dx, dy) in Offsets(random, moving, stationary, dirX, dirY, scale))
{
var events = CompareEvents(moving, stationary, dx, dy, dirX, dirY);
compared++;
if (events > 0)
nonEmpty++;
}
}
}
Assert.True(nonEmpty > compared / 4, $"only {nonEmpty} of {compared} slides emitted events");
}
[Theory]
[InlineData(0.5, false)]
[InlineData(-0.5, false)]
[InlineData(0.99, false)]
[InlineData(0.5, true)]
[InlineData(-0.99, true)]
public void VertexJustPastASegmentEnd_WithinTheRayTolerance_StillEmits(
double fractionOfTolerance,
bool indexed
)
{
// RayLine accepts a segment parameter up to Eps past either end, so a ray that misses
// the segment's extent by less than Eps of its span still reports a hit.
var span = 1000.0;
var stationary = new List { new Line(new Vector(10, 0), new Vector(10, span)) };
var y = fractionOfTolerance > 0
? span + fractionOfTolerance * Tolerance.Epsilon * span
: fractionOfTolerance * Tolerance.Epsilon * span;
var moving = new List { new Line(new Vector(0, y), new Vector(-5, y)) };
var events = CompareEvents(Pad(moving, indexed), Pad(stationary, indexed), 0, 0, 1, 0);
Assert.True(events > 0, "the legacy kernel must report this tolerance hit");
}
[Theory]
[InlineData(1e-9, false, false)]
[InlineData(1e-3, false, false)]
[InlineData(5.0, false, false)]
[InlineData(1e-9, true, false)]
[InlineData(5.0, true, false)]
[InlineData(5.0, true, true)]
[InlineData(1e-9, true, true)]
public void RayGrazingAFarCircle_KeepsEveryRoundingHit(double radius, bool indexed, bool offset)
{
// Far along the ray the discriminant cancels catastrophically; sweep the band around
// the circle's edge and require identical events, including any rounding hits. With
// `offset` the distance comes from the moving translation rather than the geometry.
var center = offset ? 0.0 : 90000.0;
var dx = offset ? -90000.0 : 0.0;
var stationary = Pad(new List { new Circle(new Vector(center, 37), radius) }, indexed);
var hits = 0;
for (var k = -400; k <= 400; k++)
{
// Tiny radii widen the rounding band to about √(ulp(ox²)); sweep it coarsely.
var step = radius < 1e-6 && offset ? 1e-6 : 1e-9 * System.Math.Max(1, radius);
var y = 37 + radius + k * step;
var moving = Pad(new List { new Line(new Vector(0, y), new Vector(-1, y)) }, indexed);
hits += CompareEvents(moving, stationary, dx, 0, 1, 0) > 0 ? 1 : 0;
}
Assert.True(hits > 0, "the sweep must include hits");
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void TangentCurvesAcrossTheBandEdge_EmitTheSameEvents(bool indexed)
{
var arc = new Arc(new Vector(0, 0), 3, 0, Angle.TwoPI * 0.75, false);
var stationary = Pad(new List { arc }, indexed);
var emitted = 0;
for (var k = -200; k <= 200; k++)
{
var dy = 3 + 2 + k * 1e-10;
var moving = Pad(new List { new Circle(new Vector(-50, 0), 2) }, indexed);
emitted += CompareEvents(moving, stationary, 0, dy, 1, 0) > 0 ? 1 : 0;
}
Assert.True(emitted > 0, "the sweep must include tangent contacts");
}
[Theory]
[InlineData(0.0, false)]
[InlineData(2e-5, false)]
[InlineData(0.0, true)]
[InlineData(2e-5, true)]
public void ArcAgainstANearlyParallelLine_AtTheBandEdge_EmitsTheSameEvents(double end, bool indexed)
{
// ArcToLine samples the circle where its tangent is parallel to the line. For a line
// nearly parallel to the push that point is the circle's top or bottom, so sweeping
// the circle across either end of the line's tiny perpendicular span walks the
// arc-to-line band edge, including RayLine's Eps·span tolerance past the end.
var stationary = Pad(new List { new Line(new Vector(0, 0), new Vector(1000, 2e-5)) }, indexed);
var radius = 5.0;
var hits = 0;
for (var k = -60; k <= 60; k++)
{
foreach (var side in new[] { -1.0, 1.0 })
{
// side -1 puts the circle above the line (bottom point), +1 below (top point).
var cy = end - side * radius + k * 1e-11;
var moving = Pad(new List { new Circle(new Vector(-50, cy), radius) }, indexed);
hits += CompareEvents(moving, stationary, 0, 0, 1, 0, vertices: false) > 0 ? 1 : 0;
}
}
Assert.True(hits > 0, "the sweep must include arc-to-line hits");
}
public static IEnumerable