mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 08:21:05 -04:00
With a pierce diameter set, a perimeter none of whose configured lead-ins keeps its pierce clear (a lead shorter than the zone radius, or neighbours closer than it) was only rejected. Plan Cutting now looks for a pierce in the open scrap around the part instead. - ScrapPierceSearch (Engine): the window four zone radii around the part, minus every nearby part's clean material grown by the zone radius and a margin (ClipperBridge, conservative flattening); the clear point nearest each ranked perimeter entry is a candidate. Other parts' cutouts count as scrap, the part's own holes do not. With no clear scrap, the PolyLabel pole of the free scrap is the best effort, and its shortfall is reported. - ContourChoice.Pierce / PreparedContours.ScrapPierceEntry: the planner's choice carries the pierce; the emitter cuts a straight lead from it to the nearest perimeter point and still reads nothing but the contour and the choice. - Every candidate passes the lead validator like any planner-chosen entry; pinned perimeters keep their pins and warn. Without a pierce diameter nothing changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
143 lines
7.2 KiB
C#
143 lines
7.2 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 — NoLeadIn is supported, and the complete plan must
|
|
/// still pass rapid travel and contour-accounting checks. 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();
|
|
private readonly LeadContactMemo? leadContacts;
|
|
|
|
public ContourEntryFeasibility(PreparedContours prepared, Vector location,
|
|
LeadMaterialSnapshot ownMaterial, IReadOnlyList<LeadMaterialSnapshot> otherMaterials)
|
|
: this(prepared, location, ownMaterial, otherMaterials, null)
|
|
{
|
|
}
|
|
|
|
/// <param name="leadContacts">Shares exact lead/material results with the caller's search.</param>
|
|
internal ContourEntryFeasibility(PreparedContours prepared, Vector location,
|
|
LeadMaterialSnapshot ownMaterial, IReadOnlyList<LeadMaterialSnapshot> otherMaterials,
|
|
LeadContactMemo? leadContacts)
|
|
{
|
|
this.leadContacts = leadContacts;
|
|
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));
|
|
// Ownership is an input boundary, not part of a memoized geometric verdict.
|
|
// Reject before lookup so neither a warm hit nor a foreign first call can
|
|
// substitute for the isolated emitter's owned-choice check.
|
|
if (!ReferenceEquals(choice.Owner, prepared))
|
|
return new(ContourFeasibilityStatus.Incomplete, "Foreign contour choice.");
|
|
// A drawing pin's entry is a fixed emission, accepted as given: it is never filtered
|
|
// out here. The emitted program's checks report its violations as warnings.
|
|
// A forged ordinal falls through to the probe, which refuses it as Incomplete.
|
|
if (choice.ContourOrdinal >= 0 && choice.ContourOrdinal < prepared.Count
|
|
&& prepared.PinnedEntry(choice.ContourOrdinal) == choice)
|
|
return new(ContourFeasibilityStatus.Clear, null);
|
|
var key = new Key(choice.ContourOrdinal, choice.EntityOrdinal,
|
|
choice.Point.X, choice.Point.Y, choice.Pierce?.X, choice.Pierce?.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 = leadContacts == null
|
|
? LeadPathValidator.Check(execution, ownMaterial, materials, prepared.PierceZoneRadius, token)
|
|
: LeadPathValidator.Check(execution, ownMaterial, materials, leadContacts, token,
|
|
prepared.PierceZoneRadius);
|
|
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,
|
|
double? PierceX, double? PierceY, string NodeContext);
|
|
}
|