mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-05 12:31:50 -05:00
The repo mixed variants such as center/centre and color/colour, so a name could be spelled either way and searches missed half the hits. Standardize on the US spelling, matching .NET's own APIs (Color, Center). Wire values owned by other systems keep their spelling: the material ledger's "Cancelled" hold status and GitHub's "cancelled" job status. No behavior change beyond the spelling of a few status messages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
231 lines
7.5 KiB
C#
231 lines
7.5 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using OpenNest.Geometry;
|
|
|
|
namespace OpenNest.Engine.Fill
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public class RemnantFiller
|
|
{
|
|
private readonly RemnantFinder finder;
|
|
private readonly double spacing;
|
|
|
|
/// <summary>
|
|
/// <paramref name="boxObstacles"/> are kept-out rectangles (e.g. inflated clamp
|
|
/// zones) treated like occupied space: they are excluded from the remnant areas
|
|
/// every fill attempt sees. Null keeps today's behavior exactly.
|
|
/// </summary>
|
|
public RemnantFiller(Box workArea, double spacing, IEnumerable<Box> boxObstacles = null)
|
|
{
|
|
this.spacing = spacing;
|
|
finder = new RemnantFinder(workArea);
|
|
if (boxObstacles != null)
|
|
foreach (var box in boxObstacles)
|
|
finder.AddObstacle(box);
|
|
}
|
|
|
|
public void AddObstacles(IEnumerable<Part> parts)
|
|
{
|
|
foreach (var part in parts)
|
|
finder.AddObstacle(part.BoundingBox.Offset(spacing));
|
|
}
|
|
|
|
public List<Part> FillItems(
|
|
List<NestItem> items,
|
|
Func<NestItem, Box, List<Part>> fillFunc,
|
|
CancellationToken token = default,
|
|
IProgress<NestProgress> progress = null
|
|
)
|
|
{
|
|
if (items == null || items.Count == 0)
|
|
return new List<Part>();
|
|
|
|
var allParts = new List<Part>();
|
|
|
|
// 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<Drawing, int> BuildLocalQuantities(List<NestItem> items)
|
|
{
|
|
var localQty = new Dictionary<Drawing, int>(
|
|
items.Count,
|
|
ReferenceEqualityComparer.Instance
|
|
);
|
|
foreach (var item in items)
|
|
localQty[item.Drawing] = item.Quantity;
|
|
return localQty;
|
|
}
|
|
|
|
private static double FindMinItemDimension(
|
|
List<NestItem> items,
|
|
Dictionary<Drawing, int> 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<NestItem> items,
|
|
List<Box> freeBoxes,
|
|
Dictionary<Drawing, int> localQty,
|
|
Func<NestItem, Box, List<Part>> fillFunc,
|
|
List<Part> 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<NestItem> items,
|
|
NestItem current,
|
|
Dictionary<Drawing, int> 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<Part> 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<Part> 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<Part> TryFillInRemnants(
|
|
NestItem item,
|
|
int qty,
|
|
List<Box> freeBoxes,
|
|
Func<NestItem, Box, List<Part>> 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;
|
|
}
|
|
}
|
|
}
|