Files
OpenNest/OpenNest.Engine.Tests/NestingEngines/RectanglesSqueezeTests.cs
T
ajandClaude Opus 5.5 9273a44258 feat(engine): squeeze the Rectangles sheet that finishes the job
Auto Nest left N26-2A5 (14 door stops, 14 lock plates on a clamped 26x26
sheet) 8.0 high, while the shop's hand layout holds all 28 under 7.5 and
leaves a half-inch-deeper drop. Every MaxRects fit rule packs into the
whole sheet, so with room to spare it spreads the parts out and never
looks for the shortest strip.

When a rule places all the remaining demand, Rectangles now repacks it
into shorter and narrower bins, every rule and mode per probe, and offers
the tightest complete layout per axis to the stock loop, which still
picks by cost and offcut. Fit is not monotonic in bin size (7.5 packs,
7.62 and 7.74 do not), so probes walk down through stack heights (sums of
spaced part sides from the bin edge or a reached clamp) instead of
bisecting. A placement budget skips sheets finishing ~2000+ parts.

Benchmark, Rectangles on 80 recent production nests with salvage credit:
11 cheaper, 0 worse, all valid, total time 14.7 s -> 13.2 s.

The zero-clamp hash is re-captured (the squeezed layout is narrower);
the far-clamp test now compares poses, since the clamp can reorder an
identical layout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 11:10:06 -05:00

70 lines
2.8 KiB
C#

using OpenNest.Converters;
using OpenNest.Engine.Jobs;
using OpenNest.Engine.Jobs.Adapters;
using OpenNest.Engine.NestingEngines.Rectangles;
using OpenNest.Geometry;
using static OpenNest.Engine.Tests.NestingEngines.JobBuilder;
using static OpenNest.Engine.Tests.NestingEngines.Shapes;
namespace OpenNest.Engine.Tests.NestingEngines;
/// <summary>
/// The sheet that finishes a job is re-packed into a shorter or narrower bin, so the drop it
/// leaves is as large as the packer can make it.
/// </summary>
public class RectanglesSqueezeTests
{
/// <summary>
/// N26-2A5: 14 door stops (4.168 x 1.5) and 14 lock plates (2 x 2) on a 26 x 26 sheet with a
/// front clamp. Every fit rule on the whole sheet stopped at 8.0 high; the shop's hand layout
/// holds all 28 under 7.5 (three rows of four stops, two stops on end over the clamp, a 2 x 2
/// block of plates, then a row of plates across the top), leaving an 18" drop.
/// </summary>
[Fact]
public void FinishingSheetPacksIntoTheShortestStripItFinds()
{
var clamp = new ClampZone("clamp-1", new Box(19.25, 0, 1.5, 0.625));
var job = Job(
[
Part("door stop", Rectangle(4.168108, 1.5), 14),
Part("lock plate", Rectangle(2, 2), 14),
],
[Stock("sheet", 26, 26, spacing: 0.25, edge: new Spacing(0.25, 0.25, 0.25, 0.25), clampZones: [clamp])]);
var result = new RectanglesNestingEngine().Solve(job);
LayoutAssert.Valid(job, result);
Assert.Equal(NestJobStatus.Complete, result.Status);
Assert.Single(result.Plates);
Assert.True(Top(job, result.Plates[0]) <= 7.5 + 1e-9, $"layout top {Top(job, result.Plates[0])}");
}
[Fact]
public void SheetsThatDoNotFinishTheJobAreNotSqueezed()
{
// Four 9 x 9 squares fill each 20 x 20 sheet; only the last sheet could be squeezed, and
// it repeats the full layout, so every sheet keeps one pass of the twelve rule/mode packs.
var job = Job([Rectangle("a", 9, 9, 40)], [Stock("s", 20, 20, spacing: 0.2)]);
var calls = 0;
var result = new RectanglesNestingEngine { SheetPacked = _ => calls++ }.Solve(job);
LayoutAssert.Valid(job, result);
Assert.Equal(12, calls);
}
private static double Top(NestJob job, NestJobPlateResult sheet) =>
sheet.Placements.Max(pose =>
{
var part = job.Parts.Single(p => p.Id == pose.PartId);
var geometry = ConvertProgram.ToGeometry(DrawingJobMapper.ToProgram(part.Geometry))
.Where(e => SpecialLayers.IsMaterial(e.Layer)).ToArray();
foreach (var entity in geometry)
{
entity.Rotate(pose.Rotation);
entity.Offset(pose.X, pose.Y);
}
return geometry.Max(e => e.Top);
});
}