using OpenNest.Geometry; using OpenNest.RectanglePacking; using System; using System.Collections.Generic; using System.Linq; using System.Threading; namespace OpenNest.Engine.Fill { public enum ShrinkAxis { Width, Height } public class ShrinkResult { public List Parts { get; set; } public double Dimension { get; set; } } /// /// Fills a box and trims excess parts by removing those farthest from /// the origin along the shrink axis. /// public static class ShrinkFiller { public static ShrinkResult Shrink( Func> fillFunc, NestItem item, Box box, double spacing, ShrinkAxis axis, CancellationToken token = default, int targetCount = 0, IProgress progress = null, int plateNumber = 0, List placedParts = null) { var startBox = box; if (targetCount > 0) startBox = EstimateStartBox(item, box, spacing, axis, targetCount); var parts = fillFunc(item, startBox); if (targetCount > 0 && startBox != box && (parts == null || parts.Count < targetCount)) { parts = fillFunc(item, box); } if (parts == null || parts.Count == 0) return new ShrinkResult { Parts = parts ?? new List(), Dimension = 0 }; var shrinkTarget = targetCount > 0 ? System.Math.Min(targetCount, parts.Count) : parts.Count; if (parts.Count > shrinkTarget) parts = TrimToCount(parts, shrinkTarget, axis); var dim = MeasureDimension(parts, box, axis); ReportShrinkProgress(progress, plateNumber, placedParts, parts, box, axis, dim); return new ShrinkResult { Parts = parts, Dimension = dim }; } private static void ReportShrinkProgress( IProgress progress, int plateNumber, List placedParts, List bestParts, Box workArea, ShrinkAxis axis, double dim) { if (progress == null) return; var allParts = placedParts != null && placedParts.Count > 0 ? new List(placedParts.Count + bestParts.Count) : new List(bestParts.Count); if (placedParts != null && placedParts.Count > 0) allParts.AddRange(placedParts); allParts.AddRange(bestParts); var desc = $"Shrink {axis}: {bestParts.Count} parts, dim={dim:F1}"; NestEngineBase.ReportProgress(progress, NestPhase.Custom, plateNumber, allParts, workArea, desc); } /// /// Uses FillBestFit (fast rectangle packing) to estimate a starting box /// that fits roughly the target count. Scales the shrink axis proportionally /// from the full-area count down to the target, with margin. /// private static Box EstimateStartBox(NestItem item, Box box, double spacing, ShrinkAxis axis, int targetCount) { var bbox = item.Drawing.Program.BoundingBox(); if (bbox.Width <= 0 || bbox.Length <= 0) return box; var maxDim = axis == ShrinkAxis.Height ? box.Length : box.Width; // Use FillBestFit for a fast, accurate rectangle count on the full box. var bin = new Bin { Size = new Size(box.Width, box.Length) }; var packItem = new Item { Size = new Size(bbox.Width + spacing, bbox.Length + spacing) }; var packer = new FillBestFit(bin); packer.Fill(packItem); var fullCount = bin.Items.Count; if (fullCount <= 0 || fullCount <= targetCount) return box; // Scale dimension proportionally: target/full * maxDim, with margin. var ratio = (double)targetCount / fullCount; var estimate = maxDim * ratio * 1.3; estimate = System.Math.Min(estimate, maxDim); if (estimate <= 0 || estimate >= maxDim) return box; return axis == ShrinkAxis.Height ? new Box(box.X, box.Y, box.Width, estimate) : new Box(box.X, box.Y, estimate, box.Length); } private static double MeasureDimension(List parts, Box box, ShrinkAxis axis) { var placedBox = parts.Cast().GetBoundingBox(); return axis == ShrinkAxis.Width ? placedBox.Right - box.X : placedBox.Top - box.Y; } /// /// Keeps the parts nearest to the origin /// along the given axis, discarding parts farthest from the origin. /// Returns the input list unchanged if count is already at or below target. /// internal static List TrimToCount(List parts, int targetCount, ShrinkAxis axis) { if (parts == null || parts.Count <= targetCount) return parts; return axis == ShrinkAxis.Width ? parts.OrderBy(p => p.BoundingBox.Right).Take(targetCount).ToList() : parts.OrderBy(p => p.BoundingBox.Top).Take(targetCount).ToList(); } } }