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)