feat(engine): add IterativeShrinkFiller with dual-direction shrink selection
Introduces IterativeShrinkFiller.Fill, which composes RemnantFiller and ShrinkFiller by wrapping the caller's fill function in a closure that tries both ShrinkAxis.Height and ShrinkAxis.Width and picks the better FillScore. Adds IterativeShrinkResult (Parts + Leftovers). Covers null/empty inputs and single-item placement with three passing xUnit tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
116
OpenNest.Engine/Fill/IterativeShrinkFiller.cs
Normal file
116
OpenNest.Engine/Fill/IterativeShrinkFiller.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
using OpenNest.Geometry;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
namespace OpenNest.Engine.Fill
|
||||
{
|
||||
/// <summary>
|
||||
/// Result returned by <see cref="IterativeShrinkFiller.Fill"/>.
|
||||
/// </summary>
|
||||
public class IterativeShrinkResult
|
||||
{
|
||||
public List<Part> Parts { get; set; } = new List<Part>();
|
||||
public List<NestItem> Leftovers { get; set; } = new List<NestItem>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Composes <see cref="RemnantFiller"/> and <see cref="ShrinkFiller"/> with
|
||||
/// dual-direction shrink selection. Wraps the caller's fill function in a
|
||||
/// closure that tries both <see cref="ShrinkAxis.Height"/> and
|
||||
/// <see cref="ShrinkAxis.Width"/>, picks the better <see cref="FillScore"/>,
|
||||
/// and passes the wrapper to <see cref="RemnantFiller.FillItems"/>.
|
||||
/// </summary>
|
||||
public static class IterativeShrinkFiller
|
||||
{
|
||||
public static IterativeShrinkResult Fill(
|
||||
List<NestItem> items,
|
||||
Box workArea,
|
||||
Func<NestItem, Box, List<Part>> fillFunc,
|
||||
double spacing,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
if (items == null || items.Count == 0)
|
||||
return new IterativeShrinkResult();
|
||||
|
||||
// RemnantFiller.FillItems skips items with Quantity <= 0 (its localQty
|
||||
// check treats them as "done"). Convert unlimited items to an estimated
|
||||
// max capacity so they are actually processed.
|
||||
var workItems = new List<NestItem>(items.Count);
|
||||
var unlimitedDrawings = new HashSet<string>();
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (item.Quantity <= 0)
|
||||
{
|
||||
var bbox = item.Drawing.Program.BoundingBox();
|
||||
var estimatedMax = bbox.Area() > 0
|
||||
? (int)(workArea.Area() / bbox.Area()) * 2
|
||||
: 1000;
|
||||
|
||||
unlimitedDrawings.Add(item.Drawing.Name);
|
||||
workItems.Add(new NestItem
|
||||
{
|
||||
Drawing = item.Drawing,
|
||||
Quantity = System.Math.Max(1, estimatedMax),
|
||||
Priority = item.Priority,
|
||||
StepAngle = item.StepAngle,
|
||||
RotationStart = item.RotationStart,
|
||||
RotationEnd = item.RotationEnd
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
workItems.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
var filler = new RemnantFiller(workArea, spacing);
|
||||
|
||||
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 heightScore = FillScore.Compute(heightResult.Parts, box);
|
||||
var widthScore = FillScore.Compute(widthResult.Parts, box);
|
||||
|
||||
return widthScore > heightScore ? widthResult.Parts : heightResult.Parts;
|
||||
};
|
||||
|
||||
var placed = filler.FillItems(workItems, shrinkWrapper, token);
|
||||
|
||||
// Build leftovers: compare placed count to original quantities.
|
||||
// RemnantFiller.FillItems does NOT mutate NestItem.Quantity.
|
||||
var leftovers = new List<NestItem>();
|
||||
foreach (var item in items)
|
||||
{
|
||||
var placedCount = 0;
|
||||
foreach (var p in placed)
|
||||
{
|
||||
if (p.BaseDrawing.Name == item.Drawing.Name)
|
||||
placedCount++;
|
||||
}
|
||||
|
||||
if (item.Quantity <= 0)
|
||||
continue; // unlimited items are always "satisfied" — no leftover
|
||||
|
||||
var remaining = item.Quantity - placedCount;
|
||||
if (remaining > 0)
|
||||
{
|
||||
leftovers.Add(new NestItem
|
||||
{
|
||||
Drawing = item.Drawing,
|
||||
Quantity = remaining,
|
||||
Priority = item.Priority,
|
||||
StepAngle = item.StepAngle,
|
||||
RotationStart = item.RotationStart,
|
||||
RotationEnd = item.RotationEnd
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return new IterativeShrinkResult { Parts = placed, Leftovers = leftovers };
|
||||
}
|
||||
}
|
||||
}
|
||||
59
OpenNest.Tests/IterativeShrinkFillerTests.cs
Normal file
59
OpenNest.Tests/IterativeShrinkFillerTests.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using OpenNest.Engine.Fill;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests;
|
||||
|
||||
public class IterativeShrinkFillerTests
|
||||
{
|
||||
[Fact]
|
||||
public void Fill_NullItems_ReturnsEmpty()
|
||||
{
|
||||
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) => new List<Part>();
|
||||
var result = IterativeShrinkFiller.Fill(null, new Box(0, 0, 100, 100), fillFunc, 1.0);
|
||||
|
||||
Assert.Empty(result.Parts);
|
||||
Assert.Empty(result.Leftovers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fill_EmptyItems_ReturnsEmpty()
|
||||
{
|
||||
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) => new List<Part>();
|
||||
var result = IterativeShrinkFiller.Fill(new List<NestItem>(), new Box(0, 0, 100, 100), fillFunc, 1.0);
|
||||
|
||||
Assert.Empty(result.Parts);
|
||||
Assert.Empty(result.Leftovers);
|
||||
}
|
||||
|
||||
private static Drawing MakeRectDrawing(double w, double h, string name = "rect")
|
||||
{
|
||||
var pgm = new OpenNest.CNC.Program();
|
||||
pgm.Codes.Add(new OpenNest.CNC.RapidMove(new Vector(0, 0)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(w, 0)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(w, h)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, h)));
|
||||
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, 0)));
|
||||
return new Drawing(name, pgm);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fill_SingleItem_PlacesParts()
|
||||
{
|
||||
var drawing = MakeRectDrawing(20, 10);
|
||||
var items = new List<NestItem>
|
||||
{
|
||||
new NestItem { Drawing = drawing, Quantity = 5 }
|
||||
};
|
||||
|
||||
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
|
||||
{
|
||||
var plate = new Plate(b.Width, b.Length);
|
||||
var engine = new DefaultNestEngine(plate);
|
||||
return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
|
||||
};
|
||||
|
||||
var result = IterativeShrinkFiller.Fill(items, new Box(0, 0, 120, 60), fillFunc, 1.0);
|
||||
|
||||
Assert.True(result.Parts.Count > 0, "Should place parts");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user