using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using OpenNest.Geometry;
namespace OpenNest.Engine.CuttingPlanning;
///
/// Proposes a whole-part order for plates with parts nested inside other parts' cutouts. A host
/// is cut after everything inside it, so the tool reaches it from whichever inner part was cut
/// last; from the middle of a crowded cutout every straight approach crosses a finished
/// neighbour. Each host's inner parts are therefore toured as one run that ends beside the
/// host's own contours, immediately before the host. Plates without nested parts get exactly
/// 's order. Like that order, this is only a proposal.
///
internal static class CuttingNestedOrder
{
/// One representative point per part ordinal.
/// Ordinals that must come before each part, learned rules included.
/// Per part ordinal, the parts nested inside its cutouts at any depth.
/// The point on a host's contours nearest a tool position.
internal sealed record Context(IReadOnlyList Centres,
IReadOnlyList> Prerequisites, IReadOnlyList> Inside,
Func HostEntry, CancellationToken Token);
/// The part ordinals to order; prerequisites outside this set count as done.
/// The modeled tool position before the first of these parts.
/// Optional position the run should end beside.
internal static int[] Plan(IReadOnlyList parts, Vector start, Context context, Vector? endpoint = null)
{
context.Token.ThrowIfCancellationRequested();
var set = parts.ToHashSet();
var rootOf = new Dictionary();
foreach (var host in parts)
foreach (var inner in context.Inside[host])
if (set.Contains(inner) && !context.Inside[inner].Contains(host))
rootOf[inner] = host;
if (rootOf.Count == 0)
return Flat();
// Follow nested hosts up to the outermost one present.
foreach (var inner in rootOf.Keys.ToArray())
while (rootOf.TryGetValue(rootOf[inner], out var outer))
rootOf[inner] = outer;
var roots = parts.Where(p => !rootOf.ContainsKey(p)).ToArray();
// A rule between parts of two groups orders the groups.
var groupRules = context.Centres.Select(_ => new HashSet()).ToArray();
foreach (var part in parts)
{
var group = rootOf.GetValueOrDefault(part, part);
foreach (var prerequisite in context.Prerequisites[part])
if (set.Contains(prerequisite) && rootOf.GetValueOrDefault(prerequisite, prerequisite) != group)
groupRules[group].Add(rootOf.GetValueOrDefault(prerequisite, prerequisite));
}
// A finished host is a large obstacle and can only be left across its own outside
// edge, so the parts beside it go first. Rules that say otherwise win.
var hostsLast = groupRules.Select(rules => new HashSet(rules)).ToArray();
var hosts = rootOf.Values.ToHashSet();
foreach (var host in hosts)
foreach (var root in roots)
if (!hosts.Contains(root))
hostsLast[host].Add(root);
int[] rootOrder;
try
{
rootOrder = CuttingPartOrder.Plan(roots, context.Centres, start, hostsLast, context.Token, endpoint);
}
catch (InvalidOperationException)
{
try
{
rootOrder = CuttingPartOrder.Plan(roots, context.Centres, start, groupRules, context.Token, endpoint);
}
catch (InvalidOperationException)
{
return Flat(); // Rules that cross groups both ways cannot keep the groups whole.
}
}
var order = new List(parts.Count);
var position = start;
foreach (var root in rootOrder)
{
var inner = parts.Where(p => rootOf.TryGetValue(p, out var host) && host == root).ToArray();
if (inner.Length != 0)
order.AddRange(Plan(inner, position, context, context.HostEntry(root, position)));
order.Add(root);
position = context.Centres[root];
}
var placed = new HashSet();
foreach (var part in order)
{
if (context.Prerequisites[part].Any(p => set.Contains(p) && !placed.Contains(p)))
return Flat();
placed.Add(part);
}
return order.ToArray();
int[] Flat() => CuttingPartOrder.Plan(parts, context.Centres, start, context.Prerequisites, context.Token, endpoint);
}
}