using OpenNest.CNC;
using OpenNest.CNC.CuttingPlanning;
using OpenNest.CNC.CuttingStrategy;
using OpenNest.Diagnostics;
using OpenNest.Engine.CuttingPlanning;
using OpenNest.Geometry;
using OpenNest.Support;
using OpenNest.Tests.CuttingPlanning;
namespace OpenNest.Tests.Support;
///
/// Slug slices (docs/support-tabs.md#slug-slices): the slug of a cutout its support bed cannot
/// hold level is cut along the bed's contact lines, a kerf short of the cutout, before the cutout.
///
public class SlugSliceTests
{
private const double Kerf = 0.06;
// A 2 inch laser grid with no registration slack, so contacts are exactly on its lines.
private static readonly SupportGrid Bed = new(2, 2);
// An 8 x 1 slot at world (3, 3.5)-(11, 4.5): its slug rests on the grid's row at Y = 4 alone.
private static readonly Vector[] Slot =
[new(2, 2.5), new(10, 2.5), new(10, 3.5), new(2, 3.5)];
private static readonly Vector Location = new(1, 1);
private static CuttingParameters Parameters(bool slice = true, double kerf = Kerf)
{
var parameters = ExplicitContourTests.Parameters();
parameters.Kerf = kerf;
parameters.SliceTippingSlugs = slice;
return parameters;
}
// A 12 inch square plate part with one rectangular hole, both local.
private static Drawing Frame(double x1 = 2, double y1 = 2.5, double x2 = 10, double y2 = 3.5)
{
var program = LeadPathValidationTests.Rectangle(0, 0, 12, 12);
var hole = LeadPathValidationTests.Rectangle(x1, y1, x2, y2);
program.Codes.AddRange(hole.Codes);
return new Drawing("frame", program);
}
private static (Nest Nest, Plate Plate, Part Part) Plate(CuttingParameters parameters, Drawing drawing = null,
SupportBed bed = null)
{
var nest = new Nest("slug slices");
var plate = new Plate(30, 30) { CuttingParameters = parameters };
nest.Plates.Add(plate);
nest.SetSupportBed(bed ?? Bed);
var part = new Part(drawing ?? Frame(), Location);
plate.Parts.Add(part);
return (nest, plate, part);
}
private static IReadOnlyList Motions(FixedProgramPlacement proposal) =>
ExecutionMotionReader.Read(proposal.CopyProgram(), proposal.Location, null, default).Motions;
private static double DistanceToSlot(Vector world)
{
// The slot in world coordinates: (3, 3.5)-(11, 4.5); inside it, the nearest side.
return System.Math.Min(System.Math.Min(world.X - 3, 11 - world.X), System.Math.Min(world.Y - 3.5, 4.5 - world.Y));
}
[Fact]
public void ContactLines_AreTheGridsRowsAndColumnsAndTheSlatsCenters()
{
var region = new Box(3, 3.5, 8, 1);
var grid = Bed.ContactLines(region, 1);
var slats = new SupportSlats(SlatAxis.Y, 3, 0.25, offset: 1.5).ContactLines(region, 1);
var mirrored = new SupportGrid(2, 2, 1, 0).ContactLines(new Box(-5, 0, 2, 1), 2);
Assert.Equal([new(true, 4), new(false, 4), new(false, 6), new(false, 8), new(false, 10)], grid);
Assert.Equal([new(false, 4.5), new(false, 7.5), new(false, 10.5)], slats);
// A sheet in quadrant 2 sees the grid's X offset mirrored: columns at -1, -3, -5, ...
Assert.Equal([new(true, 0), new(false, -5), new(false, -3)], mirrored);
}
[Fact]
public void Tips_OnlyForASlugOnContactsTheBedCannotHoldLevel()
{
SupportMaterial World(double x1, double y1, double x2, double y2) =>
SupportMaterial.FromPolygons([new(x1, y1), new(x2, y1), new(x2, y2), new(x1, y2)]);
// On one row of contacts: it balances on a line and tips.
Assert.True(SlugSlicer.Tips(Bed, 1, World(3, 3.5, 11, 4.5)));
// On a block of contacts around its center: it lies level.
Assert.False(SlugSlicer.Tips(Bed, 1, World(3, 3, 11, 9)));
// Between contacts: it falls through whole.
Assert.False(SlugSlicer.Tips(Bed, 1, World(2.5, 2.5, 3.5, 3.5)));
}
[Fact]
public void Slices_RunAlongTheContactLinesAKerfShortOfTheCutout()
{
var slices = SlugSlicer.Slices(Bed, 1, Slot, Location, Kerf, reach: 0.3, pierceClearance: 0.05);
// The row at world Y = 4 left to right, then the columns from X = 10 back to 4, back and forth.
Assert.Equal(5, slices.Count);
var row = slices[0];
Assert.Equal(3, row.Start.Y, 9);
Assert.Equal(3, row.End.Y, 9);
Assert.True(row.End.X > row.Start.X);
Assert.Equal([9, 7, 5, 3], slices.Skip(1).Select(s => System.Math.Round(s.Start.X, 9)));
for (var i = 2; i < slices.Count; i++)
Assert.NotEqual(slices[i - 1].End.Y > slices[i - 1].Start.Y, slices[i].End.Y > slices[i].Start.Y);
foreach (var slice in slices)
{
// Within the Clipper rounding allowance of the kerf, never closer.
foreach (var end in new[] { slice.Start, slice.End })
{
var distance = DistanceToSlot(end + Location);
Assert.InRange(distance, Kerf, Kerf + 1e-3);
}
// Pierced on the slice, its lead-in reach (at most halfway) from the start.
var length = slice.Start.DistanceTo(slice.End);
Assert.Equal(System.Math.Min(0.3, length / 2), slice.Pierce.DistanceTo(slice.Start), 9);
Assert.Equal(length, slice.Pierce.DistanceTo(slice.Start) + slice.Pierce.DistanceTo(slice.End), 9);
}
}
[Fact]
public void Slices_LeaveOutSlicesWhosePierceFitsNeitherEnd()
{
// Pierced 0.1 along a slice from an end 0.06 from the slot, no pierce keeps 0.2 from it.
Assert.Empty(SlugSlicer.Slices(Bed, 1, Slot, Location, Kerf, reach: 0.1, pierceClearance: 0.2));
}
[Fact]
public void PlanCutting_SlicesATippingSlugBeforeItsCutout()
{
var (nest, plate, _) = Plate(Parameters());
var proposal = CuttingPlanBatch.Capture([plate], Parameters(), preservePartOrder: false).Plan();
var result = proposal.Plates[0].Result;
Assert.True(result.Status == CuttingPlanStatus.Ready,
string.Join("\n", proposal.Describe("in").Concat(result.Findings.Select(f => f.Message))));
Assert.True(proposal.CanApply);
Assert.Empty(result.Findings);
var motions = Motions(Assert.Single(result.ProposedOrder));
var slices = motions.Where(m => !m.Rapid && m.Layer == LayerType.Break).ToList();
Assert.Equal(5, slices.Count);
foreach (var slice in slices)
{
Assert.InRange(DistanceToSlot(slice.Curve.Start), Kerf - 1e-9, 1);
Assert.InRange(DistanceToSlot(slice.Curve.End), Kerf - 1e-9, 1);
}
// Every slice is cut before any contour, so before the slot.
var lastSlice = motions.ToList().FindLastIndex(m => !m.Rapid && m.Layer == LayerType.Break);
var firstCut = motions.ToList().FindIndex(m => !m.Rapid && m.Layer is LayerType.Cut or LayerType.Display);
Assert.InRange(lastSlice, 0, firstCut - 1);
Assert.Equal(CuttingCommitStatus.Applied, proposal.Apply().Status);
var review = PostVerificationAnalyzer.Analyze(nest).Findings;
Assert.DoesNotContain(review, f => f.Kind is PostVerificationKind.Incomplete or PostVerificationKind.RapidCrossing
or PostVerificationKind.MissingLeadIn or PostVerificationKind.FixedLead or PostVerificationKind.PierceZone);
}
[Theory]
[InlineData(false, Kerf)] // off
[InlineData(true, 0)] // no kerf to stop short by
public void PlanCutting_SlicesNothing_WhenOffOrWithoutAKerf(bool slice, double kerf)
{
var (_, plate, _) = Plate(Parameters(slice, kerf));
var result = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate, Vector.Zero,
confirmedParameters: Parameters(slice, kerf)));
Assert.Equal(CuttingPlanStatus.Ready, result.Status);
Assert.DoesNotContain(Motions(Assert.Single(result.ProposedOrder)), m => m.Layer == LayerType.Break);
}
[Fact]
public void PlanCutting_DoesNotSliceASlugTheBedHoldsOrThatFallsThrough()
{
// A 6 x 6 hole rests level on a block of contacts; a 1 x 1 hole between contacts falls through.
var program = LeadPathValidationTests.Rectangle(0, 0, 12, 12);
program.Codes.AddRange(LeadPathValidationTests.Rectangle(2, 2, 8, 8).Codes);
program.Codes.AddRange(LeadPathValidationTests.Rectangle(9.5, 9.5, 10.5, 10.5).Codes);
var (_, plate, _) = Plate(Parameters(), new Drawing("two holes", program));
var result = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate, Vector.Zero,
confirmedParameters: Parameters()));
Assert.Equal(CuttingPlanStatus.Ready, result.Status);
Assert.DoesNotContain(Motions(Assert.Single(result.ProposedOrder)), m => m.Layer == LayerType.Break);
}
[Fact]
public void PlanCutting_DoesNotSliceACutoutAnotherPartIsNestedIn()
{
// A 7.5 x 0.6 strip nested in the slot would be cut through by the slices.
var (_, plate, _) = Plate(Parameters());
plate.Parts.Add(new Part(new Drawing("strip", LeadPathValidationTests.Rectangle(0, 0, 7.5, 0.6)),
new Vector(3.25, 3.7)));
var result = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate, Vector.Zero,
confirmedParameters: Parameters()));
Assert.True(result.Status == CuttingPlanStatus.Ready, string.Join("\n", result.Findings.Select(f => f.Message)));
Assert.All(result.ProposedOrder, proposal =>
Assert.DoesNotContain(Motions(proposal), m => m.Layer == LayerType.Break));
}
[Fact]
public void PlanCutting_SlicesAlongSlats()
{
// Slats along X every 2 inches: the slot's slug rests on the one at Y = 4 alone, too
// narrow under it for the stability margin.
var (_, plate, _) = Plate(Parameters(), bed: new SupportSlats(SlatAxis.X, 2, 0.125, stabilityMargin: 0.25));
var result = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate, Vector.Zero,
confirmedParameters: Parameters()));
Assert.Equal(CuttingPlanStatus.Ready, result.Status);
var slice = Assert.Single(Motions(Assert.Single(result.ProposedOrder)), m => !m.Rapid && m.Layer == LayerType.Break);
Assert.Equal(4, slice.Curve.Start.Y, 9);
Assert.Equal(4, slice.Curve.End.Y, 9);
}
[Fact]
public void Settings_RoundTripAndAreLeftOutWhenOff()
{
var on = OpenNest.IO.CuttingParametersSerializer.Serialize(Parameters());
var off = OpenNest.IO.CuttingParametersSerializer.Serialize(Parameters(slice: false));
Assert.True(OpenNest.IO.CuttingParametersSerializer.Deserialize(on).SliceTippingSlugs);
Assert.False(OpenNest.IO.CuttingParametersSerializer.Deserialize(off).SliceTippingSlugs);
Assert.DoesNotContain("sliceTippingSlugs", off);
Assert.True(OwnedCuttingParameters.Copy(Parameters()).SliceTippingSlugs);
}
[Fact]
public void ReleasedContours_CrossingCutsInsideAHoleCutOnlyItsSlug()
{
// Crossing break cuts in the slot, then the slot, then the outside cut in two open pieces,
// so the open-cut proof runs.
var findings = new ReleasedContourState().Check(
ExecutionMotionReader.Read(TwoPieceFrameWithBreaks(inHole: true), Vector.Zero, null, default), Vector.Zero, 1);
Assert.DoesNotContain(findings, f => f.Kind == PostVerificationKind.Incomplete);
}
[Fact]
public void ReleasedContours_CrossingBreakCutsInMaterialStillNeedReview()
{
var findings = new ReleasedContourState().Check(
ExecutionMotionReader.Read(TwoPieceFrameWithBreaks(inHole: false), Vector.Zero, null, default), Vector.Zero, 1);
Assert.Contains(findings, f => f.Kind == PostVerificationKind.Incomplete
&& f.Message.Contains("Multiple interrupted/open"));
}
// A 14 inch square cut in two open pieces with a slot at (3, 3.5)-(11, 4.5), and a row and two
// columns of break cuts crossing either inside the slot or in the material above it.
private static Program TwoPieceFrameWithBreaks(bool inHole)
{
var y = inHole ? 4 : 8;
var program = new Program();
program.MoveTo(3.1, y);
program.Codes.Add(new LinearMove(10.9, y) { Layer = LayerType.Break });
foreach (var x in new[] { 4.0, 6.0 })
{
program.MoveTo(x, y - 0.4);
program.Codes.Add(new LinearMove(x, y + 0.4) { Layer = LayerType.Break });
}
program.MoveTo(3, 3.5);
program.LineTo(11, 3.5);
program.LineTo(11, 4.5);
program.LineTo(3, 4.5);
program.LineTo(3, 3.5);
program.MoveTo(0.2, 0);
program.LineTo(14, 0);
program.LineTo(14, 14);
program.LineTo(7.2, 14);
program.MoveTo(6.8, 14);
program.LineTo(0, 14);
program.LineTo(0, 0.2);
return program;
}
}