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:
2026-04-07 07:43:58 -04:00
parent 804a7fd9c1
commit c20a079874
4 changed files with 157 additions and 165 deletions

View File

@@ -19,22 +19,27 @@ namespace OpenNest
{
private readonly Plate _template;
private readonly List<PlateOption> _plateOptions;
private readonly List<PlateOption> _sortedOptions;
private readonly double _salvageRate;
private readonly double _minRemnantSize;
private readonly List<PlateResult> _platePool;
private readonly IProgress<NestProgress> _progress;
private readonly CancellationToken _token;
private readonly MultiPlateNestOptions _options;
private bool HasPlateOptions => _plateOptions != null && _plateOptions.Count > 0;
private MultiPlateNester(
Plate template, List<PlateOption> plateOptions,
double salvageRate, double minRemnantSize,
MultiPlateNestOptions options,
List<Plate> existingPlates,
IProgress<NestProgress> 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<NestItem> SortItems(List<NestItem> 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<NestItem> items,
Plate template,
List<PlateOption> plateOptions,
double salvageRate,
PartSortOrder sortOrder,
double minRemnantSize,
bool allowPlateCreation,
List<Plate> existingPlates,
IProgress<NestProgress> progress,
CancellationToken token)
MultiPlateNestOptions options,
List<Plate> existingPlates = null,
IProgress<NestProgress> 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<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 ---
private MultiPlateResult Run(List<NestItem> 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;
}
}
}

View File

@@ -2,6 +2,16 @@ using System.Collections.Generic;
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 List<PlateResult> Plates { get; set; } = new();
@@ -14,5 +24,11 @@ namespace OpenNest
public List<Part> Parts { get; set; } = new();
public PlateOption ChosenSize { get; set; }
public bool IsNew { get; set; }
public void AddParts(IList<Part> parts)
{
Plate.Parts.AddRange(parts);
Parts.AddRange(parts);
}
}
}