fix(rectangles): place parts within the layout check's work-area slack
Parts drawn a few millionths over their sheet's work area (for example a 36.125006 panel on a 36.125 sheet with no edge spacing) pass NestLayoutCheck, which allows Tolerance.Epsilon of overhang, but the Rectangles engine refused them and left them unplaced. The engine now allows 90% of that slack: a box that exceeds the sheet by no more than the allowance packs as exactly the sheet's size, so it spans the axis and the overhang lands only past the edge. The check's bounds slack is named NestTolerances.WorkAreaSlack (same value, no behavior change) so the engine and the test layout assertion share it. Rectangle-lane benchmark (91 strict + 77 boxable jobs): every layout valid; 4 jobs that left panels unplaced now place them (strict complete 86 -> 88, boxable complete 74 -> 75, one more boxable job places 2 more parts); no other job changed.
This commit is contained in:
@@ -27,8 +27,9 @@ public static class LayoutAssert
|
||||
foreach (var entity in geometry) { entity.Rotate(pose.Rotation); entity.Offset(pose.X, pose.Y); }
|
||||
var b = (L: geometry.Min(e => e.Left), B: geometry.Min(e => e.Bottom),
|
||||
R: geometry.Max(e => e.Right), T: geometry.Max(e => e.Top));
|
||||
Assert.True(b.L >= work.Left - 1e-7 && b.B >= work.Bottom - 1e-7
|
||||
&& b.R <= work.Right + 1e-7 && b.T <= work.Top + 1e-7);
|
||||
var slack = NestTolerances.WorkAreaSlack;
|
||||
Assert.True(b.L >= work.Left - slack && b.B >= work.Bottom - slack
|
||||
&& b.R <= work.Right + slack && b.T <= work.Top + slack);
|
||||
}
|
||||
}
|
||||
foreach (var part in job.Parts)
|
||||
|
||||
@@ -152,6 +152,36 @@ public class RectanglesNestingEngineTests
|
||||
LayoutAssert.Valid(job, result);
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PartAFewMillionthsWiderThanTheWorkAreaIsPlaced()
|
||||
{
|
||||
// Exported panels are often drawn a few millionths over their sheet's work area; the
|
||||
// layout check accepts that overhang, so the engine must place them. The chamfer keeps the
|
||||
// material area inside the sheet's area budget, as a real panel's corner radii do.
|
||||
var panel = Polyline((0, 0), (36.125006, 0), (36.125006, 74), (35.125006, 75), (0, 75));
|
||||
var job = Job(new[] { Part("panel", panel, 1) },
|
||||
new[] { Stock("s", 75, 36.125, spacing: 0.25, quantity: 1) });
|
||||
|
||||
var result = new RectanglesNestingEngine().Solve(job);
|
||||
|
||||
LayoutAssert.Valid(job, result);
|
||||
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PartBeyondTheOverhangAllowanceStaysUnplaced()
|
||||
{
|
||||
var panel = Polyline((0, 0), (36.12502, 0), (36.12502, 74), (35.12502, 75), (0, 75));
|
||||
var job = Job(new[] { Part("panel", panel, 1) },
|
||||
new[] { Stock("s", 75, 36.125, spacing: 0.25, quantity: 1) });
|
||||
|
||||
var result = new RectanglesNestingEngine().Solve(job);
|
||||
|
||||
LayoutAssert.Valid(job, result);
|
||||
Assert.Empty(result.Plates);
|
||||
Assert.Equal(1, Assert.Single(result.Fulfillment).Unplaced);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class RectanglesContractTests : EngineContractTests<RectanglesNestingEngine> { }
|
||||
|
||||
@@ -291,10 +291,10 @@ public static class NestLayoutCheck
|
||||
{
|
||||
var bb = MaterialBounds(part);
|
||||
|
||||
var outLeft = bb.Left < workArea.X - Tolerance.Epsilon;
|
||||
var outBottom = bb.Bottom < workArea.Y - Tolerance.Epsilon;
|
||||
var outRight = bb.Right > workArea.Right + Tolerance.Epsilon;
|
||||
var outTop = bb.Top > workArea.Top + Tolerance.Epsilon;
|
||||
var outLeft = bb.Left < workArea.X - NestTolerances.WorkAreaSlack;
|
||||
var outBottom = bb.Bottom < workArea.Y - NestTolerances.WorkAreaSlack;
|
||||
var outRight = bb.Right > workArea.Right + NestTolerances.WorkAreaSlack;
|
||||
var outTop = bb.Top > workArea.Top + NestTolerances.WorkAreaSlack;
|
||||
|
||||
if (outLeft || outBottom || outRight || outTop)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Engine.Jobs;
|
||||
|
||||
@@ -18,6 +19,11 @@ public static class NestTolerances
|
||||
/// with margin and is far below anything a cutting machine can resolve.</summary>
|
||||
public const double SpacingSlack = 0.0005;
|
||||
|
||||
/// <summary>How far material may extend past the work area and still pass the layout check's
|
||||
/// bounds test. Matches the general geometry tolerance, so a part drawn a few millionths
|
||||
/// larger than its sheet's work area (as exported dimensions often are) is still accepted.</summary>
|
||||
public const double WorkAreaSlack = Tolerance.Epsilon;
|
||||
|
||||
/// <summary>Clipper decimal precision (a 1e-4 coordinate grid).</summary>
|
||||
public const int ClipperPrecision = ClipperBridge.Precision;
|
||||
|
||||
|
||||
@@ -143,8 +143,8 @@ public sealed class RectanglesNestingEngine : INestingEngine
|
||||
private static bool FitsStock(NestPlateStock stock, BoxOrientation o)
|
||||
{
|
||||
var work = stock.WorkArea;
|
||||
return o.Width <= work.Right - work.Left + MaxRectsSheet.Eps
|
||||
&& o.Height <= work.Top - work.Bottom + MaxRectsSheet.Eps;
|
||||
return o.Width <= work.Right - work.Left + SheetPacker.OverhangAllowance
|
||||
&& o.Height <= work.Top - work.Bottom + SheetPacker.OverhangAllowance;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,21 @@ internal sealed record SheetPlan(
|
||||
/// </summary>
|
||||
internal static class SheetPacker
|
||||
{
|
||||
/// <summary>
|
||||
/// How far a box may overhang the work area. Kept just inside the layout check's
|
||||
/// <see cref="NestTolerances.WorkAreaSlack"/> so floating-point rounding cannot push an
|
||||
/// accepted box over it.
|
||||
/// </summary>
|
||||
public const double OverhangAllowance = NestTolerances.WorkAreaSlack * 0.9;
|
||||
|
||||
/// <summary>
|
||||
/// Packing size along one axis: a box that exceeds the sheet by no more than
|
||||
/// <see cref="OverhangAllowance"/> packs as exactly the sheet's size. It then spans the whole
|
||||
/// axis, so it has no neighbour there and the overhang lands only past the work-area edge.
|
||||
/// </summary>
|
||||
public static double PackSize(double size, double sheet) =>
|
||||
size > sheet && size <= sheet + OverhangAllowance ? sheet : size;
|
||||
|
||||
public static SheetPlan Pack(
|
||||
IReadOnlyList<BoxType> types, IReadOnlyList<int> remaining, NestPlateStock stock,
|
||||
FitRule rule, PickMode mode, CancellationToken token)
|
||||
@@ -43,7 +58,11 @@ internal static class SheetPacker
|
||||
var sheet = new MaxRectsSheet(work.Right - work.Left + s, work.Top - work.Bottom + s);
|
||||
var packTypes = types
|
||||
.Select(t => new PackType(
|
||||
t.Priority, t.Orientations.Select(o => (o.Width + s, o.Height + s)).ToList(), t.BoxArea))
|
||||
t.Priority,
|
||||
t.Orientations
|
||||
.Select(o => (PackSize(o.Width + s, sheet.Width), PackSize(o.Height + s, sheet.Height)))
|
||||
.ToList(),
|
||||
t.BoxArea))
|
||||
.ToList();
|
||||
var placed = MaxRectsPacker
|
||||
.Pack(packTypes, remaining.ToArray(), sheet, rule, mode, token)
|
||||
|
||||
Reference in New Issue
Block a user