mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-07 17:02:11 -04:00
fix(cutting): validate original graphs before capture clones
This commit is contained in:
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user