Files
OpenNest/OpenNest.Tests/Diagnostics/PostVerificationAnalyzerTests.cs
T

483 lines
18 KiB
C#

using OpenNest.CNC;
using OpenNest.CNC.CuttingStrategy;
using OpenNest.Diagnostics;
using OpenNest.Geometry;
namespace OpenNest.Tests.Diagnostics;
public class PostVerificationAnalyzerTests
{
[Fact]
public void EmptyReport_ListsAllChecksAndLimits()
{
var report = PostVerificationAnalyzer.Analyze(new Nest());
Assert.Empty(report.Findings);
Assert.False(report.HasWarnings);
Assert.True(report.CanPost(false));
Assert.True(report.CanPost(true));
var text = report.ToDisplayText();
Assert.Contains("Overlap", text);
Assert.Contains("Missing lead-ins", text);
Assert.Contains("Rapid crossings", text);
Assert.Contains("not a physical safety certification", text);
Assert.Contains("retract", text);
Assert.Contains("routing", text);
}
[Fact]
public void Overlap_UsesCleanMaterialAndOriginalOneBasedIndices()
{
var a = Rectangle(0, 0, 4);
var b = Rectangle(1, 1, 1);
// Deliberately unrelated placed toolpaths: overlap must use clean drawings.
a.Program.Codes.Clear();
a.Program.MoveTo(100, 100);
a.Program.LineTo(101, 101);
var report = Analyze(Cutoff(30, 30), a, Scribe(40, 40), b);
var overlap = Assert.Single(Find(report, PostVerificationKind.Overlap));
Assert.Equal((1, 2, 4), (overlap.PlateNumber, overlap.PartNumber, overlap.OtherPartNumber));
Assert.True(report.HasWarnings);
Assert.False(report.CanPost(false));
Assert.True(report.CanPost(true));
Assert.False(report.CanPost(false)); // Consent is supplied per invocation, never latched.
}
[Fact]
public void Overlap_SubtractsHoles()
{
var program = Square(0, 0, 10);
program.Codes.AddRange(Square(2, 2, 6).Codes);
var frame = Part(program);
var report = Analyze(frame, Rectangle(3, 3, 1));
Assert.Empty(Find(report, PostVerificationKind.Overlap));
Assert.Empty(Find(report, PostVerificationKind.Incomplete));
}
[Fact]
public void MissingLeadIn_ManualFlagDoesNotProveCoverage()
{
var part = Rectangle(0, 0, 4);
part.HasManualLeadIns = true;
Assert.Single(Find(Analyze(part), PostVerificationKind.MissingLeadIn));
}
[Fact]
public void MissingLeadIn_ActualLeadDoesNotRequireManualFlag()
{
var part = Rectangle(0, 0, 4);
part.ApplyLeadIns(Leads(), new Vector(-2, -2));
part.HasManualLeadIns = false;
Assert.Empty(Find(Analyze(part), PostVerificationKind.MissingLeadIn));
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void MissingLeadIn_NoLeadOrZeroLengthWarns(bool zeroLength)
{
var part = Rectangle(0, 0, 4);
var parameters = Leads();
parameters.ExternalLeadIn = zeroLength ? new LineLeadIn { Length = 0 } : new NoLeadIn();
part.ApplyLeadIns(parameters, new Vector(-2, -2));
Assert.Single(Find(Analyze(part), PostVerificationKind.MissingLeadIn));
}
[Fact]
public void MissingLeadIn_PartialApplySingleLeavesOtherContourUncovered()
{
var program = Square(0, 0, 10);
program.Codes.AddRange(Square(2, 2, 2).Codes);
var part = Part(program);
part.ApplySingleLeadIn(Leads(), Vector.Zero,
new Line(Vector.Zero, new Vector(10, 0)), ContourType.External);
Assert.True(part.HasManualLeadIns);
Assert.Single(Find(Analyze(part), PostVerificationKind.MissingLeadIn));
}
[Fact]
public void ScribeAndCutoff_AreExemptFromOverlapAndLeadInChecks()
{
var report = Analyze(Scribe(0, 0), Cutoff(0, 0));
Assert.Empty(report.Findings);
}
[Fact]
public void Rapid_ThroughPreviouslyCutPartWarns()
{
var report = Analyze(Rectangle(0, 0, 4), Scribe(-2, 2), Scribe(6, 2));
Assert.Contains(Find(report, PostVerificationKind.RapidCrossing), f =>
f.PartNumber == 3 && f.OtherPartNumber == 1);
}
[Fact]
public void Rapid_DoesNotUseFuturePartsAsObstacles()
{
var report = Analyze(Scribe(-2, 2), Scribe(6, 2), Rectangle(0, 0, 4));
Assert.Empty(Find(report, PostVerificationKind.RapidCrossing));
}
[Theory]
[InlineData(-2, -2, false)] // Start-only departure.
[InlineData(2, 2, true)] // Departure into the interior.
[InlineData(4, 0, true)] // Along edge / arrival on another endpoint.
public void Rapid_HandlesDepartureVersusInteriorAndBoundary(double x, double y, bool warning)
{
var report = Analyze(Rectangle(0, 0, 4), Scribe(x, y));
Assert.Equal(warning, Find(report, PostVerificationKind.RapidCrossing).Any());
}
[Theory]
[InlineData(-2, 4, 6, 4)] // Tangent along top edge.
[InlineData(-2, 2, 0, 2)] // Endpoint arrival.
[InlineData(1, 1, 2, 2)] // Entirely inside.
public void Rapid_ConservativelyFlagsContactAndInterior(double x1, double y1, double x2, double y2)
{
var report = Analyze(Rectangle(0, 0, 4), Scribe(x1, y1), Scribe(x2, y2));
Assert.Contains(Find(report, PostVerificationKind.RapidCrossing), f => f.PartNumber == 3);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void Rapid_ActualGapNotStaleTabSettingControlsObstacle(bool staleFlag)
{
var closed = Rectangle(0, 0, 4);
closed.CuttingParameters = new CuttingParameters { TabsEnabled = staleFlag };
Assert.Contains(Find(Analyze(closed, Scribe(-2, 2), Scribe(6, 2)),
PostVerificationKind.RapidCrossing), f => f.PartNumber == 3);
var gapped = Rectangle(0, 0, 4);
((LinearMove)gapped.Program.Codes[^1]).EndPoint = new Vector(0, 0.25);
gapped.CuttingParameters = new CuttingParameters { TabsEnabled = staleFlag };
Assert.Empty(Find(Analyze(gapped, Scribe(-2, 2), Scribe(6, 2)),
PostVerificationKind.RapidCrossing));
}
[Fact]
public void Rapid_RealGeneratedTabRemainsGapWithStaleDisabledParameters()
{
var part = Rectangle(0, 0, 4);
var parameters = Leads();
parameters.TabsEnabled = true;
parameters.TabConfig = new NormalTab { Size = 0.25 };
part.ApplyLeadIns(parameters, new Vector(-2, 0));
parameters.TabsEnabled = false;
Assert.Empty(Find(Analyze(part, Scribe(-2, 2), Scribe(6, 2)), PostVerificationKind.RapidCrossing));
}
[Fact]
public void Rapid_CutoffTravelStillCrossesEarlierRealCuts()
{
var report = Analyze(Rectangle(0, 0, 4), Scribe(-2, 2), Cutoff(6, 2));
Assert.Contains(Find(report, PostVerificationKind.RapidCrossing), f =>
f.PartNumber == 3 && f.OtherPartNumber == 1);
}
[Theory]
[InlineData(Mode.Absolute)]
[InlineData(Mode.Incremental)]
public void Rapid_SamePartSharedHoleCallsUseOffsetsAndNonzeroLeadOrigins(Mode mode)
{
var part = Rectangle(100, 50, 20);
var hole = CircleWithLead();
hole.Mode = mode;
var placed = new Program();
placed.Codes.Add(new SubProgramCall { Program = hole, Offset = new Vector(5, 5), Id = 7 });
placed.MoveTo(2, 5);
placed.LineTo(2, 5); // Zero-length cut doesn't create an obstacle.
placed.MoveTo(8, 5); // Cross completed first hole.
placed.Codes.Add(new SubProgramCall { Program = hole, Offset = new Vector(15, 5), Id = 7 });
placed.Mode = mode;
Assert.True(part.RestoreLeadInProgram(placed, false));
var report = Analyze(part);
Assert.Contains(Find(report, PostVerificationKind.RapidCrossing), f =>
f.PartNumber == 1 && f.OtherPartNumber == 1);
Assert.Empty(Find(report, PostVerificationKind.Incomplete));
}
[Theory]
[InlineData(Mode.Absolute)]
[InlineData(Mode.Incremental)]
public void Rapid_NonzeroInitialLeadRapidIsNotAppliedTwice(Mode mode)
{
var part = Rectangle(100, 50, 4);
var placed = Square(0, 0, 4);
placed.Codes.Insert(0, new RapidMove(-2, 0));
placed.Codes[1] = new LinearMove(0, 0) { Layer = LayerType.Leadin };
placed.MoveTo(-2, 2);
placed.MoveTo(6, 2);
placed.Mode = mode;
part.RestoreLeadInProgram(placed, false);
var crossings = Find(Analyze(part), PostVerificationKind.RapidCrossing);
Assert.Single(crossings);
Assert.Empty(Find(Analyze(part), PostVerificationKind.MissingLeadIn));
}
[Theory]
[InlineData(RotationType.CW)]
[InlineData(RotationType.CCW)]
public void Rapid_FullCircleArcTangentIsNotLostToChordApproximation(RotationType rotation)
{
var program = new Program();
program.MoveTo(1, 0);
program.ArcTo(1, 0, 0, 0, rotation);
var report = Analyze(Part(program), Scribe(-2, 1), Scribe(2, 1));
Assert.Contains(Find(report, PostVerificationKind.RapidCrossing), f => f.PartNumber == 3);
}
[Fact]
public void Rapid_SamePartFutureContourIsNotAnObstacle()
{
var part = Rectangle(0, 0, 10);
var placed = new Program();
placed.MoveTo(-2, 2);
placed.MoveTo(6, 2);
placed.Codes.AddRange(Square(0, 0, 4).Codes);
part.RestoreLeadInProgram(placed, false);
Assert.Empty(Find(Analyze(part), PostVerificationKind.RapidCrossing));
}
[Fact]
public void MultiplePlates_AreAllCheckedWithoutSharingObstacles()
{
var nest = NestWith(Rectangle(0, 0, 4));
var second = nest.CreatePlate();
second.Parts.Add(Scribe(-2, 2));
second.Parts.Add(Scribe(6, 2));
second.Parts.Add(Rectangle(0, 0, 4));
second.Parts.Add(Rectangle(1, 1, 1));
var report = PostVerificationAnalyzer.Analyze(nest);
Assert.Contains(Find(report, PostVerificationKind.MissingLeadIn), f => f.PlateNumber == 1);
Assert.Contains(Find(report, PostVerificationKind.MissingLeadIn), f => f.PlateNumber == 2);
Assert.All(Find(report, PostVerificationKind.Overlap), f => Assert.Equal(2, f.PlateNumber));
Assert.DoesNotContain(Find(report, PostVerificationKind.RapidCrossing), f =>
f.PlateNumber == 2 && f.PartNumber <= 3);
}
[Theory]
[InlineData("recursive")]
[InlineData("missing-call")]
[InlineData("null-code")]
[InlineData("nonfinite")]
[InlineData("bad-arc")]
[InlineData("empty")]
[InlineData("null-codes")]
public void MalformedPlacedPrograms_WarnInsteadOfClearing(string fault)
{
var part = Rectangle(0, 0, 4);
var program = part.Program;
switch (fault)
{
case "recursive": program.Codes.Add(new SubProgramCall { Program = program }); break;
case "missing-call": program.Codes.Add(new SubProgramCall()); break;
case "null-code": program.Codes.Add(null); break;
case "nonfinite": program.MoveTo(double.NaN, 0); break;
case "bad-arc": program.ArcTo(1, 1, 0, 0, RotationType.CW); break;
case "empty": program.Codes.Clear(); break;
case "null-codes": program.Codes = null; break;
}
var report = Analyze(part);
Assert.NotEmpty(Find(report, PostVerificationKind.Incomplete));
Assert.True(report.HasWarnings);
Assert.False(report.CanPost(false));
Assert.DoesNotContain("No warnings", report.ToDisplayText());
}
[Fact]
public void MalformedCleanProgram_OverlapIssuePreservesInputIndex()
{
var part = Rectangle(0, 0, 4);
part.BaseDrawing.Program.Codes.RemoveAt(4);
var report = Analyze(Cutoff(30, 30), part);
Assert.Contains(Find(report, PostVerificationKind.Incomplete), f => f.PartNumber == 2);
}
[Fact]
public void Rapid_ResumedContourBecomesObstacleOnlyAfterItsLastCut()
{
var part = Rectangle(0, 0, 4);
var placed = Square(0, 0, 4);
placed.Codes.Insert(3, new RapidMove(4, 4)); // Pause, with no uncut gap.
placed.MoveTo(-2, 2);
placed.MoveTo(6, 2);
part.RestoreLeadInProgram(placed, false);
Assert.Single(Find(Analyze(part), PostVerificationKind.RapidCrossing));
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void Rapid_InternalGapDoesNotBecomeAnObstacle(bool staleFlag)
{
var part = Rectangle(0, 0, 4);
var placed = Square(0, 0, 4);
placed.Codes.Insert(3, new RapidMove(3.75, 4)); // Leave a gap on the top edge.
placed.MoveTo(-2, 2);
placed.MoveTo(6, 2);
part.RestoreLeadInProgram(placed, false);
part.CuttingParameters = new CuttingParameters { TabsEnabled = staleFlag };
Assert.Empty(Find(Analyze(part), PostVerificationKind.RapidCrossing));
}
[Fact]
public void MissingPlacedCuts_CannotPassForACleanMaterialPart()
{
var part = Rectangle(0, 0, 4);
part.Program.Codes.Clear();
part.Program.MoveTo(0, 0);
var report = Analyze(part);
Assert.NotEmpty(Find(report, PostVerificationKind.Incomplete));
Assert.False(report.CanPost(false));
}
[Fact]
public void InvalidArcDirection_IsIncomplete()
{
var part = Part(CircleWithLead());
((ArcMove)part.Program.Codes[^1]).Rotation = (RotationType)99;
Assert.NotEmpty(Find(Analyze(part), PostVerificationKind.Incomplete));
}
[Fact]
public void Rapid_LeadOutThatCompletesTabGapIsNotTreatedAsRetained()
{
var part = Rectangle(0, 0, 4);
((LinearMove)part.Program.Codes[^1]).EndPoint = new Vector(0, 0.25);
part.Program.Codes.Add(new LinearMove(0, 0) { Layer = LayerType.Leadout });
var report = Analyze(part, Scribe(-2, 2), Scribe(6, 2));
Assert.True(Find(report, PostVerificationKind.RapidCrossing).Any()
|| Find(report, PostVerificationKind.Incomplete).Any());
}
[Fact]
public void Rapid_OutOfOrderFragmentsCannotSilentlyPassAsTabbed()
{
var part = Rectangle(0, 0, 4);
var program = new Program();
program.MoveTo(0, 0);
program.LineTo(4, 0);
program.MoveTo(4, 4);
program.LineTo(0, 4);
program.MoveTo(4, 0);
program.LineTo(4, 4);
program.MoveTo(0, 4);
program.LineTo(0, 0);
part.RestoreLeadInProgram(program, false);
var report = Analyze(part, Scribe(-2, 2), Scribe(6, 2));
Assert.True(Find(report, PostVerificationKind.RapidCrossing).Any()
|| Find(report, PostVerificationKind.Incomplete).Any());
}
[Fact]
public void Cancellation_ThrowsWithoutChangingInputs()
{
var part = Rectangle(0, 0, 4);
var before = part.Program.ToString();
using var cancellation = new CancellationTokenSource();
cancellation.Cancel();
Assert.Throws<OperationCanceledException>(() =>
PostVerificationAnalyzer.Analyze(NestWith(part), cancellation.Token));
Assert.Equal(before, part.Program.ToString());
}
[Fact]
public void Analyze_PreservesProgramsPosesSharedCallsAndCuttingState()
{
var part = Rectangle(100, 50, 20);
var sub = CircleWithLead();
var program = Square(0, 0, 20);
program.Codes.Insert(0, new SubProgramCall { Program = sub, Offset = new Vector(5, 5) });
program.Codes.Insert(1, new SubProgramCall { Program = sub, Offset = new Vector(15, 5) });
program.Mode = Mode.Incremental;
part.RestoreLeadInProgram(program, true);
part.CuttingParameters = Leads();
var nest = NestWith(part);
var codes = program.Codes.ToArray();
var before = program.ToString();
var beforeSub = sub.ToString();
var parameters = part.CuttingParameters;
var bounds = part.BoundingBox;
var quantity = part.BaseDrawing.Quantity.Nested;
var report = PostVerificationAnalyzer.Analyze(nest);
Assert.Same(program, part.Program);
Assert.Equal(codes, program.Codes);
Assert.Equal(before, program.ToString());
Assert.Equal(beforeSub, sub.ToString());
Assert.Same(sub, ((SubProgramCall)program[0]).Program);
Assert.Same(sub, ((SubProgramCall)program[1]).Program);
Assert.Equal(new Vector(100, 50), part.Location);
Assert.Same(bounds, part.BoundingBox);
Assert.True(part.HasManualLeadIns);
Assert.True(part.LeadInsLocked);
Assert.Same(parameters, part.CuttingParameters);
Assert.Equal(quantity, part.BaseDrawing.Quantity.Nested);
Assert.True(((ICollection<PostVerificationFinding>)report.Findings).IsReadOnly);
var display = report.ToDisplayText();
part.Program.Codes.Clear();
Assert.Equal(display, report.ToDisplayText());
}
private static PostVerificationFinding[] Find(PostVerificationReport report, PostVerificationKind kind) =>
report.Findings.Where(f => f.Kind == kind).ToArray();
private static PostVerificationReport Analyze(params Part[] parts) =>
PostVerificationAnalyzer.Analyze(NestWith(parts));
private static Nest NestWith(params Part[] parts)
{
var nest = new Nest();
var plate = nest.CreatePlate();
foreach (var part in parts)
plate.Parts.Add(part);
return nest;
}
private static Part Part(Program program) => new(new Drawing("fixture", program));
private static Part Rectangle(double x, double y, double size) =>
new(new Drawing("rectangle", Square(0, 0, size)), new Vector(x, y));
private static Program Square(double x, double y, double size)
{
var program = new Program();
program.MoveTo(x, y);
program.LineTo(x + size, y);
program.LineTo(x + size, y + size);
program.LineTo(x, y + size);
program.LineTo(x, y);
return program;
}
private static Part Scribe(double x, double y)
{
var program = new Program();
program.MoveTo(x, y);
program.Codes.Add(new LinearMove(x, y) { Layer = LayerType.Scribe });
return Part(program);
}
private static Part Cutoff(double x, double y)
{
var program = new Program();
program.MoveTo(x, y);
program.LineTo(x + 1, y);
return new Part(new Drawing("cutoff", program) { IsCutOff = true });
}
private static Program CircleWithLead()
{
var program = new Program();
program.MoveTo(0.5, 0);
program.Codes.Add(new LinearMove(1, 0) { Layer = LayerType.Leadin });
program.Codes.Add(new ArcMove(1, 0, 0, 0) { Layer = LayerType.Display });
return program;
}
private static CuttingParameters Leads() => new()
{
ExternalLeadIn = new LineLeadIn { Length = 0.5 },
InternalLeadIn = new LineLeadIn { Length = 0.5 },
ArcCircleLeadIn = new LineLeadIn { Length = 0.5 }
};
}