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; } } }