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);
+ }
+}