Files
OpenNest/OpenNest.Engine.Tests/Jobs/ClampRailStockTests.cs
T
aj bafb311735 feat(clamps): keep the whole footprint of a partly used clamp
A clamp is an avoidance area, so a clamp that only partly reaches a
sheet (for example centered at 60.5" on a 60" sheet, or near the
origin) now keeps its whole 1.5" x 0.625" footprint instead of being
clipped to the sheet; clamps wholly beyond the sheet are still left
off. Job validation accepts a zone that hangs past the sheet edges and
still rejects one that does not reach the sheet at all. Both packing
lanes already block keep-outs extending past the bin; a hanging-clamp
solve through Rectangles, Irregular and Default keeps every part clear
of the whole inflated footprint.

User decision 2026-10-12, settling the plan's pending D2 question.
2026-10-13 18:15:49 -04:00

104 lines
4.3 KiB
C#

using OpenNest.Engine;
using OpenNest.Engine.Jobs;
using OpenNest.Engine.Jobs.Adapters;
using OpenNest.Geometry;
namespace OpenNest.Engine.Tests.Jobs;
/// <summary>
/// A template plate following the machine clamp rail offers every stock size with the rail
/// resolved for that size (D2): no clamp beyond a shorter sheet, every clamp on a longer
/// one, measured from the origin in every quadrant.
/// </summary>
public class ClampRailStockTests
{
private static readonly double[] MachineCenters = { 6, 21, 36, 51, 66, 81, 96, 111 };
private static Plate RailTemplate(int quadrant = 1)
{
var template = new Plate(48, 120) { Quadrant = quadrant, EdgeSpacing = new Spacing(0.25, 0.25, 0.25, 0.25) };
template.SetClampRail(new ClampRail(1.5, 0.625, MachineCenters));
return template;
}
private static PlateOption[] Options() =>
[
new PlateOption { Width = 48, Length = 96 },
new PlateOption { Width = 60, Length = 144 },
new PlateOption { Width = 48, Length = 60 },
];
[Fact]
public void EachOptionGetsTheRailResolvedForItsOwnSize()
{
var stock = NestStockBuilder.FromTemplate(RailTemplate(), Options());
Assert.Equal(new[] { 7, 8, 4 }, stock.Select(s => s.ClampZones.Count));
Assert.Equal(new[] { 5.25, 20.25, 35.25, 50.25 }, stock[2].ClampZones.Select(z => z.Bounds.X));
// Every offered sheet passes job validation: no clamp lies outside its sheet.
NestJobValidator.Validate(new NestJob([], stock));
}
[Theory]
[InlineData(2)]
[InlineData(3)]
public void OptionsResolveFromTheOriginWhenSheetsExtendToNegativeX(int quadrant)
{
var stock = NestStockBuilder.FromTemplate(RailTemplate(quadrant), Options());
foreach (var sheet in stock)
{
// Placement frame: the first clamp is 6" from the origin, toward -X.
var first = sheet.ClampZones.Single(z => z.Id == "clamp-1").Bounds;
Assert.Equal(-6.0, first.Center.X, 9);
}
NestJobValidator.Validate(new NestJob([], stock));
}
[Theory]
[InlineData("Rectangles")]
[InlineData("Irregular")]
[InlineData("Default")]
public void LanesAvoidTheWholeFootprintOfAClampHangingPastTheSheetEnd(string engine)
{
// 12" sheet, clamp centered 12.25" from the origin: 1" of its 1.5" lies past the end.
var template = new Plate(10, 12) { EdgeSpacing = new Spacing(0.25, 0.25, 0.25, 0.25), PartSpacing = 0.25 };
template.SetClampRail(new ClampRail(1.5, 0.625, new[] { 12.25 }));
var zone = Assert.Single(template.ClampZones);
Assert.Equal(11.5, zone.Bounds.X);
Assert.Equal(1.5, zone.Bounds.Length);
var drawing = new Drawing("square", TestDrawingFactory.Rectangle(1, 1));
var result = NestPipeline.Run(new NestPipelineRequest(engine,
[new NestItem { Drawing = drawing, Quantity = 80 }], NestStockBuilder.SinglePlate(template)));
Assert.True(result.IsValid, string.Join("; ", result.Violations));
var parts = result.Plates.SelectMany(p => p.Parts).ToList();
Assert.NotEmpty(parts);
// Independent oracle: the whole clamp grown by the bottom edge spacing.
var keepOut = zone.Bounds.Offset(0.25);
Assert.All(parts, part =>
{
var box = part.BoundingBox;
var overlapX = System.Math.Min(box.Right, keepOut.Right) - System.Math.Max(box.Left, keepOut.Left);
var overlapY = System.Math.Min(box.Top, keepOut.Top) - System.Math.Max(box.Bottom, keepOut.Bottom);
Assert.False(overlapX > 1e-6 && overlapY > 1e-6, $"{box} enters {keepOut}");
});
// Rectangles still uses the front edge beside the clamp. (Irregular may lift its whole
// block above the clamp band at the same count; that is a layout choice, not a leak.)
if (engine == "Rectangles")
Assert.Contains(parts, p => p.BoundingBox.Bottom < keepOut.Top && p.BoundingBox.Right <= keepOut.Left + 1e-6);
}
[Fact]
public void SingleSheetStockCarriesTheTemplatesRailZones()
{
var template = RailTemplate();
var single = Assert.Single(NestStockBuilder.SinglePlate(template));
Assert.Equal(8, single.ClampZones.Count);
Assert.Equal(template.ClampZones.Select(z => z.Bounds.X), single.ClampZones.Select(z => z.Bounds.X));
}
}