Qwen3.8-Flash-Next's final version after a 14.5-hour optimization run (its commit 7d7fca3): cost-first sheet trials, largest-area-first demand order, and a cached-triangulation exact gate that brought a 219-part production job from timeout to ~106 s. 13/13 tests pass against OpenNest master. README cleaned for publishing: the model-facing template rules are replaced by a one-line independence statement, the production job is described generically instead of by its PEP job/file name (also in a JobSolver comment), results show both sheet pools as re-measured here (the 9-size claim in its report didn't reproduce: it grabs 96x240 and under-fills them), and the stale StockLadder-crash note is gone now that core leaves etch marks out of nesting. Also drops a stale Aurora plugin reference from Opus55's README and lists the engine in the repo README. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
152 lines
5.1 KiB
C#
152 lines
5.1 KiB
C#
using System;
|
|
using Xunit;
|
|
using OpenNest.Geometry;
|
|
using OpenNest.Engine.Qwen38FlashNext.Engine;
|
|
|
|
namespace OpenNest.Engine.Qwen38FlashNext.Tests;
|
|
|
|
/// <summary>
|
|
/// 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).
|
|
/// </summary>
|
|
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}");
|
|
}
|
|
}
|
|
}
|