Files
OpenNest/OpenNest.Engine/CuttingPlanning/CuttingHoleOrder.cs
T
aj fa3c5e348e feat(cutting): propose hole paths toward the perimeter entry
CuttingHoleOrder proposes the cut order of a part's remaining holes as
an OPEN path arrival -> every hole centre exactly once -> the fixed
perimeter entry, reusing CuttingPartOrder's bounded nearest-neighbour +
2-opt/Or-opt machinery (no exhaustive permutations, no new routing
invention). A proposal only: the search still certifies every rapid and
emitted lead; nothing is installed into the search yet.

CuttingPartOrder gains an optional endpoint: the terminal edge joins
EVERY improvement delta (2-opt tail edge on both sides; OrOpt gap to the
open end measures the endpoint distance), so heuristics see route moves
that re-point the path at the entry rather than only final scoring. No
endpoint keeps whole-part semantics byte-identical (control test);
precedence, prerequisites and tie policy unchanged.

Tests: empty/one hole; asymmetric fixture where the endpoint changes the
ORDER (open route ends at C, entry-facing route ends at B) with each
route asserted equal to its brute-force optimum computed from
coordinates; permutation integrity excluding the perimeter; random 7-set
within 1.05x brute force; deterministic tie order; cancellation; legacy
no-endpoint equality. Mutants dropping the terminal edge from OrOpt or
TwoOpt deltas are killed.
2026-10-07 11:43:26 -04:00

46 lines
2.2 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using OpenNest.Geometry;
namespace OpenNest.Engine.CuttingPlanning;
/// <summary>
/// Proposes the ORDER in which a part's remaining holes are cut when the perimeter entry is
/// already chosen: an open path from the tool's arrival through every hole centre and ON to
/// the selected perimeter entry — the terminal edge is real rapid travel and is optimised,
/// not scored after the fact. It reuses <see cref="CuttingPartOrder"/>'s bounded nearest-
/// neighbour + 2-opt/Or-opt machinery (no exhaustive permutations) with an optional fixed
/// endpoint, so whole-part routing without an endpoint keeps byte-identical behaviour.
/// This is a proposal only: the search still certifies every rapid and emitted lead along
/// it, exactly as it does for part orders.
/// </summary>
internal static class CuttingHoleOrder
{
/// <summary>
/// Orders <paramref name="holes"/> (stable ordinals, each appearing exactly once in the
/// result, never the perimeter) to minimize arrival -&gt; holes -&gt; <paramref
/// name="perimeterEntry"/> distance. <paramref name="centresByOrdinal"/> supplies one
/// representative point per hole ordinal. Empty holes give an empty order.
/// </summary>
internal static IReadOnlyList<int> Plan(IReadOnlyList<int> holes,
IReadOnlyList<Vector> centresByOrdinal, Vector arrival, Vector perimeterEntry,
CancellationToken token = default)
{
if (holes == null)
throw new ArgumentException("Hole ordinals are required.", nameof(holes));
if (centresByOrdinal == null)
throw new ArgumentException("Hole centres are required.", nameof(centresByOrdinal));
token.ThrowIfCancellationRequested();
if (holes.Count == 0)
return Array.Empty<int>();
var none = Array.Empty<int>();
var prerequisites = holes.Select(_ => (IReadOnlyCollection<int>)none).ToList();
var centres = holes.Select(hole => centresByOrdinal[hole]).ToList();
var route = CuttingPartOrder.Plan(centres, arrival, prerequisites, token, perimeterEntry);
return route.Select(index => holes[index]).ToArray();
}
}