feat: add pierce clearance clamping for circle contour lead-ins

Scales down lead-ins that would place the pierce point too close to the
opposite wall of small holes. Uses quadratic solve to find the maximum
safe distance inside a clearance-reduced radius. Adds Scale() method to
all LeadIn types and applies clamping in both the strategy and the
interactive preview.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-30 19:35:41 -04:00
parent a399c89f58
commit e860ca3f4a
9 changed files with 96 additions and 0 deletions

View File

@@ -36,6 +36,9 @@ namespace OpenNest.CNC.CuttingStrategy
var leadIn = SelectLeadIn(contourType);
var leadOut = SelectLeadOut(contourType);
if (contourType == ContourType.ArcCircle && entity is Circle circle)
leadIn = ClampLeadInForCircle(leadIn, circle, closestPt, normal);
result.Codes.AddRange(leadIn.Generate(closestPt, normal, winding));
var reindexed = cutout.ReindexAt(closestPt, entity);
result.Codes.AddRange(ConvertShapeToMoves(reindexed, closestPt));
@@ -153,6 +156,50 @@ namespace OpenNest.CNC.CuttingStrategy
return area >= 0 ? RotationType.CCW : RotationType.CW;
}
private LeadIn ClampLeadInForCircle(LeadIn leadIn, Circle circle, Vector contourPoint, double normalAngle)
{
if (leadIn is NoLeadIn || Parameters.PierceClearance <= 0)
return leadIn;
var piercePoint = leadIn.GetPiercePoint(contourPoint, normalAngle);
var maxRadius = circle.Radius - Parameters.PierceClearance;
if (maxRadius <= 0)
return leadIn;
var distFromCenter = piercePoint.DistanceTo(circle.Center);
if (distFromCenter <= maxRadius)
return leadIn;
// Compute max distance from contourPoint toward piercePoint that stays
// inside a circle of radius maxRadius centered at circle.Center.
// Solve: |contourPoint + t*d - center|^2 = maxRadius^2
var currentDist = contourPoint.DistanceTo(piercePoint);
if (currentDist < Math.Tolerance.Epsilon)
return leadIn;
var dx = (piercePoint.X - contourPoint.X) / currentDist;
var dy = (piercePoint.Y - contourPoint.Y) / currentDist;
var vx = contourPoint.X - circle.Center.X;
var vy = contourPoint.Y - circle.Center.Y;
var b = 2.0 * (vx * dx + vy * dy);
var c = vx * vx + vy * vy - maxRadius * maxRadius;
var discriminant = b * b - 4.0 * c;
if (discriminant < 0)
return leadIn;
var t = (-b + System.Math.Sqrt(discriminant)) / 2.0;
if (t <= 0)
return leadIn;
var scale = t / currentDist;
if (scale >= 1.0)
return leadIn;
return leadIn.Scale(scale);
}
private LeadIn SelectLeadIn(ContourType contourType)
{
return contourType switch

View File

@@ -21,6 +21,8 @@ namespace OpenNest.CNC.CuttingStrategy
public LeadIn ArcCircleLeadIn { get; set; } = new NoLeadIn();
public LeadOut ArcCircleLeadOut { get; set; } = new NoLeadOut();
public double PierceClearance { get; set; } = 0.0625;
public Tab TabConfig { get; set; }
public bool TabsEnabled { get; set; }

View File

@@ -32,5 +32,8 @@ namespace OpenNest.CNC.CuttingStrategy
arcCenterX + Radius * System.Math.Cos(contourNormalAngle),
arcCenterY + Radius * System.Math.Sin(contourNormalAngle));
}
public override LeadIn Scale(double factor) =>
new ArcLeadIn { Radius = Radius * factor };
}
}

View File

@@ -45,5 +45,8 @@ namespace OpenNest.CNC.CuttingStrategy
arcStartX + LineLength * System.Math.Cos(lineAngle),
arcStartY + LineLength * System.Math.Sin(lineAngle));
}
public override LeadIn Scale(double factor) =>
new CleanHoleLeadIn { LineLength = LineLength * factor, ArcRadius = ArcRadius * factor, Kerf = Kerf };
}
}

View File

@@ -9,5 +9,7 @@ namespace OpenNest.CNC.CuttingStrategy
RotationType winding = RotationType.CW);
public abstract Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle);
public virtual LeadIn Scale(double factor) => this;
}
}

View File

@@ -45,5 +45,8 @@ namespace OpenNest.CNC.CuttingStrategy
arcStartX + LineLength * System.Math.Cos(lineAngle),
arcStartY + LineLength * System.Math.Sin(lineAngle));
}
public override LeadIn Scale(double factor) =>
new LineArcLeadIn { LineLength = LineLength * factor, ArcRadius = ArcRadius * factor, ApproachAngle = ApproachAngle };
}
}

View File

@@ -28,5 +28,8 @@ namespace OpenNest.CNC.CuttingStrategy
contourStartPoint.X + Length * System.Math.Cos(approachAngle),
contourStartPoint.Y + Length * System.Math.Sin(approachAngle));
}
public override LeadIn Scale(double factor) =>
new LineLeadIn { Length = Length * factor, ApproachAngle = ApproachAngle };
}
}

View File

@@ -40,5 +40,8 @@ namespace OpenNest.CNC.CuttingStrategy
midX + Length1 * System.Math.Cos(firstAngle),
midY + Length1 * System.Math.Sin(firstAngle));
}
public override LeadIn Scale(double factor) =>
new LineLineLeadIn { Length1 = Length1 * factor, ApproachAngle1 = ApproachAngle1, Length2 = Length2 * factor, ApproachAngle2 = ApproachAngle2 };
}
}

View File

@@ -2,6 +2,7 @@ using OpenNest.CNC.CuttingStrategy;
using OpenNest.Controls;
using OpenNest.Converters;
using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
@@ -148,6 +149,35 @@ namespace OpenNest.Actions
if (leadIn == null)
return;
// Clamp lead-in for circle contours so it stays inside the hole
if (snapContourType == ContourType.ArcCircle && snapEntity is Circle snapCircle
&& parameters.PierceClearance > 0)
{
var pierceCheck = leadIn.GetPiercePoint(snapPoint, snapNormal);
var distFromCenter = pierceCheck.DistanceTo(snapCircle.Center);
var maxRadius = snapCircle.Radius - parameters.PierceClearance;
if (maxRadius > 0 && distFromCenter > maxRadius)
{
var currentDist = snapPoint.DistanceTo(pierceCheck);
if (currentDist > Tolerance.Epsilon)
{
var dx = (pierceCheck.X - snapPoint.X) / currentDist;
var dy = (pierceCheck.Y - snapPoint.Y) / currentDist;
var vx = snapPoint.X - snapCircle.Center.X;
var vy = snapPoint.Y - snapCircle.Center.Y;
var b = 2.0 * (vx * dx + vy * dy);
var c = vx * vx + vy * vy - maxRadius * maxRadius;
var disc = b * b - 4.0 * c;
if (disc >= 0)
{
var t = (-b + System.Math.Sqrt(disc)) / 2.0;
if (t > 0 && t < currentDist)
leadIn = leadIn.Scale(t / currentDist);
}
}
}
}
// Get the pierce point (in local space)
var piercePoint = leadIn.GetPiercePoint(snapPoint, snapNormal);
var worldPierce = TransformToWorld(piercePoint);