diff --git a/OpenNest.Engine.Tests/Jobs/NestStockBuilderTests.cs b/OpenNest.Engine.Tests/Jobs/NestStockBuilderTests.cs new file mode 100644 index 0000000..c2335c3 --- /dev/null +++ b/OpenNest.Engine.Tests/Jobs/NestStockBuilderTests.cs @@ -0,0 +1,68 @@ +using OpenNest.Engine.Jobs; +using OpenNest.Geometry; + +namespace OpenNest.Engine.Tests.Jobs; + +public class NestStockBuilderTests +{ + private static Plate Template() => new(new Size(48, 96)) + { + Quantity = 7, + PartSpacing = 0.25, + EdgeSpacing = new Spacing(1, 2, 3, 4), + Quadrant = 3, + }; + + [Fact] + public void TemplateWithoutOptionsUsesUnlimitedStockAndSnapshotsSettings() + { + var template = Template(); + var stock = Assert.Single(NestStockBuilder.FromTemplate(template, null)); + Assert.Equal("plate", stock.Id); + Assert.Null(stock.Quantity); + Assert.Equal(template.Size, stock.Size); + Assert.Equal(template.PartSpacing, stock.PartSpacing); + Assert.Equal(template.EdgeSpacing, stock.EdgeSpacing); + Assert.Equal(template.Quadrant, stock.Quadrant); + template.Size = new Size(60, 120); + template.EdgeSpacing = default; + Assert.Equal(new Size(48, 96), stock.Size); + Assert.Equal(new Spacing(1, 2, 3, 4), stock.EdgeSpacing); + } + + [Fact] + public void OptionsHaveStableDistinctIdsAndCopyTemplateSettings() + { + var template = Template(); + var options = new[] + { + new PlateOption { Width = 60, Length = 120 }, + new PlateOption { Width = 72, Length = 144 }, + }; + var stock = NestStockBuilder.FromTemplate(template, options, quantityPerOption: 2); + Assert.Equal(new[] { "option-0", "option-1" }, stock.Select(s => s.Id)); + Assert.Equal(new[] { new Size(60, 120), new Size(72, 144) }, stock.Select(s => s.Size)); + Assert.All(stock, s => + { + Assert.Equal(2, s.Quantity); + Assert.Equal(template.EdgeSpacing, s.EdgeSpacing); + Assert.Equal(template.PartSpacing, s.PartSpacing); + Assert.Equal(template.Quadrant, s.Quadrant); + }); + Assert.Equal(new Size(48, 96), template.Size); + } + + [Fact] + public void SinglePlateOffersExactlyOneSheetNotPlateRepeatQuantity() + { + var stock = Assert.Single(NestStockBuilder.SinglePlate(Template())); + Assert.Equal(1, stock.Quantity); + } + + [Fact] + public void MissingTemplateIsRejected() + { + Assert.Throws(() => NestStockBuilder.FromTemplate(null, null)); + Assert.Throws(() => NestStockBuilder.SinglePlate(null)); + } +} diff --git a/OpenNest.Engine/Jobs/NestStockBuilder.cs b/OpenNest.Engine/Jobs/NestStockBuilder.cs new file mode 100644 index 0000000..b79623b --- /dev/null +++ b/OpenNest.Engine/Jobs/NestStockBuilder.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using OpenNest.Engine.Jobs.Adapters; +using OpenNest.Geometry; + +namespace OpenNest.Engine.Jobs; + +/// Shared stock snapshots for whole-job front ends. Plate repeat count is not inventory. +public static class NestStockBuilder +{ + public static IReadOnlyList FromTemplate( + Plate template, + IReadOnlyList options, + int? quantityPerOption = null + ) + { + ArgumentNullException.ThrowIfNull(template); + if (options == null || options.Count == 0) + return new[] { DrawingJobMapper.FromPlate("plate", template, quantityPerOption) }; + + var stock = new List(options.Count); + for (var i = 0; i < options.Count; i++) + { + var option = options[i]; + ArgumentNullException.ThrowIfNull(option); + stock.Add(new NestPlateStock( + $"option-{i}", + new Size(option.Width, option.Length), + quantityPerOption, + template.PartSpacing, + template.EdgeSpacing, + template.Quadrant + )); + } + return stock.AsReadOnly(); + } + + /// Single-sheet callers may not silently create additional sheets. + public static IReadOnlyList SinglePlate(Plate plate) => + FromTemplate(plate, null, quantityPerOption: 1); +}