using System;
using System.Collections.Generic;
using System.Threading;
using OpenNest.Geometry;
namespace OpenNest.Engine.Fill
{
///
/// Iteratively fills remnant boxes with items using a RemnantFinder.
/// After each fill, re-discovers free rectangles and tries again
/// until no more items can be placed.
///
public class RemnantFiller
{
private readonly RemnantFinder finder;
private readonly double spacing;
public RemnantFiller(Box workArea, double spacing)
{
this.spacing = spacing;
finder = new RemnantFinder(workArea);
}
public void AddObstacles(IEnumerable parts)
{
foreach (var part in parts)
finder.AddObstacle(part.BoundingBox.Offset(spacing));
}
public List FillItems(
List items,
Func> fillFunc,
CancellationToken token = default,
IProgress progress = null
)
{
if (items == null || items.Count == 0)
return new List();
var allParts = new List();
// Track quantities locally — do not mutate the input NestItem objects.
var localQty = BuildLocalQuantities(items);
while (!token.IsCancellationRequested)
{
var minDim = FindMinItemDimension(items, localQty);
if (minDim == double.MaxValue)
break;
var freeBoxes = finder.FindRemnants(minDim);
if (freeBoxes.Count == 0)
break;
if (!TryFillOneItem(items, freeBoxes, localQty, fillFunc, allParts, token))
break;
}
return allParts;
}
private static Dictionary BuildLocalQuantities(List items)
{
var localQty = new Dictionary(
items.Count,
ReferenceEqualityComparer.Instance
);
foreach (var item in items)
localQty[item.Drawing] = item.Quantity;
return localQty;
}
private static double FindMinItemDimension(
List items,
Dictionary localQty
)
{
var minDim = double.MaxValue;
foreach (var item in items)
{
if (localQty[item.Drawing] <= 0)
continue;
var bb = item.Drawing.Program.BoundingBox();
var dim = System.Math.Min(bb.Width, bb.Length);
if (dim < minDim)
minDim = dim;
}
return minDim;
}
private bool TryFillOneItem(
List items,
List freeBoxes,
Dictionary localQty,
Func> fillFunc,
List allParts,
CancellationToken token
)
{
foreach (var item in items)
{
if (token.IsCancellationRequested)
return false;
var qty = localQty[item.Drawing];
if (qty <= 0)
continue;
var placed = TryFillInRemnants(item, qty, freeBoxes, fillFunc);
if (placed == null)
continue;
// Remove the topmost bounding box part to create a clean
// rectangular obstacle boundary. Without this, gaps between
// individual bounding boxes cause the next drawing to fill
// into inter-row spaces, producing an interleaved layout.
// Only worthwhile while another drawing is still waiting for
// space; otherwise the removed part's slot is walled off by the
// envelope below and the part is lost for nothing.
if (placed.Count > 2 && HasOtherDemand(items, item, localQty))
RemoveTopmostPart(placed);
allParts.AddRange(placed);
localQty[item.Drawing] = System.Math.Max(0, qty - placed.Count);
// Add the envelope of all placed parts as a single obstacle
// rather than individual bounding boxes, preventing the
// remnant finder from seeing inter-part gaps.
var envelope = ComputeEnvelope(placed, spacing);
finder.AddObstacle(envelope);
return true;
}
return false;
}
private static bool HasOtherDemand(
List items,
NestItem current,
Dictionary localQty
)
{
foreach (var other in items)
{
if (ReferenceEquals(other.Drawing, current.Drawing))
continue;
if (localQty[other.Drawing] > 0)
return true;
}
return false;
}
private static void RemoveTopmostPart(List parts)
{
var topIdx = 0;
for (var i = 1; i < parts.Count; i++)
{
if (parts[i].BoundingBox.Top > parts[topIdx].BoundingBox.Top)
topIdx = i;
}
parts.RemoveAt(topIdx);
}
private static Box ComputeEnvelope(List parts, double spacing)
{
var left = double.MaxValue;
var bottom = double.MaxValue;
var right = double.MinValue;
var top = double.MinValue;
foreach (var p in parts)
{
var bb = p.BoundingBox;
if (bb.Left < left)
left = bb.Left;
if (bb.Bottom < bottom)
bottom = bb.Bottom;
if (bb.Right > right)
right = bb.Right;
if (bb.Top > top)
top = bb.Top;
}
return new Box(
left - spacing,
bottom - spacing,
right - left + spacing * 2,
top - bottom + spacing * 2
);
}
private static List TryFillInRemnants(
NestItem item,
int qty,
List freeBoxes,
Func> fillFunc
)
{
var itemBbox = item.Drawing.Program.BoundingBox();
foreach (var box in freeBoxes)
{
var fitsNormal = box.Width >= itemBbox.Width && box.Length >= itemBbox.Length;
var fitsRotated = box.Width >= itemBbox.Length && box.Length >= itemBbox.Width;
if (!fitsNormal && !fitsRotated)
continue;
var fillItem = new NestItem { Drawing = item.Drawing, Quantity = qty };
var parts = fillFunc(fillItem, box);
if (parts != null && parts.Count > 0)
return parts;
}
return null;
}
}
}