mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-08 14:55:45 -05:00
Automatic cut-offs refused any part reaching past the physical sheet by more than Tolerance.Epsilon. Imported supplied blanks (panel doors, remnants) equal their sheet but carry PEP's rounding: a 0.000006 outline skew plus placements rounded to 0.001 put the never-cut outline 0.00001 to 0.00003 in past the sheet, so Plan Cutting refused every such nest. A display perimeter may now extend up to DisplayPerimeterSheetAllowance (0.0001 model units) past the sheet. Nothing cuts it, and a blank that fills its sheet gets no cut-offs. Everything else on the part keeps the ordinary tolerance: cuts, leads, scribes and travel, except a rapid that ends on the outline itself (the move to its first corner, or an applied program's display tail). A larger overhang is still refused.
480 lines
22 KiB
C#
480 lines
22 KiB
C#
using OpenNest.CNC;
|
|
using OpenNest.CNC.CuttingPlanning;
|
|
using OpenNest.Converters;
|
|
using OpenNest.Engine.CuttingPlanning;
|
|
using OpenNest.Geometry;
|
|
|
|
namespace OpenNest.Tests.CuttingPlanning;
|
|
|
|
/// <summary>
|
|
/// A supplied blank (a purchased door) whose outline is a display perimeter: shown and treated as
|
|
/// material, never cut. Only its cutouts are cut, each as a hole.
|
|
/// </summary>
|
|
public class DisplayPerimeterTests
|
|
{
|
|
// The door fills a 30 x 20 sheet: new Size(width, length) puts Length (30) along X.
|
|
private static Plate DoorPlate(Program program)
|
|
{
|
|
var plate = new Plate(new Size(20, 30));
|
|
plate.Parts.Add(new Part(new Drawing("door", program), Vector.Zero));
|
|
return plate;
|
|
}
|
|
|
|
internal static Program Door(LayerType outline = LayerType.DisplayPerimeter, bool cutouts = true,
|
|
LayerType? lastEdge = null)
|
|
{
|
|
var program = new Program();
|
|
program.Codes.Add(new RapidMove(0, 0));
|
|
program.Codes.Add(new LinearMove(0, 20) { Layer = outline });
|
|
program.Codes.Add(new LinearMove(30, 20) { Layer = outline });
|
|
program.Codes.Add(new LinearMove(30, 0) { Layer = outline });
|
|
program.Codes.Add(new LinearMove(0, 0) { Layer = lastEdge ?? outline });
|
|
if (cutouts)
|
|
AddCutouts(program);
|
|
return program;
|
|
}
|
|
|
|
private static void AddCutouts(Program program)
|
|
{
|
|
program.MoveTo(5, 5); program.LineTo(5, 10); program.LineTo(10, 10); program.LineTo(10, 5); program.LineTo(5, 5);
|
|
program.MoveTo(15, 5); program.LineTo(15, 12); program.LineTo(25, 12); program.LineTo(25, 5); program.LineTo(15, 5);
|
|
program.MoveTo(9.5, 15);
|
|
program.Codes.Add(new ArcMove(new Vector(9.5, 15), new Vector(8, 15), RotationType.CW));
|
|
}
|
|
|
|
// Cutout extents, for checking where pierces and cuts land.
|
|
private static readonly Box[] Cutouts =
|
|
[
|
|
new Box(5, 5, 5, 5),
|
|
new Box(15, 5, 10, 7),
|
|
new Box(6.5, 13.5, 3, 3),
|
|
];
|
|
|
|
private static bool InsideACutout(Vector point) => Cutouts.Any(box =>
|
|
point.X >= box.Left - 1e-6 && point.X <= box.Right + 1e-6 && point.Y >= box.Bottom - 1e-6 && point.Y <= box.Top + 1e-6);
|
|
|
|
[Fact]
|
|
public void Door_PlansEveryCutoutAsAHoleAndNeverCutsTheOutline()
|
|
{
|
|
var plate = DoorPlate(Door());
|
|
var part = plate.Parts[0];
|
|
|
|
var proposal = CuttingPlanBatch.Capture([plate], ExplicitContourTests.Parameters(), false).Plan();
|
|
|
|
var result = Assert.Single(proposal.Plates).Result;
|
|
Assert.True(proposal.CanApply, string.Join("\n", proposal.Describe("in")));
|
|
Assert.Equal(CuttingPlanStatus.Ready, result.Status);
|
|
Assert.True(result.IndependentlyReplayed);
|
|
Assert.Equal(CuttingCommitStatus.Applied, proposal.Apply(acceptWarnings: false).Status);
|
|
|
|
var motions = ExecutionMotionReader.Read(part.Program, part.Location, null, default).Motions;
|
|
Assert.DoesNotContain(motions, m => m.Layer == LayerType.DisplayPerimeter);
|
|
var pierces = motions.Where((m, i) => m.Layer == LayerType.Leadin && m.Start is { } && (i == 0 || motions[i - 1].Rapid))
|
|
.Select(m => m.Start!.Value).ToArray();
|
|
Assert.Equal(3, pierces.Length);
|
|
Assert.All(pierces, p => Assert.True(InsideACutout(p), $"Pierce ({p.X}, {p.Y}) is on the door."));
|
|
Assert.All(motions.Where(m => !m.Rapid), m => Assert.True(InsideACutout(m.End), $"Cut to ({m.End.X}, {m.End.Y}) leaves the cutouts."));
|
|
}
|
|
|
|
private static (Plate Plate, Part Part) PlannedDoor()
|
|
{
|
|
var plate = DoorPlate(Door());
|
|
var proposal = CuttingPlanBatch.Capture([plate], ExplicitContourTests.Parameters(), false).Plan();
|
|
Assert.True(proposal.CanApply, string.Join("\n", proposal.Describe("in")));
|
|
Assert.Equal(CuttingCommitStatus.Applied, proposal.Apply(acceptWarnings: false).Status);
|
|
return (plate, plate.Parts[0]);
|
|
}
|
|
|
|
[Fact]
|
|
public void AfterApply_TheDoorKeepsItsOutlineAsANeverCutTail()
|
|
{
|
|
var (_, part) = PlannedDoor();
|
|
|
|
Assert.Equal(30, part.BoundingBox.Length, 6);
|
|
Assert.Equal(20, part.BoundingBox.Width, 6);
|
|
Assert.True(DisplayPerimeterTail.HasTail(part.Program));
|
|
var executed = DisplayPerimeterTail.ForPosting(part.Program, "Part 1");
|
|
Assert.False(DisplayPerimeterTail.HasDisplayPerimeterMotion(executed));
|
|
// Posting and executed reads agree: the tail is the only thing either leaves out.
|
|
var full = ExecutionMotionReader.Read(part.Program, part.Location, null, default).Motions;
|
|
var posted = ExecutionMotionReader.Read(executed, part.Location, null, default).Motions;
|
|
Assert.Equal(full.Count, posted.Count);
|
|
Assert.True(executed.Codes.Count < part.Program.Codes.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void AfterApply_AutomaticCutOffsStillAddNoLineAcrossTheBlank()
|
|
{
|
|
var (plate, _) = PlannedDoor();
|
|
|
|
var staging = AutomaticCutOffBatch.Stage([plate],
|
|
new AutomaticCutOffOptions { Spacing = 10, MinimumStripLength = 5 }, new CutOffSettings());
|
|
|
|
Assert.Empty(Assert.Single(staging.Plans).Definitions);
|
|
}
|
|
|
|
[Fact]
|
|
public void AfterApply_TheDoorPlansAgainWithTheSameHoles()
|
|
{
|
|
var (plate, _) = PlannedDoor();
|
|
|
|
var again = CuttingPlanBatch.Capture([plate], ExplicitContourTests.Parameters(), false).Plan();
|
|
|
|
Assert.True(again.CanApply, string.Join("\n", again.Describe("in")));
|
|
}
|
|
|
|
[Fact]
|
|
public void AfterApply_TimingRapidsAndSimulationIgnoreTheTail()
|
|
{
|
|
var (plate, part) = PlannedDoor();
|
|
|
|
var executed = DisplayPerimeterTail.WithoutTail(part.Program);
|
|
Assert.Equal(Timing.GetTimingInfo(executed).PierceCount, Timing.GetTimingInfo(part.Program).PierceCount);
|
|
Assert.Equal(Timing.GetTimingInfo(executed).CutDistance, Timing.GetTimingInfo(part.Program).CutDistance, 9);
|
|
// The tail starts at the outline's first corner (0, 0); no rapid may travel there.
|
|
var rapids = RapidEnumerator.Enumerate(plate.Parts);
|
|
Assert.DoesNotContain(rapids, r => r.To.DistanceTo(Vector.Zero) < 1e-9);
|
|
var simulation = CutSimulation.Build(plate, new CutParameters
|
|
{
|
|
Feedrate = 100,
|
|
RapidTravelRate = 600,
|
|
PierceTime = TimeSpan.FromSeconds(1),
|
|
Units = Units.Inches,
|
|
});
|
|
Assert.Equal(3, simulation.Steps.Count(step => step.Kind == CutSimulationStepKind.Pierce));
|
|
Assert.DoesNotContain(simulation.Steps, step => step.Layer == LayerType.DisplayPerimeter);
|
|
}
|
|
|
|
[Fact]
|
|
public void ManualAndLegacyLeadIns_RefuseADisplayPerimeterAndLeaveThePartAlone()
|
|
{
|
|
var part = new Part(new Drawing("door", Door()), Vector.Zero);
|
|
var before = ExplicitContourTests.Fingerprint(part.Program);
|
|
var edge = ConvertProgram.ToGeometry(part.Program).First(e => e.Layer == SpecialLayers.DisplayPerimeter);
|
|
var hole = ConvertProgram.ToGeometry(part.Program).First(e => e.Layer == SpecialLayers.Cut);
|
|
|
|
var onEdge = Assert.Throws<InvalidOperationException>(() => part.ApplySingleLeadIn(
|
|
ExplicitContourTests.Parameters(), new Vector(0, 10), edge, OpenNest.CNC.CuttingStrategy.ContourType.External));
|
|
Assert.Throws<InvalidOperationException>(() => part.ApplySingleLeadIn(
|
|
ExplicitContourTests.Parameters(), new Vector(5, 7), hole, OpenNest.CNC.CuttingStrategy.ContourType.Internal));
|
|
Assert.Throws<InvalidOperationException>(() => part.ApplyLeadIns(ExplicitContourTests.Parameters(), Vector.Zero));
|
|
|
|
Assert.Contains("Plan Cutting", onEdge.Message);
|
|
Assert.False(part.HasManualLeadIns);
|
|
Assert.Equal(before, ExplicitContourTests.Fingerprint(part.Program));
|
|
}
|
|
|
|
private static Program SquareThenMarker(bool displayTail, bool markerInSubProgram = false)
|
|
{
|
|
var cuts = new Program();
|
|
cuts.MoveTo(1, 1); cuts.LineTo(1, 2); cuts.LineTo(2, 2); cuts.LineTo(2, 1); cuts.LineTo(1, 1);
|
|
cuts.Codes.Add(new Comment(DisplayPerimeterTail.Marker));
|
|
cuts.Codes.Add(new RapidMove(3, 3));
|
|
var layer = displayTail ? LayerType.DisplayPerimeter : LayerType.Cut;
|
|
foreach (var (x, y) in new[] { (3.0, 4.0), (4.0, 4.0), (4.0, 3.0), (3.0, 3.0) })
|
|
cuts.Codes.Add(new LinearMove(x, y) { Layer = layer });
|
|
if (!markerInSubProgram)
|
|
return cuts;
|
|
var main = new Program();
|
|
main.Codes.Add(new SubProgramCall(cuts, 0) { Id = 1 });
|
|
main.SubPrograms[1] = cuts;
|
|
return main;
|
|
}
|
|
|
|
// A tail marker anywhere but the end of the top-level program, or followed by anything but
|
|
// a rapid and the outline, is a malformed program: nothing may silently drop cuts after it.
|
|
[Theory]
|
|
[InlineData(false, false)]
|
|
[InlineData(true, true)]
|
|
[InlineData(false, true)]
|
|
public void MalformedTailMarker_IsRefusedByPostsAndVerification(bool displayTail, bool inSubProgram)
|
|
{
|
|
var program = SquareThenMarker(displayTail, inSubProgram);
|
|
|
|
Assert.Throws<InvalidOperationException>(() => DisplayPerimeterTail.ForPosting(program, "Part 1"));
|
|
Assert.Throws<ArgumentException>(() => ExecutionMotionReader.Read(program, Vector.Zero, null, default));
|
|
Assert.True(DisplayPerimeterTail.HasTail(SquareThenMarker(displayTail: true)));
|
|
}
|
|
|
|
[Fact]
|
|
public void Posting_RefusesAnUnplannedDisplayPerimeter()
|
|
{
|
|
var error = Assert.Throws<InvalidOperationException>(() => DisplayPerimeterTail.ForPosting(Door(), "Part 4"));
|
|
|
|
Assert.Contains("Part 4", error.Message);
|
|
Assert.Contains("Plan Cutting", error.Message);
|
|
var holesOnly = Door(LayerType.Cut);
|
|
Assert.Same(holesOnly, DisplayPerimeterTail.ForPosting(holesOnly, "Part 1"));
|
|
}
|
|
|
|
// A sparse support bed and a pierce zone both steer perimeter-led paths (support tabs, scrap
|
|
// pierces); a blank has no cut perimeter, so neither may reach it.
|
|
[Theory]
|
|
[InlineData("supportBed")]
|
|
[InlineData("pierceZone")]
|
|
public void Door_PlansWithSupportTabsOrAPierceZoneAndStillNeverTouchesTheOutline(string setting)
|
|
{
|
|
var plate = DoorPlate(Door());
|
|
var parameters = ExplicitContourTests.Parameters();
|
|
if (setting == "supportBed")
|
|
{
|
|
plate.SupportBed = new OpenNest.Support.SupportGrid(25, 100, 2, 2, stabilityMargin: 0.1);
|
|
parameters.SupportTabs = true;
|
|
parameters.TabConfig = new OpenNest.CNC.CuttingStrategy.NormalTab { Size = 0.05 };
|
|
}
|
|
else
|
|
parameters.PierceDiameter = 0.1;
|
|
var part = plate.Parts[0];
|
|
|
|
var proposal = CuttingPlanBatch.Capture([plate], parameters, false).Plan();
|
|
|
|
Assert.True(proposal.CanApply, string.Join("\n", proposal.Describe("in")));
|
|
Assert.Equal(CuttingCommitStatus.Applied, proposal.Apply(acceptWarnings: false).Status);
|
|
var motions = ExecutionMotionReader.Read(part.Program, part.Location, null, default).Motions;
|
|
Assert.All(motions.Where(m => !m.Rapid), m => Assert.True(InsideACutout(m.End), $"({m.End.X}, {m.End.Y}) leaves the cutouts."));
|
|
}
|
|
|
|
// PEP draws a supplied blank a few hundred-thousandths of an inch past its sheet (an outline
|
|
// skew of 0.000006 plus placements rounded to 0.001). The outline is never cut, so automatic
|
|
// cut-offs accept it within DisplayPerimeterSheetAllowance; anything larger, or the same overhang
|
|
// on geometry that is cut, is still refused.
|
|
[Theory]
|
|
[InlineData(LayerType.DisplayPerimeter, 3e-5, 0, true)]
|
|
[InlineData(LayerType.DisplayPerimeter, 0, 3e-5, true)]
|
|
[InlineData(LayerType.DisplayPerimeter, 2e-4, 0, false)]
|
|
[InlineData(LayerType.Cut, 3e-5, 0, false)]
|
|
public void AutomaticCutOffs_AcceptOnlyADisplayPerimeterWithinTheAllowancePastTheSheet(
|
|
LayerType outline, double overRight, double overBottom, bool accepted)
|
|
{
|
|
var plate = new Plate(new Size(20, 30 - overRight));
|
|
plate.Parts.Add(new Part(new Drawing("door", Door(outline)), new Vector(0, -overBottom)));
|
|
|
|
var create = () => AutomaticCutOffPlanner.Create(plate,
|
|
new AutomaticCutOffOptions { Spacing = 35, MinimumStripLength = 17.5, MinimumTailLength = 12 },
|
|
new CutOffSettings());
|
|
|
|
if (!accepted)
|
|
{
|
|
var refusal = Assert.Throws<ArgumentException>(create);
|
|
Assert.Contains("outside the physical sheet", refusal.Message);
|
|
return;
|
|
}
|
|
// The blank fills the sheet: nothing is left to cut off.
|
|
Assert.Empty(create().Definitions);
|
|
}
|
|
|
|
// Cut-offs are staged before planning checks the door, so a malformed one can still carry a
|
|
// cut past the sheet while its outline is inside: the allowance covers the outline only.
|
|
[Fact]
|
|
public void AutomaticCutOffs_RefuseACutPastTheSheetEvenOnADisplayPerimeterPart()
|
|
{
|
|
var program = Door();
|
|
program.MoveTo(29, 1); program.LineTo(30.00003, 1); program.LineTo(30.00003, 2); program.LineTo(29, 2); program.LineTo(29, 1);
|
|
var plate = new Plate(new Size(20, 30));
|
|
plate.Parts.Add(new Part(new Drawing("door", program), Vector.Zero));
|
|
|
|
var refusal = Assert.Throws<ArgumentException>(() => AutomaticCutOffPlanner.Create(plate,
|
|
new AutomaticCutOffOptions { Spacing = 35, MinimumStripLength = 17.5, MinimumTailLength = 12 },
|
|
new CutOffSettings()));
|
|
|
|
Assert.Contains("outside the physical sheet", refusal.Message);
|
|
}
|
|
|
|
private static AutomaticCutOffPlan StageCutOffs(Plate plate) => AutomaticCutOffPlanner.Create(plate,
|
|
new AutomaticCutOffOptions { Spacing = 35, MinimumStripLength = 17.5, MinimumTailLength = 12 },
|
|
new CutOffSettings());
|
|
|
|
// The allowance belongs to the outline alone: a scribe (lasered) or a rapid past the sheet on a
|
|
// door is refused at the ordinary tolerance, except a rapid ending on the outline itself.
|
|
[Theory]
|
|
[InlineData("scribe")]
|
|
[InlineData("rapid")]
|
|
public void AutomaticCutOffs_RefuseAScribeOrRapidPastTheSheetOnADisplayPerimeterPart(string motion)
|
|
{
|
|
var program = Door();
|
|
if (motion == "scribe")
|
|
{
|
|
program.Codes.Add(new RapidMove(29, 1));
|
|
program.Codes.Add(new LinearMove(30.00003, 1) { Layer = LayerType.Scribe });
|
|
program.Codes.Add(new RapidMove(29, 2));
|
|
}
|
|
else
|
|
program.Codes.Add(new RapidMove(30.00005, 1));
|
|
var plate = new Plate(new Size(20, 30));
|
|
plate.Parts.Add(new Part(new Drawing("door", program), Vector.Zero));
|
|
|
|
var refusal = Assert.Throws<ArgumentException>(() => StageCutOffs(plate));
|
|
|
|
Assert.Contains("outside the physical sheet", refusal.Message);
|
|
}
|
|
|
|
// The door's rapid to its outline's first corner, in the drawing and again in an applied
|
|
// program's display tail, ends on the outline, so it shares the outline's allowance.
|
|
[Fact]
|
|
public void AutomaticCutOffs_AcceptTheRapidToAnOverhangingOutlineBeforeAndAfterApply()
|
|
{
|
|
var plate = new Plate(new Size(20, 30 - 3e-5));
|
|
var part = new Part(new Drawing("door", Door()), new Vector(-3e-5, 0));
|
|
plate.Parts.Add(part);
|
|
|
|
Assert.Empty(StageCutOffs(plate).Definitions);
|
|
var proposal = CuttingPlanBatch.Capture([plate], ExplicitContourTests.Parameters(), false).Plan();
|
|
Assert.Equal(CuttingCommitStatus.Applied, proposal.Apply(acceptWarnings: false).Status);
|
|
Assert.True(DisplayPerimeterTail.HasTail(part.Program));
|
|
Assert.Empty(StageCutOffs(plate).Definitions);
|
|
}
|
|
|
|
[Fact]
|
|
public void Door_AutomaticCutOffsAddNoLineAcrossTheBlank()
|
|
{
|
|
var plate = DoorPlate(Door());
|
|
|
|
var staging = AutomaticCutOffBatch.Stage([plate],
|
|
new AutomaticCutOffOptions { Spacing = 10, MinimumStripLength = 5 }, new CutOffSettings());
|
|
|
|
Assert.False(staging.IsBlocked);
|
|
Assert.Empty(Assert.Single(staging.Plans).Definitions);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("mixed", "one whole closed contour")]
|
|
[InlineData("hole", "enclose every other contour")]
|
|
[InlineData("nothing", "display perimeter with no cutouts")]
|
|
[InlineData("notch", "enclose every other contour")]
|
|
[InlineData("second", "enclose every other contour")]
|
|
[InlineData("tabbedOnly", "closed cutout without tabs")]
|
|
[InlineData("circleOverEdge", "enclose every other contour")]
|
|
[InlineData("edgeOverNotch", "enclose every other contour")]
|
|
public void InvalidDisplayPerimeters_AreRefusedWithTheirReason(string layout, string reason)
|
|
{
|
|
var program = layout switch
|
|
{
|
|
"mixed" => Door(lastEdge: LayerType.Cut),
|
|
"nothing" => Door(cutouts: false),
|
|
"notch" => NotchedDoor(),
|
|
"second" => SecondDisplayRing(),
|
|
"tabbedOnly" => TabbedOnly(),
|
|
"circleOverEdge" => CircleOverEdge(),
|
|
"edgeOverNotch" => EdgeOverNotch(),
|
|
_ => HoleOnDisplay(),
|
|
};
|
|
var plate = DoorPlate(program);
|
|
|
|
var result = CuttingPlanService.Plan(CuttingPlanRequest.ForPlate(plate,
|
|
confirmedParameters: ExplicitContourTests.Parameters()));
|
|
|
|
Assert.NotEqual(CuttingPlanStatus.Ready, result.Status);
|
|
Assert.Contains(result.Findings, f => f.Message.Contains(reason, StringComparison.OrdinalIgnoreCase));
|
|
// Not even an unverified plan: nothing about a refused blank may be applied.
|
|
var proposal = CuttingPlanBatch.Capture([DoorPlate(program)], ExplicitContourTests.Parameters(), false).Plan();
|
|
Assert.False(proposal.CanApply);
|
|
Assert.False(proposal.CanApplyWithWarnings);
|
|
|
|
// An L-shaped blank with a cutout in its notch: inside the outline's box, outside the blank.
|
|
static Program NotchedDoor()
|
|
{
|
|
var program = new Program();
|
|
Ring(program, LayerType.DisplayPerimeter, (0, 0), (0, 20), (15, 20), (15, 10), (30, 10), (30, 0));
|
|
Ring(program, LayerType.Cut, (2, 2), (2, 6), (6, 6), (6, 2));
|
|
Ring(program, LayerType.Cut, (20, 14), (20, 18), (25, 18), (25, 14));
|
|
return program;
|
|
}
|
|
|
|
// A round cutout whose start lies on the blank but whose arc runs past its edge.
|
|
static Program CircleOverEdge()
|
|
{
|
|
var program = Door(cutouts: false);
|
|
program.Codes.Add(new RapidMove(3, 10));
|
|
program.Codes.Add(new ArcMove(new Vector(3, 10), new Vector(1, 10), RotationType.CW));
|
|
return program;
|
|
}
|
|
|
|
// Every corner on an L-shaped blank, but the long edge crosses its notch.
|
|
static Program EdgeOverNotch()
|
|
{
|
|
var program = new Program();
|
|
Ring(program, LayerType.DisplayPerimeter, (0, 0), (0, 20), (15, 20), (15, 10), (30, 10), (30, 0));
|
|
Ring(program, LayerType.Cut, (2, 19), (29, 2), (2, 2));
|
|
return program;
|
|
}
|
|
|
|
static Program SecondDisplayRing()
|
|
{
|
|
var program = Door();
|
|
Ring(program, LayerType.DisplayPerimeter, (26, 14), (26, 18), (28, 18), (28, 14));
|
|
return program;
|
|
}
|
|
|
|
static Program TabbedOnly()
|
|
{
|
|
var program = Door(cutouts: false);
|
|
program.Codes.Add(new RapidMove(5, 5));
|
|
program.Codes.Add(new LinearMove(5, 10) { Layer = LayerType.Cut });
|
|
program.Codes.Add(new LinearMove(10, 10) { Layer = LayerType.Tab });
|
|
program.Codes.Add(new LinearMove(10, 5) { Layer = LayerType.Cut });
|
|
program.Codes.Add(new LinearMove(5, 5) { Layer = LayerType.Cut });
|
|
return program;
|
|
}
|
|
|
|
static Program HoleOnDisplay()
|
|
{
|
|
var program = Door(LayerType.Cut, cutouts: false);
|
|
program.MoveTo(5, 5);
|
|
foreach (var (x, y) in new[] { (5.0, 10.0), (10.0, 10.0), (10.0, 5.0), (5.0, 5.0) })
|
|
program.Codes.Add(new LinearMove(x, y) { Layer = LayerType.DisplayPerimeter });
|
|
return program;
|
|
}
|
|
}
|
|
|
|
private static void Ring(Program program, LayerType layer, params (double X, double Y)[] points)
|
|
{
|
|
program.Codes.Add(new RapidMove(points[0].X, points[0].Y));
|
|
foreach (var (x, y) in points.Skip(1).Append(points[0]))
|
|
program.Codes.Add(new LinearMove(x, y) { Layer = layer });
|
|
}
|
|
|
|
[Fact]
|
|
public void Preparation_HasNoCutPerimeterAndTreatsEveryContourAsAHole()
|
|
{
|
|
var prepared = PreparedContours.Capture(Door(), ExplicitContourTests.Parameters());
|
|
|
|
Assert.False(prepared.HasCutPerimeter);
|
|
Assert.Equal(3, prepared.Count);
|
|
Assert.Equal(prepared.Count, prepared.PerimeterOrdinal);
|
|
Assert.True(PreparedContours.Capture(Door(LayerType.Cut), ExplicitContourTests.Parameters()).HasCutPerimeter);
|
|
}
|
|
|
|
[Fact]
|
|
public void CleanBounds_IncludeTheDisplayPerimeter()
|
|
{
|
|
var bounds = NestedPartContainment.CleanBounds(Door(), Vector.Zero, default);
|
|
|
|
Assert.Equal(30, bounds.Length, 9);
|
|
Assert.Equal(20, bounds.Width, 9);
|
|
}
|
|
|
|
[Fact]
|
|
public void Replay_RefusesAProgramThatCutsTheDisplayPerimeter()
|
|
{
|
|
var material = LeadMaterialSnapshot.Capture(Door(), Vector.Zero);
|
|
Assert.True(material.IsComplete, material.Reason);
|
|
|
|
var holesOnly = new Program();
|
|
AddCutouts(holesOnly);
|
|
var cutsOutline = Door(LayerType.Cut);
|
|
|
|
Assert.True(ContourProgramVerifier.Verify(
|
|
ExecutionMotionReader.ReadSupported(holesOnly, Vector.Zero, null, default), material, null, default));
|
|
Assert.False(ContourProgramVerifier.Verify(
|
|
ExecutionMotionReader.ReadSupported(cutsOutline, Vector.Zero, null, default), material, null, default));
|
|
// Same count of cut rings as a valid plan: the outline in place of the circular cutout.
|
|
var outlineForAHole = Door(LayerType.Cut, cutouts: false);
|
|
outlineForAHole.MoveTo(5, 5); outlineForAHole.LineTo(5, 10); outlineForAHole.LineTo(10, 10);
|
|
outlineForAHole.LineTo(10, 5); outlineForAHole.LineTo(5, 5);
|
|
outlineForAHole.MoveTo(15, 5); outlineForAHole.LineTo(15, 12); outlineForAHole.LineTo(25, 12);
|
|
outlineForAHole.LineTo(25, 5); outlineForAHole.LineTo(15, 5);
|
|
Assert.False(ContourProgramVerifier.Verify(
|
|
ExecutionMotionReader.ReadSupported(outlineForAHole, Vector.Zero, null, default), material, null, default));
|
|
}
|
|
}
|