mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-04 17:11:34 -05:00
A cut-off always runs away from its fixed edge: the origin side, the only edge guaranteed to be a real sheet edge. The part order is proposed as a traveling-salesman path over one point per placement, and a cut-off's point was the middle of its line. The route did not know the tool must arrive at the line's start and leaves at its far end, so the parts before a sheet-splitting line finished level with its middle, and the rapid down to its start crossed parts already cut. Each placement now has an entry and an exit point; they differ only for cut-offs (start and departure). Nearest neighbor and Or-opt go on from each part's exit, and 2-opt costs a reversed run along its own directed legs; with no cut-off on the plate the exits are not passed and the symmetric route is unchanged. The part cut before a cut-off faces its start. Cut-off direction is not chosen here: the order reads each cut-off's start and end from its program as given, and the joint search may still reverse a head-locked far-edge segment.
105 lines
5.2 KiB
C#
105 lines
5.2 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using OpenNest.Geometry;
|
|
|
|
namespace OpenNest.Engine.CuttingPlanning;
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// neighbor. 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
|
|
/// <see cref="CuttingPartOrder"/>'s order. Like that order, this is only a proposal.
|
|
/// </summary>
|
|
internal static class CuttingNestedOrder
|
|
{
|
|
/// <param name="Centers">One representative point per part ordinal.</param>
|
|
/// <param name="Prerequisites">Ordinals that must come before each part, learned rules included.</param>
|
|
/// <param name="Inside">Per part ordinal, the parts nested inside its cutouts at any depth.</param>
|
|
/// <param name="HostEntry">The point on a host's contours nearest a tool position.</param>
|
|
/// <param name="Exits">Optional point each part is left from (a cut-off's far end); null when
|
|
/// every part is left from its center.</param>
|
|
internal sealed record Context(IReadOnlyList<Vector> Centers,
|
|
IReadOnlyList<IReadOnlyCollection<int>> Prerequisites, IReadOnlyList<IReadOnlyCollection<int>> Inside,
|
|
Func<int, Vector, Vector> HostEntry, CancellationToken Token, IReadOnlyList<Vector> Exits = null);
|
|
|
|
/// <param name="parts">The part ordinals to order; prerequisites outside this set count as done.</param>
|
|
/// <param name="start">The modeled tool position before the first of these parts.</param>
|
|
/// <param name="endpoint">Optional position the run should end beside.</param>
|
|
internal static int[] Plan(IReadOnlyList<int> parts, Vector start, Context context, Vector? endpoint = null)
|
|
{
|
|
context.Token.ThrowIfCancellationRequested();
|
|
var set = parts.ToHashSet();
|
|
var rootOf = new Dictionary<int, int>();
|
|
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.Centers.Select(_ => new HashSet<int>()).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<int>(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.Centers, start, hostsLast, context.Token, endpoint, context.Exits);
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
try
|
|
{
|
|
rootOrder = CuttingPartOrder.Plan(roots, context.Centers, start, groupRules, context.Token, endpoint,
|
|
context.Exits);
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
return Flat(); // Rules that cross groups both ways cannot keep the groups whole.
|
|
}
|
|
}
|
|
var order = new List<int>(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.Exits ?? context.Centers)[root];
|
|
}
|
|
var placed = new HashSet<int>();
|
|
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.Centers, start, context.Prerequisites, context.Token, endpoint,
|
|
context.Exits);
|
|
}
|
|
}
|