diff --git a/OpenNest.Core/Geometry/ConvexDecomposition.cs b/OpenNest.Core/Geometry/ConvexDecomposition.cs index f2e51a6..6c3e078 100644 --- a/OpenNest.Core/Geometry/ConvexDecomposition.cs +++ b/OpenNest.Core/Geometry/ConvexDecomposition.cs @@ -148,14 +148,12 @@ namespace OpenNest.Geometry /// private static double SignedArea(List verts) { + // World-coordinate products can erase the sign of a small polygon's area + // far from the origin, leaving CW outlines/holes untriangulated. Measure + // relative to a vertex, just as the clipping kernel measures its fragments. var area = 0.0; - - for (var i = 0; i < verts.Count; i++) - { - var j = (i + 1) % verts.Count; - area += verts[i].X * verts[j].Y; - area -= verts[j].X * verts[i].Y; - } + for (var i = 1; i + 1 < verts.Count; i++) + area += Cross(verts[0], verts[i], verts[i + 1]); return area * 0.5; } diff --git a/OpenNest.Tests/Geometry/CollisionTranslationTests.cs b/OpenNest.Tests/Geometry/CollisionTranslationTests.cs new file mode 100644 index 0000000..501ea14 --- /dev/null +++ b/OpenNest.Tests/Geometry/CollisionTranslationTests.cs @@ -0,0 +1,70 @@ +using OpenNest.Geometry; + +namespace OpenNest.Tests.Geometry; + +public class CollisionTranslationTests +{ + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Check_ContainedClockwisePolygonFarFromOriginDoesNotDependOnOperandOrder(bool swap) + { + var outer = Square(0, 0, 1e9); + var small = Square(999999998, 999999998, 1); + small.Reverse(); + + var result = swap ? Collision.Check(small, outer) : Collision.Check(outer, small); + + Assert.True(result.Overlaps); + // Do not use Polygon.Area here: this test isolates triangulation, not that legacy API. + Assert.Equal(1, result.OverlapRegions.Sum(StableArea), 8); + } + + [Fact] + public void Check_ClockwiseHoleFarFromOriginIsSubtracted() + { + var outer = Square(0, 0, 1e9); + var hole = Square(999999998, 999999998, 1); + hole.Reverse(); + var insert = Square(999999998.25, 999999998.25, 0.5); + + var result = Collision.Check(outer, insert, new List { hole }); + + Assert.False(result.Overlaps); + Assert.Empty(result.OverlapRegions); + } + + [Theory] + [InlineData(1000000000)] + [InlineData(-1000000000)] + public void Triangulate_ClockwiseTranslatedUnitSquareHasFullArea(double offset) + { + var polygon = Square(offset, offset, 1); + polygon.Reverse(); + var triangles = ConvexDecomposition.Triangulate(polygon); + Assert.Equal(2, triangles.Count); + Assert.Equal(1, triangles.Sum(StableArea), 8); + } + + private static Polygon Square(double x, double y, double size) + { + var polygon = new Polygon(); + polygon.Vertices.AddRange(new[] { new Vector(x, y), new Vector(x + size, y), + new Vector(x + size, y + size), new Vector(x, y + size), new Vector(x, y) }); + polygon.UpdateBounds(); + return polygon; + } + + private static double StableArea(Polygon polygon) + { + var area = 0.0; + var origin = polygon.Vertices[0]; + for (var i = 1; i + 1 < polygon.Vertices.Count; i++) + { + var a = polygon.Vertices[i] - origin; + var b = polygon.Vertices[i + 1] - origin; + area += a.X * b.Y - a.Y * b.X; + } + return System.Math.Abs(area) / 2; + } +}