refactor: clean up MultiPlateNester code smells and duplication
Extract shared patterns into reusable helpers: FitsBounds (fits-normal/ rotated check), OptionWorkArea (edge-spacing subtraction), DecrementQuantity, TryWithUpgradedSize (upgrade-try-revert), FindSmallestFittingOption. Add PlateResult.AddParts to consolidate dual parts-list bookkeeping. Cache sorted plate options and add HasPlateOptions property. Introduce MultiPlateNestOptions to replace 10-parameter Nest signature with a clean options object. Fix fragile Drawing.Name matching with reference equality in PackIntoExistingRemnants. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+106
-113
@@ -19,22 +19,27 @@ namespace OpenNest
|
|||||||
{
|
{
|
||||||
private readonly Plate _template;
|
private readonly Plate _template;
|
||||||
private readonly List<PlateOption> _plateOptions;
|
private readonly List<PlateOption> _plateOptions;
|
||||||
|
private readonly List<PlateOption> _sortedOptions;
|
||||||
private readonly double _salvageRate;
|
private readonly double _salvageRate;
|
||||||
private readonly double _minRemnantSize;
|
private readonly double _minRemnantSize;
|
||||||
private readonly List<PlateResult> _platePool;
|
private readonly List<PlateResult> _platePool;
|
||||||
private readonly IProgress<NestProgress> _progress;
|
private readonly IProgress<NestProgress> _progress;
|
||||||
private readonly CancellationToken _token;
|
private readonly CancellationToken _token;
|
||||||
|
private readonly MultiPlateNestOptions _options;
|
||||||
|
|
||||||
|
private bool HasPlateOptions => _plateOptions != null && _plateOptions.Count > 0;
|
||||||
|
|
||||||
private MultiPlateNester(
|
private MultiPlateNester(
|
||||||
Plate template, List<PlateOption> plateOptions,
|
MultiPlateNestOptions options,
|
||||||
double salvageRate, double minRemnantSize,
|
|
||||||
List<Plate> existingPlates,
|
List<Plate> existingPlates,
|
||||||
IProgress<NestProgress> progress, CancellationToken token)
|
IProgress<NestProgress> progress, CancellationToken token)
|
||||||
{
|
{
|
||||||
_template = template;
|
_options = options;
|
||||||
_plateOptions = plateOptions;
|
_template = options.Template;
|
||||||
_salvageRate = salvageRate;
|
_plateOptions = options.PlateOptions;
|
||||||
_minRemnantSize = minRemnantSize;
|
_sortedOptions = options.PlateOptions?.OrderBy(o => o.Cost).ToList();
|
||||||
|
_salvageRate = options.SalvageRate;
|
||||||
|
_minRemnantSize = options.MinRemnantSize;
|
||||||
_platePool = InitializePlatePool(existingPlates);
|
_platePool = InitializePlatePool(existingPlates);
|
||||||
_progress = progress;
|
_progress = progress;
|
||||||
_token = token;
|
_token = token;
|
||||||
@@ -42,6 +47,15 @@ namespace OpenNest
|
|||||||
|
|
||||||
// --- Static Utility Methods ---
|
// --- 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<NestItem> SortItems(List<NestItem> items, PartSortOrder sortOrder)
|
public static List<NestItem> SortItems(List<NestItem> items, PartSortOrder sortOrder)
|
||||||
{
|
{
|
||||||
switch (sortOrder)
|
switch (sortOrder)
|
||||||
@@ -126,15 +140,7 @@ namespace OpenNest
|
|||||||
|
|
||||||
foreach (var option in sorted)
|
foreach (var option in sorted)
|
||||||
{
|
{
|
||||||
var workW = option.Width - template.EdgeSpacing.Left - template.EdgeSpacing.Right;
|
if (FitsBounds(OptionWorkArea(option, template), minBounds))
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
plate.Size = new Size(option.Width, option.Length);
|
plate.Size = new Size(option.Width, option.Length);
|
||||||
return plate;
|
return plate;
|
||||||
@@ -170,34 +176,37 @@ namespace OpenNest
|
|||||||
|
|
||||||
public static MultiPlateResult Nest(
|
public static MultiPlateResult Nest(
|
||||||
List<NestItem> items,
|
List<NestItem> items,
|
||||||
Plate template,
|
MultiPlateNestOptions options,
|
||||||
List<PlateOption> plateOptions,
|
List<Plate> existingPlates = null,
|
||||||
double salvageRate,
|
IProgress<NestProgress> progress = null,
|
||||||
PartSortOrder sortOrder,
|
CancellationToken token = default)
|
||||||
double minRemnantSize,
|
|
||||||
bool allowPlateCreation,
|
|
||||||
List<Plate> existingPlates,
|
|
||||||
IProgress<NestProgress> progress,
|
|
||||||
CancellationToken token)
|
|
||||||
{
|
{
|
||||||
var nester = new MultiPlateNester(template, plateOptions, salvageRate,
|
var nester = new MultiPlateNester(options, existingPlates, progress, token);
|
||||||
minRemnantSize, existingPlates, progress, token);
|
return nester.Run(items, options.SortOrder, options.AllowPlateCreation);
|
||||||
return nester.Run(items, sortOrder, allowPlateCreation);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Private Helpers ---
|
// --- 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)
|
private static double ScoreZone(Box zone, Box partBounds)
|
||||||
{
|
{
|
||||||
var fitsNormal = zone.Length >= partBounds.Length && zone.Width >= partBounds.Width;
|
if (!FitsBounds(zone, partBounds))
|
||||||
var fitsRotated = zone.Length >= partBounds.Width && zone.Width >= partBounds.Length;
|
|
||||||
|
|
||||||
if (!fitsNormal && !fitsRotated)
|
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
return (partBounds.Length * partBounds.Width) / zone.Area();
|
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)
|
private int FillAndPlace(PlateResult pr, Box zone, NestItem item)
|
||||||
{
|
{
|
||||||
var engine = NestEngineRegistry.Create(pr.Plate);
|
var engine = NestEngineRegistry.Create(pr.Plate);
|
||||||
@@ -206,9 +215,8 @@ namespace OpenNest
|
|||||||
|
|
||||||
if (parts.Count > 0)
|
if (parts.Count > 0)
|
||||||
{
|
{
|
||||||
pr.Plate.Parts.AddRange(parts);
|
pr.AddParts(parts);
|
||||||
pr.Parts.AddRange(parts);
|
DecrementQuantity(item, parts.Count);
|
||||||
item.Quantity = System.Math.Max(0, item.Quantity - parts.Count);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return parts.Count;
|
return parts.Count;
|
||||||
@@ -218,7 +226,7 @@ namespace OpenNest
|
|||||||
{
|
{
|
||||||
var pr = new PlateResult { Plate = plate, IsNew = true };
|
var pr = new PlateResult { Plate = plate, IsNew = true };
|
||||||
|
|
||||||
if (_plateOptions != null)
|
if (HasPlateOptions)
|
||||||
{
|
{
|
||||||
pr.ChosenSize = _plateOptions.FirstOrDefault(o =>
|
pr.ChosenSize = _plateOptions.FirstOrDefault(o =>
|
||||||
o.Width.IsEqualTo(plate.Size.Width) && o.Length.IsEqualTo(plate.Size.Length));
|
o.Width.IsEqualTo(plate.Size.Width) && o.Length.IsEqualTo(plate.Size.Length));
|
||||||
@@ -253,6 +261,29 @@ namespace OpenNest
|
|||||||
return pool;
|
return pool;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool TryWithUpgradedSize(PlateResult pr, PlateOption upgradeOption, Func<List<Box>, 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 ---
|
// --- Orchestration ---
|
||||||
|
|
||||||
private MultiPlateResult Run(List<NestItem> items, PartSortOrder sortOrder, bool allowPlateCreation)
|
private MultiPlateResult Run(List<NestItem> items, PartSortOrder sortOrder, bool allowPlateCreation)
|
||||||
@@ -279,7 +310,7 @@ namespace OpenNest
|
|||||||
{
|
{
|
||||||
PlaceOnNewPlates(item, bb);
|
PlaceOnNewPlates(item, bb);
|
||||||
|
|
||||||
if (item.Quantity > 0 && _plateOptions != null && _plateOptions.Count > 0)
|
if (item.Quantity > 0 && HasPlateOptions)
|
||||||
TryUpgradeOrNewPlate(item, bb);
|
TryUpgradeOrNewPlate(item, bb);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -292,7 +323,7 @@ namespace OpenNest
|
|||||||
CreateSharedPlates(leftovers);
|
CreateSharedPlates(leftovers);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_plateOptions != null && _plateOptions.Count > 0 && !_token.IsCancellationRequested)
|
if (HasPlateOptions && !_token.IsCancellationRequested)
|
||||||
TryConsolidateTailPlates();
|
TryConsolidateTailPlates();
|
||||||
|
|
||||||
foreach (var item in sorted.Where(i => i.Quantity > 0))
|
foreach (var item in sorted.Where(i => i.Quantity > 0))
|
||||||
@@ -328,14 +359,13 @@ namespace OpenNest
|
|||||||
|
|
||||||
if (parts.Count > 0)
|
if (parts.Count > 0)
|
||||||
{
|
{
|
||||||
pr.Plate.Parts.AddRange(parts);
|
pr.AddParts(parts);
|
||||||
pr.Parts.AddRange(parts);
|
|
||||||
anyPlaced = true;
|
anyPlaced = true;
|
||||||
|
|
||||||
foreach (var item in remaining)
|
foreach (var item in remaining)
|
||||||
{
|
{
|
||||||
var placed = parts.Count(p => p.BaseDrawing.Name == item.Drawing.Name);
|
var placed = parts.Count(p => p.BaseDrawing == item.Drawing);
|
||||||
item.Quantity = System.Math.Max(0, item.Quantity - placed);
|
DecrementQuantity(item, placed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -370,7 +400,7 @@ namespace OpenNest
|
|||||||
if (parts.Count > 0)
|
if (parts.Count > 0)
|
||||||
{
|
{
|
||||||
plate.Parts.AddRange(parts);
|
plate.Parts.AddRange(parts);
|
||||||
item.Quantity = System.Math.Max(0, item.Quantity - parts.Count);
|
DecrementQuantity(item, parts.Count);
|
||||||
placedAny = true;
|
placedAny = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -440,9 +470,7 @@ namespace OpenNest
|
|||||||
var plate = CreatePlate(_template, _plateOptions, partBounds);
|
var plate = CreatePlate(_template, _plateOptions, partBounds);
|
||||||
var workArea = plate.WorkArea();
|
var workArea = plate.WorkArea();
|
||||||
|
|
||||||
if (partBounds.Length > workArea.Length && partBounds.Length > workArea.Width)
|
if (!FitsBounds(workArea, partBounds))
|
||||||
break;
|
|
||||||
if (partBounds.Width > workArea.Width && partBounds.Width > workArea.Length)
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
var pr = CreateNewPlateResult(plate);
|
var pr = CreateNewPlateResult(plate);
|
||||||
@@ -459,36 +487,27 @@ namespace OpenNest
|
|||||||
|
|
||||||
private bool TryUpgradeOrNewPlate(NestItem item, Box partBounds)
|
private bool TryUpgradeOrNewPlate(NestItem item, Box partBounds)
|
||||||
{
|
{
|
||||||
if (_plateOptions == null || _plateOptions.Count == 0)
|
if (!HasPlateOptions)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var sortedOptions = _plateOptions.OrderBy(o => o.Cost).ToList();
|
|
||||||
|
|
||||||
foreach (var pr in _platePool.Where(p => p.IsNew && p.ChosenSize != null))
|
foreach (var pr in _platePool.Where(p => p.IsNew && p.ChosenSize != null))
|
||||||
{
|
{
|
||||||
var currentOption = pr.ChosenSize;
|
var currentOption = pr.ChosenSize;
|
||||||
var currentIdx = sortedOptions.FindIndex(o =>
|
var currentIdx = _sortedOptions.FindIndex(o =>
|
||||||
o.Width.IsEqualTo(currentOption.Width) && o.Length.IsEqualTo(currentOption.Length));
|
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;
|
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
|
if (upgradeOption.Width < currentOption.Width - Tolerance.Epsilon
|
||||||
|| upgradeOption.Length < currentOption.Length - Tolerance.Epsilon)
|
|| upgradeOption.Length < currentOption.Length - Tolerance.Epsilon)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var smallestNew = sortedOptions.FirstOrDefault(o =>
|
var smallestNew = FindSmallestFittingOption(partBounds);
|
||||||
{
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (smallestNew == null)
|
if (smallestNew == null)
|
||||||
continue;
|
continue;
|
||||||
@@ -499,20 +518,11 @@ namespace OpenNest
|
|||||||
|
|
||||||
if (decision.ShouldUpgrade)
|
if (decision.ShouldUpgrade)
|
||||||
{
|
{
|
||||||
var oldSize = pr.Plate.Size;
|
var placed = TryWithUpgradedSize(pr, upgradeOption,
|
||||||
var oldChosenSize = pr.ChosenSize;
|
remnants => FillAndPlace(pr, remnants[0], item) > 0);
|
||||||
|
|
||||||
pr.Plate.Size = new Size(upgradeOption.Width, upgradeOption.Length);
|
if (placed)
|
||||||
pr.ChosenSize = upgradeOption;
|
|
||||||
|
|
||||||
var remainingArea = RemnantFinder.FromPlate(pr.Plate).FindRemnants();
|
|
||||||
|
|
||||||
if (remainingArea.Count > 0 && FillAndPlace(pr, remainingArea[0], item) > 0)
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
// Revert if nothing was placed.
|
|
||||||
pr.Plate.Size = oldSize;
|
|
||||||
pr.ChosenSize = oldChosenSize;
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -527,9 +537,6 @@ namespace OpenNest
|
|||||||
if (activePlates.Count < 2)
|
if (activePlates.Count < 2)
|
||||||
return;
|
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 donor = activePlates.OrderBy(p => p.Plate.Utilization()).First();
|
||||||
var donorParts = donor.Parts.ToList();
|
var donorParts = donor.Parts.ToList();
|
||||||
|
|
||||||
@@ -540,56 +547,42 @@ namespace OpenNest
|
|||||||
|
|
||||||
var currentOption = target.ChosenSize;
|
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.Width >= currentOption.Width - Tolerance.Epsilon
|
||||||
&& o.Length >= currentOption.Length - Tolerance.Epsilon
|
&& o.Length >= currentOption.Length - Tolerance.Epsilon
|
||||||
&& (o.Width > currentOption.Width + Tolerance.Epsilon
|
&& (o.Width > currentOption.Width + Tolerance.Epsilon
|
||||||
|| o.Length > currentOption.Length + Tolerance.Epsilon)))
|
|| o.Length > currentOption.Length + Tolerance.Epsilon)))
|
||||||
{
|
{
|
||||||
var oldSize = target.Plate.Size;
|
var absorbed = TryWithUpgradedSize(target, upgradeOption, remnants =>
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
target.Plate.Size = oldSize;
|
var engine = NestEngineRegistry.Create(target.Plate);
|
||||||
target.ChosenSize = oldChosenSize;
|
var tempItems = donorParts
|
||||||
continue;
|
.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 placed = engine.PackArea(remnants[0], tempItems, _progress, _token);
|
||||||
var engine = NestEngineRegistry.Create(target.Plate);
|
|
||||||
var tempItems = donorParts
|
if (placed.Count >= donorParts.Count)
|
||||||
.GroupBy(p => p.BaseDrawing.Name)
|
|
||||||
.Select(g => new NestItem
|
|
||||||
{
|
{
|
||||||
Drawing = g.First().BaseDrawing,
|
target.AddParts(placed);
|
||||||
Quantity = g.Count(),
|
|
||||||
})
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
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)
|
return false;
|
||||||
{
|
});
|
||||||
// All donor parts fit — absorb them.
|
|
||||||
target.Plate.Parts.AddRange(placed);
|
|
||||||
target.Parts.AddRange(placed);
|
|
||||||
|
|
||||||
foreach (var p in donorParts)
|
if (absorbed)
|
||||||
donor.Plate.Parts.Remove(p);
|
|
||||||
donor.Parts.Clear();
|
|
||||||
_platePool.Remove(donor);
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
// Didn't fit all parts — revert.
|
|
||||||
target.Plate.Size = oldSize;
|
|
||||||
target.ChosenSize = oldChosenSize;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,16 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace OpenNest
|
namespace OpenNest
|
||||||
{
|
{
|
||||||
|
public class MultiPlateNestOptions
|
||||||
|
{
|
||||||
|
public Plate Template { get; set; }
|
||||||
|
public List<PlateOption> 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 class MultiPlateResult
|
||||||
{
|
{
|
||||||
public List<PlateResult> Plates { get; set; } = new();
|
public List<PlateResult> Plates { get; set; } = new();
|
||||||
@@ -14,5 +24,11 @@ namespace OpenNest
|
|||||||
public List<Part> Parts { get; set; } = new();
|
public List<Part> Parts { get; set; } = new();
|
||||||
public PlateOption ChosenSize { get; set; }
|
public PlateOption ChosenSize { get; set; }
|
||||||
public bool IsNew { get; set; }
|
public bool IsNew { get; set; }
|
||||||
|
|
||||||
|
public void AddParts(IList<Part> parts)
|
||||||
|
{
|
||||||
|
Plate.Parts.AddRange(parts);
|
||||||
|
Parts.AddRange(parts);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -229,16 +229,9 @@ public class MultiPlateNesterTests
|
|||||||
MakeItem("big2", 70, 35, 1),
|
MakeItem("big2", 70, 35, 1),
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = MultiPlateNester.Nest(
|
var options = new MultiPlateNestOptions { Template = template };
|
||||||
items, template,
|
|
||||||
plateOptions: null,
|
var result = MultiPlateNester.Nest(items, options);
|
||||||
salvageRate: 0.5,
|
|
||||||
sortOrder: PartSortOrder.BoundingBoxArea,
|
|
||||||
minRemnantSize: 12.0,
|
|
||||||
allowPlateCreation: true,
|
|
||||||
existingPlates: null,
|
|
||||||
progress: null,
|
|
||||||
token: CancellationToken.None);
|
|
||||||
|
|
||||||
// Each large part should be on its own plate.
|
// Each large part should be on its own plate.
|
||||||
Assert.True(result.Plates.Count >= 2,
|
Assert.True(result.Plates.Count >= 2,
|
||||||
@@ -261,16 +254,9 @@ public class MultiPlateNesterTests
|
|||||||
MakeItem("tinyB", 4, 4, 3),
|
MakeItem("tinyB", 4, 4, 3),
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = MultiPlateNester.Nest(
|
var options = new MultiPlateNestOptions { Template = template };
|
||||||
items, template,
|
|
||||||
plateOptions: null,
|
var result = MultiPlateNester.Nest(items, options);
|
||||||
salvageRate: 0.5,
|
|
||||||
sortOrder: PartSortOrder.BoundingBoxArea,
|
|
||||||
minRemnantSize: 12.0,
|
|
||||||
allowPlateCreation: true,
|
|
||||||
existingPlates: null,
|
|
||||||
progress: null,
|
|
||||||
token: CancellationToken.None);
|
|
||||||
|
|
||||||
// Both small drawing types should share space — not each on their own plate.
|
// Both small drawing types should share space — not each on their own plate.
|
||||||
// With consolidation, they pack into remaining space alongside the big part.
|
// With consolidation, they pack into remaining space alongside the big part.
|
||||||
@@ -291,16 +277,13 @@ public class MultiPlateNesterTests
|
|||||||
MakeItem("big2", 70, 35, 1),
|
MakeItem("big2", 70, 35, 1),
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = MultiPlateNester.Nest(
|
var options = new MultiPlateNestOptions
|
||||||
items, template,
|
{
|
||||||
plateOptions: null,
|
Template = template,
|
||||||
salvageRate: 0.5,
|
AllowPlateCreation = false,
|
||||||
sortOrder: PartSortOrder.BoundingBoxArea,
|
};
|
||||||
minRemnantSize: 12.0,
|
|
||||||
allowPlateCreation: false,
|
var result = MultiPlateNester.Nest(items, options);
|
||||||
existingPlates: null,
|
|
||||||
progress: null,
|
|
||||||
token: CancellationToken.None);
|
|
||||||
|
|
||||||
// No existing plates and no plate creation — nothing can be placed.
|
// No existing plates and no plate creation — nothing can be placed.
|
||||||
Assert.Empty(result.Plates);
|
Assert.Empty(result.Plates);
|
||||||
@@ -325,16 +308,10 @@ public class MultiPlateNesterTests
|
|||||||
MakeItem("medium", 24, 22, 1),
|
MakeItem("medium", 24, 22, 1),
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = MultiPlateNester.Nest(
|
var options = new MultiPlateNestOptions { Template = template };
|
||||||
items, template,
|
|
||||||
plateOptions: null,
|
var result = MultiPlateNester.Nest(items, options,
|
||||||
salvageRate: 0.5,
|
existingPlates: new List<Plate> { existingPlate });
|
||||||
sortOrder: PartSortOrder.BoundingBoxArea,
|
|
||||||
minRemnantSize: 12.0,
|
|
||||||
allowPlateCreation: true,
|
|
||||||
existingPlates: new List<Plate> { existingPlate },
|
|
||||||
progress: null,
|
|
||||||
token: CancellationToken.None);
|
|
||||||
|
|
||||||
// Part should be placed on the existing plate, not a new one.
|
// Part should be placed on the existing plate, not a new one.
|
||||||
Assert.Single(result.Plates);
|
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($"Plate options: {string.Join(", ", plateOptions.Select(o => $"{o.Width}x{o.Length}"))}");
|
||||||
_output.WriteLine("");
|
_output.WriteLine("");
|
||||||
|
|
||||||
var result = MultiPlateNester.Nest(
|
var options = new MultiPlateNestOptions
|
||||||
items, template,
|
{
|
||||||
plateOptions: plateOptions,
|
Template = template,
|
||||||
salvageRate: 0.5,
|
PlateOptions = plateOptions,
|
||||||
sortOrder: PartSortOrder.BoundingBoxArea,
|
};
|
||||||
minRemnantSize: 12.0,
|
|
||||||
allowPlateCreation: true,
|
var result = MultiPlateNester.Nest(items, options);
|
||||||
existingPlates: null,
|
|
||||||
progress: null,
|
|
||||||
token: CancellationToken.None);
|
|
||||||
|
|
||||||
_output.WriteLine($"=== RESULTS: {result.Plates.Count} plates ===");
|
_output.WriteLine($"=== RESULTS: {result.Plates.Count} plates ===");
|
||||||
|
|
||||||
|
|||||||
@@ -1006,9 +1006,18 @@ namespace OpenNest.Forms
|
|||||||
|
|
||||||
var template = activeForm.PlateView.Plate;
|
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(() =>
|
var result = await Task.Run(() =>
|
||||||
MultiPlateNester.Nest(items, template, plateOptions, salvageRate,
|
MultiPlateNester.Nest(items, nestOptions, existingPlates, progress, token));
|
||||||
sortOrder, minRemnantSize, allowPlateCreation, existingPlates, progress, token));
|
|
||||||
|
|
||||||
foreach (var pr in result.Plates)
|
foreach (var pr in result.Plates)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user