mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-11 12:04:13 -04:00
102 lines
5.3 KiB
C#
102 lines
5.3 KiB
C#
#nullable enable
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using OpenNest.CNC;
|
|
using OpenNest.CNC.CuttingPlanning;
|
|
using OpenNest.Geometry;
|
|
|
|
namespace OpenNest.Engine.CuttingPlanning;
|
|
|
|
/// <summary>
|
|
/// One preferred sequence of owned contour choices for a holed part: every remaining hole
|
|
/// gets exactly one lead-feasible entry, resolved BACKWARD from the already-chosen
|
|
/// perimeter entry — the last hole faces the perimeter's actual emitted pierce, each earlier
|
|
/// hole faces the next hole's actual emitted pierce — so the cut chain flows toward the
|
|
/// outside start. This is a deterministic recommended v1 proposal, not an optimal joint
|
|
/// tour and not an installed program: the search still certifies every rapid, lead and
|
|
/// crossing. A blocked preferred rapid is the search's problem (S12), never ignored here.
|
|
/// </summary>
|
|
internal static class PreferredContourEntries
|
|
{
|
|
/// <summary>Proposed hole choices in cut order, or an explicit no-preference verdict.</summary>
|
|
internal sealed record Proposal(
|
|
IReadOnlyList<ContourChoice> HoleChoices,
|
|
ContourSelectionShortfall Shortfall,
|
|
int? BlockedContour,
|
|
string? Reason)
|
|
{
|
|
public bool IsPreferred => Shortfall != ContourSelectionShortfall.Incomplete
|
|
&& HoleChoices.Count > 0 && BlockedContour is null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves one preferred entry per hole on <paramref name="holeRoute"/> (cut order,
|
|
/// from <see cref="CuttingHoleOrder"/>), ending at <paramref name="perimeterChoice"/>.
|
|
/// Arrival proxies avoid circularity: the actual (local) part arrival for the first
|
|
/// hole, otherwise the previous hole's centre. Facing target (the downstream actual
|
|
/// pierce) and tier still rank first. <paramref name="evaluate"/> is the S07 adapter
|
|
/// verdict; candidates it refuses or leaves uncertain are not preferred.
|
|
/// </summary>
|
|
internal static Proposal TryPlan(
|
|
PreparedContours prepared,
|
|
ContourChoice perimeterChoice,
|
|
IReadOnlyList<int> holeRoute,
|
|
IReadOnlyList<Vector> centresByOrdinal,
|
|
Vector arrival,
|
|
Func<ContourEntryCandidate, ContourFeasibilityVerdict> evaluate,
|
|
CancellationToken token = default)
|
|
{
|
|
if (prepared == null)
|
|
throw new ArgumentException("Prepared contours are required.", nameof(prepared));
|
|
if (perimeterChoice == null || !ReferenceEquals(perimeterChoice.Owner, prepared))
|
|
throw new ArgumentException("The perimeter choice must belong to this preparation.", nameof(perimeterChoice));
|
|
if (holeRoute == null || centresByOrdinal == null || evaluate == null)
|
|
throw new ArgumentException("Hole route, centres and evaluation are required.");
|
|
token.ThrowIfCancellationRequested();
|
|
if (holeRoute.Count == 0)
|
|
return new(Array.Empty<ContourChoice>(), ContourSelectionShortfall.Exhausted, null, null);
|
|
|
|
// The downstream target is where the tool ACTUALLY arrives next: the emitted
|
|
// contour's first cut/lead motion start (native rounding/clamping included).
|
|
var target = Pierce(prepared, perimeterChoice, token);
|
|
var choices = new ContourChoice[holeRoute.Count];
|
|
for (var index = holeRoute.Count - 1; index >= 0; index--)
|
|
{
|
|
token.ThrowIfCancellationRequested();
|
|
var contour = holeRoute[index];
|
|
var arrivalProxy = index == 0 ? arrival : centresByOrdinal[holeRoute[index - 1]];
|
|
// The S04 merged catalogue: preferred kinds when present, fallback kinds
|
|
// (compass points, arc midpoints, ...) otherwise — pure circles included.
|
|
var catalogue = prepared.AutomaticEntryCandidatesWithFallbacks(contour, target, token);
|
|
var ranked = catalogue.RankTowardNextCut(target, arrivalProxy);
|
|
var selection = ContourEntrySelection.Select(ranked, evaluate, 1, token);
|
|
if (selection.Choices.Count == 0)
|
|
return new(Array.Empty<ContourChoice>(), selection.Shortfall, contour,
|
|
$"Hole contour {contour} has no preferred lead-feasible entry: {selection.Reason}");
|
|
choices[index] = selection.Choices[0];
|
|
target = Pierce(prepared, choices[index], token);
|
|
}
|
|
|
|
if (choices.Any(c => c == null!))
|
|
throw new InvalidOperationException("Preferred hole resolution left a gap.");
|
|
return new(choices, ContourSelectionShortfall.None, null, null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The actual emitted pierce of one owned choice in local coordinates: the start of the
|
|
/// first non-rapid motion (lead-in when present — that is where the next rapid must
|
|
/// arrive), which reflects native rounding and clamping of a nominal entry.
|
|
/// </summary>
|
|
internal static Vector Pierce(PreparedContours prepared, ContourChoice choice, CancellationToken token)
|
|
{
|
|
var program = prepared.EmitCandidateForValidation(choice);
|
|
var execution = ExecutionMotionReader.Read(program, Vector.Zero, null, token);
|
|
// The diagnostic emission includes the part's scribes before the selected contour.
|
|
// Those marks are not the downstream cut's pierce.
|
|
var first = execution.Motions.First(m => !m.Rapid && m.Layer != LayerType.Scribe);
|
|
return first.Start ?? first.End;
|
|
}
|
|
}
|