mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-04 12:31:35 -05:00
A converted outline whose seam closes within the admitted 1e-6 gap could only be entered at its seam: entered anywhere else, the arc after the seam started off its own circle and every emission was refused as an inconsistent radius. The search then spent its whole budget on that part and the plate never planned. Emission now bridges the gap with a straight move and cuts the arc as drawn; the verifier already admits that slack. An entry on the seam vertex is left as before. Each prefix of a part re-checked its earlier contours' leads against every other part's material. One search now remembers each verdict by the lead curve's exact bits and the material's reference, charging the same native query budget; replay never uses it. On the reported plate the memo alone gives a byte-identical result in 57 s instead of 12 min 47 s; with the seam fix the plate plans Ready in about 17 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
141 lines
7.0 KiB
C#
141 lines
7.0 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, 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, token)
|
|
: LeadPathValidator.Check(execution, ownMaterial, materials, leadContacts, 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);
|
|
}
|