using OpenNest.CNC; using OpenNest.CNC.CuttingStrategy; using OpenNest.Geometry; namespace OpenNest.Tests.CuttingStrategy; /// /// Straight lead-ins at outside perimeter corners extend the edge cut first, so the /// torch enters on that edge's line whichever of the corner's two edges was picked. /// public class PerimeterCornerLeadInTests { private const double LeadLength = 0.25; public static IEnumerable SquareCorners() { foreach (var reverse in new[] { false, true }) foreach (var rotation in new[] { 0.0, 0.63 }) for (var corner = 0; corner < 4; corner++) yield return new object[] { reverse, rotation, corner }; } [Theory] [MemberData(nameof(SquareCorners))] public void ApplyLeadIns_ConvexCorner_ExtendsFirstCutEdge(bool reverse, double rotation, int cornerIndex) { var part = MakePart(Square(), reverse); part.Rotate(rotation); var perimeter = Perimeter(part); var outgoing = Assert.IsType(perimeter.Entities[cornerIndex]); var corner = outgoing.StartPoint; var centroid = Centroid(perimeter); var approach = corner + (corner - centroid) * 2; part.ApplyLeadIns(Parameters(), approach); AssertStraightEntry(part, corner, Direction(outgoing)); } public static IEnumerable PickedEdges() { foreach (var args in SquareCorners()) foreach (var incoming in new[] { false, true }) yield return args.Append(incoming).ToArray(); } [Theory] [MemberData(nameof(PickedEdges))] public void ApplySingleLeadIn_ConvexCorner_SameStraightLeadForEitherEdge( bool reverse, double rotation, int cornerIndex, bool incoming) { var part = MakePart(Square(), reverse); part.Rotate(rotation); var perimeter = Perimeter(part); var count = perimeter.Entities.Count; var outgoing = Assert.IsType(perimeter.Entities[cornerIndex]); var corner = outgoing.StartPoint; var entity = incoming ? perimeter.Entities[(cornerIndex + count - 1) % count] : outgoing; var parameters = Parameters(); var preview = PreviewPierce(perimeter, corner, entity, parameters); part.ApplySingleLeadIn(parameters, corner, entity, ContourType.External); AssertStraightEntry(part, corner, Direction(outgoing)); AssertPoint(preview, SingleLead(part, SpecialLayers.Leadin).StartPoint); } [Theory] [InlineData(false)] [InlineData(true)] public void ApplySingleLeadIn_ConvexCorner_IgnoresApproachAngle(bool incoming) { var part = MakePart(Square()); var perimeter = Perimeter(part); var outgoing = Assert.IsType(perimeter.Entities[1]); var entity = incoming ? perimeter.Entities[0] : outgoing; var parameters = Parameters(approachAngle: 60); part.ApplySingleLeadIn(parameters, outgoing.StartPoint, entity, ContourType.External); AssertStraightEntry(part, outgoing.StartPoint, Direction(outgoing)); } [Fact] public void ApplySingleLeadIn_MidEdge_KeepsApproachAngle() { var part = MakePart(Square()); var perimeter = Perimeter(part); var entity = Assert.IsType(perimeter.Entities[0]); var point = entity.MidPoint; var parameters = Parameters(approachAngle: 60); var normal = ContourCuttingStrategy.ComputeNormal(point, entity, ContourType.External, ContourCuttingStrategy.DetermineWinding(perimeter)); part.ApplySingleLeadIn(parameters, point, entity, ContourType.External); AssertPoint(parameters.ExternalLeadIn.GetPiercePoint(point, normal), SingleLead(part, SpecialLayers.Leadin).StartPoint); } [Theory] [InlineData(90, true, false)] [InlineData(150, true, false)] [InlineData(160, true, true)] [InlineData(170, false, false)] [InlineData(170, false, true)] [InlineData(179, false, true)] public void ApplySingleLeadIn_FlatCorner_FallsBackToPerpendicularWithoutPierceClearance( double interiorDegrees, bool straight, bool incoming) { // Corner at (10, 0): cut along +X, then turn left by 180 - interior degrees. var turn = System.Math.PI - interiorDegrees * System.Math.PI / 180; var corner = new Vector(10, 0); var next = corner + new Vector(System.Math.Cos(turn), System.Math.Sin(turn)) * 5; var part = MakePart(new[] { new Vector(0, 0), corner, next, new Vector(0, next.Y) }); var perimeter = Perimeter(part); var outgoing = perimeter.Entities.OfType().Single(e => e.StartPoint == corner); var entity = incoming ? perimeter.Entities.OfType().Single(e => e.EndPoint == corner) : outgoing; var parameters = Parameters(); Assert.Equal(0.0625, parameters.PierceClearance); part.ApplySingleLeadIn(parameters, corner, entity, ContourType.External); if (straight) { AssertStraightEntry(part, corner, Direction(outgoing)); return; } var normal = ContourCuttingStrategy.ComputeNormal(corner, outgoing, ContourType.External, ContourCuttingStrategy.DetermineWinding(perimeter)); AssertPoint(parameters.ExternalLeadIn.GetPiercePoint(corner, normal), SingleLead(part, SpecialLayers.Leadin).StartPoint); } [Theory] [InlineData(false, false)] [InlineData(false, true)] [InlineData(true, false)] [InlineData(true, true)] public void ApplySingleLeadIn_ReflexCorner_BisectsNotch(bool reverse, bool pickFirst) { var notch = new Vector(4, 4); var part = MakePart(LShape(), reverse); var perimeter = Perimeter(part); var touching = perimeter.Entities.OfType() .Where(e => e.StartPoint == notch || e.EndPoint == notch).ToList(); Assert.Equal(2, touching.Count); var entity = pickFirst ? touching[0] : touching[1]; var parameters = Parameters(); var preview = PreviewPierce(perimeter, notch, entity, parameters); part.ApplySingleLeadIn(parameters, notch, entity, ContourType.External); var lead = SingleLead(part, SpecialLayers.Leadin); AssertPoint(notch, lead.EndPoint); AssertPoint(notch + new Vector(1, 1).Normalize() * LeadLength, lead.StartPoint); AssertPoint(preview, lead.StartPoint); } [Theory] [InlineData(false, false)] [InlineData(false, true)] [InlineData(true, false)] [InlineData(true, true)] public void ApplySingleLeadIn_ConvexCorner_LineLeadOutRunsOnAlongLastCutEdge(bool reverse, bool incoming) { var part = MakePart(Square(), reverse); var perimeter = Perimeter(part); var count = perimeter.Entities.Count; var outgoing = Assert.IsType(perimeter.Entities[2]); var lastCut = Assert.IsType(perimeter.Entities[1]); var corner = outgoing.StartPoint; var entity = incoming ? lastCut : outgoing; var parameters = Parameters(); parameters.ExternalLeadOut = new LineLeadOut { Length = 0.1, ApproachAngle = 60 }; part.ApplySingleLeadIn(parameters, corner, entity, ContourType.External); var leadOut = SingleLead(part, SpecialLayers.Leadout); AssertPoint(corner, leadOut.StartPoint); AssertPoint(corner + Direction(lastCut) * 0.1, leadOut.EndPoint); } [Fact] public void ApplySingleLeadIn_CutoutCorner_KeepsBisector() { // Outside perimeter handling must not leak into cutouts. var program = new Program(Mode.Absolute); AddContour(program, Square()); AddContour(program, new[] { new Vector(2, 2), new Vector(4, 2), new Vector(4, 4), new Vector(2, 4) }); var part = new Part(new Drawing("cutout", program)); var cutout = Assert.Single(Profile(part).Cutouts); var entity = cutout.Entities.OfType().First(e => e.StartPoint == new Vector(4, 4)); var parameters = Parameters(); parameters.InternalLeadIn = new LineLeadIn { Length = LeadLength, ApproachAngle = 90 }; part.ApplySingleLeadIn(parameters, entity.StartPoint, entity, ContourType.Internal); var lead = SingleLead(part, SpecialLayers.Leadin); AssertPoint(new Vector(4, 4) + new Vector(-1, -1).Normalize() * LeadLength, lead.StartPoint); } private static Vector PreviewPierce(Shape shape, Vector point, Entity entity, CuttingParameters parameters) { var leadIn = ContourCuttingStrategy.ResolveLeadIn(shape, point, entity, ContourType.External, parameters.ExternalLeadIn, ContourCuttingStrategy.DetermineWinding(shape), parameters.PierceClearance, out var normal); return leadIn.GetPiercePoint(point, normal); } /// The lead-in runs on the first-cut edge's line and cutting continues along it. private static void AssertStraightEntry(Part part, Vector corner, Vector direction) { var lead = SingleLead(part, SpecialLayers.Leadin); AssertPoint(corner, lead.EndPoint); AssertPoint(corner - direction * LeadLength, lead.StartPoint); var geometry = part.Program.ToGeometry(); var firstCut = Assert.IsType(geometry .SkipWhile(e => e.Layer != SpecialLayers.Leadin).Skip(1) .First(e => SpecialLayers.IsMaterial(e.Layer))); AssertPoint(corner, firstCut.StartPoint); AssertPoint(direction, Direction(firstCut)); } private static CuttingParameters Parameters(double approachAngle = 90) => new() { ExternalLeadIn = new LineLeadIn { Length = LeadLength, ApproachAngle = approachAngle }, }; private static Vector[] Square() => new[] { new Vector(0, 0), new Vector(10, 0), new Vector(10, 10), new Vector(0, 10), }; private static Vector[] LShape() => new[] { new Vector(0, 0), new Vector(10, 0), new Vector(10, 4), new Vector(4, 4), new Vector(4, 10), new Vector(0, 10), }; private static Part MakePart(Vector[] perimeter, bool reverse = false) { var program = new Program(Mode.Absolute); AddContour(program, reverse ? perimeter.Reverse().ToArray() : perimeter); return new Part(new Drawing("perimeter-corner-test", program)); } private static void AddContour(Program program, Vector[] vertices) { program.Codes.Add(new RapidMove(vertices[0])); foreach (var point in vertices.Skip(1).Append(vertices[0])) program.Codes.Add(new LinearMove(point)); } private static ShapeProfile Profile(Part part) => new(part.Program.ToGeometry() .Where(e => SpecialLayers.IsMaterial(e.Layer)).ToList()); private static Shape Perimeter(Part part) { var perimeter = Profile(part).Perimeter; Assert.True(perimeter.IsClosed()); return perimeter; } private static Vector Centroid(Shape shape) { var lines = shape.Entities.OfType().ToList(); var sum = lines.Aggregate(Vector.Zero, (total, line) => total + line.StartPoint); return sum / lines.Count; } private static Vector Direction(Line line) => (line.EndPoint - line.StartPoint).Normalize(); private static Line SingleLead(Part part, Layer layer) => Assert.IsType(Assert.Single( part.Program.ToGeometry().Where(e => e.Layer == layer))); private static void AssertPoint(Vector expected, Vector actual) { Assert.Equal(expected.X, actual.X, 8); Assert.Equal(expected.Y, actual.Y, 8); } }