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:
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user