diff --git a/OpenNest.Engine/BestFit/BestFitFilter.cs b/OpenNest.Engine/BestFit/BestFitFilter.cs index 3791d9b..527b46c 100644 --- a/OpenNest.Engine/BestFit/BestFitFilter.cs +++ b/OpenNest.Engine/BestFit/BestFitFilter.cs @@ -7,7 +7,14 @@ namespace OpenNest.Engine.BestFit public double MaxPlateWidth { get; set; } public double MaxPlateHeight { get; set; } public double MaxAspectRatio { get; set; } = 5.0; - public double MinUtilization { get; set; } = 0.3; + + /// + /// A high-aspect pair is kept anyway when this much of its rotated bounding box is + /// actual part area. Utilization is only ever an exception here, never a rejection: + /// thin-framed, hollow, or concave (e.g. S-shaped) parts have inherently low + /// part-to-bbox utilization, yet can nest tightly — their quality is judged by the + /// pair bounding-box area the results are sorted on, not by utilization. + /// public double UtilizationOverride { get; set; } = 0.75; public void Apply(List results) @@ -40,16 +47,6 @@ namespace OpenNest.Engine.BestFit continue; } - if (result.Utilization < MinUtilization) - { - result.Keep = false; - result.Reason = string.Format( - "Utilization {0:P0} below minimum", - result.Utilization - ); - continue; - } - result.Reason = "Valid"; } } diff --git a/OpenNest.Tests/BestFit/NfpBestFitIntegrationTests.cs b/OpenNest.Tests/BestFit/NfpBestFitIntegrationTests.cs index 4c0d6f4..5c277e3 100644 --- a/OpenNest.Tests/BestFit/NfpBestFitIntegrationTests.cs +++ b/OpenNest.Tests/BestFit/NfpBestFitIntegrationTests.cs @@ -42,6 +42,34 @@ public class NfpBestFitIntegrationTests Assert.True(bestUtilization > 0.5); } + [Fact] + public void FindBestFits_KeepsLowUtilizationFrame_CutoutsDoNotRejectPair() + { + // A thin-walled hollow frame nests tightly despite tiny part/bbox utilization. + // Utilization must never reject a pair; ranking is by pair bounding-box area. + var finder = new BestFitFinder(48, 96); + var drawing = TestHelpers.MakeFrameDrawing(w: 20, h: 6, t: 0.5); + + var results = finder.FindBestFits(drawing); + + Assert.NotEmpty(results.Where(r => r.Keep)); + Assert.All(results.Where(r => r.Keep), r => Assert.Equal("Valid", r.Reason)); + var best = results.Where(r => r.Keep).First(); + Assert.True(best.Utilization < 0.3); + } + + [Fact] + public void FindBestFits_ResultsAreSortedByPairBoundingArea() + { + var finder = new BestFitFinder(48, 96); + var results = finder.FindBestFits(TestHelpers.MakeFrameDrawing()); + + Assert.True(results.Count > 1); + Assert.True( + results.Zip(results.Skip(1), (a, b) => a.RotatedArea <= b.RotatedArea).All(x => x) + ); + } + [Fact] public void FindBestFits_NoOverlaps_InKeptResults() { diff --git a/OpenNest.Tests/TestHelpers.cs b/OpenNest.Tests/TestHelpers.cs index 8c5e747..a231a78 100644 --- a/OpenNest.Tests/TestHelpers.cs +++ b/OpenNest.Tests/TestHelpers.cs @@ -67,6 +67,23 @@ internal static class TestHelpers return new Drawing("square", pgm); } + public static Drawing MakeFrameDrawing(double w = 20, double h = 6, double t = 0.5) + { + // Thin-walled hollow frame: outer CW, inner cutout CCW (CNC convention). + var pgm = new Program(); + pgm.Codes.Add(new RapidMove(new Vector(0, 0))); + pgm.Codes.Add(new LinearMove(new Vector(0, h))); + pgm.Codes.Add(new LinearMove(new Vector(w, h))); + pgm.Codes.Add(new LinearMove(new Vector(w, 0))); + pgm.Codes.Add(new LinearMove(new Vector(0, 0))); + pgm.Codes.Add(new RapidMove(new Vector(t, t))); + pgm.Codes.Add(new LinearMove(new Vector(w - t, t))); + pgm.Codes.Add(new LinearMove(new Vector(w - t, h - t))); + pgm.Codes.Add(new LinearMove(new Vector(t, h - t))); + pgm.Codes.Add(new LinearMove(new Vector(t, t))); + return new Drawing("frame", pgm); + } + public static Drawing MakeLShapeDrawing() { // CW winding matches CNC convention (OffsetSide.Left = outward)