diff --git a/OpenNest.Engine.Tests/NestingEngines/LayoutAssert.cs b/OpenNest.Engine.Tests/NestingEngines/LayoutAssert.cs index 12109f0..53beb77 100644 --- a/OpenNest.Engine.Tests/NestingEngines/LayoutAssert.cs +++ b/OpenNest.Engine.Tests/NestingEngines/LayoutAssert.cs @@ -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) diff --git a/OpenNest.Engine.Tests/NestingEngines/RectanglesNestingEngineTests.cs b/OpenNest.Engine.Tests/NestingEngines/RectanglesNestingEngineTests.cs index ae3da14..495db33 100644 --- a/OpenNest.Engine.Tests/NestingEngines/RectanglesNestingEngineTests.cs +++ b/OpenNest.Engine.Tests/NestingEngines/RectanglesNestingEngineTests.cs @@ -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 { } diff --git a/OpenNest.Engine/Jobs/NestLayoutCheck.cs b/OpenNest.Engine/Jobs/NestLayoutCheck.cs index 8481c36..f20e245 100644 --- a/OpenNest.Engine/Jobs/NestLayoutCheck.cs +++ b/OpenNest.Engine/Jobs/NestLayoutCheck.cs @@ -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) { diff --git a/OpenNest.Engine/Jobs/NestTolerances.cs b/OpenNest.Engine/Jobs/NestTolerances.cs index 9752137..9ce222e 100644 --- a/OpenNest.Engine/Jobs/NestTolerances.cs +++ b/OpenNest.Engine/Jobs/NestTolerances.cs @@ -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. public const double SpacingSlack = 0.0005; + /// 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. + public const double WorkAreaSlack = Tolerance.Epsilon; + /// Clipper decimal precision (a 1e-4 coordinate grid). public const int ClipperPrecision = ClipperBridge.Precision; diff --git a/OpenNest.Engine/NestingEngines/Rectangles/RectanglesNestingEngine.cs b/OpenNest.Engine/NestingEngines/Rectangles/RectanglesNestingEngine.cs index cec1318..510fc44 100644 --- a/OpenNest.Engine/NestingEngines/Rectangles/RectanglesNestingEngine.cs +++ b/OpenNest.Engine/NestingEngines/Rectangles/RectanglesNestingEngine.cs @@ -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; } } diff --git a/OpenNest.Engine/NestingEngines/Rectangles/SheetPacker.cs b/OpenNest.Engine/NestingEngines/Rectangles/SheetPacker.cs index 79858c6..5f56cd3 100644 --- a/OpenNest.Engine/NestingEngines/Rectangles/SheetPacker.cs +++ b/OpenNest.Engine/NestingEngines/Rectangles/SheetPacker.cs @@ -34,6 +34,21 @@ internal sealed record SheetPlan( /// internal static class SheetPacker { + /// + /// How far a box may overhang the work area. Kept just inside the layout check's + /// so floating-point rounding cannot push an + /// accepted box over it. + /// + public const double OverhangAllowance = NestTolerances.WorkAreaSlack * 0.9; + + /// + /// Packing size along one axis: a box that exceeds the sheet by no more than + /// 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. + /// + public static double PackSize(double size, double sheet) => + size > sheet && size <= sheet + OverhangAllowance ? sheet : size; + public static SheetPlan Pack( IReadOnlyList types, IReadOnlyList 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)