From af57153269f0cf30692c43b70b89921767744a35 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Mon, 6 Apr 2026 13:53:32 -0400 Subject: [PATCH] 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 --- OpenNest.Engine/MultiPlateNester.cs | 36 ++++++++++++++++ .../Engine/MultiPlateNesterTests.cs | 43 +++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/OpenNest.Engine/MultiPlateNester.cs b/OpenNest.Engine/MultiPlateNester.cs index bfdab57..c838a79 100644 --- a/OpenNest.Engine/MultiPlateNester.cs +++ b/OpenNest.Engine/MultiPlateNester.cs @@ -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 FindScrapZones(Plate plate, double minRemnantSize) + { + var finder = RemnantFinder.FromPlate(plate); + var remnants = finder.FindRemnants(); + + var scrap = new List(); + foreach (var remnant in remnants) + { + if (IsScrapRemnant(remnant, minRemnantSize)) + scrap.Add(remnant); + } + + return scrap; + } + + public static List FindViableRemnants(Plate plate, double minRemnantSize) + { + var finder = RemnantFinder.FromPlate(plate); + var remnants = finder.FindRemnants(); + + var viable = new List(); + foreach (var remnant in remnants) + { + if (!IsScrapRemnant(remnant, minRemnantSize)) + viable.Add(remnant); + } + + return viable; + } } } diff --git a/OpenNest.Tests/Engine/MultiPlateNesterTests.cs b/OpenNest.Tests/Engine/MultiPlateNesterTests.cs index 0648e5e..c2c41a9 100644 --- a/OpenNest.Tests/Engine/MultiPlateNesterTests.cs +++ b/OpenNest.Tests/Engine/MultiPlateNesterTests.cs @@ -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"); + } + } }