Three bugs fixed in NfpSlideStrategy pipeline: 1. NoFitPolygon.Reflect() incorrectly reversed vertex order. Point reflection (negating both axes) is a 180° rotation that preserves winding — the Reverse() call was converting CCW to CW, producing self-intersecting bowtie NFPs. 2. PolygonHelper inflation used OffsetSide.Left which is inward for CCW perimeters. Changed to OffsetSide.Right for outward inflation so NFP boundary positions give properly-spaced part placements. 3. Removed incorrect correction vector — same-drawing pairs have identical polygon-to-part offsets that cancel out in the NFP displacement. Also refactored NfpSlideStrategy to be immutable (removed mutable cache fields, single constructor with required data, added Create factory method). BestFitFinder remains on RotationSlideStrategy as default. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
125 lines
4.7 KiB
C#
125 lines
4.7 KiB
C#
using OpenNest.CNC;
|
|
using OpenNest.Converters;
|
|
using OpenNest.Engine.BestFit;
|
|
using OpenNest.Geometry;
|
|
using OpenNest.Math;
|
|
|
|
namespace OpenNest.Tests;
|
|
|
|
public class NfpSlideStrategyTests
|
|
{
|
|
[Fact]
|
|
public void GenerateCandidates_ReturnsNonEmpty_ForSquare()
|
|
{
|
|
var drawing = TestHelpers.MakeSquareDrawing();
|
|
var strategy = NfpSlideStrategy.Create(drawing, 0, 1, "0 deg NFP", 0.25);
|
|
Assert.NotNull(strategy);
|
|
var candidates = strategy.GenerateCandidates(drawing, 0.25, 0.25);
|
|
Assert.NotEmpty(candidates);
|
|
}
|
|
|
|
[Fact]
|
|
public void GenerateCandidates_AllCandidatesHaveCorrectDrawing()
|
|
{
|
|
var drawing = TestHelpers.MakeSquareDrawing();
|
|
var strategy = NfpSlideStrategy.Create(drawing, 0, 1, "0 deg NFP", 0.25);
|
|
Assert.NotNull(strategy);
|
|
var candidates = strategy.GenerateCandidates(drawing, 0.25, 0.25);
|
|
Assert.All(candidates, c => Assert.Same(drawing, c.Drawing));
|
|
}
|
|
|
|
[Fact]
|
|
public void GenerateCandidates_Part1RotationIsAlwaysZero()
|
|
{
|
|
var drawing = TestHelpers.MakeSquareDrawing();
|
|
var strategy = NfpSlideStrategy.Create(drawing, Angle.HalfPI, 1, "90 deg NFP", 0.25);
|
|
Assert.NotNull(strategy);
|
|
var candidates = strategy.GenerateCandidates(drawing, 0.25, 0.25);
|
|
Assert.All(candidates, c => Assert.Equal(0, c.Part1Rotation));
|
|
}
|
|
|
|
[Fact]
|
|
public void GenerateCandidates_Part2RotationMatchesStrategy()
|
|
{
|
|
var rotation = Angle.HalfPI;
|
|
var drawing = TestHelpers.MakeSquareDrawing();
|
|
var strategy = NfpSlideStrategy.Create(drawing, rotation, 1, "90 deg NFP", 0.25);
|
|
Assert.NotNull(strategy);
|
|
var candidates = strategy.GenerateCandidates(drawing, 0.25, 0.25);
|
|
Assert.All(candidates, c => Assert.Equal(rotation, c.Part2Rotation));
|
|
}
|
|
|
|
[Fact]
|
|
public void GenerateCandidates_ProducesReasonableCandidateCount()
|
|
{
|
|
var drawing = TestHelpers.MakeSquareDrawing();
|
|
var strategy = NfpSlideStrategy.Create(drawing, 0, 1, "0 deg NFP", 0.25);
|
|
Assert.NotNull(strategy);
|
|
var candidates = strategy.GenerateCandidates(drawing, 0.25, 0.25);
|
|
|
|
// Convex hull NFP for a square produces vertices + edge samples.
|
|
// Should have more than just vertices but not thousands.
|
|
Assert.True(candidates.Count >= 4);
|
|
Assert.True(candidates.Count < 1000);
|
|
}
|
|
|
|
[Fact]
|
|
public void GenerateCandidates_MoreCandidates_WithSmallerStepSize()
|
|
{
|
|
var drawing = TestHelpers.MakeSquareDrawing();
|
|
var largeStepStrategy = NfpSlideStrategy.Create(drawing, 0, 1, "0 deg NFP", 0.25);
|
|
var smallStepStrategy = NfpSlideStrategy.Create(drawing, 0, 1, "0 deg NFP", 0.25);
|
|
Assert.NotNull(largeStepStrategy);
|
|
Assert.NotNull(smallStepStrategy);
|
|
var largeStep = largeStepStrategy.GenerateCandidates(drawing, 0.25, 5.0);
|
|
var smallStep = smallStepStrategy.GenerateCandidates(drawing, 0.25, 0.5);
|
|
Assert.True(smallStep.Count >= largeStep.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void Create_ReturnsNull_ForEmptyDrawing()
|
|
{
|
|
var pgm = new Program();
|
|
var drawing = new Drawing("empty", pgm);
|
|
var strategy = NfpSlideStrategy.Create(drawing, 0, 1, "0 deg NFP", 0.25);
|
|
Assert.Null(strategy);
|
|
}
|
|
|
|
[Fact]
|
|
public void GenerateCandidates_LShape_ProducesCandidates()
|
|
{
|
|
var lshape = TestHelpers.MakeLShapeDrawing();
|
|
var strategy = NfpSlideStrategy.Create(lshape, 0, 1, "0 deg NFP", 0.25);
|
|
Assert.NotNull(strategy);
|
|
var candidates = strategy.GenerateCandidates(lshape, 0.25, 0.25);
|
|
Assert.NotEmpty(candidates);
|
|
}
|
|
|
|
[Fact]
|
|
public void GenerateCandidates_At180Degrees_ProducesAtLeastOneNonOverlappingCandidate()
|
|
{
|
|
var drawing = TestHelpers.MakeSquareDrawing();
|
|
var strategy = NfpSlideStrategy.Create(drawing, System.Math.PI, 1, "180 deg NFP", 1.0);
|
|
Assert.NotNull(strategy);
|
|
// Use a large spacing (1.0) and step size.
|
|
// This should make NFP much larger than the parts.
|
|
var candidates = strategy.GenerateCandidates(drawing, 1.0, 1.0);
|
|
|
|
Assert.NotEmpty(candidates);
|
|
|
|
var part1 = Part.CreateAtOrigin(drawing);
|
|
var validCount = 0;
|
|
foreach (var candidate in candidates)
|
|
{
|
|
var part2 = Part.CreateAtOrigin(drawing, candidate.Part2Rotation);
|
|
part2.Location = candidate.Part2Offset;
|
|
|
|
// With 1.0 spacing, parts should NOT intersect even with tiny precision errors.
|
|
if (!part1.Intersects(part2, out _))
|
|
validCount++;
|
|
}
|
|
|
|
Assert.True(validCount > 0, $"No non-overlapping candidates found out of {candidates.Count} total. Candidate 0 offset: {candidates[0].Part2Offset}");
|
|
}
|
|
}
|