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:
2026-03-19 10:55:01 -04:00
parent 1e9640d4fc
commit e3b89f2660
2 changed files with 67 additions and 7 deletions

View File

@@ -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);

View File

@@ -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();