diff --git a/OpenNest.Engine/MultiPlateNester.cs b/OpenNest.Engine/MultiPlateNester.cs index 7cc5a11..d60611d 100644 --- a/OpenNest.Engine/MultiPlateNester.cs +++ b/OpenNest.Engine/MultiPlateNester.cs @@ -19,22 +19,27 @@ namespace OpenNest { private readonly Plate _template; private readonly List _plateOptions; + private readonly List _sortedOptions; private readonly double _salvageRate; private readonly double _minRemnantSize; private readonly List _platePool; private readonly IProgress _progress; private readonly CancellationToken _token; + private readonly MultiPlateNestOptions _options; + + private bool HasPlateOptions => _plateOptions != null && _plateOptions.Count > 0; private MultiPlateNester( - Plate template, List plateOptions, - double salvageRate, double minRemnantSize, + MultiPlateNestOptions options, List existingPlates, IProgress progress, CancellationToken token) { - _template = template; - _plateOptions = plateOptions; - _salvageRate = salvageRate; - _minRemnantSize = minRemnantSize; + _options = options; + _template = options.Template; + _plateOptions = options.PlateOptions; + _sortedOptions = options.PlateOptions?.OrderBy(o => o.Cost).ToList(); + _salvageRate = options.SalvageRate; + _minRemnantSize = options.MinRemnantSize; _platePool = InitializePlatePool(existingPlates); _progress = progress; _token = token; @@ -42,6 +47,15 @@ namespace OpenNest // --- Static Utility Methods --- + public static bool FitsBounds(Box container, Box part) + { + var fitsNormal = container.Width >= part.Width - Tolerance.Epsilon + && container.Length >= part.Length - Tolerance.Epsilon; + var fitsRotated = container.Width >= part.Length - Tolerance.Epsilon + && container.Length >= part.Width - Tolerance.Epsilon; + return fitsNormal || fitsRotated; + } + public static List SortItems(List items, PartSortOrder sortOrder) { switch (sortOrder) @@ -126,15 +140,7 @@ namespace OpenNest foreach (var option in sorted) { - var workW = option.Width - template.EdgeSpacing.Left - template.EdgeSpacing.Right; - var workL = option.Length - template.EdgeSpacing.Top - template.EdgeSpacing.Bottom; - - var fitsNormal = workW >= minBounds.Width - Tolerance.Epsilon - && workL >= minBounds.Length - Tolerance.Epsilon; - var fitsRotated = workW >= minBounds.Length - Tolerance.Epsilon - && workL >= minBounds.Width - Tolerance.Epsilon; - - if (fitsNormal || fitsRotated) + if (FitsBounds(OptionWorkArea(option, template), minBounds)) { plate.Size = new Size(option.Width, option.Length); return plate; @@ -170,34 +176,37 @@ namespace OpenNest public static MultiPlateResult Nest( List items, - Plate template, - List plateOptions, - double salvageRate, - PartSortOrder sortOrder, - double minRemnantSize, - bool allowPlateCreation, - List existingPlates, - IProgress progress, - CancellationToken token) + MultiPlateNestOptions options, + List existingPlates = null, + IProgress progress = null, + CancellationToken token = default) { - var nester = new MultiPlateNester(template, plateOptions, salvageRate, - minRemnantSize, existingPlates, progress, token); - return nester.Run(items, sortOrder, allowPlateCreation); + var nester = new MultiPlateNester(options, existingPlates, progress, token); + return nester.Run(items, options.SortOrder, options.AllowPlateCreation); } // --- Private Helpers --- + private static Box OptionWorkArea(PlateOption option, Plate template) + { + var w = option.Width - template.EdgeSpacing.Left - template.EdgeSpacing.Right; + var h = option.Length - template.EdgeSpacing.Top - template.EdgeSpacing.Bottom; + return new Box(0, 0, w, h); + } + private static double ScoreZone(Box zone, Box partBounds) { - var fitsNormal = zone.Length >= partBounds.Length && zone.Width >= partBounds.Width; - var fitsRotated = zone.Length >= partBounds.Width && zone.Width >= partBounds.Length; - - if (!fitsNormal && !fitsRotated) + if (!FitsBounds(zone, partBounds)) return -1; return (partBounds.Length * partBounds.Width) / zone.Area(); } + private static void DecrementQuantity(NestItem item, int placed) + { + item.Quantity = System.Math.Max(0, item.Quantity - placed); + } + private int FillAndPlace(PlateResult pr, Box zone, NestItem item) { var engine = NestEngineRegistry.Create(pr.Plate); @@ -206,9 +215,8 @@ namespace OpenNest if (parts.Count > 0) { - pr.Plate.Parts.AddRange(parts); - pr.Parts.AddRange(parts); - item.Quantity = System.Math.Max(0, item.Quantity - parts.Count); + pr.AddParts(parts); + DecrementQuantity(item, parts.Count); } return parts.Count; @@ -218,7 +226,7 @@ namespace OpenNest { var pr = new PlateResult { Plate = plate, IsNew = true }; - if (_plateOptions != null) + if (HasPlateOptions) { pr.ChosenSize = _plateOptions.FirstOrDefault(o => o.Width.IsEqualTo(plate.Size.Width) && o.Length.IsEqualTo(plate.Size.Length)); @@ -253,6 +261,29 @@ namespace OpenNest return pool; } + private bool TryWithUpgradedSize(PlateResult pr, PlateOption upgradeOption, Func, bool> tryFill) + { + var oldSize = pr.Plate.Size; + var oldChosenSize = pr.ChosenSize; + + pr.Plate.Size = new Size(upgradeOption.Width, upgradeOption.Length); + pr.ChosenSize = upgradeOption; + + var remnants = RemnantFinder.FromPlate(pr.Plate).FindRemnants(); + + if (remnants.Count > 0 && tryFill(remnants)) + return true; + + pr.Plate.Size = oldSize; + pr.ChosenSize = oldChosenSize; + return false; + } + + private PlateOption FindSmallestFittingOption(Box partBounds) + { + return _sortedOptions?.FirstOrDefault(o => FitsBounds(OptionWorkArea(o, _template), partBounds)); + } + // --- Orchestration --- private MultiPlateResult Run(List items, PartSortOrder sortOrder, bool allowPlateCreation) @@ -279,7 +310,7 @@ namespace OpenNest { PlaceOnNewPlates(item, bb); - if (item.Quantity > 0 && _plateOptions != null && _plateOptions.Count > 0) + if (item.Quantity > 0 && HasPlateOptions) TryUpgradeOrNewPlate(item, bb); } } @@ -292,7 +323,7 @@ namespace OpenNest CreateSharedPlates(leftovers); } - if (_plateOptions != null && _plateOptions.Count > 0 && !_token.IsCancellationRequested) + if (HasPlateOptions && !_token.IsCancellationRequested) TryConsolidateTailPlates(); foreach (var item in sorted.Where(i => i.Quantity > 0)) @@ -328,14 +359,13 @@ namespace OpenNest if (parts.Count > 0) { - pr.Plate.Parts.AddRange(parts); - pr.Parts.AddRange(parts); + pr.AddParts(parts); anyPlaced = true; foreach (var item in remaining) { - var placed = parts.Count(p => p.BaseDrawing.Name == item.Drawing.Name); - item.Quantity = System.Math.Max(0, item.Quantity - placed); + var placed = parts.Count(p => p.BaseDrawing == item.Drawing); + DecrementQuantity(item, placed); } } } @@ -370,7 +400,7 @@ namespace OpenNest if (parts.Count > 0) { plate.Parts.AddRange(parts); - item.Quantity = System.Math.Max(0, item.Quantity - parts.Count); + DecrementQuantity(item, parts.Count); placedAny = true; } } @@ -440,9 +470,7 @@ namespace OpenNest var plate = CreatePlate(_template, _plateOptions, partBounds); var workArea = plate.WorkArea(); - if (partBounds.Length > workArea.Length && partBounds.Length > workArea.Width) - break; - if (partBounds.Width > workArea.Width && partBounds.Width > workArea.Length) + if (!FitsBounds(workArea, partBounds)) break; var pr = CreateNewPlateResult(plate); @@ -459,36 +487,27 @@ namespace OpenNest private bool TryUpgradeOrNewPlate(NestItem item, Box partBounds) { - if (_plateOptions == null || _plateOptions.Count == 0) + if (!HasPlateOptions) return false; - var sortedOptions = _plateOptions.OrderBy(o => o.Cost).ToList(); - foreach (var pr in _platePool.Where(p => p.IsNew && p.ChosenSize != null)) { var currentOption = pr.ChosenSize; - var currentIdx = sortedOptions.FindIndex(o => + var currentIdx = _sortedOptions.FindIndex(o => o.Width.IsEqualTo(currentOption.Width) && o.Length.IsEqualTo(currentOption.Length)); - if (currentIdx < 0 || currentIdx >= sortedOptions.Count - 1) + if (currentIdx < 0 || currentIdx >= _sortedOptions.Count - 1) continue; - for (var i = currentIdx + 1; i < sortedOptions.Count; i++) + for (var i = currentIdx + 1; i < _sortedOptions.Count; i++) { - var upgradeOption = sortedOptions[i]; + var upgradeOption = _sortedOptions[i]; - // Only consider options that are at least as large in both dimensions. if (upgradeOption.Width < currentOption.Width - Tolerance.Epsilon || upgradeOption.Length < currentOption.Length - Tolerance.Epsilon) continue; - var smallestNew = sortedOptions.FirstOrDefault(o => - { - var ww = o.Width - _template.EdgeSpacing.Left - _template.EdgeSpacing.Right; - var wl = o.Length - _template.EdgeSpacing.Top - _template.EdgeSpacing.Bottom; - return (ww >= partBounds.Width && wl >= partBounds.Length) - || (ww >= partBounds.Length && wl >= partBounds.Width); - }); + var smallestNew = FindSmallestFittingOption(partBounds); if (smallestNew == null) continue; @@ -499,20 +518,11 @@ namespace OpenNest if (decision.ShouldUpgrade) { - var oldSize = pr.Plate.Size; - var oldChosenSize = pr.ChosenSize; + var placed = TryWithUpgradedSize(pr, upgradeOption, + remnants => FillAndPlace(pr, remnants[0], item) > 0); - pr.Plate.Size = new Size(upgradeOption.Width, upgradeOption.Length); - pr.ChosenSize = upgradeOption; - - var remainingArea = RemnantFinder.FromPlate(pr.Plate).FindRemnants(); - - if (remainingArea.Count > 0 && FillAndPlace(pr, remainingArea[0], item) > 0) + if (placed) return true; - - // Revert if nothing was placed. - pr.Plate.Size = oldSize; - pr.ChosenSize = oldChosenSize; } break; } @@ -527,9 +537,6 @@ namespace OpenNest if (activePlates.Count < 2) return; - var sortedOptions = _plateOptions.OrderBy(o => o.Cost).ToList(); - - // Try to absorb the smallest-utilization new plate into another plate via upgrade. var donor = activePlates.OrderBy(p => p.Plate.Utilization()).First(); var donorParts = donor.Parts.ToList(); @@ -540,56 +547,42 @@ namespace OpenNest var currentOption = target.ChosenSize; - // Try each larger option that doesn't shrink any dimension. - foreach (var upgradeOption in sortedOptions.Where(o => + foreach (var upgradeOption in _sortedOptions.Where(o => o.Width >= currentOption.Width - Tolerance.Epsilon && o.Length >= currentOption.Length - Tolerance.Epsilon && (o.Width > currentOption.Width + Tolerance.Epsilon || o.Length > currentOption.Length + Tolerance.Epsilon))) { - var oldSize = target.Plate.Size; - var oldChosenSize = target.ChosenSize; - - target.Plate.Size = new Size(upgradeOption.Width, upgradeOption.Length); - target.ChosenSize = upgradeOption; - - var remnants = RemnantFinder.FromPlate(target.Plate).FindRemnants(); - if (remnants.Count == 0) + var absorbed = TryWithUpgradedSize(target, upgradeOption, remnants => { - target.Plate.Size = oldSize; - target.ChosenSize = oldChosenSize; - continue; - } + var engine = NestEngineRegistry.Create(target.Plate); + var tempItems = donorParts + .GroupBy(p => p.BaseDrawing.Name) + .Select(g => new NestItem + { + Drawing = g.First().BaseDrawing, + Quantity = g.Count(), + }) + .ToList(); - // Try to pack all donor parts into the remnant space. - var engine = NestEngineRegistry.Create(target.Plate); - var tempItems = donorParts - .GroupBy(p => p.BaseDrawing.Name) - .Select(g => new NestItem + var placed = engine.PackArea(remnants[0], tempItems, _progress, _token); + + if (placed.Count >= donorParts.Count) { - Drawing = g.First().BaseDrawing, - Quantity = g.Count(), - }) - .ToList(); + target.AddParts(placed); - var placed = engine.PackArea(remnants[0], tempItems, _progress, _token); + foreach (var p in donorParts) + donor.Plate.Parts.Remove(p); + donor.Parts.Clear(); + _platePool.Remove(donor); + return true; + } - if (placed.Count >= donorParts.Count) - { - // All donor parts fit — absorb them. - target.Plate.Parts.AddRange(placed); - target.Parts.AddRange(placed); + return false; + }); - foreach (var p in donorParts) - donor.Plate.Parts.Remove(p); - donor.Parts.Clear(); - _platePool.Remove(donor); + if (absorbed) return; - } - - // Didn't fit all parts — revert. - target.Plate.Size = oldSize; - target.ChosenSize = oldChosenSize; } } } diff --git a/OpenNest.Engine/MultiPlateResult.cs b/OpenNest.Engine/MultiPlateResult.cs index 3d5dd38..b4d2ca0 100644 --- a/OpenNest.Engine/MultiPlateResult.cs +++ b/OpenNest.Engine/MultiPlateResult.cs @@ -2,6 +2,16 @@ using System.Collections.Generic; namespace OpenNest { + public class MultiPlateNestOptions + { + public Plate Template { get; set; } + public List PlateOptions { get; set; } + public double SalvageRate { get; set; } = 0.5; + public PartSortOrder SortOrder { get; set; } = PartSortOrder.BoundingBoxArea; + public double MinRemnantSize { get; set; } = 12.0; + public bool AllowPlateCreation { get; set; } = true; + } + public class MultiPlateResult { public List Plates { get; set; } = new(); @@ -14,5 +24,11 @@ namespace OpenNest public List Parts { get; set; } = new(); public PlateOption ChosenSize { get; set; } public bool IsNew { get; set; } + + public void AddParts(IList parts) + { + Plate.Parts.AddRange(parts); + Parts.AddRange(parts); + } } } diff --git a/OpenNest.Tests/Engine/MultiPlateNesterTests.cs b/OpenNest.Tests/Engine/MultiPlateNesterTests.cs index 7d6243c..c7a3320 100644 --- a/OpenNest.Tests/Engine/MultiPlateNesterTests.cs +++ b/OpenNest.Tests/Engine/MultiPlateNesterTests.cs @@ -229,16 +229,9 @@ public class MultiPlateNesterTests MakeItem("big2", 70, 35, 1), }; - 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); + var options = new MultiPlateNestOptions { Template = template }; + + var result = MultiPlateNester.Nest(items, options); // Each large part should be on its own plate. Assert.True(result.Plates.Count >= 2, @@ -261,16 +254,9 @@ public class MultiPlateNesterTests MakeItem("tinyB", 4, 4, 3), }; - 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); + var options = new MultiPlateNestOptions { Template = template }; + + var result = MultiPlateNester.Nest(items, options); // Both small drawing types should share space — not each on their own plate. // With consolidation, they pack into remaining space alongside the big part. @@ -291,16 +277,13 @@ public class MultiPlateNesterTests MakeItem("big2", 70, 35, 1), }; - var result = MultiPlateNester.Nest( - items, template, - plateOptions: null, - salvageRate: 0.5, - sortOrder: PartSortOrder.BoundingBoxArea, - minRemnantSize: 12.0, - allowPlateCreation: false, - existingPlates: null, - progress: null, - token: CancellationToken.None); + var options = new MultiPlateNestOptions + { + Template = template, + AllowPlateCreation = false, + }; + + var result = MultiPlateNester.Nest(items, options); // No existing plates and no plate creation — nothing can be placed. Assert.Empty(result.Plates); @@ -325,16 +308,10 @@ public class MultiPlateNesterTests MakeItem("medium", 24, 22, 1), }; - var result = MultiPlateNester.Nest( - items, template, - plateOptions: null, - salvageRate: 0.5, - sortOrder: PartSortOrder.BoundingBoxArea, - minRemnantSize: 12.0, - allowPlateCreation: true, - existingPlates: new List { existingPlate }, - progress: null, - token: CancellationToken.None); + var options = new MultiPlateNestOptions { Template = template }; + + var result = MultiPlateNester.Nest(items, options, + existingPlates: new List { existingPlate }); // Part should be placed on the existing plate, not a new one. Assert.Single(result.Plates); @@ -403,16 +380,13 @@ public class MultiPlateNesterTests _output.WriteLine($"Plate options: {string.Join(", ", plateOptions.Select(o => $"{o.Width}x{o.Length}"))}"); _output.WriteLine(""); - var result = MultiPlateNester.Nest( - items, template, - plateOptions: plateOptions, - salvageRate: 0.5, - sortOrder: PartSortOrder.BoundingBoxArea, - minRemnantSize: 12.0, - allowPlateCreation: true, - existingPlates: null, - progress: null, - token: CancellationToken.None); + var options = new MultiPlateNestOptions + { + Template = template, + PlateOptions = plateOptions, + }; + + var result = MultiPlateNester.Nest(items, options); _output.WriteLine($"=== RESULTS: {result.Plates.Count} plates ==="); diff --git a/OpenNest/Forms/MainForm.cs b/OpenNest/Forms/MainForm.cs index 4a61d7b..b07a239 100644 --- a/OpenNest/Forms/MainForm.cs +++ b/OpenNest/Forms/MainForm.cs @@ -1006,9 +1006,18 @@ namespace OpenNest.Forms var template = activeForm.PlateView.Plate; + var nestOptions = new MultiPlateNestOptions + { + Template = template, + PlateOptions = plateOptions, + SalvageRate = salvageRate, + SortOrder = sortOrder, + MinRemnantSize = minRemnantSize, + AllowPlateCreation = allowPlateCreation, + }; + var result = await Task.Run(() => - MultiPlateNester.Nest(items, template, plateOptions, salvageRate, - sortOrder, minRemnantSize, allowPlateCreation, existingPlates, progress, token)); + MultiPlateNester.Nest(items, nestOptions, existingPlates, progress, token)); foreach (var pr in result.Plates) {