test(geometry): cover Collision with ClipperBridge inputs; document GPU contract
Collision stays hand-rolled because it is the reference for a future GPU kernel, but its inputs now come from Clipper region offsets. Pin down that lines-only, round-join, 1e-4-precision polygons keep the contact and part-in-part semantics: a neighbor inside a collapsed slot, a part inside a hole that shrank by the spacing, and zero-spacing edge contact. Document which steps are per-polygon preparation to cache and upload once, and which are per-pair kernel-shaped work. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,22 @@ using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Geometry
|
||||
{
|
||||
/// <summary>
|
||||
/// Polygon overlap test with hole subtraction. This is the reference implementation
|
||||
/// for a future GPU kernel, so it deliberately stays hand-rolled instead of using
|
||||
/// Clipper (which is CPU-only and allocation-heavy; see <see cref="ClipperBridge"/>
|
||||
/// for the CPU preparation that feeds it).
|
||||
/// <para>
|
||||
/// GPU-port contract. Per-polygon preparation, done once per drawing and rotation,
|
||||
/// then cached and uploaded: the spacing offset (<see cref="ClipperBridge"/>),
|
||||
/// triangulation (<see cref="ConvexDecomposition.Triangulate"/>) of the outline and
|
||||
/// each hole, and the bounding box of every polygon and triangle. Per-pair work,
|
||||
/// kernel-shaped (fixed-size, loop-only, no recursion): the bounding-box rejects,
|
||||
/// Sutherland-Hodgman clipping of convex triangle pairs (<c>ClipConvex</c>), and
|
||||
/// subtraction of hole triangles from the clipped regions (<c>SubtractTriangles</c>).
|
||||
/// Inputs are closed, lines-only polygons; winding is normalized by triangulation.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class Collision
|
||||
{
|
||||
public static CollisionResult Check(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
@@ -204,6 +205,84 @@ public class CollisionTests
|
||||
Assert.False(Collision.HasAnyOverlap(new List<Polygon>()));
|
||||
}
|
||||
|
||||
// The cases below feed Collision with ClipperBridge offsets, the way the spacing
|
||||
// checks prepare their inputs: lines only, round joins, 1e-4 precision.
|
||||
|
||||
[Theory]
|
||||
[InlineData(4.9, 5.1, true)] // Inside the collapsed slot: 0.05 from its walls.
|
||||
[InlineData(10.3, 12, false)] // Beside the part, 0.3 away.
|
||||
public void HasOverlap_NeighborOfPartWithCollapsedSlot(
|
||||
double left,
|
||||
double right,
|
||||
bool expected
|
||||
)
|
||||
{
|
||||
// 10x10 part with a 0.3-wide slot down from the top, inflated by 0.25.
|
||||
var part = MakeProfile(
|
||||
MakePolygon((0, 0), (10, 0), (10, 10), (5.15, 10), (5.15, 7), (4.85, 7), (4.85, 10), (0, 10))
|
||||
);
|
||||
var inflated = ClipperBridge.Offset(part, 0.25, 0.001);
|
||||
var neighbor = MakeSquare(left, 8, right, 10);
|
||||
|
||||
Assert.Equal(
|
||||
expected,
|
||||
Collision.HasOverlap(inflated.LargestOuter(), neighbor, inflated.Holes)
|
||||
);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(5.5, 14.5, false)] // 0.5 from the hole's edges.
|
||||
[InlineData(5.1, 14.9, true)] // 0.1 from the hole's edges.
|
||||
public void HasOverlap_PartInsideHoleShrunkBySpacing(double min, double max, bool expected)
|
||||
{
|
||||
var part = MakeProfile(
|
||||
MakePolygon((0, 0), (20, 0), (20, 20), (0, 20)),
|
||||
MakePolygon((5, 5), (15, 5), (15, 15), (5, 15))
|
||||
);
|
||||
var inflated = ClipperBridge.Offset(part, 0.25, 0.001);
|
||||
var inner = MakeSquare(min, min, max, max);
|
||||
|
||||
Assert.Single(inflated.Holes);
|
||||
Assert.Equal(
|
||||
expected,
|
||||
Collision.HasOverlap(inflated.LargestOuter(), inner, inflated.Holes)
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasOverlap_ZeroSpacingEdgeContact_ReturnsFalse()
|
||||
{
|
||||
var a = ClipperBridge.Offset(
|
||||
MakeProfile(MakePolygon((0, 0), (10, 0), (10, 10), (0, 10))),
|
||||
0,
|
||||
0.001
|
||||
);
|
||||
var b = ClipperBridge.Offset(
|
||||
MakeProfile(MakePolygon((10, 0), (20, 0), (20, 10), (10, 10))),
|
||||
0,
|
||||
0.001
|
||||
);
|
||||
|
||||
Assert.False(Collision.HasOverlap(a.LargestOuter(), b.LargestOuter()));
|
||||
}
|
||||
|
||||
private static Shape MakePolygon(params (double X, double Y)[] pts)
|
||||
{
|
||||
var shape = new Shape();
|
||||
|
||||
for (var i = 0; i < pts.Length; i++)
|
||||
{
|
||||
var from = pts[i];
|
||||
var to = pts[(i + 1) % pts.Length];
|
||||
shape.Entities.Add(new Line(from.X, from.Y, to.X, to.Y));
|
||||
}
|
||||
|
||||
return shape;
|
||||
}
|
||||
|
||||
private static ShapeProfile MakeProfile(params Shape[] shapes) =>
|
||||
new(shapes.SelectMany(s => s.Entities).ToList());
|
||||
|
||||
private static Polygon MakeSquare(double left, double bottom, double right, double top)
|
||||
{
|
||||
var p = new Polygon();
|
||||
|
||||
Reference in New Issue
Block a user