mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-10 20:14:13 -04:00
No-hole parts now choose the outside entry through the S03-S08 pipeline at the part boundary: rank the native catalogue toward the NEXT cut's placed-material centre, lazily certify each emitted lead with the shared validator, cap at maxEntries with side coverage. The next cut is the next unfinished part on the supplied order (re-read after every learned-order replan) or, in sequence-free fallback, the nearest dependency-ready remaining part with stable ordinal ties; the last part has no target and ranks by tier then arrival distance — never the plate origin. Target and arrival are converted to prepared LOCAL coordinates exactly once; geometry is already rotated. The look-ahead rank survives Follow's ordering: children sort by nearest source first (the sequence-free tour stays nearest-first), then contour, then the selection rank — plain OrderBy(Distance) can no longer undo the facing. Measured fixture (three squares, 0.15 leads, origin start): legacy cut every sheet at its arrival-nearest lower-left corner with 10.5-unit cross-sheet rapids totalling 21.0; look-ahead cuts the +X-facing corner with the same 21.0 total but each inter-part rapid now starts at the facing edge instead of trailing across the whole sheet. Uncertain validator answers are never precheck-refused: they skip the selected slots but flow to the emitted-prefix Check and complete replay, which stay the authority — a refused-looking incomplete is reported, not hidden. A fully evaluated catalogue with no fitting lead surfaces 'No tested lead-in fits on part N, contour M'; budget exhaustion stays a budget finding. Lead prechecks count separately from DFS expansions; the pipeline itself is one counted expansion per contour. Full backtracking, dependencies, locked/fixed programs, cutoff handling, emitted-prefix Check and complete replay are unchanged; budgets and CuttingPartOrder untouched. Hole parts keep the legacy path until S12.
118 lines
5.6 KiB
C#
118 lines
5.6 KiB
C#
#nullable enable
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using OpenNest.CNC.CuttingPlanning;
|
|
using OpenNest.Geometry;
|
|
|
|
namespace OpenNest.Engine.CuttingPlanning;
|
|
|
|
/// <summary>The three verdicts a candidate entry can receive from the shared lead validator.</summary>
|
|
public enum ContourFeasibilityStatus
|
|
{
|
|
/// <summary>Every emitted lead of this contour is complete AND clear.</summary>
|
|
Clear,
|
|
|
|
/// <summary>A complete check found a lead contact/overlap — the reason is preserved.</summary>
|
|
Blocked,
|
|
|
|
/// <summary>The check could not complete (incomplete material, malformed emission) — never clear.</summary>
|
|
Incomplete,
|
|
}
|
|
|
|
/// <summary>Verdict for one candidate entry. Complete/blocked/incomplete stay distinct.</summary>
|
|
public sealed record ContourFeasibilityVerdict(ContourFeasibilityStatus Status, string? Reason)
|
|
{
|
|
public bool IsClear => Status == ContourFeasibilityStatus.Clear;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Feasibility adapter over the EXISTING <see cref="LeadPathValidator"/>: emits one owned
|
|
/// candidate contour through the S06 diagnostic seam, reads it at the placement position,
|
|
/// and certifies the emitted lead-in and lead-out against ALL placed material. It is not a
|
|
/// new collision implementation and not a plan approval: a Clear verdict certifies this
|
|
/// contour's emitted leads only — the missing-lead (NoLeadIn) check still runs later on the
|
|
/// complete plan, and rapids/pierce clearance belong to their existing checkers. A
|
|
/// candidate rejected here is not proven infeasible by anything else: this adapter only
|
|
/// reports what the validator reported.
|
|
/// One instance is one captured planning attempt: verdicts cache per exact choice and node
|
|
/// context for the instance's lifetime — settings and placement are fixed per instance —
|
|
/// and nothing is cached across instances or statically. Evaluation is lazy: only the
|
|
/// choice handed to <see cref="Check"/> is ever emitted or validated, and
|
|
/// <see cref="EvaluationCount"/> counts validator executions (not cache hits) for cost tests.
|
|
/// </summary>
|
|
public sealed class ContourEntryFeasibility
|
|
{
|
|
private readonly PreparedContours prepared;
|
|
private readonly Vector location;
|
|
private readonly LeadMaterialSnapshot ownMaterial;
|
|
private readonly LeadMaterialSnapshot[] materials;
|
|
private readonly Dictionary<Key, ContourFeasibilityVerdict> cache = new();
|
|
|
|
public ContourEntryFeasibility(PreparedContours prepared, Vector location,
|
|
LeadMaterialSnapshot ownMaterial, IReadOnlyList<LeadMaterialSnapshot> otherMaterials)
|
|
{
|
|
this.prepared = prepared ?? throw new ArgumentException("Prepared contours are required.", nameof(prepared));
|
|
this.ownMaterial = ownMaterial ?? throw new ArgumentException("Own material snapshot is required.", nameof(ownMaterial));
|
|
if (otherMaterials == null)
|
|
throw new ArgumentException("Placed-material snapshots are required.", nameof(otherMaterials));
|
|
this.location = location;
|
|
// Immutable copy; the validator's own-material-first convention is preserved.
|
|
var all = new List<LeadMaterialSnapshot> { this.ownMaterial };
|
|
foreach (var material in otherMaterials)
|
|
{
|
|
if (material == null)
|
|
throw new ArgumentException("Placed-material snapshots must not be null.", nameof(otherMaterials));
|
|
if (!ReferenceEquals(material, this.ownMaterial))
|
|
all.Add(material);
|
|
}
|
|
materials = all.ToArray();
|
|
}
|
|
|
|
/// <summary>Validator executions performed (cache misses only) during this attempt.</summary>
|
|
public int EvaluationCount { get; private set; }
|
|
|
|
/// <summary>
|
|
/// The verdict for one owned candidate. Same choice + node context within this attempt
|
|
/// is answered from cache. Cancellation propagates; a malformed emission is an
|
|
/// Incomplete verdict with the emission's own reason, never a crash and never Clear.
|
|
/// </summary>
|
|
public ContourFeasibilityVerdict Check(ContourChoice choice, string nodeContext = "",
|
|
CancellationToken token = default)
|
|
{
|
|
token.ThrowIfCancellationRequested();
|
|
if (choice == null)
|
|
throw new ArgumentException("A contour choice is required.", nameof(choice));
|
|
var key = new Key(choice.ContourOrdinal, choice.EntityOrdinal,
|
|
choice.Point.X, choice.Point.Y, nodeContext ?? string.Empty);
|
|
if (cache.TryGetValue(key, out var known))
|
|
return known;
|
|
var verdict = Probe(choice, token);
|
|
cache[key] = verdict;
|
|
EvaluationCount++;
|
|
return verdict;
|
|
}
|
|
|
|
private ContourFeasibilityVerdict Probe(ContourChoice choice, CancellationToken token)
|
|
{
|
|
try
|
|
{
|
|
var program = prepared.EmitCandidateForValidation(choice);
|
|
var execution = ExecutionMotionReader.Read(program, location, null, token);
|
|
var result = LeadPathValidator.Check(execution, ownMaterial, materials, token);
|
|
if (!result.IsComplete)
|
|
return new(ContourFeasibilityStatus.Incomplete, result.Reason);
|
|
return result.IsClear
|
|
? new(ContourFeasibilityStatus.Clear, null)
|
|
: new(ContourFeasibilityStatus.Blocked, result.Reason);
|
|
}
|
|
catch (Exception ex) when (ex is ArgumentException or NotSupportedException)
|
|
{
|
|
// Malformed emission or foreign choice: refused, reason preserved, never cached as clear.
|
|
return new(ContourFeasibilityStatus.Incomplete, ex.Message);
|
|
}
|
|
}
|
|
|
|
private readonly record struct Key(int ContourOrdinal, int EntityOrdinal, double X, double Y, string NodeContext);
|
|
}
|