feat: add scrap zone identification to MultiPlateNester

Adds IsScrapRemnant(), FindScrapZones(), and FindViableRemnants() to
MultiPlateNester. A remnant is scrap only when both dimensions fall
below the minimum remnant size threshold (AND logic, not OR).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-06 13:53:32 -04:00
parent 35e89600d0
commit af57153269
2 changed files with 79 additions and 0 deletions

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using OpenNest.Engine.Fill;
using OpenNest.Geometry;
namespace OpenNest
@@ -56,5 +57,40 @@ namespace OpenNest
return PartClass.Small;
}
public static bool IsScrapRemnant(Box remnant, double minRemnantSize)
{
return remnant.Width < minRemnantSize && remnant.Length < minRemnantSize;
}
public static List<Box> FindScrapZones(Plate plate, double minRemnantSize)
{
var finder = RemnantFinder.FromPlate(plate);
var remnants = finder.FindRemnants();
var scrap = new List<Box>();
foreach (var remnant in remnants)
{
if (IsScrapRemnant(remnant, minRemnantSize))
scrap.Add(remnant);
}
return scrap;
}
public static List<Box> FindViableRemnants(Plate plate, double minRemnantSize)
{
var finder = RemnantFinder.FromPlate(plate);
var remnants = finder.FindRemnants();
var viable = new List<Box>();
foreach (var remnant in remnants)
{
if (!IsScrapRemnant(remnant, minRemnantSize))
viable.Add(remnant);
}
return viable;
}
}
}

View File

@@ -103,4 +103,47 @@ public class MultiPlateNesterTests
var result = MultiPlateNester.Classify(bb, workArea);
Assert.Equal(PartClass.Small, result);
}
// --- Task 5: Scrap Zone Identification ---
[Fact]
public void IsScrapRemnant_BothDimensionsBelowThreshold_ReturnsTrue()
{
var remnant = new Box(0, 0, 10, 8);
Assert.True(MultiPlateNester.IsScrapRemnant(remnant, 12.0));
}
[Fact]
public void IsScrapRemnant_OneDimensionAboveThreshold_ReturnsFalse()
{
// 11 x 120 — narrow but long, should be preserved
var remnant = new Box(0, 0, 11, 120);
Assert.False(MultiPlateNester.IsScrapRemnant(remnant, 12.0));
}
[Fact]
public void IsScrapRemnant_BothDimensionsAboveThreshold_ReturnsFalse()
{
var remnant = new Box(0, 0, 20, 30);
Assert.False(MultiPlateNester.IsScrapRemnant(remnant, 12.0));
}
[Fact]
public void FindScrapZones_ReturnsOnlyScrapRemnants()
{
// 96x48 plate with a 70x40 part placed at origin
var plate = new Plate(96, 48) { PartSpacing = 0.25 };
var drawing = MakeDrawing("big", 70, 40);
var part = new Part(drawing);
plate.Parts.Add(part);
var scrap = MultiPlateNester.FindScrapZones(plate, 12.0);
// All returned zones should have both dims < 12
foreach (var zone in scrap)
{
Assert.True(zone.Width < 12.0 && zone.Length < 12.0,
$"Zone {zone.Width:F1}x{zone.Length:F1} is not scrap — at least one dimension >= 12");
}
}
}