refactor: simplify MultiPlateNester by converting to instance class
- Convert static class to instance with private constructor; shared parameters (template, plateOptions, salvageRate, minRemnantSize, progress, token) become fields, eliminating parameter threading across all private methods (10→3 params on Nest entry point stays unchanged; private methods drop from 7-9 params to 1-2) - Extract FillAndPlace helper consolidating the repeated clone→fill→add-to-plate→deduct-quantity pattern (was duplicated in 4 call sites) - Merge FindScrapZones/FindViableRemnants (98% duplicate) into single FindRemnants(plate, minRemnantSize, scrapOnly) method - Extract ScoreZone helper and collapse duplicate normal/rotated orientation checks into single conditional - Extract CreateNewPlateResult helper for repeated PlateResult construction + PlateOption lookup pattern Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+315
-378
@@ -15,8 +15,33 @@ namespace OpenNest
|
|||||||
Small,
|
Small,
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class MultiPlateNester
|
public class MultiPlateNester
|
||||||
{
|
{
|
||||||
|
private readonly Plate _template;
|
||||||
|
private readonly List<PlateOption> _plateOptions;
|
||||||
|
private readonly double _salvageRate;
|
||||||
|
private readonly double _minRemnantSize;
|
||||||
|
private readonly List<PlateResult> _platePool;
|
||||||
|
private readonly IProgress<NestProgress> _progress;
|
||||||
|
private readonly CancellationToken _token;
|
||||||
|
|
||||||
|
private MultiPlateNester(
|
||||||
|
Plate template, List<PlateOption> plateOptions,
|
||||||
|
double salvageRate, double minRemnantSize,
|
||||||
|
List<Plate> existingPlates,
|
||||||
|
IProgress<NestProgress> progress, CancellationToken token)
|
||||||
|
{
|
||||||
|
_template = template;
|
||||||
|
_plateOptions = plateOptions;
|
||||||
|
_salvageRate = salvageRate;
|
||||||
|
_minRemnantSize = minRemnantSize;
|
||||||
|
_platePool = InitializePlatePool(existingPlates);
|
||||||
|
_progress = progress;
|
||||||
|
_token = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Static Utility Methods ---
|
||||||
|
|
||||||
public static List<NestItem> SortItems(List<NestItem> items, PartSortOrder sortOrder)
|
public static List<NestItem> SortItems(List<NestItem> items, PartSortOrder sortOrder)
|
||||||
{
|
{
|
||||||
switch (sortOrder)
|
switch (sortOrder)
|
||||||
@@ -66,34 +91,10 @@ namespace OpenNest
|
|||||||
return remnant.Width < minRemnantSize && remnant.Length < minRemnantSize;
|
return remnant.Width < minRemnantSize && remnant.Length < minRemnantSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<Box> FindScrapZones(Plate plate, double minRemnantSize)
|
public static List<Box> FindRemnants(Plate plate, double minRemnantSize, bool scrapOnly)
|
||||||
{
|
{
|
||||||
var finder = RemnantFinder.FromPlate(plate);
|
var remnants = RemnantFinder.FromPlate(plate).FindRemnants();
|
||||||
var remnants = finder.FindRemnants();
|
return remnants.Where(r => IsScrapRemnant(r, minRemnantSize) == scrapOnly).ToList();
|
||||||
|
|
||||||
var scrap = new List<Box>();
|
|
||||||
foreach (var remnant in remnants)
|
|
||||||
{
|
|
||||||
if (IsScrapRemnant(remnant, minRemnantSize))
|
|
||||||
scrap.Add(remnant);
|
|
||||||
}
|
|
||||||
|
|
||||||
return scrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<Box> FindViableRemnants(Plate plate, double minRemnantSize)
|
|
||||||
{
|
|
||||||
var finder = RemnantFinder.FromPlate(plate);
|
|
||||||
var remnants = finder.FindRemnants();
|
|
||||||
|
|
||||||
var viable = new List<Box>();
|
|
||||||
foreach (var remnant in remnants)
|
|
||||||
{
|
|
||||||
if (!IsScrapRemnant(remnant, minRemnantSize))
|
|
||||||
viable.Add(remnant);
|
|
||||||
}
|
|
||||||
|
|
||||||
return viable;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct UpgradeDecision
|
public struct UpgradeDecision
|
||||||
@@ -121,7 +122,6 @@ namespace OpenNest
|
|||||||
if (options == null || options.Count == 0 || minBounds == null)
|
if (options == null || options.Count == 0 || minBounds == null)
|
||||||
return plate;
|
return plate;
|
||||||
|
|
||||||
// Find smallest option that fits the part (by cost ascending).
|
|
||||||
var sorted = options.OrderBy(o => o.Cost).ToList();
|
var sorted = options.OrderBy(o => o.Cost).ToList();
|
||||||
|
|
||||||
foreach (var option in sorted)
|
foreach (var option in sorted)
|
||||||
@@ -141,7 +141,6 @@ namespace OpenNest
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// No option fits — use template size.
|
|
||||||
return plate;
|
return plate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,7 +166,7 @@ namespace OpenNest
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Main Orchestration ---
|
// --- Main Entry Point ---
|
||||||
|
|
||||||
public static MultiPlateResult Nest(
|
public static MultiPlateResult Nest(
|
||||||
List<NestItem> items,
|
List<NestItem> items,
|
||||||
@@ -181,378 +180,51 @@ namespace OpenNest
|
|||||||
IProgress<NestProgress> progress,
|
IProgress<NestProgress> progress,
|
||||||
CancellationToken token)
|
CancellationToken token)
|
||||||
{
|
{
|
||||||
var result = new MultiPlateResult();
|
var nester = new MultiPlateNester(template, plateOptions, salvageRate,
|
||||||
|
minRemnantSize, existingPlates, progress, token);
|
||||||
if (items == null || items.Count == 0)
|
return nester.Run(items, sortOrder, allowPlateCreation);
|
||||||
return result;
|
|
||||||
|
|
||||||
// Initialize plate pool from existing plates.
|
|
||||||
var platePool = new List<PlateResult>();
|
|
||||||
|
|
||||||
if (existingPlates != null)
|
|
||||||
{
|
|
||||||
foreach (var plate in existingPlates)
|
|
||||||
platePool.Add(new PlateResult { Plate = plate, IsNew = false });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort items by selected order.
|
// --- Private Helpers ---
|
||||||
var sorted = SortItems(items.Where(i => i.Quantity > 0).ToList(), sortOrder);
|
|
||||||
|
|
||||||
// Single pass — process each drawing batch.
|
private static double ScoreZone(Box zone, Box partBounds)
|
||||||
foreach (var item in sorted)
|
|
||||||
{
|
{
|
||||||
if (token.IsCancellationRequested)
|
var fitsNormal = zone.Length >= partBounds.Length && zone.Width >= partBounds.Width;
|
||||||
break;
|
var fitsRotated = zone.Length >= partBounds.Width && zone.Width >= partBounds.Length;
|
||||||
|
|
||||||
if (item.Quantity <= 0)
|
if (!fitsNormal && !fitsRotated)
|
||||||
continue;
|
return -1;
|
||||||
|
|
||||||
var bb = item.Drawing.Program.BoundingBox();
|
return (partBounds.Length * partBounds.Width) / zone.Area();
|
||||||
var placed = false;
|
|
||||||
|
|
||||||
// Try to place on existing plates in the pool.
|
|
||||||
placed = TryPlaceOnExistingPlates(item, bb, platePool, template,
|
|
||||||
minRemnantSize, progress, token);
|
|
||||||
|
|
||||||
// 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.
|
private int FillAndPlace(PlateResult pr, Box zone, NestItem item)
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't add to unplaced yet — consolidation pass will handle leftovers.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Consolidation pass: pack remaining items together on shared plates
|
|
||||||
// using the engine's multi-item Nest() method instead of one-drawing-per-plate.
|
|
||||||
var leftovers = sorted.Where(i => i.Quantity > 0).ToList();
|
|
||||||
|
|
||||||
if (leftovers.Count > 0 && allowPlateCreation && !token.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
// First try to pack leftovers into remaining space on existing plates.
|
|
||||||
foreach (var pr in platePool)
|
|
||||||
{
|
|
||||||
if (token.IsCancellationRequested)
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Repeatedly find the largest remnant and pack into it.
|
|
||||||
// Recalculate after each fill to avoid overlapping stale remnants.
|
|
||||||
var anyPlacedOnThis = true;
|
|
||||||
while (anyPlacedOnThis && !token.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
anyPlacedOnThis = false;
|
|
||||||
|
|
||||||
var remaining = leftovers.Where(i => i.Quantity > 0).ToList();
|
|
||||||
if (remaining.Count == 0)
|
|
||||||
break;
|
|
||||||
|
|
||||||
var finder = RemnantFinder.FromPlate(pr.Plate);
|
|
||||||
var remnants = finder.FindRemnants();
|
|
||||||
if (remnants.Count == 0)
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Try the largest remnant.
|
|
||||||
var remnant = remnants[0];
|
|
||||||
|
|
||||||
var engine = NestEngineRegistry.Create(pr.Plate);
|
var engine = NestEngineRegistry.Create(pr.Plate);
|
||||||
var cloned = remaining.Select(CloneItem).ToList();
|
var clonedItem = CloneItem(item);
|
||||||
var parts = engine.PackArea(remnant, cloned, progress, token);
|
var parts = engine.Fill(clonedItem, zone, _progress, _token);
|
||||||
|
|
||||||
if (parts.Count > 0)
|
if (parts.Count > 0)
|
||||||
{
|
{
|
||||||
pr.Plate.Parts.AddRange(parts);
|
pr.Plate.Parts.AddRange(parts);
|
||||||
pr.Parts.AddRange(parts);
|
pr.Parts.AddRange(parts);
|
||||||
anyPlacedOnThis = 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 allParts = new List<Part>();
|
|
||||||
var anyPlacedOnPlate = false;
|
|
||||||
|
|
||||||
// Fill each leftover drawing onto this plate.
|
|
||||||
// Recalculate remnants after each fill to avoid overlaps.
|
|
||||||
foreach (var item in leftovers)
|
|
||||||
{
|
|
||||||
if (item.Quantity <= 0 || token.IsCancellationRequested)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Find remaining space on the plate (recalculated each item).
|
|
||||||
var remnants = allParts.Count == 0
|
|
||||||
? new List<Box> { plate.WorkArea() }
|
|
||||||
: RemnantFinder.FromPlate(plate).FindRemnants();
|
|
||||||
|
|
||||||
if (remnants.Count == 0)
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Use only the largest remnant to avoid stale overlap issues.
|
|
||||||
var remnant = remnants[0];
|
|
||||||
|
|
||||||
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);
|
item.Quantity = System.Math.Max(0, item.Quantity - parts.Count);
|
||||||
anyPlacedOnPlate = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!anyPlacedOnPlate)
|
return parts.Count;
|
||||||
break;
|
}
|
||||||
|
|
||||||
var pr = new PlateResult
|
private PlateResult CreateNewPlateResult(Plate plate)
|
||||||
{
|
{
|
||||||
Plate = plate,
|
var pr = new PlateResult { Plate = plate, IsNew = true };
|
||||||
Parts = allParts,
|
|
||||||
IsNew = true,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (plateOptions != null)
|
if (_plateOptions != null)
|
||||||
{
|
{
|
||||||
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
platePool.Add(pr);
|
return pr;
|
||||||
leftovers = leftovers.Where(i => i.Quantity > 0).ToList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Anything still remaining is truly unplaced.
|
|
||||||
foreach (var item in sorted.Where(i => i.Quantity > 0))
|
|
||||||
result.UnplacedItems.Add(item);
|
|
||||||
|
|
||||||
result.Plates.AddRange(platePool.Where(p => p.Parts.Count > 0 || p.IsNew));
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryPlaceOnExistingPlates(
|
|
||||||
NestItem item, Box partBounds,
|
|
||||||
List<PlateResult> platePool, Plate template,
|
|
||||||
double minRemnantSize,
|
|
||||||
IProgress<NestProgress> progress, CancellationToken token)
|
|
||||||
{
|
|
||||||
var anyPlaced = false;
|
|
||||||
|
|
||||||
while (item.Quantity > 0 && !token.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
// Find the best zone across all plates for this item.
|
|
||||||
PlateResult bestPlate = null;
|
|
||||||
Box bestZone = null;
|
|
||||||
var bestScore = double.MinValue;
|
|
||||||
|
|
||||||
foreach (var pr in platePool)
|
|
||||||
{
|
|
||||||
if (token.IsCancellationRequested)
|
|
||||||
break;
|
|
||||||
|
|
||||||
var workArea = pr.Plate.WorkArea();
|
|
||||||
var classification = Classify(partBounds, workArea);
|
|
||||||
|
|
||||||
// Small parts only go into scrap zones to preserve viable remnants.
|
|
||||||
// Medium and Large parts go into viable remnants (large parts can
|
|
||||||
// still fit in remnant space left by other large parts).
|
|
||||||
var remnants = classification == PartClass.Small
|
|
||||||
? FindScrapZones(pr.Plate, minRemnantSize)
|
|
||||||
: FindViableRemnants(pr.Plate, minRemnantSize);
|
|
||||||
|
|
||||||
foreach (var zone in remnants)
|
|
||||||
{
|
|
||||||
// Check normal orientation.
|
|
||||||
if (zone.Length >= partBounds.Length && zone.Width >= partBounds.Width)
|
|
||||||
{
|
|
||||||
var score = (partBounds.Length * partBounds.Width) / zone.Area();
|
|
||||||
if (score > bestScore)
|
|
||||||
{
|
|
||||||
bestPlate = pr;
|
|
||||||
bestZone = zone;
|
|
||||||
bestScore = score;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check rotated orientation.
|
|
||||||
if (zone.Length >= partBounds.Width && zone.Width >= partBounds.Length)
|
|
||||||
{
|
|
||||||
var score = (partBounds.Length * partBounds.Width) / zone.Area();
|
|
||||||
if (score > bestScore)
|
|
||||||
{
|
|
||||||
bestPlate = pr;
|
|
||||||
bestZone = zone;
|
|
||||||
bestScore = score;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bestPlate == null || bestZone == null)
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Use the engine to fill into the zone.
|
|
||||||
var engine = NestEngineRegistry.Create(bestPlate.Plate);
|
|
||||||
var clonedItem = CloneItem(item);
|
|
||||||
var parts = engine.Fill(clonedItem, bestZone, progress, token);
|
|
||||||
|
|
||||||
if (parts.Count == 0)
|
|
||||||
break;
|
|
||||||
|
|
||||||
bestPlate.Plate.Parts.AddRange(parts);
|
|
||||||
bestPlate.Parts.AddRange(parts);
|
|
||||||
item.Quantity = System.Math.Max(0, item.Quantity - parts.Count);
|
|
||||||
anyPlaced = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return anyPlaced;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool PlaceOnNewPlates(
|
|
||||||
NestItem item, Box partBounds,
|
|
||||||
List<PlateResult> platePool, Plate template,
|
|
||||||
List<PlateOption> plateOptions,
|
|
||||||
double minRemnantSize,
|
|
||||||
IProgress<NestProgress> progress, CancellationToken token)
|
|
||||||
{
|
|
||||||
var anyPlaced = false;
|
|
||||||
|
|
||||||
while (item.Quantity > 0 && !token.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
var plate = CreatePlate(template, plateOptions, partBounds);
|
|
||||||
var workArea = plate.WorkArea();
|
|
||||||
|
|
||||||
// Can't fit on any plate we can create.
|
|
||||||
if (partBounds.Length > workArea.Length && partBounds.Length > workArea.Width)
|
|
||||||
break;
|
|
||||||
if (partBounds.Width > workArea.Width && partBounds.Width > workArea.Length)
|
|
||||||
break;
|
|
||||||
|
|
||||||
var engine = NestEngineRegistry.Create(plate);
|
|
||||||
var clonedItem = CloneItem(item);
|
|
||||||
var parts = engine.Fill(clonedItem, workArea, progress, token);
|
|
||||||
|
|
||||||
if (parts.Count == 0)
|
|
||||||
break;
|
|
||||||
|
|
||||||
plate.Parts.AddRange(parts);
|
|
||||||
var pr = new PlateResult
|
|
||||||
{
|
|
||||||
Plate = plate,
|
|
||||||
IsNew = true,
|
|
||||||
};
|
|
||||||
pr.Parts.AddRange(parts);
|
|
||||||
|
|
||||||
// Find the PlateOption used (if any).
|
|
||||||
if (plateOptions != null)
|
|
||||||
{
|
|
||||||
pr.ChosenSize = plateOptions.FirstOrDefault(o =>
|
|
||||||
o.Width.IsEqualTo(plate.Size.Width) && o.Length.IsEqualTo(plate.Size.Length));
|
|
||||||
}
|
|
||||||
|
|
||||||
platePool.Add(pr);
|
|
||||||
item.Quantity = System.Math.Max(0, item.Quantity - parts.Count);
|
|
||||||
anyPlaced = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return anyPlaced;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryUpgradeOrNewPlate(
|
|
||||||
NestItem item, Box partBounds,
|
|
||||||
List<PlateResult> platePool, Plate template,
|
|
||||||
List<PlateOption> plateOptions,
|
|
||||||
double salvageRate, double minRemnantSize,
|
|
||||||
IProgress<NestProgress> progress, CancellationToken token)
|
|
||||||
{
|
|
||||||
if (plateOptions == null || plateOptions.Count == 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Find cheapest upgrade candidate among existing new plates.
|
|
||||||
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 =>
|
|
||||||
o.Width.IsEqualTo(currentOption.Width) && o.Length.IsEqualTo(currentOption.Length));
|
|
||||||
|
|
||||||
if (currentIdx < 0 || currentIdx >= sortedOptions.Count - 1)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Try each larger option.
|
|
||||||
for (var i = currentIdx + 1; i < sortedOptions.Count; i++)
|
|
||||||
{
|
|
||||||
var upgradeOption = sortedOptions[i];
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (smallestNew == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var utilEst = pr.Plate.Utilization();
|
|
||||||
var decision = EvaluateUpgradeVsNew(currentOption, upgradeOption, smallestNew,
|
|
||||||
salvageRate, utilEst);
|
|
||||||
|
|
||||||
if (decision.ShouldUpgrade)
|
|
||||||
{
|
|
||||||
// Upgrade the plate size and re-nest with remaining items.
|
|
||||||
pr.Plate.Size = new Size(upgradeOption.Width, upgradeOption.Length);
|
|
||||||
pr.ChosenSize = upgradeOption;
|
|
||||||
|
|
||||||
var engine = NestEngineRegistry.Create(pr.Plate);
|
|
||||||
var clonedItem = CloneItem(item);
|
|
||||||
var remainingArea = RemnantFinder.FromPlate(pr.Plate).FindRemnants();
|
|
||||||
|
|
||||||
if (remainingArea.Count > 0)
|
|
||||||
{
|
|
||||||
var parts = engine.Fill(clonedItem, remainingArea[0], progress, token);
|
|
||||||
if (parts.Count > 0)
|
|
||||||
{
|
|
||||||
pr.Plate.Parts.AddRange(parts);
|
|
||||||
pr.Parts.AddRange(parts);
|
|
||||||
item.Quantity = System.Math.Max(0, item.Quantity - parts.Count);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break; // Only try next size up.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static NestItem CloneItem(NestItem item)
|
private static NestItem CloneItem(NestItem item)
|
||||||
@@ -567,5 +239,270 @@ namespace OpenNest
|
|||||||
RotationEnd = item.RotationEnd,
|
RotationEnd = item.RotationEnd,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static List<PlateResult> InitializePlatePool(List<Plate> existingPlates)
|
||||||
|
{
|
||||||
|
var pool = new List<PlateResult>();
|
||||||
|
|
||||||
|
if (existingPlates != null)
|
||||||
|
{
|
||||||
|
foreach (var plate in existingPlates)
|
||||||
|
pool.Add(new PlateResult { Plate = plate, IsNew = false });
|
||||||
|
}
|
||||||
|
|
||||||
|
return pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Orchestration ---
|
||||||
|
|
||||||
|
private MultiPlateResult Run(List<NestItem> items, PartSortOrder sortOrder, bool allowPlateCreation)
|
||||||
|
{
|
||||||
|
var result = new MultiPlateResult();
|
||||||
|
|
||||||
|
if (items == null || items.Count == 0)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
var sorted = SortItems(items.Where(i => i.Quantity > 0).ToList(), sortOrder);
|
||||||
|
|
||||||
|
foreach (var item in sorted)
|
||||||
|
{
|
||||||
|
if (_token.IsCancellationRequested || item.Quantity <= 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var bb = item.Drawing.Program.BoundingBox();
|
||||||
|
|
||||||
|
TryPlaceOnExistingPlates(item, bb);
|
||||||
|
|
||||||
|
var templateClass = Classify(bb, _template.WorkArea());
|
||||||
|
|
||||||
|
if (item.Quantity > 0 && allowPlateCreation && templateClass != PartClass.Small)
|
||||||
|
{
|
||||||
|
PlaceOnNewPlates(item, bb);
|
||||||
|
|
||||||
|
if (item.Quantity > 0 && _plateOptions != null && _plateOptions.Count > 0)
|
||||||
|
TryUpgradeOrNewPlate(item, bb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var leftovers = sorted.Where(i => i.Quantity > 0).ToList();
|
||||||
|
|
||||||
|
if (leftovers.Count > 0 && allowPlateCreation && !_token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
PackIntoExistingRemnants(leftovers);
|
||||||
|
CreateSharedPlates(leftovers);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var item in sorted.Where(i => i.Quantity > 0))
|
||||||
|
result.UnplacedItems.Add(item);
|
||||||
|
|
||||||
|
result.Plates.AddRange(_platePool.Where(p => p.Parts.Count > 0 || p.IsNew));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PackIntoExistingRemnants(List<NestItem> leftovers)
|
||||||
|
{
|
||||||
|
foreach (var pr in _platePool)
|
||||||
|
{
|
||||||
|
if (_token.IsCancellationRequested)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var anyPlaced = true;
|
||||||
|
while (anyPlaced && !_token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
anyPlaced = false;
|
||||||
|
|
||||||
|
var remaining = leftovers.Where(i => i.Quantity > 0).ToList();
|
||||||
|
if (remaining.Count == 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var remnants = RemnantFinder.FromPlate(pr.Plate).FindRemnants();
|
||||||
|
if (remnants.Count == 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var engine = NestEngineRegistry.Create(pr.Plate);
|
||||||
|
var cloned = remaining.Select(CloneItem).ToList();
|
||||||
|
var parts = engine.PackArea(remnants[0], cloned, _progress, _token);
|
||||||
|
|
||||||
|
if (parts.Count > 0)
|
||||||
|
{
|
||||||
|
pr.Plate.Parts.AddRange(parts);
|
||||||
|
pr.Parts.AddRange(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateSharedPlates(List<NestItem> leftovers)
|
||||||
|
{
|
||||||
|
leftovers.RemoveAll(i => i.Quantity <= 0);
|
||||||
|
|
||||||
|
while (leftovers.Count > 0 && !_token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var plate = CreatePlate(_template, _plateOptions, null);
|
||||||
|
var placedAny = false;
|
||||||
|
|
||||||
|
foreach (var item in leftovers)
|
||||||
|
{
|
||||||
|
if (item.Quantity <= 0 || _token.IsCancellationRequested)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var remnants = !placedAny
|
||||||
|
? new List<Box> { plate.WorkArea() }
|
||||||
|
: RemnantFinder.FromPlate(plate).FindRemnants();
|
||||||
|
|
||||||
|
if (remnants.Count == 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var engine = NestEngineRegistry.Create(plate);
|
||||||
|
var clonedItem = CloneItem(item);
|
||||||
|
var parts = engine.Fill(clonedItem, remnants[0], _progress, _token);
|
||||||
|
|
||||||
|
if (parts.Count > 0)
|
||||||
|
{
|
||||||
|
plate.Parts.AddRange(parts);
|
||||||
|
item.Quantity = System.Math.Max(0, item.Quantity - parts.Count);
|
||||||
|
placedAny = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!placedAny)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var pr = CreateNewPlateResult(plate);
|
||||||
|
pr.Parts.AddRange(plate.Parts);
|
||||||
|
_platePool.Add(pr);
|
||||||
|
leftovers.RemoveAll(i => i.Quantity <= 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryPlaceOnExistingPlates(NestItem item, Box partBounds)
|
||||||
|
{
|
||||||
|
var anyPlaced = false;
|
||||||
|
|
||||||
|
while (item.Quantity > 0 && !_token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
PlateResult bestPlate = null;
|
||||||
|
Box bestZone = null;
|
||||||
|
var bestScore = double.MinValue;
|
||||||
|
|
||||||
|
foreach (var pr in _platePool)
|
||||||
|
{
|
||||||
|
if (_token.IsCancellationRequested)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var workArea = pr.Plate.WorkArea();
|
||||||
|
var classification = Classify(partBounds, workArea);
|
||||||
|
|
||||||
|
var remnants = classification == PartClass.Small
|
||||||
|
? FindRemnants(pr.Plate, _minRemnantSize, scrapOnly: true)
|
||||||
|
: FindRemnants(pr.Plate, _minRemnantSize, scrapOnly: false);
|
||||||
|
|
||||||
|
foreach (var zone in remnants)
|
||||||
|
{
|
||||||
|
var score = ScoreZone(zone, partBounds);
|
||||||
|
if (score > bestScore)
|
||||||
|
{
|
||||||
|
bestPlate = pr;
|
||||||
|
bestZone = zone;
|
||||||
|
bestScore = score;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestPlate == null || bestZone == null)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (FillAndPlace(bestPlate, bestZone, item) == 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
anyPlaced = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return anyPlaced;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool PlaceOnNewPlates(NestItem item, Box partBounds)
|
||||||
|
{
|
||||||
|
var anyPlaced = false;
|
||||||
|
|
||||||
|
while (item.Quantity > 0 && !_token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var pr = CreateNewPlateResult(plate);
|
||||||
|
|
||||||
|
if (FillAndPlace(pr, workArea, item) == 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
_platePool.Add(pr);
|
||||||
|
anyPlaced = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return anyPlaced;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryUpgradeOrNewPlate(NestItem item, Box partBounds)
|
||||||
|
{
|
||||||
|
if (_plateOptions == null || _plateOptions.Count == 0)
|
||||||
|
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 =>
|
||||||
|
o.Width.IsEqualTo(currentOption.Width) && o.Length.IsEqualTo(currentOption.Length));
|
||||||
|
|
||||||
|
if (currentIdx < 0 || currentIdx >= sortedOptions.Count - 1)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
for (var i = currentIdx + 1; i < sortedOptions.Count; i++)
|
||||||
|
{
|
||||||
|
var upgradeOption = sortedOptions[i];
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (smallestNew == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var utilEst = pr.Plate.Utilization();
|
||||||
|
var decision = EvaluateUpgradeVsNew(currentOption, upgradeOption, smallestNew,
|
||||||
|
_salvageRate, utilEst);
|
||||||
|
|
||||||
|
if (decision.ShouldUpgrade)
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ public class MultiPlateNesterTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void FindScrapZones_ReturnsOnlyScrapRemnants()
|
public void FindRemnants_ScrapOnly_ReturnsOnlyScrapRemnants()
|
||||||
{
|
{
|
||||||
// 96x48 plate with a 70x40 part placed at origin
|
// 96x48 plate with a 70x40 part placed at origin
|
||||||
var plate = new Plate(96, 48) { PartSpacing = 0.25 };
|
var plate = new Plate(96, 48) { PartSpacing = 0.25 };
|
||||||
@@ -148,7 +148,7 @@ public class MultiPlateNesterTests
|
|||||||
var part = new Part(drawing);
|
var part = new Part(drawing);
|
||||||
plate.Parts.Add(part);
|
plate.Parts.Add(part);
|
||||||
|
|
||||||
var scrap = MultiPlateNester.FindScrapZones(plate, 12.0);
|
var scrap = MultiPlateNester.FindRemnants(plate, 12.0, scrapOnly: true);
|
||||||
|
|
||||||
// All returned zones should have both dims < 12
|
// All returned zones should have both dims < 12
|
||||||
foreach (var zone in scrap)
|
foreach (var zone in scrap)
|
||||||
|
|||||||
Reference in New Issue
Block a user