fix(packing): apply work-area slack only at plate boundaries

This commit is contained in:
aj committed 2026-10-01 08:15:57 -04:00
1 parent a26e663f01
commit 6b6928213d
5 files changed
+159 -7

No files matched your search

@@ -1,5 +1,7 @@
using OpenNest.Engine.Fill;
using OpenNest.Engine.Jobs;
using OpenNest.Engine.Jobs.Placement;
using OpenNest.Engine.NestingEngines.Rectangles;
using OpenNest.Engine.RectanglePacking;
using OpenNest.Engine.Tests.Jobs;
using OpenNest.Geometry;
@@ -44,6 +46,132 @@ public class AreaPackerTests
}
}
[Theory]
[InlineData(0, 0)]
[InlineData(1, 0)]
[InlineData(0, 3)]
[InlineData(1, 3)]
public void PackArea_AllowsBoundaryOverhangAndValidates(double spacing, double edge)
{
var plate = new Plate(new Size(10 + 2 * edge, 10 + 2 * edge))
{
PartSpacing = spacing,
EdgeSpacing = new Spacing(edge, edge),
};
var area = plate.WorkArea();
var items = new List<NestItem> { Item("panel", 10 + SheetPacker.OverhangAllowance * 0.8, 4, 2) };
var parts = PlateFillService.PackArea("Default", plate, area, items, null, CancellationToken.None);
var again = PlateFillService.PackArea("Default", plate, area, items, null, CancellationToken.None);
Assert.Equal(2, parts.Count);
Assert.Equal(parts.Select(p => (p.Location, p.Rotation)), again.Select(p => (p.Location, p.Rotation)));
Assert.All(parts, p =>
{
Assert.Equal(area.Left, p.BoundingBox.Left, 9);
Assert.True(p.BoundingBox.Right > area.Right);
Assert.True(p.BoundingBox.Right <= area.Right + NestTolerances.WorkAreaSlack);
});
Assert.True(parts[1].BoundingBox.Bottom - parts[0].BoundingBox.Top >= spacing - Eps);
var requirements = items.ToDictionary(i => i.Drawing, i => (i.Drawing.Name, i.Quantity));
Assert.Empty(NestLayoutCheck.Validate(new() { (plate, parts) }, requirements));
}
[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
[InlineData(4)]
public void PackArea_TopOverhangKeepsSpacingInEveryQuadrant(int quadrant)
{
var plate = new Plate(new Size(12, 12))
{
Quadrant = quadrant,
EdgeSpacing = new Spacing(1, 1),
PartSpacing = 1,
};
var area = plate.WorkArea();
var items = new List<NestItem> { Item("panel", 4, 10 + SheetPacker.OverhangAllowance * 0.8, 2) };
var parts = PlateFillService.PackArea("Default", plate, area, items, null, CancellationToken.None);
Assert.Equal(2, parts.Count);
Assert.All(parts, p =>
{
Assert.Equal(area.Bottom, p.BoundingBox.Bottom, 9);
Assert.True(p.BoundingBox.Top > area.Top);
Assert.True(p.BoundingBox.Top <= area.Top + NestTolerances.WorkAreaSlack);
});
var ordered = parts.OrderBy(p => p.BoundingBox.Left).ToList();
Assert.True(ordered[1].BoundingBox.Left - ordered[0].BoundingBox.Right >= 1 - Eps);
Assert.Empty(NestLayoutCheck.Validate(new() { (plate, parts) },
items.ToDictionary(i => i.Drawing, i => (i.Drawing.Name, i.Quantity))));
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void PackArea_FullAreaDemandCapIncludesSlack(bool rotated)
{
var plate = new Plate(new Size(6, 10)) { PartSpacing = 0 };
var excess = SheetPacker.OverhangAllowance * 0.8;
var items = new List<NestItem> { rotated ? Item("panel", 6 + excess, 10 + excess, 1)
: Item("panel", 10 + excess, 6 + excess, 1) };
var part = Assert.Single(PlateFillService.PackArea("Default", plate, plate.WorkArea(), items, null, CancellationToken.None));
Assert.True(part.BoundingBox.Right <= 10 + NestTolerances.WorkAreaSlack);
Assert.True(part.BoundingBox.Top <= 6 + NestTolerances.WorkAreaSlack);
}
[Theory]
[InlineData(0)]
[InlineData(1)]
public void PackArea_RejectsBeyondBoundaryAllowance(double spacing)
{
var plate = new Plate(new Size(6, 10)) { PartSpacing = spacing };
var items = new List<NestItem> { Item("panel", 10 + NestTolerances.WorkAreaSlack * 1.1, 4, 1) };
Assert.Empty(PlateFillService.PackArea("Default", plate, plate.WorkArea(), items, null, CancellationToken.None));
}
[Theory]
[InlineData(0, 0, 5, 6, false)]
[InlineData(5, 0, 5, 6, true)]
[InlineData(0, 0, 10, 3, false)]
[InlineData(0, 3, 10, 3, true)]
public void PackArea_OnlyMatchingPositivePlateEdgesAllowSlack(double x, double y, double w, double h, bool fits)
{
var plate = new Plate(new Size(6, 10)) { PartSpacing = 0.5 };
var area = new Box(x, y, w, h);
var excess = SheetPacker.OverhangAllowance * 0.8;
var items = new List<NestItem> { w == 5 ? Item("panel", w + excess, h - 0.25, 1) : Item("panel", 8, h + excess, 1) };
var parts = PlateFillService.PackArea("Default", plate, area, items, null, CancellationToken.None);
Assert.Equal(fits ? 1 : 0, parts.Count);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void MaxRects_InternalFreeEdgesStayStrict(bool horizontal)
{
var sheet = new MaxRectsSheet(10, 10, SheetPacker.OverhangAllowance, SheetPacker.OverhangAllowance);
sheet.Place(horizontal ? new Rect(5, 0, 5, 10) : new Rect(0, 5, 10, 5));
var excess = SheetPacker.OverhangAllowance * 0.8;
Assert.Null(sheet.FindBest(horizontal ? 5 + excess : 10, horizontal ? 10 : 5 + excess, FitRule.BottomLeft));
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void MaxRects_BoundaryFreeEdgesAllowSlackAfterPlacement(bool horizontal)
{
var sheet = new MaxRectsSheet(10, 10, SheetPacker.OverhangAllowance, SheetPacker.OverhangAllowance);
sheet.Place(horizontal ? new Rect(0, 0, 5, 10) : new Rect(0, 0, 10, 5));
var excess = SheetPacker.OverhangAllowance * 0.8;
var fit = sheet.FindBest(horizontal ? 5 + excess : 10, horizontal ? 10 : 5 + excess, FitRule.BottomLeft);
Assert.NotNull(fit);
Assert.Equal(horizontal ? new Rect(5, 0, 5, 10) : new Rect(0, 5, 10, 5), fit.Value.Place);
}
[Fact]
public void DefaultPackArea_PlacesDemandThatCornerPointPackingMissed()
{