diff --git a/OpenNest.Engine/MultiPlateNester.cs b/OpenNest.Engine/MultiPlateNester.cs index 1dbeb85..d297900 100644 --- a/OpenNest.Engine/MultiPlateNester.cs +++ b/OpenNest.Engine/MultiPlateNester.cs @@ -214,15 +214,20 @@ namespace OpenNest placed = TryPlaceOnExistingPlates(item, bb, platePool, template, minRemnantSize, progress, token); - // If items remain, try creating new plates. - if (item.Quantity > 0 && allowPlateCreation) + // Classify against template to decide if this item warrants its own plate. + // Small parts are deferred to the consolidation pass where they get packed + // together on shared plates instead of each getting their own. + var templateClass = Classify(bb, template.WorkArea()); + + if (item.Quantity > 0 && allowPlateCreation && templateClass != PartClass.Small) { placed = PlaceOnNewPlates(item, bb, platePool, template, plateOptions, minRemnantSize, progress, token) || placed; } // If items remain, try upgrade-vs-new-plate. - if (item.Quantity > 0 && allowPlateCreation && plateOptions != null && plateOptions.Count > 0) + if (item.Quantity > 0 && allowPlateCreation && templateClass != PartClass.Small + && plateOptions != null && plateOptions.Count > 0) { placed = TryUpgradeOrNewPlate(item, bb, platePool, template, plateOptions, salvageRate, minRemnantSize, progress, token) || placed; @@ -276,23 +281,54 @@ namespace OpenNest } // Then create new shared plates for anything still remaining. + // Fill each drawing onto shared plates one at a time, packing + // multiple drawings onto the same plate before creating a new one. leftovers = leftovers.Where(i => i.Quantity > 0).ToList(); while (leftovers.Count > 0 && !token.IsCancellationRequested) { var plate = CreatePlate(template, plateOptions, null); - var engine = NestEngineRegistry.Create(plate); - var cloned = leftovers.Select(CloneItem).ToList(); - var parts = engine.Nest(cloned, progress, token); + var allParts = new List(); + var anyPlacedOnPlate = false; - if (parts.Count == 0) + // Fill each leftover drawing onto this plate. + foreach (var item in leftovers) + { + if (item.Quantity <= 0 || token.IsCancellationRequested) + continue; + + // Find remaining space on the plate. + var finder = RemnantFinder.FromPlate(plate); + var remnants = allParts.Count == 0 + ? new List { plate.WorkArea() } + : finder.FindRemnants(); + + foreach (var remnant in remnants) + { + if (item.Quantity <= 0) + break; + + var engine = NestEngineRegistry.Create(plate); + var clonedItem = CloneItem(item); + var parts = engine.Fill(clonedItem, remnant, progress, token); + + if (parts.Count > 0) + { + plate.Parts.AddRange(parts); + allParts.AddRange(parts); + item.Quantity = System.Math.Max(0, item.Quantity - parts.Count); + anyPlacedOnPlate = true; + } + } + } + + if (!anyPlacedOnPlate) break; - plate.Parts.AddRange(parts); var pr = new PlateResult { Plate = plate, - Parts = parts.ToList(), + Parts = allParts, IsNew = true, }; @@ -303,14 +339,6 @@ namespace OpenNest } platePool.Add(pr); - - // Deduct placed quantities from originals. - foreach (var item in leftovers) - { - var placed = parts.Count(p => p.BaseDrawing.Name == item.Drawing.Name); - item.Quantity = System.Math.Max(0, item.Quantity - placed); - } - leftovers = leftovers.Where(i => i.Quantity > 0).ToList(); } } diff --git a/OpenNest.Tests/Engine/MultiPlateNesterTests.cs b/OpenNest.Tests/Engine/MultiPlateNesterTests.cs index 495c425..945d787 100644 --- a/OpenNest.Tests/Engine/MultiPlateNesterTests.cs +++ b/OpenNest.Tests/Engine/MultiPlateNesterTests.cs @@ -1,13 +1,23 @@ using OpenNest.Geometry; +using OpenNest.IO; +using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading; using Xunit; +using Xunit.Abstractions; namespace OpenNest.Tests.Engine; public class MultiPlateNesterTests { + private readonly ITestOutputHelper _output; + + public MultiPlateNesterTests(ITestOutputHelper output) + { + _output = output; + } private static Drawing MakeDrawing(string name, double width, double length) { var program = new OpenNest.CNC.Program(); @@ -236,18 +246,19 @@ public class MultiPlateNesterTests } [Fact] - public void Nest_SmallPartsDontConsumeViableRemnants() + public void Nest_SmallPartsConsolidateOntoSharedPlates() { - // 96x48 plate with 80x40 big part leaves viable remnants (strips > 12" in one dim). - // Small parts should NOT consume those viable remnants — they should go to - // a separate plate instead, preserving the remnant for future use. + // Small parts should be packed together on shared plates rather than + // each drawing getting its own plate. The consolidation pass fills + // small parts into remaining space on existing plates. var template = new Plate(96, 48) { PartSpacing = 0.25, Quadrant = 1 }; template.EdgeSpacing = new Spacing(); var items = new List { MakeItem("big", 80, 40, 1), - MakeItem("tiny", 5, 5, 3), + MakeItem("tinyA", 5, 5, 3), + MakeItem("tinyB", 4, 4, 3), }; var result = MultiPlateNester.Nest( @@ -261,14 +272,11 @@ public class MultiPlateNesterTests progress: null, token: CancellationToken.None); - // Big part on plate 1, tiny parts on plate 2 (viable remnant preserved). - Assert.Equal(2, result.Plates.Count); - - // First plate should have only the big part. - var bigPlate = result.Plates.First(p => p.Parts.Any( - part => part.BaseDrawing.Name == "big")); - var tinyOnBigPlate = bigPlate.Parts.Count(p => p.BaseDrawing.Name == "tiny"); - Assert.Equal(0, tinyOnBigPlate); + // Both small drawing types should share space — not each on their own plate. + // With consolidation, they pack into remaining space alongside the big part. + Assert.True(result.Plates.Count <= 2, + $"Expected at most 2 plates (small parts consolidated), got {result.Plates.Count}"); + Assert.Equal(0, result.UnplacedItems.Count); } [Fact] @@ -332,4 +340,83 @@ public class MultiPlateNesterTests Assert.Single(result.Plates); Assert.False(result.Plates[0].IsNew); } + + [Fact] + public void Nest_RealNestFile_PartFirst() + { + var nestPath = @"C:\Users\aisaacs\Desktop\4526 A14 - 0.188 AISI 304.nest"; + if (!File.Exists(nestPath)) + { + _output.WriteLine("SKIP: nest file not found"); + return; + } + + var nest = new NestReader(nestPath).Read(); + var template = nest.PlateDefaults.CreateNew(); + + _output.WriteLine($"Plate: {template.Size.Width}x{template.Size.Length}, " + + $"spacing={template.PartSpacing}, edge=({template.EdgeSpacing.Left},{template.EdgeSpacing.Bottom},{template.EdgeSpacing.Right},{template.EdgeSpacing.Top})"); + + var wa = template.WorkArea(); + _output.WriteLine($"Work area: {wa.Width:F1}x{wa.Length:F1}"); + _output.WriteLine($"Classification thresholds: Large if dim > {wa.Width / 2:F1} or {wa.Length / 2:F1}, " + + $"Medium if area > {wa.Width * wa.Length / 9:F0}"); + _output.WriteLine("---"); + + var items = new List(); + foreach (var d in nest.Drawings) + { + var qty = d.Quantity.Required > 0 ? d.Quantity.Required : d.Quantity.Remaining; + if (qty <= 0) qty = 1; + + var bb = d.Program.BoundingBox(); + var classification = MultiPlateNester.Classify(bb, wa); + + _output.WriteLine($" {d.Name,-25} {bb.Width:F1}x{bb.Length:F1} (area={bb.Width * bb.Length:F0}) qty={qty} class={classification}"); + + items.Add(new NestItem + { + Drawing = d, + Quantity = qty, + StepAngle = d.Constraints.StepAngle, + RotationStart = d.Constraints.StartAngle, + RotationEnd = d.Constraints.EndAngle, + }); + } + + _output.WriteLine("---"); + _output.WriteLine($"Total: {items.Count} drawings, {items.Sum(i => i.Quantity)} parts"); + _output.WriteLine(""); + + var result = MultiPlateNester.Nest( + items, template, + plateOptions: null, + salvageRate: 0.5, + sortOrder: PartSortOrder.BoundingBoxArea, + minRemnantSize: 12.0, + allowPlateCreation: true, + existingPlates: null, + progress: null, + token: CancellationToken.None); + + _output.WriteLine($"=== RESULTS: {result.Plates.Count} plates ==="); + + for (var i = 0; i < result.Plates.Count; i++) + { + var pr = result.Plates[i]; + var groups = pr.Parts.GroupBy(p => p.BaseDrawing.Name) + .Select(g => $"{g.Key} x{g.Count()}") + .ToList(); + _output.WriteLine($" Plate {i + 1} ({pr.Plate.Size.Width}x{pr.Plate.Size.Length}): " + + $"{pr.Parts.Count} parts, util={pr.Plate.Utilization():P1} [{string.Join(", ", groups)}]"); + } + + if (result.UnplacedItems.Count > 0) + { + _output.WriteLine($" Unplaced: {string.Join(", ", result.UnplacedItems.Select(i => $"{i.Drawing.Name} x{i.Quantity}"))}"); + } + + _output.WriteLine($"\nTotal parts placed: {result.Plates.Sum(p => p.Parts.Count)}"); + _output.WriteLine($"Total plates used: {result.Plates.Count}"); + } }