diff --git a/OpenNest.Engine/RemnantFiller.cs b/OpenNest.Engine/RemnantFiller.cs new file mode 100644 index 0000000..44036d8 --- /dev/null +++ b/OpenNest.Engine/RemnantFiller.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using OpenNest.Geometry; + +namespace OpenNest +{ + /// + /// Iteratively fills remnant boxes with items using a RemnantFinder. + /// After each fill, re-discovers free rectangles and tries again + /// until no more items can be placed. + /// + public class RemnantFiller + { + private readonly RemnantFinder finder; + private readonly double spacing; + + public RemnantFiller(Box workArea, double spacing) + { + this.spacing = spacing; + finder = new RemnantFinder(workArea); + } + + public void AddObstacles(IEnumerable parts) + { + foreach (var part in parts) + finder.AddObstacle(part.BoundingBox.Offset(spacing)); + } + + public List FillItems( + List items, + Func> fillFunc, + CancellationToken token = default, + IProgress progress = null) + { + if (items == null || items.Count == 0) + return new List(); + + var allParts = new List(); + var madeProgress = true; + + // Track quantities locally — do not mutate the input NestItem objects. + var localQty = new Dictionary(); + foreach (var item in items) + localQty[item.Drawing.Name] = item.Quantity; + + while (madeProgress && !token.IsCancellationRequested) + { + madeProgress = false; + + var minRemnantDim = double.MaxValue; + foreach (var item in items) + { + var qty = localQty[item.Drawing.Name]; + if (qty <= 0) + continue; + var bb = item.Drawing.Program.BoundingBox(); + var dim = System.Math.Min(bb.Width, bb.Length); + if (dim < minRemnantDim) + minRemnantDim = dim; + } + + if (minRemnantDim == double.MaxValue) + break; + + var freeBoxes = finder.FindRemnants(minRemnantDim); + + if (freeBoxes.Count == 0) + break; + + foreach (var item in items) + { + if (token.IsCancellationRequested) + break; + + var qty = localQty[item.Drawing.Name]; + if (qty == 0) + continue; + + var itemBbox = item.Drawing.Program.BoundingBox(); + var minItemDim = System.Math.Min(itemBbox.Width, itemBbox.Length); + + foreach (var box in freeBoxes) + { + if (System.Math.Min(box.Width, box.Length) < minItemDim) + continue; + + var fillItem = new NestItem { Drawing = item.Drawing, Quantity = qty }; + var remnantParts = fillFunc(fillItem, box); + + if (remnantParts != null && remnantParts.Count > 0) + { + allParts.AddRange(remnantParts); + localQty[item.Drawing.Name] = System.Math.Max(0, qty - remnantParts.Count); + + foreach (var p in remnantParts) + finder.AddObstacle(p.BoundingBox.Offset(spacing)); + + madeProgress = true; + break; + } + } + + if (madeProgress) + break; + } + } + + return allParts; + } + } +} diff --git a/OpenNest.Tests/RemnantFillerTests2.cs b/OpenNest.Tests/RemnantFillerTests2.cs new file mode 100644 index 0000000..2ee8c6a --- /dev/null +++ b/OpenNest.Tests/RemnantFillerTests2.cs @@ -0,0 +1,105 @@ +using OpenNest.Geometry; + +namespace OpenNest.Tests; + +public class RemnantFillerTests2 +{ + private static Drawing MakeSquareDrawing(double size) + { + var pgm = new OpenNest.CNC.Program(); + pgm.Codes.Add(new OpenNest.CNC.RapidMove(new Vector(0, 0))); + pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(size, 0))); + pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(size, size))); + pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, size))); + pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, 0))); + return new Drawing("sq", pgm); + } + + [Fact] + public void FillItems_PlacesPartsInRemnants() + { + var workArea = new Box(0, 0, 100, 100); + var filler = new RemnantFiller(workArea, 1.0); + + // Place a large obstacle leaving a 40x100 strip on the right + filler.AddObstacles(new[] { TestHelpers.MakePartAt(0, 0, 50) }); + + var drawing = MakeSquareDrawing(10); + var items = new List + { + new NestItem { Drawing = drawing, Quantity = 5 } + }; + + Func> fillFunc = (ni, b) => + { + var plate = new Plate(b.Width, b.Length); + var engine = new DefaultNestEngine(plate); + return engine.Fill(ni, b, null, System.Threading.CancellationToken.None); + }; + + var placed = filler.FillItems(items, fillFunc); + + Assert.True(placed.Count > 0, "Should place parts in remaining space"); + } + + [Fact] + public void FillItems_DoesNotMutateItemQuantities() + { + var workArea = new Box(0, 0, 100, 100); + var filler = new RemnantFiller(workArea, 1.0); + + var drawing = MakeSquareDrawing(10); + var items = new List + { + new NestItem { Drawing = drawing, Quantity = 3 } + }; + + Func> fillFunc = (ni, b) => + { + var plate = new Plate(b.Width, b.Length); + var engine = new DefaultNestEngine(plate); + return engine.Fill(ni, b, null, System.Threading.CancellationToken.None); + }; + + filler.FillItems(items, fillFunc); + + Assert.Equal(3, items[0].Quantity); + } + + [Fact] + public void FillItems_EmptyItems_ReturnsEmpty() + { + var workArea = new Box(0, 0, 100, 100); + var filler = new RemnantFiller(workArea, 1.0); + + Func> fillFunc = (ni, b) => new List(); + + var result = filler.FillItems(new List(), fillFunc); + + Assert.Empty(result); + } + + [Fact] + public void FillItems_RespectsCancellation() + { + var cts = new System.Threading.CancellationTokenSource(); + cts.Cancel(); + + var workArea = new Box(0, 0, 100, 100); + var filler = new RemnantFiller(workArea, 1.0); + + var drawing = MakeSquareDrawing(10); + var items = new List + { + new NestItem { Drawing = drawing, Quantity = 5 } + }; + + Func> fillFunc = (ni, b) => + new List { TestHelpers.MakePartAt(0, 0, 10) }; + + var result = filler.FillItems(items, fillFunc, cts.Token); + + // Should not throw, returns whatever was placed + Assert.NotNull(result); + } +}