diff --git a/OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs b/OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs
index 454d62f..c3a3f3c 100644
--- a/OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs
+++ b/OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs
@@ -367,7 +367,8 @@ namespace OpenNest.CNC.CuttingStrategy
return;
}
- program.Codes.AddRange(leadIn.Generate(point, normal, winding));
+ var leadInNormal = ComputeLeadInNormal(shape, point, entity, contourType, leadIn, winding);
+ program.Codes.AddRange(leadIn.Generate(point, leadInNormal, winding));
var reindexedShape = shape.ReindexAt(point, entity);
@@ -436,6 +437,65 @@ namespace OpenNest.CNC.CuttingStrategy
return ContourType.Internal;
}
+ ///
+ /// Uses the inward angle bisector for straight lead-ins at cutout corners.
+ /// Edge interiors and other lead-in styles keep the entity normal. Shared
+ /// by program generation and the manual placement preview.
+ ///
+ public static double ComputeLeadInNormal(
+ Shape shape,
+ Vector point,
+ Entity entity,
+ ContourType contourType,
+ LeadIn leadIn,
+ RotationType winding = RotationType.CW
+ )
+ {
+ var normal = ComputeNormal(point, entity, contourType, winding);
+ if (contourType != ContourType.Internal || leadIn is not LineLeadIn
+ || entity is not (Line or Arc) || entity.Length <= Tolerance.Epsilon
+ || shape.Entities.Count < 2 || !shape.IsClosed())
+ return normal;
+
+ var index = shape.Entities.IndexOf(entity);
+ if (index < 0)
+ return normal;
+
+ var atStart = point.DistanceTo(EntityStartPoint(entity)) <= Tolerance.Epsilon;
+ if (!atStart && point.DistanceTo(EntityEndPoint(entity)) > Tolerance.Epsilon)
+ return normal;
+
+ var adjacentIndex = atStart
+ ? (index + shape.Entities.Count - 1) % shape.Entities.Count
+ : (index + 1) % shape.Entities.Count;
+ var adjacent = shape.Entities[adjacentIndex];
+ var adjacentPoint = atStart ? EntityEndPoint(adjacent) : EntityStartPoint(adjacent);
+
+ if (adjacent is not (Line or Arc) || adjacent.Length <= Tolerance.Epsilon
+ || point.DistanceTo(adjacentPoint) > Tolerance.Epsilon)
+ return normal;
+
+ var adjacentNormal = ComputeNormal(point, adjacent, contourType, winding);
+ // Sum unit normals rather than averaging angles (which fails at 0/2π).
+ // Winding makes this point into the scrap even at reflex corners.
+ var x = System.Math.Cos(normal) + System.Math.Cos(adjacentNormal);
+ var y = System.Math.Sin(normal) + System.Math.Sin(adjacentNormal);
+ if (!double.IsFinite(x) || !double.IsFinite(y)
+ || x * x + y * y <= Tolerance.Epsilon * Tolerance.Epsilon)
+ return normal; // Opposing normals at a cusp have no unique bisector.
+
+ return Angle.NormalizeRad(System.Math.Atan2(y, x));
+ }
+
+ private static Vector EntityEndPoint(Entity entity)
+ {
+ if (entity is Line line)
+ return line.EndPoint;
+ if (entity is Arc arc)
+ return arc.EndPoint();
+ return Vector.Invalid;
+ }
+
public static double ComputeNormal(
Vector point,
Entity entity,
diff --git a/OpenNest.Tests/CuttingStrategy/CutoutCornerLeadInTests.cs b/OpenNest.Tests/CuttingStrategy/CutoutCornerLeadInTests.cs
new file mode 100644
index 0000000..3b69fa1
--- /dev/null
+++ b/OpenNest.Tests/CuttingStrategy/CutoutCornerLeadInTests.cs
@@ -0,0 +1,306 @@
+using OpenNest.CNC;
+using OpenNest.CNC.CuttingStrategy;
+using OpenNest.Geometry;
+
+namespace OpenNest.Tests.CuttingStrategy;
+
+public class CutoutCornerLeadInTests
+{
+ private const double LeadLength = 0.125;
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void ApplyLeadIns_SmallRectangularCutout_PiercesInsideOnBisector(bool reverse)
+ {
+ // Dimensions of the small slots in 4980 A01 PT07.dxf.
+ var vertices = Rectangle();
+ var part = MakePart(vertices, reverse);
+
+ part.ApplyLeadIns(Parameters(), Vector.Zero);
+
+ var lead = SingleLeadIn(part);
+ var corner = new Vector(2.282, 2.532);
+ AssertPoint(corner, lead.EndPoint);
+ AssertBisector(lead, corner, new Vector(-1, -1));
+ Assert.True(lead.StartPoint.X > 2 && lead.StartPoint.X < 2.282);
+ Assert.True(lead.StartPoint.Y > 2 && lead.StartPoint.Y < 2.532);
+ }
+
+ public static IEnumerable