#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
namespace OpenNest.Engine.NestingEngines.Irregular;
///
/// Fills one sheet with like parts grouped: each type with three or more copies packs as one
/// group, the groups in turn ( group ranks), and then the copies held
/// back from the groups and the types too few to group fill the gaps the groups left.
///
/// Holding copies back is what keeps the groups dense: the last copies of a group often start a
/// new row or column on their own, where they block room a later group could use whole; placed
/// last, they drop into whatever gaps remain. The search tries group orders with nothing held
/// back, then, from the best orders, holds back copies type by type while that improves the sheet.
/// When the sheet then holds everything, the best candidates and every order are packed again
/// inside a strip ending just short of the front, while that still holds everything and improves the sheet:
/// Fill blocks are sized to the free room, so a shorter strip yields denser groups.
///
/// Every fill is metered by the shared ; the search starts no fill once
/// the budget is spent and keeps the best sheet so far.
///
internal sealed class GroupedFill(
IReadOnlyList types,
Func?, double?, FrontierPacker> packer,
PackAxis axis,
bool shrink,
Comparison compare,
WorkCounter counter,
long budget)
{
/// Fewest copies of a type that pack as a group.
internal const int MinGroup = 3;
/// Group counts up to this are searched in every order; more use ranked orders.
private const int MaxPermuted = 4;
/// Best candidates carried into hold-back search and shorter strips.
private const int Refined = 3;
/// Most copies held back from one group.
private const int MaxHeldBack = 4;
/// Most shorter strips tried once everything fits the sheet.
private const int MaxShrinks = 4;
/// How much shorter than the best front each shorter strip ends.
private const double Shrink = 0.25;
/// Whether grouping can change anything: two or more types of which one groups.
public static bool Applies(IReadOnlyList types, IReadOnlyList remaining) =>
types.Count(t => remaining[t.Index] > 0) > 1 && types.Any(t => remaining[t.Index] >= MinGroup);
private sealed record Candidate(int[] Order, int[] Held, SheetFill Fill);
public SheetFill Fill(IReadOnlyList remaining, CancellationToken token)
{
var grouped = types.Where(t => remaining[t.Index] >= MinGroup && t.Orientations.Count > 0)
.Select(t => t.Index).ToArray();
var none = new int[types.Count];
var orders = Orders(grouped, remaining).ToList();
var ranked = Evaluate(orders.Select(o => (o, none)), remaining, null, token);
ranked = Refine(ranked.Take(Refined), remaining, null, token);
var best = ranked[0].Fill;
var total = remaining.Sum();
for (var i = 0; shrink && i < MaxShrinks && best.Parts.Count == total && counter.Value < budget; i++)
{
var reach = Front(best) - Shrink;
var seeds = ranked.Take(Refined).Select(c => (c.Order, c.Held)).Concat(orders.Select(o => (o, none)));
var tried = Evaluate(seeds, remaining, reach, token);
tried = Refine(tried.Take(1), remaining, reach, token).Concat(tried.Skip(1)).ToList();
if (tried[0].Fill.Parts.Count < total || compare(tried[0].Fill, best) >= 0)
break;
best = tried[0].Fill;
ranked = tried;
}
return best;
}
private double Front(SheetFill fill) =>
axis == PackAxis.X ? fill.Parts.Max(p => p.Right) : fill.Parts.Max(p => p.Top);
/// Packs each (order, held) in turn, best first; ties keep the earlier one. At least
/// one is packed whatever the budget.
private List Evaluate(IEnumerable<(int[] Order, int[] Held)> seeds, IReadOnlyList remaining,
double? reach, CancellationToken token)
{
var results = new List();
foreach (var (order, held) in seeds)
{
if (results.Count > 0 && counter.Value >= budget)
break;
results.Add(new Candidate(order, held, Run(order, held, remaining, reach, token)));
}
return Sort(results);
}
/// Holds back copies of one type at a time, keeping each count that improves the sheet.
private List Refine(IEnumerable starts, IReadOnlyList remaining, double? reach,
CancellationToken token)
{
var results = new List();
foreach (var start in starts)
{
var incumbent = start;
foreach (var type in start.Order)
{
var limit = System.Math.Min(MaxHeldBack, remaining[type] - MinGroup);
for (var count = 0; count <= limit && counter.Value < budget; count++)
{
if (count == incumbent.Held[type])
continue;
var held = incumbent.Held.ToArray();
held[type] = count;
var fill = Run(start.Order, held, remaining, reach, token);
if (compare(fill, incumbent.Fill) < 0)
incumbent = new Candidate(start.Order, held, fill);
}
}
results.Add(incumbent);
}
return Sort(results);
}
private List Sort(List candidates) =>
candidates.Select((c, i) => (c, i)).OrderBy(x => x.c.Fill, Comparer.Create(compare))
.ThenBy(x => x.i).Select(x => x.c).ToList();
/// The groups packed in , less the held-back copies, then
/// everything else filling around them.
private SheetFill Run(int[] order, int[] held, IReadOnlyList remaining, double? reach, CancellationToken token)
{
token.ThrowIfCancellationRequested();
var ranks = new int[types.Count];
var groups = new int[types.Count];
for (var i = 0; i < order.Length; i++)
{
ranks[order[i]] = i;
groups[order[i]] = remaining[order[i]] - held[order[i]];
}
var first = packer(ranks, reach).Fill(groups, token);
var rest = remaining.ToArray();
foreach (var part in first.Parts)
rest[part.Orientation.TypeIndex]--;
if (rest.All(r => r == 0))
return first;
var second = packer(null, reach).Fill(rest, token, first.Parts);
return first with
{
Parts = first.Parts.Concat(second.Parts).ToList(),
PartArea = first.PartArea + second.PartArea,
};
}
///
/// Every order of up to groups; otherwise the groups ranked by total
/// area, part area and part size, largest first and last.
///
private IEnumerable Orders(int[] grouped, IReadOnlyList remaining)
{
if (grouped.Length <= MaxPermuted)
return Permutations(grouped);
var size = new Func(t => types[t].Orientations.Max(o => System.Math.Max(o.Width, o.Height)));
var keys = new Func[] { t => types[t].Area * remaining[t], t => types[t].Area, size };
var orders = new List();
foreach (var key in keys)
{
var descending = grouped.OrderByDescending(key).ThenBy(t => t).ToArray();
foreach (var order in new[] { descending, descending.Reverse().ToArray() })
if (!orders.Any(o => o.SequenceEqual(order)))
orders.Add(order);
}
return orders;
}
private static IEnumerable Permutations(int[] items)
{
if (items.Length <= 1)
{
yield return items.ToArray();
yield break;
}
for (var i = 0; i < items.Length; i++)
{
var rest = items.Where((_, j) => j != i).ToArray();
foreach (var tail in Permutations(rest))
yield return new[] { items[i] }.Concat(tail).ToArray();
}
}
}