using System; using System.Collections.Generic; using System.Linq; using System.Threading; using OpenNest.Geometry; using OpenNest.Math; namespace OpenNest.Engine.CuttingPlanning; /// /// Proposes a serpentine whole-part order for large plates whose parts sit in aligned rows and /// columns: each row in turn, alternating direction, starting from the corner nearest the tool. /// Part centers are binned into rows by Y and columns by X within a quarter of the smallest part /// extent; a plate is a grid when every gap between neighboring rows and columns is wider than /// that, no two parts share a cell and the cells are at least a quarter occupied (interleaved /// rows fill half). Rows run along whichever axis gives the shorter route. It replaces the /// traveling-salesman improvement, whose passes grow with the square of the part count, and, /// like that order, is only a proposal: the forward search still checks every rapid and lead. /// internal static class CuttingGridOrder { /// Smaller plates keep the traveling-salesman order, which is fast enough there. internal const int MinimumParts = 200; /// The bin tolerance as a fraction of the smallest part extent on that axis. internal const double BinFraction = 0.25; /// The fewest grid cells, as a fraction, that must hold a part. internal const double MinimumOccupancy = 0.25; /// Each part's cut extent, by ordinal. /// The modeled tool position before the first part. /// The serpentine order of every ordinal, or null when the parts do not form a grid. internal static int[] Plan(IReadOnlyList extents, Vector start, CancellationToken token) { var count = extents.Count; if (count < MinimumParts) return null; var centers = extents.Select(box => box.Center).ToArray(); var rows = Bins(centers.Select(c => c.Y).ToArray(), BinFraction * extents.Min(box => box.Top - box.Bottom), token); var columns = Bins(centers.Select(c => c.X).ToArray(), BinFraction * extents.Min(box => box.Right - box.Left), token); if (rows == null || columns == null) return null; var rowCount = rows.Max() + 1; var columnCount = columns.Max() + 1; if (rowCount < 2 || columnCount < 2 || count < MinimumOccupancy * rowCount * (double)columnCount) return null; var cells = new HashSet<(int, int)>(); for (var part = 0; part < count; part++) if (!cells.Add((rows[part], columns[part]))) return null; token.ThrowIfCancellationRequested(); int[] best = null; var bestLength = double.PositiveInfinity; foreach (var (lines, along) in new[] { (rows, columns), (columns, rows) }) foreach (var reverseLines in new[] { false, true }) foreach (var reverseFirst in new[] { false, true }) { var order = Serpentine(lines, along, reverseLines, reverseFirst); var length = Length(order, centers, start); if (length < bestLength) { best = order; bestLength = length; } } return best; } /// /// Bin index per value: sorted values open a new bin once they pass the bin's first value by /// more than . Null when two bins are not separated by more than /// the tolerance, since the values then drift rather than line up. /// internal static int[] Bins(IReadOnlyList values, double tolerance, CancellationToken token) { if (!double.IsFinite(tolerance) || tolerance <= Tolerance.Epsilon || values.Any(v => !double.IsFinite(v))) return null; var sorted = Enumerable.Range(0, values.Count).OrderBy(i => values[i]).ToArray(); var bins = new int[values.Count]; var bin = 0; var first = values[sorted[0]]; var previous = first; foreach (var index in sorted) { token.ThrowIfCancellationRequested(); var value = values[index]; if (value - first > tolerance) { if (value - previous <= tolerance) return null; bin++; first = value; } bins[index] = bin; previous = value; } return bins; } // Lines in bin order (or reversed), each sorted along the other axis, alternating direction. private static int[] Serpentine(int[] lines, int[] along, bool reverseLines, bool reverseFirst) { var groups = Enumerable.Range(0, lines.Length).GroupBy(part => lines[part]) .OrderBy(group => reverseLines ? -group.Key : group.Key).ToArray(); var order = new List(lines.Length); for (var line = 0; line < groups.Length; line++) { var forward = (line % 2 == 0) != reverseFirst; order.AddRange(forward ? groups[line].OrderBy(part => along[part]) : groups[line].OrderByDescending(part => along[part])); } return order.ToArray(); } private static double Length(int[] order, Vector[] centers, Vector start) { var length = start.DistanceTo(centers[order[0]]); for (var i = 1; i < order.Length; i++) length += centers[order[i - 1]].DistanceTo(centers[order[i]]); return length; } }