feat: adaptive arc segmentation based on chord tolerance

Add SegmentsForTolerance(double) to Arc and Circle that calculates the
minimum segments needed to keep sagitta within the given tolerance.
Add Shape.ToPolygonWithTolerance() that uses it per arc/circle.

Push distance now uses 0.08" chord tolerance instead of a fixed segment
count, giving appropriate resolution for each arc based on its radius.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 18:59:30 -05:00
parent ec5eff4884
commit 28238cc246
4 changed files with 72 additions and 3 deletions
+13
View File
@@ -185,6 +185,19 @@ namespace OpenNest.Geometry
return center == circle.Center; return center == circle.Center;
} }
/// <summary>
/// Returns the minimum number of segments needed so that the chord-to-arc
/// deviation (sagitta) does not exceed the given tolerance.
/// </summary>
public int SegmentsForTolerance(double tolerance)
{
if (tolerance >= Radius)
return 1;
var maxAngle = 2.0 * System.Math.Acos(1.0 - tolerance / Radius);
return System.Math.Max(1, (int)System.Math.Ceiling(System.Math.Abs(SweepAngle()) / maxAngle));
}
/// <summary> /// <summary>
/// Converts the arc to a group of points. /// Converts the arc to a group of points.
/// </summary> /// </summary>
+13
View File
@@ -122,6 +122,19 @@ namespace OpenNest.Geometry
return Center.DistanceTo(pt) <= Radius; return Center.DistanceTo(pt) <= Radius;
} }
/// <summary>
/// Returns the minimum number of segments needed so that the chord-to-arc
/// deviation (sagitta) does not exceed the given tolerance.
/// </summary>
public int SegmentsForTolerance(double tolerance)
{
if (tolerance >= Radius)
return 3;
var maxAngle = 2.0 * System.Math.Acos(1.0 - tolerance / Radius);
return System.Math.Max(3, (int)System.Math.Ceiling(Angle.TwoPI / maxAngle));
}
public List<Vector> ToPoints(int segments = 1000) public List<Vector> ToPoints(int segments = 1000)
{ {
var points = new List<Vector>(); var points = new List<Vector>();
+43
View File
@@ -243,6 +243,49 @@ namespace OpenNest.Geometry
return polygon; return polygon;
} }
/// <summary>
/// Converts the shape to a polygon using a chord tolerance to determine
/// the number of segments per arc/circle.
/// </summary>
public Polygon ToPolygonWithTolerance(double tolerance)
{
var polygon = new Polygon();
foreach (var entity in Entities)
{
switch (entity.Type)
{
case EntityType.Arc:
var arc = (Arc)entity;
polygon.Vertices.AddRange(arc.ToPoints(arc.SegmentsForTolerance(tolerance)));
break;
case EntityType.Line:
var line = (Line)entity;
polygon.Vertices.AddRange(new[]
{
line.StartPoint,
line.EndPoint
});
break;
case EntityType.Circle:
var circle = (Circle)entity;
polygon.Vertices.AddRange(circle.ToPoints(circle.SegmentsForTolerance(tolerance)));
break;
default:
Debug.Fail("Unhandled geometry type");
break;
}
}
polygon.Close();
polygon.Cleanup();
return polygon;
}
/// <summary> /// <summary>
/// Reverses the rotation direction of the shape. /// Reverses the rotation direction of the shape.
/// </summary> /// </summary>
+3 -3
View File
@@ -739,7 +739,7 @@ namespace OpenNest
return pts.Count > 0; return pts.Count > 0;
} }
private const int PushArcSegments = 36; private const double PushChordTolerance = 0.08;
public static List<Line> GetPartLines(Part part) public static List<Line> GetPartLines(Part part)
{ {
@@ -749,7 +749,7 @@ namespace OpenNest
foreach (var shape in shapes) foreach (var shape in shapes)
{ {
var polygon = shape.ToPolygon(PushArcSegments); var polygon = shape.ToPolygonWithTolerance(PushChordTolerance);
polygon.Offset(part.Location); polygon.Offset(part.Location);
lines.AddRange(polygon.ToLines()); lines.AddRange(polygon.ToLines());
} }
@@ -770,7 +770,7 @@ namespace OpenNest
if (offsetEntity == null) if (offsetEntity == null)
continue; continue;
var polygon = offsetEntity.ToPolygon(PushArcSegments); var polygon = offsetEntity.ToPolygonWithTolerance(PushChordTolerance);
polygon.Offset(part.Location); polygon.Offset(part.Location);
lines.AddRange(polygon.ToLines()); lines.AddRange(polygon.ToLines());
} }