diff --git a/OpenNest.Engine/Fill/IterativeShrinkFiller.cs b/OpenNest.Engine/Fill/IterativeShrinkFiller.cs
new file mode 100644
index 0000000..46c6e6f
--- /dev/null
+++ b/OpenNest.Engine/Fill/IterativeShrinkFiller.cs
@@ -0,0 +1,116 @@
+using OpenNest.Geometry;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+
+namespace OpenNest.Engine.Fill
+{
+ ///
+ /// Result returned by .
+ ///
+ public class IterativeShrinkResult
+ {
+ public List Parts { get; set; } = new List();
+ public List Leftovers { get; set; } = new List();
+ }
+
+ ///
+ /// Composes and with
+ /// dual-direction shrink selection. Wraps the caller's fill function in a
+ /// closure that tries both and
+ /// , picks the better ,
+ /// and passes the wrapper to .
+ ///
+ public static class IterativeShrinkFiller
+ {
+ public static IterativeShrinkResult Fill(
+ List items,
+ Box workArea,
+ Func> fillFunc,
+ double spacing,
+ CancellationToken token = default)
+ {
+ if (items == null || items.Count == 0)
+ return new IterativeShrinkResult();
+
+ // RemnantFiller.FillItems skips items with Quantity <= 0 (its localQty
+ // check treats them as "done"). Convert unlimited items to an estimated
+ // max capacity so they are actually processed.
+ var workItems = new List(items.Count);
+ var unlimitedDrawings = new HashSet();
+
+ foreach (var item in items)
+ {
+ if (item.Quantity <= 0)
+ {
+ var bbox = item.Drawing.Program.BoundingBox();
+ var estimatedMax = bbox.Area() > 0
+ ? (int)(workArea.Area() / bbox.Area()) * 2
+ : 1000;
+
+ unlimitedDrawings.Add(item.Drawing.Name);
+ workItems.Add(new NestItem
+ {
+ Drawing = item.Drawing,
+ Quantity = System.Math.Max(1, estimatedMax),
+ Priority = item.Priority,
+ StepAngle = item.StepAngle,
+ RotationStart = item.RotationStart,
+ RotationEnd = item.RotationEnd
+ });
+ }
+ else
+ {
+ workItems.Add(item);
+ }
+ }
+
+ var filler = new RemnantFiller(workArea, spacing);
+
+ Func> shrinkWrapper = (ni, box) =>
+ {
+ var heightResult = ShrinkFiller.Shrink(fillFunc, ni, box, spacing, ShrinkAxis.Height, token);
+ var widthResult = ShrinkFiller.Shrink(fillFunc, ni, box, spacing, ShrinkAxis.Width, token);
+
+ var heightScore = FillScore.Compute(heightResult.Parts, box);
+ var widthScore = FillScore.Compute(widthResult.Parts, box);
+
+ return widthScore > heightScore ? widthResult.Parts : heightResult.Parts;
+ };
+
+ var placed = filler.FillItems(workItems, shrinkWrapper, token);
+
+ // Build leftovers: compare placed count to original quantities.
+ // RemnantFiller.FillItems does NOT mutate NestItem.Quantity.
+ var leftovers = new List();
+ foreach (var item in items)
+ {
+ var placedCount = 0;
+ foreach (var p in placed)
+ {
+ if (p.BaseDrawing.Name == item.Drawing.Name)
+ placedCount++;
+ }
+
+ if (item.Quantity <= 0)
+ continue; // unlimited items are always "satisfied" — no leftover
+
+ var remaining = item.Quantity - placedCount;
+ if (remaining > 0)
+ {
+ leftovers.Add(new NestItem
+ {
+ Drawing = item.Drawing,
+ Quantity = remaining,
+ Priority = item.Priority,
+ StepAngle = item.StepAngle,
+ RotationStart = item.RotationStart,
+ RotationEnd = item.RotationEnd
+ });
+ }
+ }
+
+ return new IterativeShrinkResult { Parts = placed, Leftovers = leftovers };
+ }
+ }
+}
diff --git a/OpenNest.Tests/IterativeShrinkFillerTests.cs b/OpenNest.Tests/IterativeShrinkFillerTests.cs
new file mode 100644
index 0000000..cf26bbb
--- /dev/null
+++ b/OpenNest.Tests/IterativeShrinkFillerTests.cs
@@ -0,0 +1,59 @@
+using OpenNest.Engine.Fill;
+using OpenNest.Geometry;
+
+namespace OpenNest.Tests;
+
+public class IterativeShrinkFillerTests
+{
+ [Fact]
+ public void Fill_NullItems_ReturnsEmpty()
+ {
+ Func> fillFunc = (ni, b) => new List();
+ var result = IterativeShrinkFiller.Fill(null, new Box(0, 0, 100, 100), fillFunc, 1.0);
+
+ Assert.Empty(result.Parts);
+ Assert.Empty(result.Leftovers);
+ }
+
+ [Fact]
+ public void Fill_EmptyItems_ReturnsEmpty()
+ {
+ Func> fillFunc = (ni, b) => new List();
+ var result = IterativeShrinkFiller.Fill(new List(), new Box(0, 0, 100, 100), fillFunc, 1.0);
+
+ Assert.Empty(result.Parts);
+ Assert.Empty(result.Leftovers);
+ }
+
+ private static Drawing MakeRectDrawing(double w, double h, string name = "rect")
+ {
+ 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(w, 0)));
+ pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(w, h)));
+ pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, h)));
+ pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, 0)));
+ return new Drawing(name, pgm);
+ }
+
+ [Fact]
+ public void Fill_SingleItem_PlacesParts()
+ {
+ var drawing = MakeRectDrawing(20, 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 result = IterativeShrinkFiller.Fill(items, new Box(0, 0, 120, 60), fillFunc, 1.0);
+
+ Assert.True(result.Parts.Count > 0, "Should place parts");
+ }
+}