mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-13 23:55:08 -05:00
On an enclosed machine a cutout slug that tips once cut free can stand up above the sheet and catch the enclosure door when the pallet changes. The head already keeps off cut cutouts, so this is about clearing the door, not the head. Cutting Settings > Cutouts > "Slice slugs the support grid can't hold level" (CuttingParameters.SliceTippingSlugs, off by default, saved per machine thickness and written only when on) makes Plan Cutting cut each such slug along the bed's contact lines (grid rows and columns, slat center lines) before the cutout, so no piece keeps a contact and the pieces drop between the supports. Each slice stops a kerf short of the cutout, so the pieces stay attached until the cutout's own kerf frees them together. A slug the bed holds level, or one on no contact, is left whole; a cutout holding another part or another of its part's cuts is never sliced. Slices are break cuts emitted right after the drawing's own break cuts, pierced on themselves and led straight back to their start. The released-contour proof now leaves out straight break cuts lying wholly inside one of the part's closed holes, since the grid cells they free are scrap; break cuts in material still take part. The plan fingerprint only changes when the setting is on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
286 lines
13 KiB
C#
286 lines
13 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<ExecutionMotion> 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;
|
|
}
|
|
}
|