merge: resolve polylabel conflicts, keep remote version with hole support

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-17 09:02:30 -04:00
31 changed files with 4505 additions and 673 deletions

View File

@@ -0,0 +1,51 @@
namespace OpenNest.Tests;
public class AccumulatingProgressTests
{
private class CapturingProgress : IProgress<NestProgress>
{
public NestProgress Last { get; private set; }
public void Report(NestProgress value) => Last = value;
}
[Fact]
public void Report_PrependsPreviousParts()
{
var inner = new CapturingProgress();
var previous = new List<Part> { TestHelpers.MakePartAt(0, 0, 10) };
var accumulating = new AccumulatingProgress(inner, previous);
var newParts = new List<Part> { TestHelpers.MakePartAt(20, 0, 10) };
accumulating.Report(new NestProgress { BestParts = newParts, BestPartCount = 1 });
Assert.NotNull(inner.Last);
Assert.Equal(2, inner.Last.BestParts.Count);
Assert.Equal(2, inner.Last.BestPartCount);
}
[Fact]
public void Report_NoPreviousParts_PassesThrough()
{
var inner = new CapturingProgress();
var accumulating = new AccumulatingProgress(inner, new List<Part>());
var newParts = new List<Part> { TestHelpers.MakePartAt(0, 0, 10) };
accumulating.Report(new NestProgress { BestParts = newParts, BestPartCount = 1 });
Assert.NotNull(inner.Last);
Assert.Single(inner.Last.BestParts);
}
[Fact]
public void Report_NullBestParts_PassesThrough()
{
var inner = new CapturingProgress();
var previous = new List<Part> { TestHelpers.MakePartAt(0, 0, 10) };
var accumulating = new AccumulatingProgress(inner, previous);
accumulating.Report(new NestProgress { BestParts = null });
Assert.NotNull(inner.Last);
Assert.Null(inner.Last.BestParts);
}
}

View File

@@ -0,0 +1,83 @@
using OpenNest.Geometry;
namespace OpenNest.Tests;
public class AngleCandidateBuilderTests
{
private static Drawing MakeRectDrawing(double w, double h)
{
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("rect", pgm);
}
[Fact]
public void Build_ReturnsAtLeastTwoAngles()
{
var builder = new AngleCandidateBuilder();
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
var workArea = new Box(0, 0, 100, 100);
var angles = builder.Build(item, 0, workArea);
Assert.True(angles.Count >= 2);
}
[Fact]
public void Build_NarrowWorkArea_ProducesMoreAngles()
{
var builder = new AngleCandidateBuilder();
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
var wideArea = new Box(0, 0, 100, 100);
var narrowArea = new Box(0, 0, 100, 8); // narrower than part's longest side
var wideAngles = builder.Build(item, 0, wideArea);
var narrowAngles = builder.Build(item, 0, narrowArea);
Assert.True(narrowAngles.Count > wideAngles.Count,
$"Narrow ({narrowAngles.Count}) should have more angles than wide ({wideAngles.Count})");
}
[Fact]
public void ForceFullSweep_ProducesFullSweep()
{
var builder = new AngleCandidateBuilder { ForceFullSweep = true };
var item = new NestItem { Drawing = MakeRectDrawing(5, 5) };
var workArea = new Box(0, 0, 100, 100);
var angles = builder.Build(item, 0, workArea);
// Full sweep at 5deg steps = ~36 angles (0 to 175), plus base angles
Assert.True(angles.Count > 10);
}
[Fact]
public void RecordProductive_PrunesSubsequentBuilds()
{
var builder = new AngleCandidateBuilder { ForceFullSweep = true };
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
var workArea = new Box(0, 0, 100, 8);
// First build — full sweep
var firstAngles = builder.Build(item, 0, workArea);
// Record some as productive
var productive = new List<AngleResult>
{
new AngleResult { AngleDeg = 0, PartCount = 5 },
new AngleResult { AngleDeg = 45, PartCount = 3 },
};
builder.RecordProductive(productive);
// Second build — should be pruned to known-good + base angles
builder.ForceFullSweep = false;
var secondAngles = builder.Build(item, 0, workArea);
Assert.True(secondAngles.Count < firstAngles.Count,
$"Pruned ({secondAngles.Count}) should be fewer than full ({firstAngles.Count})");
}
}

View File

@@ -0,0 +1,99 @@
using OpenNest.Geometry;
namespace OpenNest.Tests;
public class EngineRefactorSmokeTests
{
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 DefaultEngine_FillNestItem_ProducesResults()
{
var plate = new Plate(120, 60);
var engine = new DefaultNestEngine(plate);
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
var parts = engine.Fill(item, plate.WorkArea(), null, System.Threading.CancellationToken.None);
Assert.True(parts.Count > 0, "DefaultNestEngine should fill parts");
}
[Fact]
public void DefaultEngine_FillGroupParts_ProducesResults()
{
var plate = new Plate(120, 60);
var engine = new DefaultNestEngine(plate);
var drawing = MakeRectDrawing(20, 10);
var groupParts = new List<Part> { new Part(drawing) };
var parts = engine.Fill(groupParts, plate.WorkArea(), null, System.Threading.CancellationToken.None);
Assert.True(parts.Count > 0, "DefaultNestEngine group fill should produce parts");
}
[Fact]
public void DefaultEngine_ForceFullAngleSweep_StillWorks()
{
var plate = new Plate(120, 60);
var engine = new DefaultNestEngine(plate);
engine.ForceFullAngleSweep = true;
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
var parts = engine.Fill(item, plate.WorkArea(), null, System.Threading.CancellationToken.None);
Assert.True(parts.Count > 0, "ForceFullAngleSweep should still produce results");
}
[Fact]
public void StripEngine_Nest_ProducesResults()
{
var plate = new Plate(120, 60);
var engine = new StripNestEngine(plate);
var items = new List<NestItem>
{
new NestItem { Drawing = MakeRectDrawing(20, 10, "large"), Quantity = 10 },
new NestItem { Drawing = MakeRectDrawing(8, 5, "small"), Quantity = 5 },
};
var parts = engine.Nest(items, null, System.Threading.CancellationToken.None);
Assert.True(parts.Count > 0, "StripNestEngine should nest parts");
}
[Fact]
public void DefaultEngine_Nest_ProducesResults()
{
var plate = new Plate(120, 60);
var engine = new DefaultNestEngine(plate);
var items = new List<NestItem>
{
new NestItem { Drawing = MakeRectDrawing(20, 10, "a"), Quantity = 5 },
new NestItem { Drawing = MakeRectDrawing(15, 8, "b"), Quantity = 3 },
};
var parts = engine.Nest(items, null, System.Threading.CancellationToken.None);
Assert.True(parts.Count > 0, "Base Nest method should place parts");
}
[Fact]
public void BruteForceRunner_StillWorks()
{
var plate = new Plate(120, 60);
var drawing = MakeRectDrawing(20, 10);
var result = OpenNest.Engine.ML.BruteForceRunner.Run(drawing, plate, forceFullAngleSweep: true);
Assert.NotNull(result);
Assert.True(result.PartCount > 0);
}
}

View File

@@ -0,0 +1,63 @@
using OpenNest.Geometry;
namespace OpenNest.Tests;
public class PairFillerTests
{
private static Drawing MakeRectDrawing(double w, double h)
{
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("rect", pgm);
}
[Fact]
public void Fill_ReturnsPartsForSimpleDrawing()
{
var plateSize = new Size(120, 60);
var filler = new PairFiller(plateSize, 0.5);
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
var workArea = new Box(0, 0, 120, 60);
var parts = filler.Fill(item, workArea);
Assert.NotNull(parts);
// Pair filling may or may not find interlocking pairs for rectangles,
// but should return a non-null list.
}
[Fact]
public void Fill_EmptyResult_WhenPartTooLarge()
{
var plateSize = new Size(10, 10);
var filler = new PairFiller(plateSize, 0.5);
var item = new NestItem { Drawing = MakeRectDrawing(20, 20) };
var workArea = new Box(0, 0, 10, 10);
var parts = filler.Fill(item, workArea);
Assert.NotNull(parts);
Assert.Empty(parts);
}
[Fact]
public void Fill_RespectsCancellation()
{
var cts = new System.Threading.CancellationTokenSource();
cts.Cancel();
var plateSize = new Size(120, 60);
var filler = new PairFiller(plateSize, 0.5);
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
var workArea = new Box(0, 0, 120, 60);
var parts = filler.Fill(item, workArea, token: cts.Token);
// Should return empty or partial — not throw
Assert.NotNull(parts);
}
}

View File

@@ -0,0 +1,129 @@
using OpenNest.Geometry;
namespace OpenNest.Tests;
public class PolyLabelTests
{
private static Polygon Square(double size)
{
var p = new Polygon();
p.Vertices.Add(new Vector(0, 0));
p.Vertices.Add(new Vector(size, 0));
p.Vertices.Add(new Vector(size, size));
p.Vertices.Add(new Vector(0, size));
return p;
}
[Fact]
public void Square_ReturnsCenterPoint()
{
var poly = Square(100);
var result = PolyLabel.Find(poly);
Assert.Equal(50, result.X, 1.0);
Assert.Equal(50, result.Y, 1.0);
}
[Fact]
public void Triangle_ReturnsIncenter()
{
var p = new Polygon();
p.Vertices.Add(new Vector(0, 0));
p.Vertices.Add(new Vector(100, 0));
p.Vertices.Add(new Vector(50, 86.6));
var result = PolyLabel.Find(p);
// Incenter of equilateral triangle is at (50, ~28.9)
Assert.Equal(50, result.X, 1.0);
Assert.Equal(28.9, result.Y, 1.0);
Assert.True(p.ContainsPoint(result));
}
[Fact]
public void LShape_ReturnsPointInBottomLobe()
{
// L-shape: 100x100 with 50x50 cut from top-right
var p = new Polygon();
p.Vertices.Add(new Vector(0, 0));
p.Vertices.Add(new Vector(100, 0));
p.Vertices.Add(new Vector(100, 50));
p.Vertices.Add(new Vector(50, 50));
p.Vertices.Add(new Vector(50, 100));
p.Vertices.Add(new Vector(0, 100));
var result = PolyLabel.Find(p);
Assert.True(p.ContainsPoint(result));
// The bottom 100x50 lobe is the widest region
Assert.True(result.Y < 50, $"Expected label in bottom lobe, got Y={result.Y}");
}
[Fact]
public void ThinRectangle_CenteredOnBothAxes()
{
var p = new Polygon();
p.Vertices.Add(new Vector(0, 0));
p.Vertices.Add(new Vector(200, 0));
p.Vertices.Add(new Vector(200, 10));
p.Vertices.Add(new Vector(0, 10));
var result = PolyLabel.Find(p);
Assert.Equal(100, result.X, 1.0);
Assert.Equal(5, result.Y, 1.0);
Assert.True(p.ContainsPoint(result));
}
[Fact]
public void SquareWithLargeHole_AvoidsHole()
{
var outer = Square(100);
var hole = new Polygon();
hole.Vertices.Add(new Vector(20, 20));
hole.Vertices.Add(new Vector(80, 20));
hole.Vertices.Add(new Vector(80, 80));
hole.Vertices.Add(new Vector(20, 80));
var result = PolyLabel.Find(outer, new[] { hole });
// Point should be inside outer but outside hole
Assert.True(outer.ContainsPoint(result));
Assert.False(hole.ContainsPoint(result));
}
[Fact]
public void CShape_ReturnsPointInLeftBar()
{
// C-shape opening to the right: left bar is 20 wide, top/bottom arms are 20 tall
var p = new Polygon();
p.Vertices.Add(new Vector(0, 0));
p.Vertices.Add(new Vector(100, 0));
p.Vertices.Add(new Vector(100, 20));
p.Vertices.Add(new Vector(20, 20));
p.Vertices.Add(new Vector(20, 80));
p.Vertices.Add(new Vector(100, 80));
p.Vertices.Add(new Vector(100, 100));
p.Vertices.Add(new Vector(0, 100));
var result = PolyLabel.Find(p);
Assert.True(p.ContainsPoint(result));
// Label should be in the left vertical bar (x < 20), not at bbox center (50, 50)
Assert.True(result.X < 20, $"Expected label in left bar, got X={result.X}");
}
[Fact]
public void DegeneratePolygon_ReturnsFallback()
{
var p = new Polygon();
p.Vertices.Add(new Vector(5, 5));
var result = PolyLabel.Find(p);
Assert.Equal(5, result.X, 0.01);
Assert.Equal(5, result.Y, 0.01);
}
}

View File

@@ -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<NestItem>
{
new NestItem { Drawing = drawing, Quantity = 5 }
};
Func<NestItem, Box, List<Part>> 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<NestItem>
{
new NestItem { Drawing = drawing, Quantity = 3 }
};
Func<NestItem, Box, List<Part>> 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<NestItem, Box, List<Part>> fillFunc = (ni, b) => new List<Part>();
var result = filler.FillItems(new List<NestItem>(), 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<NestItem>
{
new NestItem { Drawing = drawing, Quantity = 5 }
};
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
new List<Part> { TestHelpers.MakePartAt(0, 0, 10) };
var result = filler.FillItems(items, fillFunc, cts.Token);
// Should not throw, returns whatever was placed
Assert.NotNull(result);
}
}

View File

@@ -1,4 +1,5 @@
using OpenNest.Geometry;
using OpenNest.IO;
namespace OpenNest.Tests;
@@ -269,4 +270,101 @@ public class RemnantFinderTests
// Should find gaps between obstacles
Assert.True(remnants.Count > 0);
}
[Fact]
public void SingleObstacle_NearEdge_FindsRemnantsOnAllSides()
{
// Obstacle near top-left: should find remnants above, below, and to the right.
var finder = new RemnantFinder(new Box(0, 0, 120, 60));
finder.AddObstacle(new Box(0, 47, 21, 6));
var remnants = finder.FindRemnants();
var above = remnants.FirstOrDefault(r => r.Bottom >= 53 - 0.1 && r.Width > 50);
var below = remnants.FirstOrDefault(r => r.Top <= 47 + 0.1 && r.Width > 50);
var right = remnants.FirstOrDefault(r => r.Left >= 21 - 0.1 && r.Length > 50);
Assert.NotNull(above);
Assert.NotNull(below);
Assert.NotNull(right);
}
[Fact]
public void LoadNestFile_FindsGapAboveMainGrid()
{
var nestFile = @"C:\Users\AJ\Desktop\no_remnant_found.nest";
if (!File.Exists(nestFile))
return; // Skip if file not available.
var reader = new NestReader(nestFile);
var nest = reader.Read();
var plate = nest.Plates[0];
var finder = RemnantFinder.FromPlate(plate);
// Use smallest drawing bbox dimension as minDim (same as UI).
var minDim = nest.Drawings.Min(d =>
System.Math.Min(d.Program.BoundingBox().Width, d.Program.BoundingBox().Length));
var tiered = finder.FindTieredRemnants(minDim);
// Should find a remnant near (0.25, 53.13) — the gap above the main grid.
var topGap = tiered.FirstOrDefault(t =>
t.Box.Bottom > 50 && t.Box.Bottom < 55 &&
t.Box.Left < 1 &&
t.Box.Width > 100 &&
t.Box.Length > 5);
Assert.True(topGap.Box.Width > 0, "Expected remnant above main grid");
}
[Fact]
public void DensePack_FindsGapAtTop()
{
// Reproduce real plate: 120x60, 68 parts of SULLYS-004.
// Main grid tops out at y=53.14 (obstacle). Two rotated parts on the
// right extend to y=58.49 but only at x > 106. The gap at x < 106
// from y=53.14 to y=59.8 is ~106 x 6.66 — should be found.
var workArea = new Box(0.2, 0.8, 119.5, 59.0);
var obstacles = new List<Box>();
var spacing = 0.25;
// Main grid: 5 columns x 12 rows (6 pairs).
// Even rows: bbox bottom offsets, odd rows: different offsets.
double[] colX = { 0.25, 21.08, 41.90, 62.73, 83.56 };
double[] colXOdd = { 0.81, 21.64, 42.46, 63.29, 84.12 };
double[] evenY = { 3.67, 12.41, 21.14, 29.87, 38.60, 47.33 };
double[] oddY = { 0.75, 9.48, 18.21, 26.94, 35.67, 44.40 };
foreach (var cx in colX)
foreach (var ey in evenY)
obstacles.Add(new Box(cx - spacing, ey - spacing, 20.65 + spacing * 2, 5.56 + spacing * 2));
foreach (var cx in colXOdd)
foreach (var oy in oddY)
obstacles.Add(new Box(cx - spacing, oy - spacing, 20.65 + spacing * 2, 5.56 + spacing * 2));
// Right-side rotated parts (only 2 extend high: parts 62 and 66).
obstacles.Add(new Box(106.70 - spacing, 37.59 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
obstacles.Add(new Box(114.19 - spacing, 37.59 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
// Parts 63, 67 (lower rotated)
obstacles.Add(new Box(105.02 - spacing, 29.35 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
obstacles.Add(new Box(112.51 - spacing, 29.35 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
// Parts 60, 64 (upper-right rotated, lower)
obstacles.Add(new Box(106.70 - spacing, 8.99 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
obstacles.Add(new Box(114.19 - spacing, 8.99 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
// Parts 61, 65
obstacles.Add(new Box(105.02 - spacing, 0.75 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
obstacles.Add(new Box(112.51 - spacing, 0.75 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2));
var finder = new RemnantFinder(workArea, obstacles);
var remnants = finder.FindRemnants(5.375);
// The gap at x < 106 from y=53.14 to y=59.8 should be found.
Assert.True(remnants.Count > 0, "Should find gap above main grid");
var topRemnant = remnants.FirstOrDefault(r => r.Length >= 5.375 && r.Width > 50);
Assert.NotNull(topRemnant);
// Verify dimensions are close to the expected ~104 x 6.6 gap.
Assert.True(topRemnant.Width > 100, $"Expected width > 100, got {topRemnant.Width:F1}");
Assert.True(topRemnant.Length > 6, $"Expected length > 6, got {topRemnant.Length:F1}");
}
}

View File

@@ -0,0 +1,99 @@
using OpenNest.Geometry;
namespace OpenNest.Tests;
public class ShrinkFillerTests
{
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("square", pgm);
}
[Fact]
public void Shrink_ReducesDimension_UntilCountDrops()
{
var drawing = MakeSquareDrawing(10);
var item = new NestItem { Drawing = drawing };
var box = new Box(0, 0, 100, 50);
Func<NestItem, Box, List<Part>> 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 = ShrinkFiller.Shrink(fillFunc, item, box, 1.0, ShrinkAxis.Height);
Assert.NotNull(result);
Assert.True(result.Parts.Count > 0);
Assert.True(result.Dimension <= 50, "Dimension should be <= original");
Assert.True(result.Dimension > 0);
}
[Fact]
public void Shrink_Width_ReducesHorizontally()
{
var drawing = MakeSquareDrawing(10);
var item = new NestItem { Drawing = drawing };
var box = new Box(0, 0, 100, 50);
Func<NestItem, Box, List<Part>> 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 = ShrinkFiller.Shrink(fillFunc, item, box, 1.0, ShrinkAxis.Width);
Assert.NotNull(result);
Assert.True(result.Parts.Count > 0);
Assert.True(result.Dimension <= 100);
}
[Fact]
public void Shrink_RespectsMaxIterations()
{
var callCount = 0;
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
{
callCount++;
return new List<Part> { TestHelpers.MakePartAt(0, 0, 5) };
};
var item = new NestItem { Drawing = MakeSquareDrawing(5) };
var box = new Box(0, 0, 100, 100);
ShrinkFiller.Shrink(fillFunc, item, box, 1.0, ShrinkAxis.Height, maxIterations: 3);
// 1 initial + up to 3 shrink iterations = max 4 calls
Assert.True(callCount <= 4);
}
[Fact]
public void Shrink_RespectsCancellation()
{
var cts = new System.Threading.CancellationTokenSource();
cts.Cancel();
var drawing = MakeSquareDrawing(10);
var item = new NestItem { Drawing = drawing };
var box = new Box(0, 0, 100, 50);
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
new List<Part> { TestHelpers.MakePartAt(0, 0, 10) };
var result = ShrinkFiller.Shrink(fillFunc, item, box, 1.0,
ShrinkAxis.Height, token: cts.Token);
Assert.NotNull(result);
Assert.True(result.Parts.Count > 0);
}
}