mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-13 23:55:08 -05:00
The Strip whole-job engine laid its strips, settled them and picked its pack remnant from the bare work area, never adding the plate's clamp keep-outs the Lattice orchestration uses. On a clamped sheet it put parts over the clamp, the runner's layout check rejected the candidate, and Strip produced nothing (N26-2A5 rev 5: door stop at (21.09,1.50) over clamp-1). NestStrips now treats InteractiveClamps.ClearanceRectangles as occupied space for the iterative shrink strips (through RemnantFiller), settles with the clamp obstacles when there are zones, and finds the pack remnant around them even before any strip is placed. Unclamped plates take the old path unchanged. A FixedStrategyNestingEngine theory runs all four fixed strategies on a clamped 26 x 26 sheet; only Strip failed it before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
330 lines
12 KiB
C#
330 lines
12 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using OpenNest.Engine.Jobs.Placement.Fillers;
|
|
using OpenNest.Geometry;
|
|
|
|
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.Length"/> 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,
|
|
IProgress<NestProgress> progress = null,
|
|
int plateNumber = 0,
|
|
Func<NestItem, Box, List<Part>> widthFillFunc = null,
|
|
IEnumerable<Box> keepOuts = null
|
|
)
|
|
{
|
|
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 (Quantity <= 0)
|
|
// to an estimated max capacity so they are actually processed.
|
|
var workItems = new List<NestItem>(items.Count);
|
|
|
|
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;
|
|
|
|
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, keepOuts);
|
|
|
|
// Track parts placed by previous items so ShrinkFiller can
|
|
// include them in progress reports.
|
|
var placedSoFar = new List<Part>();
|
|
|
|
var wFillFunc = widthFillFunc ?? fillFunc;
|
|
|
|
Func<NestItem, Box, List<Part>> shrinkWrapper = (ni, box) =>
|
|
{
|
|
var target = ni.Quantity > 0 ? ni.Quantity : 0;
|
|
|
|
// Run height and width shrinks in parallel — they are independent
|
|
// (same inputs, no shared mutable state).
|
|
ShrinkResult heightResult = null;
|
|
ShrinkResult widthResult = null;
|
|
|
|
Parallel.Invoke(
|
|
() =>
|
|
heightResult = ShrinkFiller.Shrink(
|
|
fillFunc,
|
|
ni,
|
|
box,
|
|
spacing,
|
|
ShrinkAxis.Length,
|
|
token,
|
|
targetCount: target,
|
|
progress: progress,
|
|
plateNumber: plateNumber,
|
|
placedParts: placedSoFar
|
|
),
|
|
() =>
|
|
widthResult = ShrinkFiller.Shrink(
|
|
wFillFunc,
|
|
ni,
|
|
box,
|
|
spacing,
|
|
ShrinkAxis.Width,
|
|
token,
|
|
targetCount: target,
|
|
progress: progress,
|
|
plateNumber: plateNumber,
|
|
placedParts: placedSoFar
|
|
)
|
|
);
|
|
|
|
var heightScore = FillScore.Compute(heightResult.Parts, box);
|
|
var widthScore = FillScore.Compute(widthResult.Parts, box);
|
|
|
|
var best = widthScore > heightScore ? widthResult.Parts : heightResult.Parts;
|
|
|
|
// Sort pair groups so shorter/narrower groups are closer to the origin,
|
|
// creating a staircase profile that maximizes remnant area.
|
|
// Height shrink → columns vary in height → sort columns.
|
|
// Width shrink → rows vary in width → sort rows.
|
|
if (widthScore > heightScore)
|
|
SortRowsByWidth(best, spacing);
|
|
else
|
|
SortColumnsByHeight(best, spacing);
|
|
|
|
// Report the winner as overall best so the UI shows it as settled.
|
|
if (progress != null && best != null && best.Count > 0)
|
|
{
|
|
var allParts = new List<Part>(placedSoFar.Count + best.Count);
|
|
allParts.AddRange(placedSoFar);
|
|
allParts.AddRange(best);
|
|
NestProgressReporter.Report(
|
|
progress,
|
|
new ProgressReport
|
|
{
|
|
Phase = NestPhase.Custom,
|
|
PlateNumber = plateNumber,
|
|
Parts = allParts,
|
|
WorkArea = box,
|
|
Description = $"Shrink: {best.Count} parts placed",
|
|
IsOverallBest = true,
|
|
}
|
|
);
|
|
}
|
|
|
|
// Accumulate for the next item's progress reports.
|
|
placedSoFar.AddRange(best);
|
|
|
|
return best;
|
|
};
|
|
|
|
var placed = filler.FillItems(workItems, shrinkWrapper, token);
|
|
|
|
// Build leftovers: compare placed count to original quantities by drawing
|
|
// reference. RemnantFiller.FillItems does NOT mutate NestItem.Quantity.
|
|
var leftovers = new List<NestItem>();
|
|
foreach (var item in items)
|
|
{
|
|
var placedCount = placed.Count(p => ReferenceEquals(p.BaseDrawing, item.Drawing));
|
|
|
|
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 };
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sorts pair columns by height (shortest first on the left) to create
|
|
/// a staircase profile that maximizes usable remnant area.
|
|
/// </summary>
|
|
internal static void SortColumnsByHeight(List<Part> parts, double spacing) =>
|
|
SortStrips(
|
|
parts,
|
|
spacing,
|
|
primaryEdge: b => b.Left,
|
|
extentEdge: b => b.Right,
|
|
sortMetric: MaxTop,
|
|
stripMin: MinLeft,
|
|
stripMax: MaxRight,
|
|
makeOffset: d => new Vector(d, 0)
|
|
);
|
|
|
|
/// <summary>
|
|
/// Sorts pair rows by width (narrowest first on the bottom) to create
|
|
/// a staircase profile on the right side that maximizes usable remnant area.
|
|
/// </summary>
|
|
internal static void SortRowsByWidth(List<Part> parts, double spacing) =>
|
|
SortStrips(
|
|
parts,
|
|
spacing,
|
|
primaryEdge: b => b.Bottom,
|
|
extentEdge: b => b.Top,
|
|
sortMetric: MaxRight,
|
|
stripMin: MinBottom,
|
|
stripMax: MaxTop,
|
|
makeOffset: d => new Vector(0, d)
|
|
);
|
|
|
|
private static void SortStrips(
|
|
List<Part> parts,
|
|
double spacing,
|
|
Func<Box, double> primaryEdge,
|
|
Func<Box, double> extentEdge,
|
|
Func<List<Part>, double> sortMetric,
|
|
Func<List<Part>, double> stripMin,
|
|
Func<List<Part>, double> stripMax,
|
|
Func<double, Vector> makeOffset
|
|
)
|
|
{
|
|
if (parts == null || parts.Count <= 1)
|
|
return;
|
|
|
|
parts.Sort((a, b) => primaryEdge(a.BoundingBox).CompareTo(primaryEdge(b.BoundingBox)));
|
|
|
|
var strips = new List<List<Part>>();
|
|
var strip = new List<Part> { parts[0] };
|
|
var stripExtent = extentEdge(parts[0].BoundingBox);
|
|
|
|
for (var i = 1; i < parts.Count; i++)
|
|
{
|
|
if (primaryEdge(parts[i].BoundingBox) > stripExtent + spacing / 2)
|
|
{
|
|
strips.Add(strip);
|
|
strip = new List<Part> { parts[i] };
|
|
stripExtent = extentEdge(parts[i].BoundingBox);
|
|
}
|
|
else
|
|
{
|
|
strip.Add(parts[i]);
|
|
var extent = extentEdge(parts[i].BoundingBox);
|
|
if (extent > stripExtent)
|
|
stripExtent = extent;
|
|
}
|
|
}
|
|
strips.Add(strip);
|
|
|
|
if (strips.Count <= 1)
|
|
return;
|
|
|
|
// Use the required clearance as the inter-strip gap, not a gap sampled from one
|
|
// original pair: actual placement gaps vary for irregular/mixed-size geometry, and
|
|
// replaying a larger sampled gap across every reordered pair can push the trailing
|
|
// strip past the original (already plate-fitted) footprint.
|
|
strips.Sort((a, b) => sortMetric(a).CompareTo(sortMetric(b)));
|
|
|
|
var pos = primaryEdge(parts[0].BoundingBox);
|
|
|
|
foreach (var s in strips)
|
|
{
|
|
var delta = pos - stripMin(s);
|
|
|
|
if (System.Math.Abs(delta) > OpenNest.Math.Tolerance.Epsilon)
|
|
{
|
|
var offset = makeOffset(delta);
|
|
foreach (var part in s)
|
|
part.Offset(offset);
|
|
}
|
|
|
|
pos = stripMax(s) + spacing;
|
|
}
|
|
|
|
parts.Clear();
|
|
foreach (var s in strips)
|
|
parts.AddRange(s);
|
|
}
|
|
|
|
private static double MaxTop(List<Part> col)
|
|
{
|
|
var max = double.MinValue;
|
|
foreach (var p in col)
|
|
if (p.BoundingBox.Top > max)
|
|
max = p.BoundingBox.Top;
|
|
return max;
|
|
}
|
|
|
|
private static double MaxRight(List<Part> col)
|
|
{
|
|
var max = double.MinValue;
|
|
foreach (var p in col)
|
|
if (p.BoundingBox.Right > max)
|
|
max = p.BoundingBox.Right;
|
|
return max;
|
|
}
|
|
|
|
private static double MinLeft(List<Part> col)
|
|
{
|
|
var min = double.MaxValue;
|
|
foreach (var p in col)
|
|
if (p.BoundingBox.Left < min)
|
|
min = p.BoundingBox.Left;
|
|
return min;
|
|
}
|
|
|
|
private static double MinBottom(List<Part> row)
|
|
{
|
|
var min = double.MaxValue;
|
|
foreach (var p in row)
|
|
if (p.BoundingBox.Bottom < min)
|
|
min = p.BoundingBox.Bottom;
|
|
return min;
|
|
}
|
|
}
|
|
}
|