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
+106 -113
View File
@@ -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;
} }
} }
} }
+16
View File
@@ -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);
}
} }
} }
+24 -50
View File
@@ -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 ===");
+11 -2
View File
@@ -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)
{ {