perf(engine): add target count to ShrinkFiller with FillBestFit estimate
When a target count is known, ShrinkFiller now uses FillBestFit (fast rectangle packing) to estimate how many parts fit on the full area, then scales the shrink axis proportionally to avoid an expensive full-area fill. Falls back to full box if estimate is too aggressive. Also shrinks to targetCount (not full count) to produce tighter boxes when fewer parts are needed than the area can hold. IterativeShrinkFiller passes NestItem.Quantity as the target count. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -67,8 +67,9 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
Func<NestItem, Box, List<Part>> shrinkWrapper = (ni, box) =>
|
||||
{
|
||||
var heightResult = ShrinkFiller.Shrink(fillFunc, ni, box, spacing, ShrinkAxis.Height, token);
|
||||
var widthResult = ShrinkFiller.Shrink(fillFunc, ni, box, spacing, ShrinkAxis.Width, token);
|
||||
var target = ni.Quantity > 0 ? ni.Quantity : 0;
|
||||
var heightResult = ShrinkFiller.Shrink(fillFunc, ni, box, spacing, ShrinkAxis.Height, token, targetCount: target);
|
||||
var widthResult = ShrinkFiller.Shrink(fillFunc, ni, box, spacing, ShrinkAxis.Width, token, targetCount: target);
|
||||
|
||||
var heightScore = FillScore.Compute(heightResult.Parts, box);
|
||||
var widthScore = FillScore.Compute(widthResult.Parts, box);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.RectanglePacking;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -17,7 +18,7 @@ namespace OpenNest.Engine.Fill
|
||||
/// <summary>
|
||||
/// Fills a box then iteratively shrinks one axis by the spacing amount
|
||||
/// until the part count drops. Returns the tightest box that still fits
|
||||
/// the same number of parts.
|
||||
/// the target number of parts.
|
||||
/// </summary>
|
||||
public static class ShrinkFiller
|
||||
{
|
||||
@@ -27,14 +28,35 @@ namespace OpenNest.Engine.Fill
|
||||
double spacing,
|
||||
ShrinkAxis axis,
|
||||
CancellationToken token = default,
|
||||
int maxIterations = 20)
|
||||
int maxIterations = 20,
|
||||
int targetCount = 0)
|
||||
{
|
||||
var parts = fillFunc(item, box);
|
||||
// If a target count is specified, estimate a smaller starting box
|
||||
// to avoid an expensive full-area fill.
|
||||
var startBox = box;
|
||||
if (targetCount > 0)
|
||||
startBox = EstimateStartBox(item, box, spacing, axis, targetCount);
|
||||
|
||||
var parts = fillFunc(item, startBox);
|
||||
|
||||
// If estimate was too aggressive and we got fewer than target,
|
||||
// fall back to the full box.
|
||||
if (targetCount > 0 && startBox != box
|
||||
&& (parts == null || parts.Count < targetCount))
|
||||
{
|
||||
parts = fillFunc(item, box);
|
||||
}
|
||||
|
||||
if (parts == null || parts.Count == 0)
|
||||
return new ShrinkResult { Parts = parts ?? new List<Part>(), Dimension = 0 };
|
||||
|
||||
var targetCount = parts.Count;
|
||||
// Shrink target: if a target count was given and we got at least that many,
|
||||
// shrink to fit targetCount (not the full count). This produces a tighter box.
|
||||
// If we got fewer than target, shrink to maintain what we have.
|
||||
var shrinkTarget = targetCount > 0
|
||||
? System.Math.Min(targetCount, parts.Count)
|
||||
: parts.Count;
|
||||
|
||||
var bestParts = parts;
|
||||
var bestDim = MeasureDimension(parts, box, axis);
|
||||
|
||||
@@ -53,7 +75,7 @@ namespace OpenNest.Engine.Fill
|
||||
|
||||
var trialParts = fillFunc(item, trialBox);
|
||||
|
||||
if (trialParts == null || trialParts.Count < targetCount)
|
||||
if (trialParts == null || trialParts.Count < shrinkTarget)
|
||||
break;
|
||||
|
||||
bestParts = trialParts;
|
||||
@@ -63,6 +85,43 @@ namespace OpenNest.Engine.Fill
|
||||
return new ShrinkResult { Parts = bestParts, Dimension = bestDim };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses FillBestFit (fast rectangle packing) to estimate a starting box
|
||||
/// that fits roughly the target count. Scales the shrink axis proportionally
|
||||
/// from the full-area count down to the target, with margin.
|
||||
/// </summary>
|
||||
private static Box EstimateStartBox(NestItem item, Box box,
|
||||
double spacing, ShrinkAxis axis, int targetCount)
|
||||
{
|
||||
var bbox = item.Drawing.Program.BoundingBox();
|
||||
if (bbox.Width <= 0 || bbox.Length <= 0)
|
||||
return box;
|
||||
|
||||
var maxDim = axis == ShrinkAxis.Height ? box.Length : box.Width;
|
||||
|
||||
// Use FillBestFit for a fast, accurate rectangle count on the full box.
|
||||
var bin = new Bin { Size = new Size(box.Width, box.Length) };
|
||||
var packItem = new Item { Size = new Size(bbox.Width + spacing, bbox.Length + spacing) };
|
||||
var packer = new FillBestFit(bin);
|
||||
packer.Fill(packItem);
|
||||
var fullCount = bin.Items.Count;
|
||||
|
||||
if (fullCount <= 0 || fullCount <= targetCount)
|
||||
return box;
|
||||
|
||||
// Scale dimension proportionally: target/full * maxDim, with margin.
|
||||
var ratio = (double)targetCount / fullCount;
|
||||
var estimate = maxDim * ratio * 1.3;
|
||||
estimate = System.Math.Min(estimate, maxDim);
|
||||
|
||||
if (estimate <= 0 || estimate >= maxDim)
|
||||
return box;
|
||||
|
||||
return axis == ShrinkAxis.Height
|
||||
? new Box(box.X, box.Y, box.Width, estimate)
|
||||
: new Box(box.X, box.Y, estimate, box.Length);
|
||||
}
|
||||
|
||||
private static double MeasureDimension(List<Part> parts, Box box, ShrinkAxis axis)
|
||||
{
|
||||
var placedBox = parts.Cast<IBoundable>().GetBoundingBox();
|
||||
|
||||
Reference in New Issue
Block a user