From 912a47c5e8e857b30949ccde6a59a5a2e81ee999 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sat, 28 Mar 2026 16:11:47 -0400 Subject: [PATCH] refactor: extract shared ArcFit utilities from SplineConverter and GeometrySimplifier Move identical FitWithStartTangent and MaxRadialDeviation methods to a shared ArcFit class, eliminating 40-line duplicate. Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest.Core/Geometry/ArcFit.cs | 76 ++++++++++++++++++++ OpenNest.Core/Geometry/GeometrySimplifier.cs | 58 ++------------- OpenNest.Core/Geometry/SplineConverter.cs | 58 ++------------- 3 files changed, 84 insertions(+), 108 deletions(-) create mode 100644 OpenNest.Core/Geometry/ArcFit.cs diff --git a/OpenNest.Core/Geometry/ArcFit.cs b/OpenNest.Core/Geometry/ArcFit.cs new file mode 100644 index 0000000..edf3b7b --- /dev/null +++ b/OpenNest.Core/Geometry/ArcFit.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; + +namespace OpenNest.Geometry +{ + /// + /// Shared arc-fitting utilities used by SplineConverter and GeometrySimplifier. + /// + internal static class ArcFit + { + /// + /// Fits a circular arc constrained to be tangent to the given direction at the + /// first point. The center lies at the intersection of the normal at P1 (perpendicular + /// to the tangent) and the perpendicular bisector of the chord P1->Pn, guaranteeing + /// the arc passes through both endpoints and departs P1 in the given direction. + /// + internal static (Vector center, double radius, double deviation) FitWithStartTangent( + List points, Vector tangent) + { + if (points.Count < 3) + return (Vector.Invalid, 0, double.MaxValue); + + var p1 = points[0]; + var pn = points[^1]; + + var mx = (p1.X + pn.X) / 2; + var my = (p1.Y + pn.Y) / 2; + var dx = pn.X - p1.X; + var dy = pn.Y - p1.Y; + var chordLen = System.Math.Sqrt(dx * dx + dy * dy); + if (chordLen < 1e-10) + return (Vector.Invalid, 0, double.MaxValue); + + var bx = -dy / chordLen; + var by = dx / chordLen; + + var tLen = System.Math.Sqrt(tangent.X * tangent.X + tangent.Y * tangent.Y); + if (tLen < 1e-10) + return (Vector.Invalid, 0, double.MaxValue); + + var nx = -tangent.Y / tLen; + var ny = tangent.X / tLen; + + var det = nx * by - ny * bx; + if (System.Math.Abs(det) < 1e-10) + return (Vector.Invalid, 0, double.MaxValue); + + var s = ((mx - p1.X) * by - (my - p1.Y) * bx) / det; + + var cx = p1.X + s * nx; + var cy = p1.Y + s * ny; + var radius = System.Math.Sqrt((cx - p1.X) * (cx - p1.X) + (cy - p1.Y) * (cy - p1.Y)); + + if (radius < 1e-10) + return (Vector.Invalid, 0, double.MaxValue); + + return (new Vector(cx, cy), radius, MaxRadialDeviation(points, cx, cy, radius)); + } + + /// + /// Computes the maximum radial deviation of interior points from a circle. + /// + internal static double MaxRadialDeviation(List points, double cx, double cy, double radius) + { + var maxDev = 0.0; + for (var i = 1; i < points.Count - 1; i++) + { + var px = points[i].X - cx; + var py = points[i].Y - cy; + var dist = System.Math.Sqrt(px * px + py * py); + var dev = System.Math.Abs(dist - radius); + if (dev > maxDev) maxDev = dev; + } + return maxDev; + } + } +} diff --git a/OpenNest.Core/Geometry/GeometrySimplifier.cs b/OpenNest.Core/Geometry/GeometrySimplifier.cs index 33078af..250fed3 100644 --- a/OpenNest.Core/Geometry/GeometrySimplifier.cs +++ b/OpenNest.Core/Geometry/GeometrySimplifier.cs @@ -437,47 +437,8 @@ public class GeometrySimplifier /// the arc passes through both endpoints and departs P1 in the given direction. /// private static (Vector center, double radius, double deviation) FitWithStartTangent( - List points, Vector tangent) - { - if (points.Count < 3) - return (Vector.Invalid, 0, double.MaxValue); - - var p1 = points[0]; - var pn = points[^1]; - - var mx = (p1.X + pn.X) / 2; - var my = (p1.Y + pn.Y) / 2; - var dx = pn.X - p1.X; - var dy = pn.Y - p1.Y; - var chordLen = System.Math.Sqrt(dx * dx + dy * dy); - if (chordLen < 1e-10) - return (Vector.Invalid, 0, double.MaxValue); - - var bx = -dy / chordLen; - var by = dx / chordLen; - - var tLen = System.Math.Sqrt(tangent.X * tangent.X + tangent.Y * tangent.Y); - if (tLen < 1e-10) - return (Vector.Invalid, 0, double.MaxValue); - - var nx = -tangent.Y / tLen; - var ny = tangent.X / tLen; - - var det = nx * by - ny * bx; - if (System.Math.Abs(det) < 1e-10) - return (Vector.Invalid, 0, double.MaxValue); - - var t = ((mx - p1.X) * by - (my - p1.Y) * bx) / det; - - var cx = p1.X + t * nx; - var cy = p1.Y + t * ny; - var radius = System.Math.Sqrt((cx - p1.X) * (cx - p1.X) + (cy - p1.Y) * (cy - p1.Y)); - - if (radius < 1e-10) - return (Vector.Invalid, 0, double.MaxValue); - - return (new Vector(cx, cy), radius, MaxRadialDeviation(points, cx, cy, radius)); - } + List points, Vector tangent) => + ArcFit.FitWithStartTangent(points, tangent); /// /// Computes the tangent direction at the last point of a fitted arc, @@ -629,19 +590,8 @@ public class GeometrySimplifier /// /// Max deviation of intermediate points (excluding endpoints) from a circle. /// - private static double MaxRadialDeviation(List points, double cx, double cy, double radius) - { - var maxDev = 0.0; - for (var i = 1; i < points.Count - 1; i++) - { - var px = points[i].X - cx; - var py = points[i].Y - cy; - var dist = System.Math.Sqrt(px * px + py * py); - var dev = System.Math.Abs(dist - radius); - if (dev > maxDev) maxDev = dev; - } - return maxDev; - } + private static double MaxRadialDeviation(List points, double cx, double cy, double radius) => + ArcFit.MaxRadialDeviation(points, cx, cy, radius); /// /// Measures the maximum distance from sampled points along the fitted arc diff --git a/OpenNest.Core/Geometry/SplineConverter.cs b/OpenNest.Core/Geometry/SplineConverter.cs index 81b2674..f7e7c6e 100644 --- a/OpenNest.Core/Geometry/SplineConverter.cs +++ b/OpenNest.Core/Geometry/SplineConverter.cs @@ -131,61 +131,11 @@ namespace OpenNest.Geometry } private static (Vector center, double radius, double deviation) FitWithStartTangent( - List points, Vector tangent) - { - if (points.Count < 3) - return (Vector.Invalid, 0, double.MaxValue); + List points, Vector tangent) => + ArcFit.FitWithStartTangent(points, tangent); - var p1 = points[0]; - var pn = points[^1]; - - var mx = (p1.X + pn.X) / 2; - var my = (p1.Y + pn.Y) / 2; - var dx = pn.X - p1.X; - var dy = pn.Y - p1.Y; - var chordLen = System.Math.Sqrt(dx * dx + dy * dy); - if (chordLen < 1e-10) - return (Vector.Invalid, 0, double.MaxValue); - - var bx = -dy / chordLen; - var by = dx / chordLen; - - var tLen = System.Math.Sqrt(tangent.X * tangent.X + tangent.Y * tangent.Y); - if (tLen < 1e-10) - return (Vector.Invalid, 0, double.MaxValue); - - var nx = -tangent.Y / tLen; - var ny = tangent.X / tLen; - - var det = nx * by - ny * bx; - if (System.Math.Abs(det) < 1e-10) - return (Vector.Invalid, 0, double.MaxValue); - - var s = ((mx - p1.X) * by - (my - p1.Y) * bx) / det; - - var cx = p1.X + s * nx; - var cy = p1.Y + s * ny; - var radius = System.Math.Sqrt((cx - p1.X) * (cx - p1.X) + (cy - p1.Y) * (cy - p1.Y)); - - if (radius < 1e-10) - return (Vector.Invalid, 0, double.MaxValue); - - return (new Vector(cx, cy), radius, MaxRadialDeviation(points, cx, cy, radius)); - } - - private static double MaxRadialDeviation(List points, double cx, double cy, double radius) - { - var maxDev = 0.0; - for (var i = 1; i < points.Count - 1; i++) - { - var px = points[i].X - cx; - var py = points[i].Y - cy; - var dist = System.Math.Sqrt(px * px + py * py); - var dev = System.Math.Abs(dist - radius); - if (dev > maxDev) maxDev = dev; - } - return maxDev; - } + private static double MaxRadialDeviation(List points, double cx, double cy, double radius) => + ArcFit.MaxRadialDeviation(points, cx, cy, radius); private static double SumSignedAngles(Vector center, List points) {