diff --git a/OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs b/OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs
index 8812b49..f056d41 100644
--- a/OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs
+++ b/OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs
@@ -102,7 +102,7 @@ namespace OpenNest.CNC.CuttingStrategy
return ordered;
}
- private ContourType DetectContourType(Shape cutout)
+ internal static ContourType DetectContourType(Shape cutout)
{
if (cutout.Entities.Count == 1 && cutout.Entities[0] is Circle)
return ContourType.ArcCircle;
@@ -110,7 +110,7 @@ namespace OpenNest.CNC.CuttingStrategy
return ContourType.Internal;
}
- private double ComputeNormal(Vector point, Entity entity, ContourType contourType)
+ internal static double ComputeNormal(Vector point, Entity entity, ContourType contourType)
{
double normal;
@@ -141,7 +141,7 @@ namespace OpenNest.CNC.CuttingStrategy
return Math.Angle.NormalizeRad(normal);
}
- private RotationType DetermineWinding(Shape shape)
+ internal static RotationType DetermineWinding(Shape shape)
{
// Use signed area: positive = CCW, negative = CW
var area = shape.Area();
diff --git a/OpenNest.Core/Part.cs b/OpenNest.Core/Part.cs
index b45c117..075115e 100644
--- a/OpenNest.Core/Part.cs
+++ b/OpenNest.Core/Part.cs
@@ -22,6 +22,7 @@ namespace OpenNest
{
private Vector location;
private bool ownsProgram;
+ private double preLeadInRotation;
public readonly Drawing BaseDrawing;
@@ -56,6 +57,38 @@ namespace OpenNest
public bool HasManualLeadIns { get; set; }
+ public bool LeadInsLocked { get; set; }
+
+ public CNC.CuttingStrategy.CuttingParameters CuttingParameters { get; set; }
+
+ public void ApplyLeadIns(CNC.CuttingStrategy.CuttingParameters parameters, Vector approachPoint)
+ {
+ preLeadInRotation = Rotation;
+ var strategy = new CNC.CuttingStrategy.ContourCuttingStrategy { Parameters = parameters };
+ var result = strategy.Apply(Program, approachPoint);
+ Program = result.Program;
+ CuttingParameters = parameters;
+ HasManualLeadIns = true;
+ UpdateBounds();
+ }
+
+ public void RemoveLeadIns()
+ {
+ var rotation = preLeadInRotation;
+ var location = Location;
+ Program = BaseDrawing.Program.Clone() as Program;
+ ownsProgram = true;
+
+ if (!Math.Tolerance.IsEqualTo(rotation, 0))
+ Program.Rotate(rotation);
+
+ Location = location;
+ HasManualLeadIns = false;
+ LeadInsLocked = false;
+ CuttingParameters = null;
+ UpdateBounds();
+ }
+
///
/// Gets the rotation of the part in radians.
///
diff --git a/OpenNest.Tests/PartLeadInTests.cs b/OpenNest.Tests/PartLeadInTests.cs
new file mode 100644
index 0000000..a4ca78b
--- /dev/null
+++ b/OpenNest.Tests/PartLeadInTests.cs
@@ -0,0 +1,127 @@
+using OpenNest.CNC;
+using OpenNest.CNC.CuttingStrategy;
+using OpenNest.Geometry;
+
+namespace OpenNest.Tests;
+
+public class PartLeadInTests
+{
+ private static Part MakeSquarePart()
+ {
+ var pgm = new Program();
+ pgm.Codes.Add(new RapidMove(new Vector(0, 0)));
+ pgm.Codes.Add(new LinearMove(new Vector(0, 10)));
+ pgm.Codes.Add(new LinearMove(new Vector(10, 10)));
+ pgm.Codes.Add(new LinearMove(new Vector(10, 0)));
+ pgm.Codes.Add(new LinearMove(new Vector(0, 0)));
+ var drawing = new Drawing("test", pgm);
+ return new Part(drawing);
+ }
+
+ [Fact]
+ public void ApplyLeadIns_SetsHasManualLeadIns()
+ {
+ var part = MakeSquarePart();
+ var parameters = new CuttingParameters
+ {
+ ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
+ InternalLeadIn = new LineLeadIn { Length = 0.25, ApproachAngle = 90 }
+ };
+
+ part.ApplyLeadIns(parameters, new Vector(-5, -5));
+
+ Assert.True(part.HasManualLeadIns);
+ }
+
+ [Fact]
+ public void ApplyLeadIns_StoresCuttingParameters()
+ {
+ var part = MakeSquarePart();
+ var parameters = new CuttingParameters
+ {
+ ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 },
+ InternalLeadIn = new LineLeadIn { Length = 0.25, ApproachAngle = 90 }
+ };
+
+ part.ApplyLeadIns(parameters, new Vector(-5, -5));
+
+ Assert.Same(parameters, part.CuttingParameters);
+ }
+
+ [Fact]
+ public void ApplyLeadIns_ProgramContainsLeadinCodes()
+ {
+ var part = MakeSquarePart();
+ var parameters = new CuttingParameters
+ {
+ ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }
+ };
+
+ part.ApplyLeadIns(parameters, new Vector(-5, -5));
+
+ var hasLeadin = part.Program.Codes.OfType().Any(m => m.Layer == LayerType.Leadin);
+ Assert.True(hasLeadin);
+ }
+
+ [Fact]
+ public void RemoveLeadIns_RestoresOriginalProgram()
+ {
+ var part = MakeSquarePart();
+ var originalCodeCount = part.Program.Codes.Count;
+ var parameters = new CuttingParameters
+ {
+ ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }
+ };
+
+ part.ApplyLeadIns(parameters, new Vector(-5, -5));
+ part.RemoveLeadIns();
+
+ Assert.False(part.HasManualLeadIns);
+ Assert.Null(part.CuttingParameters);
+ Assert.Equal(originalCodeCount, part.Program.Codes.Count);
+ }
+
+ [Fact]
+ public void RemoveLeadIns_PreservesRotation()
+ {
+ var part = MakeSquarePart();
+ part.Rotate(System.Math.PI / 4); // 45 degrees
+ var rotation = part.Rotation;
+
+ var parameters = new CuttingParameters
+ {
+ ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }
+ };
+
+ part.ApplyLeadIns(parameters, new Vector(-5, -5));
+ part.RemoveLeadIns();
+
+ Assert.Equal(rotation, part.Rotation, 6);
+ }
+
+ [Fact]
+ public void RemoveLeadIns_PreservesLocation()
+ {
+ var part = MakeSquarePart();
+ part.Offset(20, 30);
+ var location = part.Location;
+
+ var parameters = new CuttingParameters
+ {
+ ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }
+ };
+
+ part.ApplyLeadIns(parameters, new Vector(-5, -5));
+ part.RemoveLeadIns();
+
+ Assert.Equal(location.X, part.Location.X, 6);
+ Assert.Equal(location.Y, part.Location.Y, 6);
+ }
+
+ [Fact]
+ public void LeadInsLocked_DefaultsFalse()
+ {
+ var part = MakeSquarePart();
+ Assert.False(part.LeadInsLocked);
+ }
+}