fix: correct NFP polygon computation and inflation direction
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>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using Clipper2Lib;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace OpenNest.Geometry
|
||||
@@ -22,8 +23,20 @@ namespace OpenNest.Geometry
|
||||
return MinkowskiSum(stationary, reflected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Optimized version of Compute for polygons known to be convex.
|
||||
/// Bypasses expensive triangulation and Clipper unions.
|
||||
/// </summary>
|
||||
public static Polygon ComputeConvex(Polygon stationary, Polygon orbiting)
|
||||
{
|
||||
var reflected = Reflect(orbiting);
|
||||
return ConvexMinkowskiSum(stationary, reflected);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reflects a polygon through the origin (negates all vertex coordinates).
|
||||
/// Point reflection (negating both axes) is equivalent to 180° rotation,
|
||||
/// which preserves winding order. No reversal needed.
|
||||
/// </summary>
|
||||
private static Polygon Reflect(Polygon polygon)
|
||||
{
|
||||
@@ -32,8 +45,6 @@ namespace OpenNest.Geometry
|
||||
foreach (var v in polygon.Vertices)
|
||||
result.Vertices.Add(new Vector(-v.X, -v.Y));
|
||||
|
||||
// Reflecting reverses winding order — reverse to maintain CCW.
|
||||
result.Vertices.Reverse();
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -83,14 +94,19 @@ namespace OpenNest.Geometry
|
||||
var edgesA = GetEdgeVectors(a);
|
||||
var edgesB = GetEdgeVectors(b);
|
||||
|
||||
// Find bottom-most (then left-most) vertex for each polygon as starting point.
|
||||
// Find indices of bottom-left vertices for both.
|
||||
var startA = FindBottomLeft(a);
|
||||
var startB = FindBottomLeft(b);
|
||||
|
||||
var result = new Polygon();
|
||||
|
||||
// The starting point of the Minkowski sum A + B is the sum of the
|
||||
// starting points of A and B. For NFP = A + (-B), this is
|
||||
// startA + startReflectedB.
|
||||
var current = new Vector(
|
||||
a.Vertices[startA].X + b.Vertices[startB].X,
|
||||
a.Vertices[startA].Y + b.Vertices[startB].Y);
|
||||
|
||||
result.Vertices.Add(current);
|
||||
|
||||
var ia = 0;
|
||||
@@ -98,7 +114,6 @@ namespace OpenNest.Geometry
|
||||
var na = edgesA.Count;
|
||||
var nb = edgesB.Count;
|
||||
|
||||
// Reorder edges to start from the bottom-left vertex.
|
||||
var orderedA = ReorderEdges(edgesA, startA);
|
||||
var orderedB = ReorderEdges(edgesB, startB);
|
||||
|
||||
@@ -117,7 +132,10 @@ namespace OpenNest.Geometry
|
||||
else
|
||||
{
|
||||
var angleA = System.Math.Atan2(orderedA[ia].Y, orderedA[ia].X);
|
||||
if (angleA < 0) angleA += Angle.TwoPI;
|
||||
|
||||
var angleB = System.Math.Atan2(orderedB[ib].Y, orderedB[ib].X);
|
||||
if (angleB < 0) angleB += Angle.TwoPI;
|
||||
|
||||
if (angleA < angleB)
|
||||
{
|
||||
@@ -129,7 +147,6 @@ namespace OpenNest.Geometry
|
||||
}
|
||||
else
|
||||
{
|
||||
// Same angle — merge both edges.
|
||||
edge = new Vector(
|
||||
orderedA[ia].X + orderedB[ib].X,
|
||||
orderedA[ia].Y + orderedB[ib].Y);
|
||||
@@ -143,6 +160,7 @@ namespace OpenNest.Geometry
|
||||
}
|
||||
|
||||
result.Close();
|
||||
result.UpdateBounds();
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace OpenNest.Engine.BestFit
|
||||
double stepSize = 0.25,
|
||||
BestFitSortField sortBy = BestFitSortField.Area)
|
||||
{
|
||||
var strategies = BuildStrategies(drawing);
|
||||
var strategies = BuildStrategies(drawing, spacing);
|
||||
|
||||
var candidateBags = new ConcurrentBag<List<PairCandidate>>();
|
||||
|
||||
@@ -75,7 +75,7 @@ namespace OpenNest.Engine.BestFit
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private List<IBestFitStrategy> BuildStrategies(Drawing drawing)
|
||||
private List<IBestFitStrategy> BuildStrategies(Drawing drawing, double spacing)
|
||||
{
|
||||
var angles = GetRotationAngles(drawing);
|
||||
var strategies = new List<IBestFitStrategy>();
|
||||
|
||||
@@ -1,22 +1,61 @@
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace OpenNest.Engine.BestFit
|
||||
{
|
||||
public class NfpSlideStrategy : IBestFitStrategy
|
||||
{
|
||||
private readonly double _part2Rotation;
|
||||
private static readonly string LogPath = Path.Combine(
|
||||
System.Environment.GetFolderPath(System.Environment.SpecialFolder.Desktop),
|
||||
"nfp-slide-debug.log");
|
||||
|
||||
public NfpSlideStrategy(double part2Rotation, int type, string description)
|
||||
private static readonly object LogLock = new object();
|
||||
|
||||
private readonly double _part2Rotation;
|
||||
private readonly Polygon _stationaryPerimeter;
|
||||
private readonly Polygon _stationaryHull;
|
||||
private readonly Vector _correction;
|
||||
|
||||
public NfpSlideStrategy(double part2Rotation, int type, string description,
|
||||
Polygon stationaryPerimeter, Polygon stationaryHull, Vector correction)
|
||||
{
|
||||
_part2Rotation = part2Rotation;
|
||||
Type = type;
|
||||
Description = description;
|
||||
_stationaryPerimeter = stationaryPerimeter;
|
||||
_stationaryHull = stationaryHull;
|
||||
_correction = correction;
|
||||
}
|
||||
|
||||
public int Type { get; }
|
||||
public string Description { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates an NfpSlideStrategy by extracting polygon data from a drawing.
|
||||
/// Returns null if the drawing has no valid perimeter.
|
||||
/// </summary>
|
||||
public static NfpSlideStrategy Create(Drawing drawing, double part2Rotation,
|
||||
int type, string description, double spacing)
|
||||
{
|
||||
var result = PolygonHelper.ExtractPerimeterPolygon(drawing, spacing / 2);
|
||||
|
||||
if (result.Polygon == null)
|
||||
return null;
|
||||
|
||||
var hull = ConvexHull.Compute(result.Polygon.Vertices);
|
||||
|
||||
Log($"=== Create: drawing={drawing.Name}, rotation={Angle.ToDegrees(part2Rotation):F1}deg ===");
|
||||
Log($" Perimeter: {result.Polygon.Vertices.Count} verts, bounds={FormatBounds(result.Polygon)}");
|
||||
Log($" Hull: {hull.Vertices.Count} verts, bounds={FormatBounds(hull)}");
|
||||
Log($" Correction: ({result.Correction.X:F4}, {result.Correction.Y:F4})");
|
||||
Log($" ProgramBBox: {drawing.Program.BoundingBox()}");
|
||||
|
||||
return new NfpSlideStrategy(part2Rotation, type, description,
|
||||
result.Polygon, hull, result.Correction);
|
||||
}
|
||||
|
||||
public List<PairCandidate> GenerateCandidates(Drawing drawing, double spacing, double stepSize)
|
||||
{
|
||||
var candidates = new List<PairCandidate>();
|
||||
@@ -24,42 +63,45 @@ namespace OpenNest.Engine.BestFit
|
||||
if (stepSize <= 0)
|
||||
return candidates;
|
||||
|
||||
var halfSpacing = spacing / 2;
|
||||
Log($"--- GenerateCandidates: drawing={drawing.Name}, part2Rot={Angle.ToDegrees(_part2Rotation):F1}deg, spacing={spacing}, stepSize={stepSize} ---");
|
||||
|
||||
// Extract stationary polygon (Part1 at rotation 0), with spacing applied.
|
||||
var stationaryResult = PolygonHelper.ExtractPerimeterPolygon(drawing, halfSpacing);
|
||||
// Orbiting polygon: same shape rotated to Part2's angle.
|
||||
var orbitingPerimeter = PolygonHelper.RotatePolygon(_stationaryPerimeter, _part2Rotation, reNormalize: true);
|
||||
var orbitingPoly = ConvexHull.Compute(orbitingPerimeter.Vertices);
|
||||
|
||||
if (stationaryResult.Polygon == null)
|
||||
return candidates;
|
||||
Log($" Stationary hull: {_stationaryHull.Vertices.Count} verts, bounds={FormatBounds(_stationaryHull)}");
|
||||
Log($" Orbiting perimeter (rotated): {orbitingPerimeter.Vertices.Count} verts, bounds={FormatBounds(orbitingPerimeter)}");
|
||||
Log($" Orbiting hull: {orbitingPoly.Vertices.Count} verts, bounds={FormatBounds(orbitingPoly)}");
|
||||
|
||||
// Use convex hulls for NFP — avoids expensive triangulation of
|
||||
// concave parts. Hull inputs produce few triangles, so the
|
||||
// Minkowski sum + Clipper union inside Compute stays fast.
|
||||
var stationaryPoly = ConvexHull.Compute(stationaryResult.Polygon.Vertices);
|
||||
|
||||
// Orbiting polygon: same shape rotated to Part2's angle, then hulled.
|
||||
var rotated = PolygonHelper.RotatePolygon(stationaryResult.Polygon, _part2Rotation);
|
||||
var orbitingPoly = ConvexHull.Compute(rotated.Vertices);
|
||||
|
||||
// Let Compute handle reflection, winding, and Minkowski sum correctly.
|
||||
var nfp = NoFitPolygon.Compute(stationaryPoly, orbitingPoly);
|
||||
var nfp = NoFitPolygon.ComputeConvex(_stationaryHull, orbitingPoly);
|
||||
|
||||
if (nfp == null || nfp.Vertices.Count < 3)
|
||||
{
|
||||
Log($" NFP failed or degenerate (verts={nfp?.Vertices.Count ?? 0})");
|
||||
return candidates;
|
||||
}
|
||||
|
||||
// Coordinate correction: NFP offsets are in polygon-space.
|
||||
// Part.CreateAtOrigin uses program bbox origin.
|
||||
var correction = stationaryResult.Correction;
|
||||
|
||||
// Walk NFP boundary — vertices + edge samples.
|
||||
var verts = nfp.Vertices;
|
||||
var vertCount = nfp.IsClosed() ? verts.Count - 1 : verts.Count;
|
||||
|
||||
Log($" NFP: {verts.Count} verts (closed={nfp.IsClosed()}, walking {vertCount}), bounds={FormatBounds(nfp)}");
|
||||
Log($" Correction: ({_correction.X:F4}, {_correction.Y:F4})");
|
||||
|
||||
// Log NFP vertices
|
||||
for (var v = 0; v < vertCount; v++)
|
||||
Log($" NFP vert[{v}]: ({verts[v].X:F4}, {verts[v].Y:F4}) -> corrected: ({verts[v].X - _correction.X:F4}, {verts[v].Y - _correction.Y:F4})");
|
||||
|
||||
// Compare with what RotationSlideStrategy would produce
|
||||
var part1 = Part.CreateAtOrigin(drawing);
|
||||
var part2 = Part.CreateAtOrigin(drawing, _part2Rotation);
|
||||
Log($" Part1 (rot=0): loc=({part1.Location.X:F4}, {part1.Location.Y:F4}), bbox={part1.BoundingBox}");
|
||||
Log($" Part2 (rot={Angle.ToDegrees(_part2Rotation):F1}): loc=({part2.Location.X:F4}, {part2.Location.Y:F4}), bbox={part2.BoundingBox}");
|
||||
|
||||
var testNumber = 0;
|
||||
|
||||
for (var i = 0; i < vertCount; i++)
|
||||
{
|
||||
// Add vertex candidate.
|
||||
var offset = ApplyCorrection(verts[i], correction);
|
||||
var offset = ApplyCorrection(verts[i], _correction);
|
||||
candidates.Add(MakeCandidate(drawing, offset, spacing, testNumber++));
|
||||
|
||||
// Add edge samples for long edges.
|
||||
@@ -77,18 +119,32 @@ namespace OpenNest.Engine.BestFit
|
||||
var sample = new Vector(
|
||||
verts[i].X + dx * t,
|
||||
verts[i].Y + dy * t);
|
||||
var sampleOffset = ApplyCorrection(sample, correction);
|
||||
var sampleOffset = ApplyCorrection(sample, _correction);
|
||||
candidates.Add(MakeCandidate(drawing, sampleOffset, spacing, testNumber++));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log overlap check for vertex candidates (first few)
|
||||
var checkCount = System.Math.Min(vertCount, 8);
|
||||
for (var c = 0; c < checkCount; c++)
|
||||
{
|
||||
var cand = candidates[c];
|
||||
var p2 = Part.CreateAtOrigin(drawing, cand.Part2Rotation);
|
||||
p2.Location = cand.Part2Offset;
|
||||
var overlaps = part1.Intersects(p2, out _);
|
||||
Log($" Candidate[{c}]: offset=({cand.Part2Offset.X:F4}, {cand.Part2Offset.Y:F4}), overlaps={overlaps}");
|
||||
}
|
||||
|
||||
Log($" Total candidates: {candidates.Count}");
|
||||
Log("");
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
private static Vector ApplyCorrection(Vector nfpVertex, Vector correction)
|
||||
{
|
||||
return new Vector(nfpVertex.X + correction.X, nfpVertex.Y + correction.Y);
|
||||
return new Vector(nfpVertex.X - correction.X, nfpVertex.Y - correction.Y);
|
||||
}
|
||||
|
||||
private PairCandidate MakeCandidate(Drawing drawing, Vector offset, double spacing, int testNumber)
|
||||
@@ -104,5 +160,20 @@ namespace OpenNest.Engine.BestFit
|
||||
Spacing = spacing
|
||||
};
|
||||
}
|
||||
|
||||
private static string FormatBounds(Polygon polygon)
|
||||
{
|
||||
polygon.UpdateBounds();
|
||||
var bb = polygon.BoundingBox;
|
||||
return $"[({bb.Left:F4}, {bb.Bottom:F4})-({bb.Right:F4}, {bb.Top:F4}), {bb.Width:F2}x{bb.Length:F2}]";
|
||||
}
|
||||
|
||||
private static void Log(string message)
|
||||
{
|
||||
lock (LogLock)
|
||||
{
|
||||
File.AppendAllText(LogPath, message + "\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,13 +22,10 @@ namespace OpenNest.Engine.BestFit
|
||||
if (perimeter == null)
|
||||
return new PolygonExtractionResult(null, Vector.Zero);
|
||||
|
||||
// Compute the perimeter bounding box before inflation for coordinate correction.
|
||||
perimeter.UpdateBounds();
|
||||
var perimeterBb = perimeter.BoundingBox;
|
||||
|
||||
// Inflate by half-spacing if spacing is non-zero.
|
||||
// OffsetSide.Right = outward for CCW perimeters (standard for outer contours).
|
||||
var inflated = halfSpacing > 0
|
||||
? (perimeter.OffsetEntity(halfSpacing, OffsetSide.Left) as Shape ?? perimeter)
|
||||
? (perimeter.OffsetEntity(halfSpacing, OffsetSide.Right) as Shape ?? perimeter)
|
||||
: perimeter;
|
||||
|
||||
// Convert to polygon with circumscribed arcs for tight nesting.
|
||||
@@ -37,22 +34,18 @@ namespace OpenNest.Engine.BestFit
|
||||
if (polygon.Vertices.Count < 3)
|
||||
return new PolygonExtractionResult(null, Vector.Zero);
|
||||
|
||||
// Compute correction: difference between program origin and perimeter origin.
|
||||
// Part.CreateAtOrigin normalizes to program bbox; polygon normalizes to perimeter bbox.
|
||||
var programBb = drawing.Program.BoundingBox();
|
||||
var correction = new Vector(
|
||||
perimeterBb.Left - programBb.Location.X,
|
||||
perimeterBb.Bottom - programBb.Location.Y);
|
||||
|
||||
// Normalize: move reference point to origin.
|
||||
// Normalize: move polygon to origin.
|
||||
polygon.UpdateBounds();
|
||||
var bb = polygon.BoundingBox;
|
||||
polygon.Offset(-bb.Left, -bb.Bottom);
|
||||
|
||||
return new PolygonExtractionResult(polygon, correction);
|
||||
// No correction needed: BestFitFinder always pairs the same drawing with
|
||||
// itself, so the polygon-to-part offset is identical for both parts and
|
||||
// cancels out in the NFP displacement.
|
||||
return new PolygonExtractionResult(polygon, Vector.Zero);
|
||||
}
|
||||
|
||||
public static Polygon RotatePolygon(Polygon polygon, double angle)
|
||||
public static Polygon RotatePolygon(Polygon polygon, double angle, bool reNormalize = true)
|
||||
{
|
||||
if (angle.IsEqualTo(0))
|
||||
return polygon;
|
||||
@@ -68,10 +61,13 @@ namespace OpenNest.Engine.BestFit
|
||||
v.X * sin + v.Y * cos));
|
||||
}
|
||||
|
||||
// Re-normalize to origin.
|
||||
result.UpdateBounds();
|
||||
var bb = result.BoundingBox;
|
||||
result.Offset(-bb.Left, -bb.Bottom);
|
||||
if (reNormalize)
|
||||
{
|
||||
// Re-normalize to origin.
|
||||
result.UpdateBounds();
|
||||
var bb = result.BoundingBox;
|
||||
result.Offset(-bb.Left, -bb.Bottom);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Engine.BestFit;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
@@ -10,8 +11,9 @@ public class NfpSlideStrategyTests
|
||||
[Fact]
|
||||
public void GenerateCandidates_ReturnsNonEmpty_ForSquare()
|
||||
{
|
||||
var strategy = new NfpSlideStrategy(0, 1, "0 deg NFP");
|
||||
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);
|
||||
}
|
||||
@@ -19,8 +21,9 @@ public class NfpSlideStrategyTests
|
||||
[Fact]
|
||||
public void GenerateCandidates_AllCandidatesHaveCorrectDrawing()
|
||||
{
|
||||
var strategy = new NfpSlideStrategy(0, 1, "0 deg NFP");
|
||||
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));
|
||||
}
|
||||
@@ -28,8 +31,9 @@ public class NfpSlideStrategyTests
|
||||
[Fact]
|
||||
public void GenerateCandidates_Part1RotationIsAlwaysZero()
|
||||
{
|
||||
var strategy = new NfpSlideStrategy(Angle.HalfPI, 1, "90 deg NFP");
|
||||
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));
|
||||
}
|
||||
@@ -38,8 +42,9 @@ public class NfpSlideStrategyTests
|
||||
public void GenerateCandidates_Part2RotationMatchesStrategy()
|
||||
{
|
||||
var rotation = Angle.HalfPI;
|
||||
var strategy = new NfpSlideStrategy(rotation, 1, "90 deg NFP");
|
||||
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));
|
||||
}
|
||||
@@ -47,8 +52,9 @@ public class NfpSlideStrategyTests
|
||||
[Fact]
|
||||
public void GenerateCandidates_ProducesReasonableCandidateCount()
|
||||
{
|
||||
var strategy = new NfpSlideStrategy(0, 1, "0 deg NFP");
|
||||
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.
|
||||
@@ -60,39 +66,59 @@ public class NfpSlideStrategyTests
|
||||
[Fact]
|
||||
public void GenerateCandidates_MoreCandidates_WithSmallerStepSize()
|
||||
{
|
||||
var strategy = new NfpSlideStrategy(0, 1, "0 deg NFP");
|
||||
var drawing = TestHelpers.MakeSquareDrawing();
|
||||
var largeStep = strategy.GenerateCandidates(drawing, 0.25, 5.0);
|
||||
var smallStep = strategy.GenerateCandidates(drawing, 0.25, 0.5);
|
||||
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 GenerateCandidates_ReturnsEmpty_ForEmptyDrawing()
|
||||
public void Create_ReturnsNull_ForEmptyDrawing()
|
||||
{
|
||||
var strategy = new NfpSlideStrategy(0, 1, "0 deg NFP");
|
||||
var pgm = new Program();
|
||||
var drawing = new Drawing("empty", pgm);
|
||||
var candidates = strategy.GenerateCandidates(drawing, 0.25, 0.25);
|
||||
Assert.Empty(candidates);
|
||||
var strategy = NfpSlideStrategy.Create(drawing, 0, 1, "0 deg NFP", 0.25);
|
||||
Assert.Null(strategy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateCandidates_LShape_ProducesCandidates()
|
||||
{
|
||||
var strategy = new NfpSlideStrategy(0, 1, "0 deg NFP");
|
||||
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_ProducesCandidates()
|
||||
public void GenerateCandidates_At180Degrees_ProducesAtLeastOneNonOverlappingCandidate()
|
||||
{
|
||||
var strategy = new NfpSlideStrategy(System.Math.PI, 1, "180 deg NFP");
|
||||
var drawing = TestHelpers.MakeSquareDrawing();
|
||||
var candidates = strategy.GenerateCandidates(drawing, 0.25, 0.25);
|
||||
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);
|
||||
Assert.All(candidates, c => Assert.Equal(System.Math.PI, c.Part2Rotation));
|
||||
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user