diff --git a/OpenNest.Engine.Tests/Jobs/FillStackCharacterizationTests.cs b/OpenNest.Engine.Tests/Jobs/FillStackCharacterizationTests.cs
new file mode 100644
index 00000000..88830f6c
--- /dev/null
+++ b/OpenNest.Engine.Tests/Jobs/FillStackCharacterizationTests.cs
@@ -0,0 +1,413 @@
+using System.Globalization;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading;
+using OpenNest.CNC;
+using OpenNest.Engine.Jobs;
+using OpenNest.Engine.Jobs.Placement;
+using OpenNest.Engine.Jobs.Placement.Fillers;
+using OpenNest.Engine.Tests.NestingEngines;
+using OpenNest.Geometry;
+
+namespace OpenNest.Engine.Tests.Jobs;
+
+///
+/// Characterization pins for the Fill/fixed-strategy placement stack on irregular parts: the
+/// interactive surface (single-item fill, whole-plate nest, clamp
+/// refills) and one whole job per . Each case pins the
+/// sorted poses (part, X, Y, rotation rounded to 1e-6) as a count plus SHA-256 digest; a failure
+/// prints the actual layout. They exist to prove filler/nester refactors layout-neutral, so a
+/// changed digest is a layout change, never a value to re-capture without explaining it.
+///
+///
+/// Captured on Windows/.NET 8 without the optional ML angle model, so irregular parts take the
+/// 5-degree sweep; each case gave one layout over 5 test processes of 2 solves each, and every
+/// case solves twice in-process and must agree. The whole-plate Fill nests fill several
+/// irregular drawings with one filler, so later items use the angles
+/// AngleCandidateBuilder.RecordProductive learned from earlier ones.
+/// Not pinned: richer mixes that pair parallelogram, chevron or tee parts at oblique angles
+/// with other irregular items. Their Fill and Vertical Remnant nests vary between runs, most
+/// likely because best-fit pair candidates are gathered in a ConcurrentBag and equal
+/// scores keep that arbitrary order.
+///
+public class FillStackCharacterizationTests
+{
+ private static Program LShape()
+ {
+ var program = new Program();
+ program.MoveTo(0, 0);
+ program.LineTo(6, 0);
+ program.LineTo(6, 4);
+ program.LineTo(3, 4);
+ program.LineTo(3, 2);
+ program.LineTo(0, 2);
+ program.LineTo(0, 0);
+ return program;
+ }
+
+ /// 6x4 block with a 2x2 notch cut into the middle of its top edge.
+ private static Program Notched()
+ {
+ var program = new Program();
+ program.MoveTo(0, 0);
+ program.LineTo(6, 0);
+ program.LineTo(6, 4);
+ program.LineTo(4, 4);
+ program.LineTo(4, 2);
+ program.LineTo(2, 2);
+ program.LineTo(2, 4);
+ program.LineTo(0, 4);
+ program.LineTo(0, 0);
+ return program;
+ }
+
+ /// D-shape: 3x5 block closed by a semicircular arc.
+ private static Program ArcPart()
+ {
+ var program = new Program();
+ program.MoveTo(0, 0);
+ program.LineTo(3, 0);
+ program.ArcTo(3, 5, 3, 2.5, RotationType.CCW);
+ program.LineTo(0, 5);
+ program.LineTo(0, 0);
+ return program;
+ }
+
+ ///
+ /// 46 x 3 bar with a stepped end: irregular, and too long for the 48 x 28 work area at all
+ /// but the sweep angles near 0 and 180 degrees, so only those prove productive and get learned.
+ ///
+ private static Program LongBar()
+ {
+ var program = new Program();
+ program.MoveTo(0, 0);
+ program.LineTo(46, 0);
+ program.LineTo(46, 3);
+ program.LineTo(42, 3);
+ program.LineTo(42, 1.5);
+ program.LineTo(0, 1.5);
+ program.LineTo(0, 0);
+ return program;
+ }
+
+ ///
+ /// 6 x 4 chevron. Filled after , its layout differs between the learned
+ /// (pruned) angle list and a full sweep.
+ ///
+ private static Program Chevron()
+ {
+ var program = new Program();
+ program.MoveTo(0, 0);
+ program.LineTo(3, 2);
+ program.LineTo(6, 0);
+ program.LineTo(6, 2);
+ program.LineTo(3, 4);
+ program.LineTo(0, 2);
+ program.LineTo(0, 0);
+ return program;
+ }
+
+ private static Program Shape(string name) =>
+ name switch
+ {
+ "lshape" => LShape(),
+ "notched" => Notched(),
+ "arc" => ArcPart(),
+ "bar" => LongBar(),
+ "chevron" => Chevron(),
+ _ => TestDrawingFactory.Rectangle(4, 3),
+ };
+
+ private static Plate MakePlate()
+ {
+ var plate = new Plate(new Size(30, 50))
+ {
+ EdgeSpacing = new Spacing(1, 1, 1, 1),
+ PartSpacing = 0.5,
+ };
+ return plate;
+ }
+
+ /// Two front-edge clamps (all on one edge, so the trimmed-strip layout competes).
+ private static Plate MakeClampedPlate()
+ {
+ var plate = MakePlate();
+ plate.ClampZones.Add(new ClampZone("front-1", new Box(12, 0, 2, 1.5)));
+ plate.ClampZones.Add(new ClampZone("front-2", new Box(32, 0, 2, 1.5)));
+ return plate;
+ }
+
+ // --- pose digests ---
+
+ private static string FormatPose(string id, double x, double y, double rotation)
+ {
+ var twoPi = System.Math.PI * 2;
+ var angle = rotation % twoPi;
+ if (angle < 0)
+ angle += twoPi;
+ if (twoPi - angle < 1e-9)
+ angle = 0;
+ return string.Format(
+ CultureInfo.InvariantCulture,
+ "{0}@({1},{2},{3})",
+ id,
+ Round(x),
+ Round(y),
+ Round(angle)
+ );
+ }
+
+ // Normalizes negative zero so rounding never splits one pose into two spellings.
+ private static string Round(double value) =>
+ (System.Math.Round(value, 6) + 0.0).ToString("F6", CultureInfo.InvariantCulture);
+
+ private static string Layout(IEnumerable parts) =>
+ string.Join(
+ "\n",
+ parts
+ .Select(p => FormatPose(p.BaseDrawing.Name, p.Location.X, p.Location.Y, p.Rotation))
+ .Order(StringComparer.Ordinal)
+ );
+
+ private static string Layout(NestJobResult result) =>
+ string.Join(
+ "\n",
+ result
+ .Plates.SelectMany(plate =>
+ plate.Placements.Select(p =>
+ $"{plate.PlateIndex}:{p.InstanceIndex}:"
+ + FormatPose(p.PartId, p.X, p.Y, p.Rotation)
+ )
+ )
+ .Order(StringComparer.Ordinal)
+ );
+
+ private static string Digest(string layout) =>
+ Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(layout)))[..16];
+
+ /// Runs twice and requires identical layouts.
+ private static string Stable(Func solve)
+ {
+ var first = solve();
+ var second = solve();
+ Assert.True(
+ first == second,
+ $"layout must be deterministic across repeat runs:\n{first}\n---\n{second}"
+ );
+ return first;
+ }
+
+ private static void AssertPinned(string layout, int expectedCount, string expectedDigest)
+ {
+ var count = layout.Split('\n').Count(line => line.Contains('@'));
+ var digest = Digest(layout);
+ Assert.True(
+ count == expectedCount && digest == expectedDigest,
+ $"layout changed: expected {expectedCount} parts digest {expectedDigest}, "
+ + $"actual {count} parts digest {digest}\n{layout}"
+ );
+ }
+
+ // --- interactive single-item fill ---
+
+ [Theory]
+ [InlineData("Fill", "lshape", 52, "F1FB09254D935531")]
+ [InlineData("Fill", "notched", 46, "32FB33C3C19FDCC8")]
+ [InlineData("Fill", "arc", 40, "4E2F91AA3151E9E7")]
+ [InlineData("Strip", "lshape", 52, "F1FB09254D935531")]
+ [InlineData("Strip", "notched", 46, "32FB33C3C19FDCC8")]
+ [InlineData("Strip", "arc", 40, "4E2F91AA3151E9E7")]
+ [InlineData("Vertical Remnant", "lshape", 52, "F1FB09254D935531")]
+ [InlineData("Vertical Remnant", "notched", 46, "32FB33C3C19FDCC8")]
+ [InlineData("Vertical Remnant", "arc", 40, "BBC5C583C3C7A9A7")]
+ [InlineData("Horizontal Remnant", "lshape", 52, "F1FB09254D935531")]
+ [InlineData("Horizontal Remnant", "notched", 46, "32FB33C3C19FDCC8")]
+ [InlineData("Horizontal Remnant", "arc", 40, "4E2F91AA3151E9E7")]
+ public void FillItem_IrregularPart_PinnedLayout(
+ string strategy,
+ string shape,
+ int expectedCount,
+ string expectedDigest
+ )
+ {
+ var layout = Stable(() =>
+ {
+ var plate = MakePlate();
+ var item = new NestItem { Drawing = new Drawing(shape, Shape(shape)), Quantity = 0 };
+ return Layout(
+ PlateFillService.FillItem(
+ strategy,
+ plate,
+ item,
+ plate.WorkArea(),
+ null,
+ CancellationToken.None
+ )
+ );
+ });
+
+ AssertPinned(layout, expectedCount, expectedDigest);
+ }
+
+ // --- interactive whole-plate nest ---
+
+ ///
+ /// Three irregular fill items (each over 10% of the plate) plus a single rectangle that
+ /// takes the pack path. Strip runs the iterative shrink fill on the multi-quantity items.
+ ///
+ private static List MixedItems() =>
+ [
+ new() { Drawing = new Drawing("lshape", LShape()), Quantity = 14 },
+ new() { Drawing = new Drawing("notched", Notched()), Quantity = 9 },
+ new() { Drawing = new Drawing("arc", ArcPart()), Quantity = 11 },
+ new() { Drawing = new Drawing("rect", TestDrawingFactory.Rectangle(4, 3)), Quantity = 1 },
+ ];
+
+ ///
+ /// The bar fills first and is productive only near 0 and 180 degrees, so the Fill filler
+ /// prunes the chevron and notched fills to the angles it learned (see
+ /// ).
+ ///
+ private static List LearnedAngleItems() =>
+ [
+ new() { Drawing = new Drawing("bar", LongBar()), Quantity = 3 },
+ new() { Drawing = new Drawing("chevron", Chevron()), Quantity = 20 },
+ new() { Drawing = new Drawing("notched", Notched()), Quantity = 6 },
+ new() { Drawing = new Drawing("rect", TestDrawingFactory.Rectangle(4, 3)), Quantity = 1 },
+ ];
+
+ private static string NestLayout(string strategy, Func> createItems)
+ {
+ var plate = MakePlate();
+ var items = createItems();
+ var parts = PlateFillService.Nest(strategy, plate, items, null, CancellationToken.None);
+ // Remaining demand is part of the contract callers read back.
+ var remaining = string.Join(",", items.Select(i => $"{i.Drawing.Name}={i.Quantity}"));
+ return Layout(parts) + "\nremaining:" + remaining;
+ }
+
+ [Theory]
+ [InlineData("Fill", 33, "5F41788D261A5085")]
+ [InlineData("Strip", 35, "865B16D4365A2492")]
+ [InlineData("Vertical Remnant", 35, "8CC17C7708C8049F")]
+ [InlineData("Horizontal Remnant", 30, "A0700052AE771E06")]
+ public void Nest_MixedIrregularItems_PinnedLayout(
+ string strategy,
+ int expectedCount,
+ string expectedDigest
+ )
+ {
+ var layout = Stable(() => NestLayout(strategy, MixedItems));
+
+ AssertPinned(layout, expectedCount, expectedDigest);
+ }
+
+ [Theory]
+ [InlineData("Fill", 30, "5732030365D13B9A")]
+ [InlineData("Strip", 30, "A573BA6919FCD300")]
+ [InlineData("Vertical Remnant", 30, "5732030365D13B9A")]
+ [InlineData("Horizontal Remnant", 30, "C05F6A30A648B929")]
+ public void Nest_LearnedAngleItems_PinnedLayout(
+ string strategy,
+ int expectedCount,
+ string expectedDigest
+ )
+ {
+ var layout = Stable(() => NestLayout(strategy, LearnedAngleItems));
+
+ AssertPinned(layout, expectedCount, expectedDigest);
+ }
+
+ ///
+ /// Keeps sensitive to learned angles: the
+ /// Fill layout must differ from the same nest with a full sweep for every item. If this
+ /// fails, the fixture no longer catches a filler that forgets learned angles between items.
+ ///
+ [Fact]
+ public void Nest_LearnedAngleItems_DependsOnLearnedAngles()
+ {
+ string Solve(bool fullSweep)
+ {
+ var filler = new DefaultPlateFiller(MakePlate()) { ForceFullAngleSweep = fullSweep };
+ return Layout(filler.Nest(LearnedAngleItems(), null, CancellationToken.None));
+ }
+
+ Assert.NotEqual(Solve(fullSweep: true), Solve(fullSweep: false));
+ }
+
+ // --- interactive fill beside clamps ---
+
+ [Theory]
+ [InlineData("Fill", 40, "9BF5FC7FC47364A9")]
+ [InlineData("Strip", 40, "9BF5FC7FC47364A9")]
+ [InlineData("Vertical Remnant", 40, "9BF5FC7FC47364A9")]
+ [InlineData("Horizontal Remnant", 40, "C1E81564A620302A")]
+ public void FillItem_WithClampZones_PinnedLayout(
+ string strategy,
+ int expectedCount,
+ string expectedDigest
+ )
+ {
+ var layout = Stable(() =>
+ {
+ var plate = MakeClampedPlate();
+ // A quantity close to a full sheet makes clamp-dropped copies trigger refills.
+ var item = new NestItem { Drawing = new Drawing("lshape", LShape()), Quantity = 40 };
+ return Layout(
+ PlateFillService.FillItem(
+ strategy,
+ plate,
+ item,
+ plate.WorkArea(),
+ null,
+ CancellationToken.None
+ )
+ );
+ });
+
+ AssertPinned(layout, expectedCount, expectedDigest);
+ }
+
+ // --- whole-job fixed strategies ---
+
+ private static NestJobPart JobPart(string id, int quantity) =>
+ new(id, PartGeometrySnapshot.FromProgram(Shape(id)), quantity);
+
+ private static NestJob IrregularJob() =>
+ new(
+ new[]
+ {
+ JobPart("lshape", 10),
+ JobPart("notched", 6),
+ JobPart("arc", 7),
+ JobPart("rect", 3),
+ },
+ new[] { new NestPlateStock("stock", new Size(30, 50), 3, 0.5, new Spacing(1, 1, 1, 1)) },
+ new NestJobOptions("Fill")
+ );
+
+ [Theory]
+ [InlineData("Fill", 26, "64CE33B01B205978")]
+ [InlineData("Strip", 26, "A66602F707290A36")]
+ [InlineData("Vertical Remnant", 26, "BDCA1DA111B396F0")]
+ [InlineData("Horizontal Remnant", 26, "C356EC216CD56627")]
+ public void FixedStrategyEngine_IrregularJob_PinnedLayout(
+ string strategy,
+ int expectedCount,
+ string expectedDigest
+ )
+ {
+ var layout = Stable(() =>
+ {
+ var job = IrregularJob();
+ var result = new FixedStrategyNestingEngine(strategy).Solve(job);
+ Assert.Equal(NestJobStatus.Complete, result.Status);
+ // Not LayoutAssert.Valid: today the Fill and Vertical Remnant layouts of this job
+ // fail NestLayoutCheck's arc-to-arc spacing check on an interlocked arc pair, though
+ // the runner's placement validator accepts them. These tests pin current behavior.
+ return Layout(result);
+ });
+
+ AssertPinned(layout, expectedCount, expectedDigest);
+ }
+}