using System; using Xunit; using OpenNest.Geometry; using OpenNest.Engine.Qwen38FlashNext.Engine; namespace OpenNest.Engine.Qwen38FlashNext.Tests; /// /// These tests target the engine's internal NFP math through its public surface /// (SheetPacker via reflection is overkill; ConvexContour/NfpGeometry are internal, /// so InternalsVisibleTo is required). /// public class NfpGeometryTests { private static ConvexContour Square(double x0, double y0, double x1, double y1) => ConvexContour.FromVertices( new[] { new Vector(x0, y0), new Vector(x1, y0), new Vector(x1, y1), new Vector(x0, y1), } ); [Fact] public void MinkowskiOfTwoSquaresIsTheExpectedRectangle() { var a = Square(0, 0, 10, 10); var b = Square(-5, -5, 5, 5); // centered square, side 10 var sum = NfpGeometry.Minkowski(a, b); // [0,10]^2 + [-5,5]^2 = [-5,15]^2 Assert.Equal(-5, sum.MinX, 6); Assert.Equal(-5, sum.MinY, 6); Assert.Equal(15, sum.MaxX, 6); Assert.Equal(15, sum.MaxY, 6); // Strict containment sanity: center inside, far corner outside. Assert.True(sum.ContainsPoint(0, 0)); Assert.True(sum.ContainsPoint(14.9, 14.9)); Assert.False(sum.ContainsPoint(20, 20)); var n = sum.Count; for (var i = 0; i < n; i++) { var ax = sum.X(i); var ay = sum.Y(i); var bx = sum.X((i + 1) % n); var by = sum.Y((i + 1) % n); var cx = sum.X((i + 2) % n); var cy = sum.Y((i + 2) % n); var cross = (bx - ax) * (cy - by) - (by - ay) * (cx - bx); Assert.True(cross >= -1e-9, $"non-convex (clockwise) turn at vertex {i} of Minkowski result"); } } [Fact] public void MinkowskiOfTrianglesIsConvexAndContainsTheSums() { var a = ConvexContour.FromVertices( new[] { new Vector(0, 0), new Vector(10, 0), new Vector(0, 10) } ); var b = ConvexContour.FromVertices( new[] { new Vector(0, 0), new Vector(4, 0), new Vector(0, 4) } ); var sum = NfpGeometry.Minkowski(a, b); // Vertex sums must lie on the boundary of the true Minkowski sum. Assert.True(sum.ContainsPoint(1, 1)); Assert.True(sum.ContainsPoint(9, 1)); Assert.True(sum.ContainsPoint(1, 12)); var n = sum.Count; for (var i = 0; i < n; i++) { var ax = sum.X(i); var ay = sum.Y(i); var bx = sum.X((i + 1) % n); var by = sum.Y((i + 1) % n); var cx = sum.X((i + 2) % n); var cy = sum.Y((i + 2) % n); var cross = (bx - ax) * (cy - by) - (by - ay) * (cx - bx); Assert.True(cross >= -1e-9, $"non-convex turn at vertex {i}"); } } [Fact] public void ReflectPreservesCcwWinding() { var a = Square(0, 0, 10, 10); var r = NfpGeometry.Reflect(a); Assert.Equal(-10, r.MinX, 6); Assert.Equal(-10, r.MinY, 6); Assert.Equal(0, r.MaxX, 6); Assert.Equal(0, r.MaxY, 6); var n = r.Count; for (var i = 0; i < n; i++) { var ax = r.X(i); var ay = r.Y(i); var bx = r.X((i + 1) % n); var by = r.Y((i + 1) % n); var cx = r.X((i + 2) % n); var cy = r.Y((i + 2) % n); var cross = (bx - ax) * (cy - by) - (by - ay) * (cx - bx); Assert.True(cross >= -1e-9, $"Reflect produced a non-CCW contour at vertex {i}"); } } [Fact] public void NfpOfTwoSquaresIsTheForbiddenAnchorSquare() { // Placed [0,10]^2, candidate [0,10]^2, zero spacing: NFP of forbidden // anchors = placed (+) reflect(candidate) = (-10,10)^2. Anchors strictly // inside it overlap; anchors outside it clear. var placed = Square(0, 0, 10, 10); var candidate = Square(0, 0, 10, 10); var nfp = NfpGeometry.Minkowski(placed, NfpGeometry.Reflect(candidate)); Assert.Equal(-10, nfp.MinX, 6); Assert.Equal(-10, nfp.MinY, 6); Assert.Equal(10, nfp.MaxX, 6); Assert.Equal(10, nfp.MaxY, 6); Assert.True(nfp.ContainsPoint(5, 5)); // overlap Assert.True(nfp.ContainsPoint(-5, -5)); // overlap // Boundary contact counts as forbidden (conservative): the fast-path // certification only accepts anchors CLEAR of the NFP; contact defers to // the exact material gate. Assert.True(nfp.ContainsPoint(10, 0)); Assert.False(nfp.ContainsPoint(0, 10.001)); // beyond top, legal var n = nfp.Count; for (var i = 0; i < n; i++) { var ax = nfp.X(i); var ay = nfp.Y(i); var bx = nfp.X((i + 1) % n); var by = nfp.Y((i + 1) % n); var cx = nfp.X((i + 2) % n); var cy = nfp.Y((i + 2) % n); var cross = (bx - ax) * (cy - by) - (by - ay) * (cx - bx); Assert.True(cross >= -1e-9, $"non-convex turn at vertex {i}"); } } }