fix(cutting): certify complete selected contour programs

This commit is contained in:
aj committed 2026-10-04 22:02:56 -04:00
1 parent 9f7580a486
commit e8d974a2db
8 files changed
+538 -47

No files matched your search

@@ -94,8 +94,27 @@ public sealed class FixedProgramPlacement
public bool IsRegenerated => ContourChoices.Count != 0;
/// <summary>Returns an independent deep copy; never an alias to captured/proposed code.</summary>
public Program CopyProgram() => program == null ? null : OwnedProgramCopy.Copy(program);
internal FixedProgramPlacement Propose(Program proposed, OwnedExecution execution, IReadOnlyList<ContourChoice> choices) =>
new(SourcePart, SourceOrdinal, Location, Rotation, LeadInsLocked, execution, OwnedProgramCopy.Copy(proposed), Prepared, Material, choices);
internal SelectedContourProgram SelectedProgram { get; private init; }
internal FixedProgramPlacement Propose(Program proposed, OwnedExecution execution, IReadOnlyList<ContourChoice> choices,
System.Threading.CancellationToken token = default)
{
SelectedContourProgram selected = null;
try
{
// The expected program is constructed from owned choices/settings, NEVER
// from proposed or its cached execution. Counterfeit payloads remain subject
// to independent final checks, including callers of this internal test seam.
selected = Prepared?.CaptureSelectedProgram(choices, Location, token);
}
catch (Exception ex) when (ex is ArgumentException or NotSupportedException or InvalidOperationException or ArithmeticException)
{
// Invalid metadata is a refused proposal, not an exception from final replay.
}
return new(SourcePart, SourceOrdinal, Location, Rotation, LeadInsLocked, execution,
OwnedProgramCopy.Copy(proposed, token), Prepared, Material, choices)
{ SelectedProgram = selected };
}
public Part SourcePart { get; }
public int SourceOrdinal { get; }
@@ -201,18 +201,15 @@ public static class CuttingPlanService
if (!ReferenceEquals(source, proposal))
return new(CuttingPlanStatus.InvalidInput, expansions: expansions);
}
else if (proposal.ContourChoices.Count != source.Prepared.Count
|| proposal.ContourChoices.Any(c => !ReferenceEquals(c.Owner, source.Prepared)
|| c.ContourOrdinal < 0 || c.ContourOrdinal >= source.Prepared.Count)
|| proposal.ContourChoices.Select(c => c.ContourOrdinal).Distinct().Count() != source.Prepared.Count
|| proposal.ContourChoices[^1].ContourOrdinal != source.Prepared.PerimeterOrdinal)
else if (proposal.SelectedProgram == null
|| !proposal.SelectedProgram.Matches(source.Prepared, proposal.ContourChoices))
return new(CuttingPlanStatus.InvalidInput, expansions: expansions);
OwnedExecution execution;
bool complete;
try
{
execution = ExecutionMotionReader.Read(proposal.CopyProgram(), proposal.Location, position, token);
if (source.Prepared != null && !HasCompleteContourAccounting(execution, source.Material, token))
return new(CuttingPlanStatus.InvalidInput, expansions: expansions);
complete = ContourProgramVerifier.Verify(execution, source.Material, proposal.SelectedProgram, token);
}
catch (Exception ex) when (ex is ArgumentException or NotSupportedException)
{
@@ -225,6 +222,11 @@ public static class CuttingPlanService
if (!lead.IsComplete || !lead.IsClear)
findings.Add(JointCuttingPlanSearch.Finding(source,
lead.IsComplete ? null : PostVerificationKind.Incomplete, lead.Reason));
if (!complete && findings.Count == 0)
return new(CuttingPlanStatus.InvalidInput,
findings: [JointCuttingPlanSearch.Finding(source, null,
"Selected program does not preserve complete directed contours, certified tabs or selected entry/style geometry.")],
expansions: expansions);
distance += execution.RapidDistanceFrom(position);
position = execution.DeparturePoint;
}
@@ -237,42 +239,6 @@ public static class CuttingPlanService
rapidDistance: distance, independentlyReplayed: true);
}
private static bool HasCompleteContourAccounting(OwnedExecution execution, LeadMaterialSnapshot material, CancellationToken token)
{
var visited = new HashSet<int>();
var current = -1;
var budget = 1000000;
foreach (var move in execution.Motions)
{
token.ThrowIfCancellationRequested();
if (move.Rapid || move.Layer is not (LayerType.Cut or LayerType.Display))
{
current = -1;
continue;
}
var ring = -1;
for (var i = 0; i < material.Rings.Count; i++)
if (material.Rings[i].Any(c => Matches(c, move)))
{
ring = i;
break;
}
if (ring < 0 || current >= 0 && ring != current || current < 0 && !visited.Add(ring))
return false;
current = ring;
}
return visited.Count == material.Rings.Count;
bool Matches(PostVerificationGeometry.Curve curve, ExecutionMotion move)
{
token.ThrowIfCancellationRequested();
if (--budget < 0) throw new NotSupportedException("Replay contour accounting exceeds the native query limit.");
return curve.SameSupport(move.Curve) && curve.Contains(move.Curve.Start)
&& curve.Contains(move.End) && curve.Contains(move.Curve.Midpoint)
&& move.Length <= curve.Length + PostVerificationGeometry.Epsilon;
}
}
private static bool SameBits(double source, double proposed) =>
BitConverter.DoubleToInt64Bits(source) == BitConverter.DoubleToInt64Bits(proposed);
@@ -105,7 +105,7 @@ internal static class JointCuttingPlanSearch
if (!Check(source, execution, arrival, checker)) continue;
var distance = execution.RapidDistanceFrom(arrival);
var next = prefix.Length == prepared.Count
? new Node([.. node.Order, source.Propose(program, execution, prefix)], execution.DeparturePoint, checker, null)
? new Node([.. node.Order, source.Propose(program, execution, prefix, token)], execution.DeparturePoint, checker, null)
: new Node(node.Order, execution.DeparturePoint, checker,
new(source, prefix, arrival, before, distance));
yield return new(next, distance - (node.Active?.Distance ?? 0), source.SourceOrdinal, contour, entry);