fix(bestfit): never reject pair candidates by utilization
Thin-framed, hollow, or concave parts (e.g. SULLYS-035's frame) have inherently low part-to-bbox utilization yet nest tightly, so the 30% MinUtilization floor wrongly dropped every candidate for them. Pair quality is judged by the rotated pair bounding-box area the results are already sorted on; utilization now only ever serves as the high-aspect exception (UtilizationOverride), never as a rejection. Adds a hollow-frame helper plus regression tests that kept pairs exist with low utilization and results stay sorted by pair area.
This commit is contained in:
@@ -7,7 +7,14 @@ namespace OpenNest.Engine.BestFit
|
|||||||
public double MaxPlateWidth { get; set; }
|
public double MaxPlateWidth { get; set; }
|
||||||
public double MaxPlateHeight { get; set; }
|
public double MaxPlateHeight { get; set; }
|
||||||
public double MaxAspectRatio { get; set; } = 5.0;
|
public double MaxAspectRatio { get; set; } = 5.0;
|
||||||
public double MinUtilization { get; set; } = 0.3;
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
public double UtilizationOverride { get; set; } = 0.75;
|
public double UtilizationOverride { get; set; } = 0.75;
|
||||||
|
|
||||||
public void Apply(List<BestFitResult> results)
|
public void Apply(List<BestFitResult> results)
|
||||||
@@ -40,16 +47,6 @@ namespace OpenNest.Engine.BestFit
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.Utilization < MinUtilization)
|
|
||||||
{
|
|
||||||
result.Keep = false;
|
|
||||||
result.Reason = string.Format(
|
|
||||||
"Utilization {0:P0} below minimum",
|
|
||||||
result.Utilization
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
result.Reason = "Valid";
|
result.Reason = "Valid";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,34 @@ public class NfpBestFitIntegrationTests
|
|||||||
Assert.True(bestUtilization > 0.5);
|
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]
|
[Fact]
|
||||||
public void FindBestFits_NoOverlaps_InKeptResults()
|
public void FindBestFits_NoOverlaps_InKeptResults()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -67,6 +67,23 @@ internal static class TestHelpers
|
|||||||
return new Drawing("square", pgm);
|
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()
|
public static Drawing MakeLShapeDrawing()
|
||||||
{
|
{
|
||||||
// CW winding matches CNC convention (OffsetSide.Left = outward)
|
// CW winding matches CNC convention (OffsetSide.Left = outward)
|
||||||
|
|||||||
Reference in New Issue
Block a user