Files
OpenNest/OpenNest.Engine/Fill/ShrinkFiller.cs
T
ajandClaude Opus 5.5 4b2aba83bb feat(fill): keep interlocked pairs whole and place leftovers as pairs
A quantity-limited fill trimmed the parts farthest along the trim axis,
which could take one half from each of two pairs. TrimToCount now finds
interlocked pairs and keeps or drops them whole (from d1d68a91), so the
spare space stays one usable cell.

After a fill, leftovers now run in two orders, pack-first and pairs-first,
and the strategy's comparer keeps the better whole layout, so its ranking
of this step cannot get worse. Even quantities go in as best-fit pairs,
may turn a quarter when unrestricted, are tried in every remnant and are
ranked with the layout around them. Each placed pair slides left and down
against true outlines to take the space its bounding box could not.

On compare.nest Vertical Remnant goes from 6665.1 to 5862.6 and Lattice
from 7200 to 5915.8. Nine characterization pins change from the trim with
counts unchanged; the fill pins now also check layout validity. Vertical
Remnant's golden layout shortens from 28 to 26.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 21:17:15 -05:00

204 lines
7.2 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using OpenNest.Engine.Jobs.Placement.Fillers;
using OpenNest.Engine.RectanglePacking;
using OpenNest.Geometry;
namespace OpenNest.Engine.Fill
{
public enum ShrinkAxis
{
Width,
Length,
}
public class ShrinkResult
{
public List<Part> Parts { get; set; }
public double Dimension { get; set; }
}
/// <summary>
/// Fills a box and trims excess parts by removing those farthest from
/// the origin along the shrink axis.
/// </summary>
public static class ShrinkFiller
{
public static ShrinkResult Shrink(
Func<NestItem, Box, List<Part>> fillFunc,
NestItem item,
Box box,
double spacing,
ShrinkAxis axis,
CancellationToken token = default,
int targetCount = 0,
IProgress<NestProgress> progress = null,
int plateNumber = 0,
List<Part> placedParts = null
)
{
var startBox = box;
if (targetCount > 0)
startBox = EstimateStartBox(item, box, spacing, axis, targetCount);
var parts = fillFunc(item, startBox);
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 shrinkTarget =
targetCount > 0 ? System.Math.Min(targetCount, parts.Count) : parts.Count;
if (parts.Count > shrinkTarget)
parts = TrimToCount(parts, shrinkTarget, axis);
var dim = MeasureDimension(parts, box, axis);
ReportShrinkProgress(progress, plateNumber, placedParts, parts, box, axis, dim);
return new ShrinkResult { Parts = parts, Dimension = dim };
}
private static void ReportShrinkProgress(
IProgress<NestProgress> progress,
int plateNumber,
List<Part> placedParts,
List<Part> bestParts,
Box workArea,
ShrinkAxis axis,
double dim
)
{
if (progress == null)
return;
var allParts =
placedParts != null && placedParts.Count > 0
? new List<Part>(placedParts.Count + bestParts.Count)
: new List<Part>(bestParts.Count);
if (placedParts != null && placedParts.Count > 0)
allParts.AddRange(placedParts);
allParts.AddRange(bestParts);
var desc = $"Shrink {axis}: {bestParts.Count} parts, dim={dim:F1}";
NestProgressReporter.Report(
progress,
new ProgressReport
{
Phase = NestPhase.Custom,
PlateNumber = plateNumber,
Parts = allParts,
WorkArea = workArea,
Description = desc,
}
);
}
/// <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>
internal static Box EstimateStartBox(
NestItem item,
Box box,
double spacing,
ShrinkAxis axis,
int targetCount,
double marginFactor = 1.3
)
{
var bbox = item.Drawing.Program.BoundingBox();
if (bbox.Width <= 0 || bbox.Length <= 0)
return box;
// Match MeasureDimension/TrimToCount: Length shrinks Y, Width shrinks X.
// Box's constructor takes X extent (Length), then Y extent (Width).
var maxDim = axis == ShrinkAxis.Length ? box.Width : box.Length;
// 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 * marginFactor;
estimate = System.Math.Min(estimate, maxDim);
if (estimate <= 0 || estimate >= maxDim)
return box;
return axis == ShrinkAxis.Length
? new Box(box.X, box.Y, box.Length, estimate)
: new Box(box.X, box.Y, estimate, box.Width);
}
private static double MeasureDimension(List<Part> parts, Box box, ShrinkAxis axis)
{
var placedBox = parts.Cast<IBoundable>().GetBoundingBox();
return axis == ShrinkAxis.Width ? placedBox.Right - box.X : placedBox.Top - box.Y;
}
/// <summary>
/// Keeps the <paramref name="targetCount"/> parts nearest to the origin
/// along the given axis, discarding parts farthest from the origin.
/// Interlocked pairs are kept or discarded whole; a pair is split only when an
/// odd count leaves no unpaired part to keep instead.
/// Returns the input list unchanged if count is already at or below target.
/// </summary>
internal static List<Part> TrimToCount(List<Part> parts, int targetCount, ShrinkAxis axis)
{
if (parts == null || parts.Count <= targetCount)
return parts;
var boxes = parts.Select(p => p.BoundingBox).ToArray();
(double, double) Key(int i) => axis == ShrinkAxis.Width
? (boxes[i].Right, boxes[i].Top)
: (boxes[i].Top, boxes[i].Right);
var partner = InterlockedPairs.Partners(boxes);
// A pair's later member decides when it is discarded; within a unit, the nearer first.
var units = Enumerable.Range(0, parts.Count)
.Where(i => partner[i] < 0 || i < partner[i])
.Select(i => (partner[i] < 0 ? new[] { i } : new[] { i, partner[i] })
.OrderBy(Key).ThenBy(j => j).ToArray())
.OrderBy(u => Key(u[^1])).ThenBy(u => u[0])
.ToList();
var kept = new List<int>(targetCount);
int[] split = null;
foreach (var unit in units)
{
if (kept.Count == targetCount)
break;
if (kept.Count + unit.Length <= targetCount)
kept.AddRange(unit);
else
split ??= unit;
}
if (kept.Count < targetCount)
kept.Add(split[0]);
return kept.OrderBy(Key).ThenBy(i => i).Select(i => parts[i]).ToList();
}
}
}