From 4cdb39870be1740368d60c61bc7c0cfbace7b1ef Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Fri, 25 Sep 2026 07:55:10 -0400 Subject: [PATCH] feat(engine): expose NestPlateStock.WorkArea, Area and Fits Every plugin engine re-derived the quadrant/edge-spacing work area by hand (Gpt6Astra, Opus55 and Qwen each had a copy, as did the placement validator). One definition on the stock removes that duplication and the chance of an engine disagreeing with the validator's bounds. Co-Authored-By: Codex Co-Authored-By: Claude Opus 5.5 --- .../Jobs/NestPlateStockTests.cs | 72 +++++++++++++++++++ .../Jobs/NestJobPlacementValidator.cs | 14 +--- OpenNest.Engine/Jobs/NestPlateStock.cs | 23 ++++++ 3 files changed, 96 insertions(+), 13 deletions(-) create mode 100644 OpenNest.Engine.Tests/Jobs/NestPlateStockTests.cs diff --git a/OpenNest.Engine.Tests/Jobs/NestPlateStockTests.cs b/OpenNest.Engine.Tests/Jobs/NestPlateStockTests.cs new file mode 100644 index 0000000..11a35e5 --- /dev/null +++ b/OpenNest.Engine.Tests/Jobs/NestPlateStockTests.cs @@ -0,0 +1,72 @@ +using OpenNest.Engine.Jobs; +using OpenNest.Engine.Jobs.Adapters; +using OpenNest.Geometry; + +namespace OpenNest.Engine.Tests.Jobs; + +public class NestPlateStockTests +{ + [Theory] + [InlineData(1, 0)] + [InlineData(2, 0)] + [InlineData(3, 0)] + [InlineData(4, 0)] + [InlineData(1, 1)] + [InlineData(2, 1)] + [InlineData(3, 1)] + [InlineData(4, 1)] + public void WorkAreaMatchesPlateInEveryQuadrant(int quadrant, double spacing) + { + var stock = new NestPlateStock("s", new Size(80, 120), + edgeSpacing: new Spacing(spacing, 2 * spacing, 3 * spacing, 4 * spacing), + quadrant: quadrant); + var actual = stock.WorkArea; + var expected = DrawingJobMapper.CreatePlate(stock).WorkArea(); + + Assert.Equal(expected.X, actual.X); + Assert.Equal(expected.Y, actual.Y); + Assert.Equal(expected.Length, actual.Length); + Assert.Equal(expected.Width, actual.Width); + Assert.Equal((quadrant is 1 or 4 ? 0 : -120) + spacing, actual.X); + Assert.Equal((quadrant is 1 or 2 ? 0 : -80) + 2 * spacing, actual.Y); + Assert.Equal(120 - 4 * spacing, actual.Length); + Assert.Equal(80 - 6 * spacing, actual.Width); + Assert.Equal(9600, stock.Area); + actual.Length = 0; + Assert.Equal(expected.Length, stock.WorkArea.Length); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + public void FractionalWorkAreaPreservesValidatorDoublePrecision(int quadrant) + { + var stock = new NestPlateStock("s", new Size(80.123456789, 120.123456789), + edgeSpacing: new Spacing(0.1, 0.2, 0.3, 0.4), quadrant: quadrant); + var work = stock.WorkArea; + + // Plate.BoundingBox casts negative origins to float. Keep the validator's original + // double arithmetic, including subtraction order, instead of adopting that rounding. + Assert.Equal((quadrant is 1 or 4 ? 0 : -stock.Size.Length) + 0.1, work.X); + Assert.Equal((quadrant is 1 or 2 ? 0 : -stock.Size.Width) + 0.2, work.Y); + Assert.Equal(stock.Size.Length - 0.1 - 0.3, work.Length); + Assert.Equal(stock.Size.Width - 0.2 - 0.4, work.Width); + } + + [Fact] + public void FitsUsesXYExtentsAndTolerance() + { + var stock = new NestPlateStock("s", new Size(80, 120), + edgeSpacing: new Spacing(1, 2, 3, 4)); + + Assert.True(stock.Fits(116, 74)); + Assert.False(stock.Fits(74, 116)); + Assert.True(stock.Fits(116 + 5e-10, 74 + 5e-10)); + Assert.False(stock.Fits(116 + 2e-9, 74)); + Assert.False(stock.Fits(116, 74 + 2e-9)); + Assert.True(stock.Fits(116.01, 74.01, 0.02)); + Assert.False(stock.Fits(116.01, 74, 0)); + } +} diff --git a/OpenNest.Engine/Jobs/NestJobPlacementValidator.cs b/OpenNest.Engine/Jobs/NestJobPlacementValidator.cs index 81424fe..2d92cd4 100644 --- a/OpenNest.Engine/Jobs/NestJobPlacementValidator.cs +++ b/OpenNest.Engine/Jobs/NestJobPlacementValidator.cs @@ -271,7 +271,7 @@ internal static class NestJobPlacementValidator private static bool FitsWorkArea(ShapeTopology shape, NestPlateStock stock) { - var workArea = WorkArea(stock); + var workArea = stock.WorkArea; if (!FitsWorkArea(shape.Perimeter, workArea)) return false; foreach (var cutout in shape.Cutouts) @@ -280,18 +280,6 @@ internal static class NestJobPlacementValidator return true; } - private static Box WorkArea(NestPlateStock stock) - { - var left = stock.Quadrant is 1 or 4 ? 0 : -stock.Size.Length; - var bottom = stock.Quadrant is 1 or 2 ? 0 : -stock.Size.Width; - return new Box( - left + stock.EdgeSpacing.Left, - bottom + stock.EdgeSpacing.Bottom, - stock.Size.Length - stock.EdgeSpacing.Left - stock.EdgeSpacing.Right, - stock.Size.Width - stock.EdgeSpacing.Bottom - stock.EdgeSpacing.Top - ); - } - private static bool FitsWorkArea(Shape contour, Box workArea) { var bounds = contour.BoundingBox; diff --git a/OpenNest.Engine/Jobs/NestPlateStock.cs b/OpenNest.Engine/Jobs/NestPlateStock.cs index 61b2e94..d6c7f58 100644 --- a/OpenNest.Engine/Jobs/NestPlateStock.cs +++ b/OpenNest.Engine/Jobs/NestPlateStock.cs @@ -6,6 +6,8 @@ namespace OpenNest.Engine.Jobs; /// Immutable stock settings. Size and spacing are copied value types, not caller-owned settings. public sealed class NestPlateStock { + private readonly Box workArea; + public NestPlateStock( string id, Size size, @@ -24,6 +26,14 @@ public sealed class NestPlateStock PartSpacing = partSpacing; EdgeSpacing = edgeSpacing; Quadrant = quadrant; + var left = quadrant is 1 or 4 ? 0 : -size.Length; + var bottom = quadrant is 1 or 2 ? 0 : -size.Width; + workArea = new Box( + left + edgeSpacing.Left, + bottom + edgeSpacing.Bottom, + size.Length - edgeSpacing.Left - edgeSpacing.Right, + size.Width - edgeSpacing.Bottom - edgeSpacing.Top + ); } public string Id { get; } @@ -34,4 +44,17 @@ public sealed class NestPlateStock public double PartSpacing { get; } public Spacing EdgeSpacing { get; } public int Quadrant { get; } + + /// + /// Usable region in the placement frame (quadrant applied, edge spacing removed). + /// Box.Length is the X extent, Box.Width the Y extent. Returns a detached copy. + /// + public Box WorkArea => new(workArea.X, workArea.Y, workArea.Length, workArea.Width); + + /// Full sheet area before edge spacing is removed. + public double Area => Size.Width * Size.Length; + + /// True when a width (X) by height (Y) envelope fits the work area within epsilon. + public bool Fits(double width, double height, double epsilon = 1e-9) => + width <= workArea.Length + epsilon && height <= workArea.Width + epsilon; }