feat: add Collision static class with Sutherland-Hodgman clipping and tests
Polygon-polygon collision detection using convex decomposition (ear-clipping triangulation) followed by Sutherland-Hodgman clipping on each triangle pair. Handles overlapping, non-overlapping, edge-touching, containment, and concave polygons. Includes hole subtraction support for future use. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Tests;
|
||||
|
||||
public class CollisionTests
|
||||
{
|
||||
/// Two unit squares overlapping by 0.5 in X.
|
||||
/// Square A: (0,0)-(1,1), Square B: (0.5,0)-(1.5,1)
|
||||
/// Expected overlap: (0.5,0)-(1,1), area = 0.5
|
||||
[Fact]
|
||||
public void Check_OverlappingSquares_ReturnsOverlapRegion()
|
||||
{
|
||||
var a = MakeSquare(0, 0, 1, 1);
|
||||
var b = MakeSquare(0.5, 0, 1.5, 1);
|
||||
|
||||
var result = Collision.Check(a, b);
|
||||
|
||||
Assert.True(result.Overlaps);
|
||||
Assert.True(result.OverlapArea > 0.49 && result.OverlapArea < 0.51);
|
||||
Assert.NotEmpty(result.OverlapRegions);
|
||||
}
|
||||
|
||||
/// Two squares that don't touch at all.
|
||||
[Fact]
|
||||
public void Check_NonOverlappingSquares_ReturnsNone()
|
||||
{
|
||||
var a = MakeSquare(0, 0, 1, 1);
|
||||
var b = MakeSquare(5, 5, 6, 6);
|
||||
|
||||
var result = Collision.Check(a, b);
|
||||
|
||||
Assert.False(result.Overlaps);
|
||||
Assert.Empty(result.OverlapRegions);
|
||||
Assert.Equal(0, result.OverlapArea);
|
||||
}
|
||||
|
||||
/// Two squares sharing an edge (touching but not overlapping).
|
||||
[Fact]
|
||||
public void Check_EdgeTouchingSquares_ReturnsNone()
|
||||
{
|
||||
var a = MakeSquare(0, 0, 1, 1);
|
||||
var b = MakeSquare(1, 0, 2, 1);
|
||||
|
||||
var result = Collision.Check(a, b);
|
||||
|
||||
Assert.False(result.Overlaps);
|
||||
}
|
||||
|
||||
/// One square fully inside another. Inner: (0.25,0.25)-(0.75,0.75), area = 0.25
|
||||
[Fact]
|
||||
public void Check_ContainedSquare_ReturnsInnerArea()
|
||||
{
|
||||
var a = MakeSquare(0, 0, 1, 1);
|
||||
var b = MakeSquare(0.25, 0.25, 0.75, 0.75);
|
||||
|
||||
var result = Collision.Check(a, b);
|
||||
|
||||
Assert.True(result.Overlaps);
|
||||
Assert.True(result.OverlapArea > 0.24 && result.OverlapArea < 0.26);
|
||||
}
|
||||
|
||||
/// L-shaped concave polygon overlapping a square.
|
||||
[Fact]
|
||||
public void Check_ConcavePolygonOverlap_ReturnsOverlap()
|
||||
{
|
||||
// L-shape: 2x2 with a 1x1 notch cut from top-right
|
||||
var lShape = new Polygon();
|
||||
lShape.Vertices.Add(new Vector(0, 0));
|
||||
lShape.Vertices.Add(new Vector(2, 0));
|
||||
lShape.Vertices.Add(new Vector(2, 1));
|
||||
lShape.Vertices.Add(new Vector(1, 1));
|
||||
lShape.Vertices.Add(new Vector(1, 2));
|
||||
lShape.Vertices.Add(new Vector(0, 2));
|
||||
lShape.Close();
|
||||
lShape.UpdateBounds();
|
||||
|
||||
// Square overlapping the notch area and bottom-right
|
||||
var square = MakeSquare(1.5, 0, 2.5, 1.5);
|
||||
|
||||
var result = Collision.Check(lShape, square);
|
||||
|
||||
Assert.True(result.Overlaps);
|
||||
// Overlap is 0.5 x 1.0 = 0.5 (the part of the square inside the L bottom-right)
|
||||
Assert.True(result.OverlapArea > 0.49 && result.OverlapArea < 0.51);
|
||||
}
|
||||
|
||||
private static Polygon MakeSquare(double left, double bottom, double right, double top)
|
||||
{
|
||||
var p = new Polygon();
|
||||
p.Vertices.Add(new Vector(left, bottom));
|
||||
p.Vertices.Add(new Vector(right, bottom));
|
||||
p.Vertices.Add(new Vector(right, top));
|
||||
p.Vertices.Add(new Vector(left, top));
|
||||
p.Close();
|
||||
p.UpdateBounds();
|
||||
return p;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user