feat: improve multi-plate nesting with multi-remnant filling and better zone scoring

- Iterate all remnants instead of only the first when packing and filling
- Improve ScoreZone with estimated part count and aspect ratio matching
- Cache bounding boxes in SortItems and remnants in TryPlaceOnExistingPlates
- Make TryConsolidateTailPlates loop until stable, trying all donor/target pairs
- Fix consolidation grouping to use BaseDrawing reference instead of name

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-07 19:20:29 -04:00
parent 8dfa45c446
commit 810e37cacf
+104 -33
View File
@@ -58,24 +58,20 @@ namespace OpenNest
public static List<NestItem> SortItems(List<NestItem> items, PartSortOrder sortOrder) public static List<NestItem> SortItems(List<NestItem> items, PartSortOrder sortOrder)
{ {
var withBounds = items.Select(i => (Item: i, Bounds: i.Drawing.Program.BoundingBox())).ToList();
switch (sortOrder) switch (sortOrder)
{ {
case PartSortOrder.BoundingBoxArea: case PartSortOrder.BoundingBoxArea:
return items return withBounds
.OrderByDescending(i => .OrderByDescending(x => x.Bounds.Width * x.Bounds.Length)
{ .Select(x => x.Item)
var bb = i.Drawing.Program.BoundingBox();
return bb.Width * bb.Length;
})
.ToList(); .ToList();
case PartSortOrder.Size: case PartSortOrder.Size:
return items return withBounds
.OrderByDescending(i => .OrderByDescending(x => System.Math.Max(x.Bounds.Width, x.Bounds.Length))
{ .Select(x => x.Item)
var bb = i.Drawing.Program.BoundingBox();
return System.Math.Max(bb.Width, bb.Length);
})
.ToList(); .ToList();
default: default:
@@ -199,7 +195,19 @@ namespace OpenNest
if (!FitsBounds(zone, partBounds)) if (!FitsBounds(zone, partBounds))
return -1; return -1;
return (partBounds.Length * partBounds.Width) / zone.Area(); var cols = (int)(zone.Width / partBounds.Width);
var rows = (int)(zone.Length / partBounds.Length);
var colsR = (int)(zone.Width / partBounds.Length);
var rowsR = (int)(zone.Length / partBounds.Width);
var estimatedCount = System.Math.Max(cols * rows, colsR * rowsR);
var utilization = (estimatedCount * partBounds.Width * partBounds.Length) / zone.Area();
var zoneAspect = zone.Width / zone.Length;
var partAspect = partBounds.Width / partBounds.Length;
var aspectMatch = System.Math.Min(zoneAspect, partAspect) / System.Math.Max(zoneAspect, partAspect);
return utilization * 0.7 + aspectMatch * 0.3;
} }
private static void DecrementQuantity(NestItem item, int placed) private static void DecrementQuantity(NestItem item, int placed)
@@ -354,8 +362,15 @@ namespace OpenNest
break; break;
var engine = NestEngineRegistry.Create(pr.Plate); var engine = NestEngineRegistry.Create(pr.Plate);
foreach (var remnant in remnants)
{
remaining = leftovers.Where(i => i.Quantity > 0).ToList();
if (remaining.Count == 0)
break;
var cloned = remaining.Select(CloneItem).ToList(); var cloned = remaining.Select(CloneItem).ToList();
var parts = engine.PackArea(remnants[0], cloned, _progress, _token); var parts = engine.PackArea(remnant, cloned, _progress, _token);
if (parts.Count > 0) if (parts.Count > 0)
{ {
@@ -371,6 +386,7 @@ namespace OpenNest
} }
} }
} }
}
private void CreateSharedPlates(List<NestItem> leftovers) private void CreateSharedPlates(List<NestItem> leftovers)
{ {
@@ -379,6 +395,7 @@ namespace OpenNest
while (leftovers.Count > 0 && !_token.IsCancellationRequested) while (leftovers.Count > 0 && !_token.IsCancellationRequested)
{ {
var plate = CreatePlate(_template, _plateOptions, null); var plate = CreatePlate(_template, _plateOptions, null);
var pr = CreateNewPlateResult(plate);
var placedAny = false; var placedAny = false;
foreach (var item in leftovers) foreach (var item in leftovers)
@@ -394,22 +411,27 @@ namespace OpenNest
break; break;
var engine = NestEngineRegistry.Create(plate); var engine = NestEngineRegistry.Create(plate);
foreach (var remnant in remnants)
{
if (item.Quantity <= 0)
break;
var clonedItem = CloneItem(item); var clonedItem = CloneItem(item);
var parts = engine.Fill(clonedItem, remnants[0], _progress, _token); var parts = engine.Fill(clonedItem, remnant, _progress, _token);
if (parts.Count > 0) if (parts.Count > 0)
{ {
plate.Parts.AddRange(parts); pr.AddParts(parts);
DecrementQuantity(item, parts.Count); DecrementQuantity(item, parts.Count);
placedAny = true; placedAny = true;
} }
} }
}
if (!placedAny) if (!placedAny)
break; break;
var pr = CreateNewPlateResult(plate);
pr.Parts.AddRange(plate.Parts);
_platePool.Add(pr); _platePool.Add(pr);
leftovers.RemoveAll(i => i.Quantity <= 0); leftovers.RemoveAll(i => i.Quantity <= 0);
} }
@@ -418,6 +440,8 @@ namespace OpenNest
private bool TryPlaceOnExistingPlates(NestItem item, Box partBounds) private bool TryPlaceOnExistingPlates(NestItem item, Box partBounds)
{ {
var anyPlaced = false; var anyPlaced = false;
var remnantCache = new Dictionary<PlateResult, List<Box>>();
PlateResult lastModified = null;
while (item.Quantity > 0 && !_token.IsCancellationRequested) while (item.Quantity > 0 && !_token.IsCancellationRequested)
{ {
@@ -430,14 +454,17 @@ namespace OpenNest
if (_token.IsCancellationRequested) if (_token.IsCancellationRequested)
break; break;
if (pr == lastModified || !remnantCache.ContainsKey(pr))
{
var workArea = pr.Plate.WorkArea(); var workArea = pr.Plate.WorkArea();
var classification = Classify(partBounds, workArea); var classification = Classify(partBounds, workArea);
var remnants = classification == PartClass.Small remnantCache[pr] = classification == PartClass.Small
? FindRemnants(pr.Plate, _minRemnantSize, scrapOnly: true) ? FindRemnants(pr.Plate, _minRemnantSize, scrapOnly: true)
: FindRemnants(pr.Plate, _minRemnantSize, scrapOnly: false); : FindRemnants(pr.Plate, _minRemnantSize, scrapOnly: false);
}
foreach (var zone in remnants) foreach (var zone in remnantCache[pr])
{ {
var score = ScoreZone(zone, partBounds); var score = ScoreZone(zone, partBounds);
if (score > bestScore) if (score > bestScore)
@@ -455,6 +482,7 @@ namespace OpenNest
if (FillAndPlace(bestPlate, bestZone, item) == 0) if (FillAndPlace(bestPlate, bestZone, item) == 0)
break; break;
lastModified = bestPlate;
anyPlaced = true; anyPlaced = true;
} }
@@ -518,13 +546,19 @@ namespace OpenNest
if (decision.ShouldUpgrade) if (decision.ShouldUpgrade)
{ {
var placed = TryWithUpgradedSize(pr, upgradeOption, var placed = TryWithUpgradedSize(pr, upgradeOption, remnants =>
remnants => FillAndPlace(pr, remnants[0], item) > 0); {
foreach (var remnant in remnants)
{
if (FillAndPlace(pr, remnant, item) > 0)
return true;
}
return false;
});
if (placed) if (placed)
return true; return true;
} }
break;
} }
} }
@@ -533,16 +567,28 @@ namespace OpenNest
private void TryConsolidateTailPlates() private void TryConsolidateTailPlates()
{ {
var consolidated = true;
while (consolidated)
{
consolidated = false;
var activePlates = _platePool.Where(p => p.Parts.Count > 0 && p.IsNew).ToList(); var activePlates = _platePool.Where(p => p.Parts.Count > 0 && p.IsNew).ToList();
if (activePlates.Count < 2) if (activePlates.Count < 2)
return; return;
var donor = activePlates.OrderBy(p => p.Plate.Utilization()).First(); var donors = activePlates.OrderBy(p => p.Plate.Utilization()).ToList();
foreach (var donor in donors)
{
if (donor.Parts.Count == 0)
continue;
var donorParts = donor.Parts.ToList(); var donorParts = donor.Parts.ToList();
var absorbed = false;
foreach (var target in activePlates) foreach (var target in activePlates)
{ {
if (target == donor || target.ChosenSize == null) if (target == donor || target.ChosenSize == null || target.Parts.Count == 0)
continue; continue;
var currentOption = target.ChosenSize; var currentOption = target.ChosenSize;
@@ -553,23 +599,37 @@ namespace OpenNest
&& (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 absorbed = TryWithUpgradedSize(target, upgradeOption, remnants => absorbed = TryWithUpgradedSize(target, upgradeOption, remnants =>
{ {
var engine = NestEngineRegistry.Create(target.Plate); var engine = NestEngineRegistry.Create(target.Plate);
var tempItems = donorParts var tempItems = donorParts
.GroupBy(p => p.BaseDrawing.Name) .GroupBy(p => p.BaseDrawing)
.Select(g => new NestItem .Select(g => new NestItem
{ {
Drawing = g.First().BaseDrawing, Drawing = g.Key,
Quantity = g.Count(), Quantity = g.Count(),
}) })
.ToList(); .ToList();
var placed = engine.PackArea(remnants[0], tempItems, _progress, _token); var totalPlaced = new List<Part>();
foreach (var remnant in remnants)
if (placed.Count >= donorParts.Count)
{ {
target.AddParts(placed); var placed = engine.PackArea(remnant, tempItems, _progress, _token);
totalPlaced.AddRange(placed);
foreach (var ti in tempItems)
{
var count = placed.Count(p => p.BaseDrawing == ti.Drawing);
ti.Quantity = System.Math.Max(0, ti.Quantity - count);
}
if (tempItems.All(ti => ti.Quantity <= 0))
break;
}
if (totalPlaced.Count >= donorParts.Count)
{
target.AddParts(totalPlaced);
foreach (var p in donorParts) foreach (var p in donorParts)
donor.Plate.Parts.Remove(p); donor.Plate.Parts.Remove(p);
@@ -582,7 +642,18 @@ namespace OpenNest
}); });
if (absorbed) if (absorbed)
return; break;
}
if (absorbed)
break;
}
if (absorbed)
{
consolidated = true;
break;
}
} }
} }
} }