Files
OpenNest/OpenNest.Tests/Geometry/SlideContactTests.cs
T

242 lines
10 KiB
C#

using OpenNest.Engine.BestFit;
using OpenNest.Geometry;
namespace OpenNest.Tests.Geometry;
public class SlideContactTests
{
public static IEnumerable<object[]> LinePaths()
{
foreach (var path in new[] { "axis", "offset", "edges", "vector", "entities", "cpu-lines", "cpu-entities" })
foreach (var reverse in new[] { false, true })
yield return new object[] { path, reverse };
}
[Theory]
[MemberData(nameof(LinePaths))]
public void TouchingRectangles_LeaveOrSlideButCannotEnter(string path, bool reverse)
{
var stationary = Rect(0, 0, 2, 2);
var moving = Rect(2, 0, 2, 2);
if (reverse)
{
Reverse(stationary);
Reverse(moving);
}
Assert.Equal(0, Distance(path, moving, stationary, PushDirection.Left));
Assert.Equal(double.MaxValue, Distance(path, moving, stationary, PushDirection.Right));
Assert.Equal(double.MaxValue, Distance(path, moving, stationary, PushDirection.Up));
Assert.Equal(double.MaxValue, Distance(path, moving, stationary, PushDirection.Down));
}
[Theory]
[MemberData(nameof(LinePaths))]
public void SlidingContact_StillStopsAtLaterHookOnSameObstacle(string path, bool reverse)
{
var stationary = Loop((0, 0), (2, 0), (2, 4), (5, 4), (5, 6), (0, 6));
var moving = Rect(2, 0, 1, 1);
if (reverse)
{
Reverse(stationary);
Reverse(moving);
}
Assert.Equal(3, Distance(path, moving, stationary, PushDirection.Up), 9);
}
[Theory]
[MemberData(nameof(LinePaths))]
public void HoleContact_LeavingWallStillStopsAtOppositeWall(string path, bool reverse)
{
var stationary = Rect(0, 0, 10, 10);
stationary.AddRange(Rect(2, 2, 6, 6)); // depth, not winding, defines the hole
var moving = Rect(2, 3, 1, 1);
if (reverse)
{
Reverse(stationary);
Reverse(moving);
}
Assert.Equal(0, Distance(path, moving, stationary, PushDirection.Left));
Assert.Equal(5, Distance(path, moving, stationary, PushDirection.Right), 9);
Assert.Equal(4, Distance(path, moving, stationary, PushDirection.Up), 9);
}
[Theory]
[InlineData(false, 0)]
[InlineData(true, 0)]
[InlineData(false, 0.37)]
[InlineData(true, 0.37)]
public void RotatedHook_StopsAtFirstBlockingContact(bool cpu, double angle)
{
var stationary = Loop((0, 0), (2, 0), (2, 4), (5, 4), (5, 6), (0, 6)).Cast<Entity>().ToList();
var moving = Rect(2, 0, 1, 1).Cast<Entity>().ToList();
foreach (var entity in stationary.Concat(moving))
{
entity.Rotate(angle);
entity.Offset(17, -23);
}
Assert.Equal(3, EntityDistance(cpu, moving, stationary, new Vector(0, 1).Rotate(angle)), 8);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void Circles_TangentEscapeAndEnteringContact(bool cpu)
{
var moving = new List<Entity> { new Circle(2, 0, 1) };
var stationary = new List<Entity> { new Circle(0, 0, 1) };
Assert.Equal(0, EntityDistance(cpu, moving, stationary, new Vector(-1, 0)));
Assert.Equal(double.MaxValue, EntityDistance(cpu, moving, stationary, new Vector(1, 0)));
Assert.Equal(double.MaxValue, EntityDistance(cpu, moving, stationary, new Vector(0, 1)));
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void CircleInsideHole_TangentBlocksButDepartureFindsFarSide(bool cpu)
{
var moving = new List<Entity> { new Circle(3, 0, 1) };
var stationary = new List<Entity> { new Circle(0, 0, 6), new Circle(0, 0, 4) };
Assert.Equal(0, EntityDistance(cpu, moving, stationary, new Vector(1, 0)));
Assert.Equal(0, EntityDistance(cpu, moving, stationary, new Vector(0, 1)));
Assert.Equal(6, EntityDistance(cpu, moving, stationary, new Vector(-1, 0)), 9);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void PositiveGrazingContact_DoesNotHideLaterCircle(bool cpu)
{
var moving = new List<Entity> { new Circle(0, 0, 1) };
var stationary = new List<Entity> { new Circle(4, 2, 1), new Circle(10, 0, 1) };
Assert.Equal(8, EntityDistance(cpu, moving, stationary, new Vector(1, 0)), 9);
}
[Fact]
public void ReusedEdgeArrays_KeepTopologyAfterPreviousQuerySortedThem()
{
var moving = Rect(2, 0, 2, 2).Select(l => (l.StartPoint, l.EndPoint)).ToArray();
var stationary = Rect(0, 0, 2, 2).Select(l => (l.StartPoint, l.EndPoint)).ToArray();
Assert.Equal(0, SpatialQuery.DirectionalDistance(moving, Vector.Zero, stationary, Vector.Zero, PushDirection.Left));
Assert.Equal(double.MaxValue, SpatialQuery.DirectionalDistance(moving, Vector.Zero, stationary, Vector.Zero, PushDirection.Right));
Assert.Equal(double.MaxValue, SpatialQuery.DirectionalDistance(moving, Vector.Zero, stationary, Vector.Zero, PushDirection.Up));
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void FullCircleArcSeam_AllowsSeparation(bool cpu)
{
var moving = new List<Entity> { new Arc(0, 0, 1, 0, 2 * System.Math.PI) };
var stationary = new List<Entity> { new Circle(2, 0, 1) };
Assert.Equal(double.MaxValue, EntityDistance(cpu, moving, stationary, new Vector(-1, 0)));
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void ConcaveCorner_DoesNotBlockSlidingAlongItsStraightSide(bool cpu)
{
var moving = Rect(3, -1, 2, 1).Cast<Entity>().ToList();
var stationary = new List<Entity>
{
new Arc(0, 0, 5, System.Math.PI / 2, 0, true),
new Line(5, 0, 10, 0),
new Arc(0, 0, 10, 0, System.Math.PI / 2),
new Line(0, 10, 0, 5),
};
Assert.Equal(double.MaxValue, EntityDistance(cpu, moving, stationary, new Vector(1, 0)));
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void ThinRing_ArcSeamDoesNotChangeHoleClassification(bool cpu)
{
var a = System.Math.PI / 72;
var moving = new List<Entity> { new Circle(8.995, 0, 1) };
var stationary = new List<Entity>
{
new Circle(0, 0, 10),
new Arc(0, 0, 9.995, a, a + System.Math.PI),
new Arc(0, 0, 9.995, a + System.Math.PI, a + 2 * System.Math.PI),
};
Assert.Equal(17.99, EntityDistance(cpu, moving, stationary, new Vector(-1, 0)), 9);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void CircleAgainstInclinedWall_UsesInteriorCurveContact(bool cpu)
{
var moving = new List<Entity> { new Circle(0, 0, 1) };
var stationary = Loop((-10, 0), (10, 10), (10, 12), (-10, 2)).Cast<Entity>().ToList();
Assert.Equal(5 - System.Math.Sqrt(1.25), EntityDistance(cpu, moving, stationary, new Vector(0, 1)), 9);
}
[Fact]
public void GpuAdapter_ArbitraryDirectionUsesExactCpuFallback()
{
using var axesOnly = new RejectSlideComputer();
var distance = new GpuDistanceComputer(axesOnly).ComputeDistances(Rect(10, 10, 2, 2), Rect(0, 0, 2, 2),
new[] { new SlideOffset(0, 0, 0.6, 0.8) })[0];
Assert.Equal(8 / 0.6, distance, 9);
}
private sealed class RejectSlideComputer : ISlideComputer
{
public double[] ComputeBatch(double[] s, int sc, double[] m, int mc, double[] o, int oc, PushDirection d) =>
throw new InvalidOperationException("Non-cardinal direction reached axis-only GPU");
public double[] ComputeBatchMultiDir(double[] s, int sc, double[] m, int mc, double[] o, int oc, int[] d) =>
throw new InvalidOperationException("Non-cardinal direction reached axis-only GPU");
public void Dispose() { }
}
[Fact]
public void OpenBoundaries_RemainConservative()
{
var moving = new List<Line> { new Line(2, 0, 2, 2) };
var stationary = new List<Line> { new Line(2, 0, 2, 2) };
Assert.Equal(0, SpatialQuery.DirectionalDistance(moving, stationary, PushDirection.Right));
}
private static double EntityDistance(bool cpu, List<Entity> moving, List<Entity> stationary, Vector direction) =>
cpu ? new CpuDistanceComputer().ComputeDistances(stationary, moving,
new[] { new SlideOffset(0, 0, direction.X, direction.Y) })[0]
: SpatialQuery.DirectionalDistance(moving, stationary, direction);
private static double Distance(string path, List<Line> moving, List<Line> stationary, PushDirection direction)
{
var unit = SpatialQuery.DirectionToOffset(direction, 1);
// A nonzero template origin catches mixed local/world contact coordinates.
var origin = new Vector(13, -7);
var local = moving.Select(l => new Line(l.StartPoint - origin, l.EndPoint - origin)).ToList();
return path switch
{
"axis" => SpatialQuery.DirectionalDistance(moving, stationary, direction),
"offset" => SpatialQuery.DirectionalDistance(local, origin.X, origin.Y, stationary, direction),
"edges" => SpatialQuery.DirectionalDistance(local.Select(l => (l.StartPoint, l.EndPoint)).ToArray(), origin,
stationary.Select(l => (l.StartPoint, l.EndPoint)).ToArray(), Vector.Zero, direction),
"vector" => SpatialQuery.DirectionalDistance(moving, stationary, unit),
"entities" => SpatialQuery.DirectionalDistance(moving.Cast<Entity>().ToList(), stationary.Cast<Entity>().ToList(), unit),
"cpu-lines" => new CpuDistanceComputer().ComputeDistances(stationary, local,
new[] { new SlideOffset(origin.X, origin.Y, unit.X, unit.Y) })[0],
"cpu-entities" => new CpuDistanceComputer().ComputeDistances(stationary.Cast<Entity>().ToList(), local.Cast<Entity>().ToList(),
new[] { new SlideOffset(origin.X, origin.Y, unit.X, unit.Y) })[0],
_ => throw new ArgumentOutOfRangeException(nameof(path)),
};
}
private static void Reverse(List<Line> lines)
{
lines.Reverse();
foreach (var line in lines)
line.Reverse();
}
private static List<Line> Rect(double x, double y, double w, double h) =>
Loop((x, y), (x + w, y), (x + w, y + h), (x, y + h));
private static List<Line> Loop(params (double X, double Y)[] points) =>
points.Select((p, i) => new Line(p.X, p.Y, points[(i + 1) % points.Length].X, points[(i + 1) % points.Length].Y)).ToList();
}