mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-20 18:31:37 -04:00
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.
46 lines
2.2 KiB
C#
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 -> holes -> <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();
|
|
}
|
|
}
|