fix(cutting): validate original graphs before capture clones

This commit is contained in:
aj committed 2026-10-04 21:56:49 -04:00
1 parent d7204e8d07
commit e00aa113a0
2 files changed
+139 -8

No files matched your search

@@ -46,19 +46,21 @@ public static class CuttingPlanService
throw new ArgumentException("Missing/duplicate source placement or invalid pose.");
if (source.BaseDrawing.IsCutOff)
throw new NotSupportedException("Cutoff dependency ordering is outside the fixed-program route slice.");
var clean = ExecutionMotionReader.Read(source.BaseDrawing.Program, Vector.Zero, null, token);
// Validate both original graphs before Clone or any virtual transform can
// erase unsupported runtime semantics, including fixed/ineligible targets.
ValidateCloneGraph(source.BaseDrawing.Program, token);
ValidateCloneGraph(source.Program, token);
var clean = ExecutionMotionReader.ReadSupported(source.BaseDrawing.Program, Vector.Zero, null, token);
if (!clean.HasCuttingContour)
throw new NotSupportedException("Scribe-only or noncutting source drawings are outside this route slice.");
var execution = ExecutionMotionReader.Read(source.Program, source.Location, request.StartPoint, token);
var execution = ExecutionMotionReader.ReadSupported(source.Program, source.Location, request.StartPoint, token);
if (!execution.HasCuttingContour)
throw new ArgumentException("Placed program has no nonzero cutting contour motions.");
ValidateCloneGraph(source.Program, token);
var ownedProgram = (Program)source.Program.Clone();
PreparedContours prepared = null;
LeadMaterialSnapshot material = null;
if (request.ConfirmedParameters != null)
{
ValidateCloneGraph(source.BaseDrawing.Program, token);
var ownedClean = (Program)source.BaseDrawing.Program.Clone();
ownedClean.Rotate(source.Rotation - source.BaseDrawing.Program.Rotation);
material = LeadMaterialSnapshot.Capture(ownedClean, source.Location, token);
@@ -274,6 +276,7 @@ public static class CuttingPlanService
{
var active = new HashSet<Program>(ReferenceEqualityComparer.Instance);
var done = new HashSet<Program>(ReferenceEqualityComparer.Instance);
var budget = 1000000;
Visit(program);
void Visit(Program current)
{
@@ -281,13 +284,34 @@ public static class CuttingPlanService
if (current == null || active.Contains(current) || active.Count >= 64)
throw new ArgumentException("Missing, recursive or excessively nested clone graph.");
if (done.Contains(current)) return;
// The reader already validated the executed graph. Inactive registered
// subprograms may be motionless; preserve them exactly, but guard Clone traversal.
if (current.Codes == null || current.Codes.Any(c => c == null))
// Inactive registered subprograms may be motionless, but Clone still
// traverses them. Refuse unknown semantics without invoking virtual Clone.
if (current.GetType() != typeof(Program) || !Enum.IsDefined(current.Mode))
throw new NotSupportedException("Unsupported program runtime type or mode.");
if (current.Codes == null)
throw new ArgumentException("Missing clone graph instructions.");
active.Add(current);
foreach (var child in current.SubPrograms.Values.Concat(current.Codes.OfType<SubProgramCall>().Select(c => c.Program)))
foreach (var code in current.Codes)
{
token.ThrowIfCancellationRequested();
if (--budget < 0)
throw new ArgumentException("Clone graph exceeds the verification limit.");
if (code == null)
throw new ArgumentException("Missing clone graph instruction.");
var type = code.GetType();
if (type != typeof(RapidMove) && type != typeof(LinearMove) && type != typeof(ArcMove)
&& type != typeof(SubProgramCall) && type != typeof(Comment) && type != typeof(Feedrate) && type != typeof(Kerf))
throw new NotSupportedException("Unsupported instruction runtime type.");
if (code is SubProgramCall call)
Visit(call.Program);
}
foreach (var child in current.SubPrograms.Values)
{
token.ThrowIfCancellationRequested();
if (--budget < 0)
throw new ArgumentException("Clone graph exceeds the verification limit.");
Visit(child);
}
active.Remove(current);
done.Add(current);
}
@@ -0,0 +1,107 @@
using System.Reflection;
using OpenNest.CNC;
using OpenNest.CNC.CuttingPlanning;
using OpenNest.Engine.CuttingPlanning;
using OpenNest.Geometry;
namespace OpenNest.Tests.CuttingPlanning;
public class CuttingCaptureFidelityTests
{
public static IEnumerable<object[]> UnsupportedOriginals()
{
foreach (var kind in new[] { "linear", "program", "mode" })
foreach (var nested in new[] { false, true })
foreach (var target in new[] { "eligible", "locked", "ineligible", "placed", "compatibility" })
yield return new object[] { kind, nested, target };
}
[Theory]
[MemberData(nameof(UnsupportedOriginals))]
public void Service_RefusesOriginalUnsupportedSemanticsBeforeClone(string kind, bool nested, string target)
{
var original = Rectangle();
if (kind == "linear")
original.Codes[1] = new CustomLinear { EndPoint = ((Motion)original.Codes[1]).EndPoint };
if (kind == "program")
{
var custom = new CustomProgram();
custom.Codes.AddRange(original.Codes);
original = custom;
}
var invalid = original;
if (nested)
{
var root = new Program();
root.Codes.Add(new SubProgramCall(original, 0) { Id = -3 });
original = root;
}
// Part construction uses legacy Clone, so original subclass information is lost.
var part = new Part(new Drawing("original", original));
if (kind == "linear")
{
var placed = nested ? ((SubProgramCall)part.Program.Codes[0]).Program : part.Program;
Assert.IsType<LinearMove>(placed.Codes[1]);
}
if (target == "placed")
{
part = new Part(new Drawing("supported", Rectangle()));
Assert.True(part.RestoreLeadInProgram(original, false));
}
if (kind == "mode")
typeof(Program).GetField("mode", BindingFlags.Instance | BindingFlags.NonPublic)!.SetValue(invalid, (Mode)100);
part.LeadInsLocked = target == "locked";
var clean = part.BaseDrawing.Program;
var placedProgram = part.Program;
var codes = original.Codes.ToArray();
var invalidCodes = invalid.Codes.ToArray();
var bounds = part.BoundingBox;
var quantity = part.BaseDrawing.Quantity.Nested;
var cloneCalls = invalid.Codes.OfType<CustomLinear>().Sum(c => c.CloneCalls);
var result = CuttingPlanService.Plan(new CuttingPlanRequest([part],
confirmedParameters: target == "compatibility" ? null : ExplicitContourTests.Parameters(),
eligibleParts: target == "ineligible" ? [] : null));
Assert.Equal(CuttingPlanStatus.UnsupportedGeometry, result.Status);
Assert.Empty(result.ProposedOrder);
Assert.False(result.IndependentlyReplayed);
Assert.Contains(result.Findings, f => ReferenceEquals(f.SourcePart, part) && f.SourceOrdinal == 0);
Assert.Same(clean, part.BaseDrawing.Program);
Assert.Same(placedProgram, part.Program);
Assert.Equal(codes, original.Codes);
Assert.Equal(invalidCodes, invalid.Codes);
Assert.Same(bounds, part.BoundingBox);
Assert.Equal(quantity, part.BaseDrawing.Quantity.Nested);
Assert.Equal(cloneCalls, invalid.Codes.OfType<CustomLinear>().Sum(c => c.CloneCalls));
}
[Theory]
[InlineData("linear")]
[InlineData("program")]
[InlineData("mode")]
public void Service_RefusesUnsupportedInactiveRegisteredGraphBeforeClone(string kind)
{
var part = new Part(new Drawing("registered", Rectangle()));
Program inactive = kind == "program" ? new CustomProgram() : new Program();
if (kind == "linear") inactive.Codes.Add(new CustomLinear());
if (kind == "mode")
typeof(Program).GetField("mode", BindingFlags.Instance | BindingFlags.NonPublic)!.SetValue(inactive, (Mode)100);
part.Program.SubPrograms[-27] = inactive;
var result = CuttingPlanService.Plan(new CuttingPlanRequest([part]));
Assert.Equal(CuttingPlanStatus.UnsupportedGeometry, result.Status);
Assert.Empty(result.ProposedOrder);
Assert.Same(inactive, part.Program.SubPrograms[-27]);
Assert.All(inactive.Codes.OfType<CustomLinear>(), c => Assert.Equal(0, c.CloneCalls));
}
private static Program Rectangle() => LeadPathValidationTests.Rectangle(0, 0, 10, 10);
private sealed class CustomProgram : Program { }
private sealed class CustomLinear : LinearMove
{
public int CloneCalls { get; private set; }
public override ICode Clone()
{
CloneCalls++;
return base.Clone();
}
}
}