From 1c2b569ff4186c474977177fcf874ebaed828ac9 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sat, 28 Mar 2026 16:37:01 -0400 Subject: [PATCH] fix: eliminate endpoint gaps in EllipseConverter arc output EllipseConverter computed arc radius from start point only, causing ~0.0009 unit gaps between consecutive arcs. Use circumcircle of (start, mid, end) points so both endpoints lie exactly on the arc. Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest.Core/Geometry/EllipseConverter.cs | 30 +++++++++++++++++++++- OpenNest.Tests/EllipseConverterTests.cs | 8 +++--- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/OpenNest.Core/Geometry/EllipseConverter.cs b/OpenNest.Core/Geometry/EllipseConverter.cs index 86097be..9642520 100644 --- a/OpenNest.Core/Geometry/EllipseConverter.cs +++ b/OpenNest.Core/Geometry/EllipseConverter.cs @@ -64,6 +64,25 @@ namespace OpenNest.Geometry return new Vector(p1.X + s * n1.X, p1.Y + s * n1.Y); } + internal static Vector Circumcenter(Vector a, Vector b, Vector c) + { + var ax = a.X - c.X; + var ay = a.Y - c.Y; + var bx = b.X - c.X; + var by = b.Y - c.Y; + var D = 2.0 * (ax * by - ay * bx); + + if (System.Math.Abs(D) < 1e-10) + return Vector.Invalid; + + var a2 = ax * ax + ay * ay; + var b2 = bx * bx + by * by; + var ux = (by * a2 - ay * b2) / D; + var uy = (ax * b2 - bx * a2) / D; + + return new Vector(ux + c.X, uy + c.Y); + } + public static List Convert(Vector center, double semiMajor, double semiMinor, double rotation, double startParam, double endParam, double tolerance = 0.001) { @@ -185,11 +204,20 @@ namespace OpenNest.Geometry { var p0 = EvaluatePoint(semiMajor, semiMinor, rotation, ellipseCenter, t0); var p1 = EvaluatePoint(semiMajor, semiMinor, rotation, ellipseCenter, t1); + var pMid = EvaluatePoint(semiMajor, semiMinor, rotation, ellipseCenter, (t0 + t1) / 2); + + // Use circumcircle of (p0, pMid, p1) so the arc passes through both + // endpoints exactly, eliminating gaps between adjacent arcs. + var cc = Circumcenter(p0, pMid, p1); + if (cc.IsValid()) + { + arcCenter = cc; + radius = p0.DistanceTo(cc); + } var startAngle = System.Math.Atan2(p0.Y - arcCenter.Y, p0.X - arcCenter.X); var endAngle = System.Math.Atan2(p1.Y - arcCenter.Y, p1.X - arcCenter.X); - var pMid = EvaluatePoint(semiMajor, semiMinor, rotation, ellipseCenter, (t0 + t1) / 2); var points = new List { p0, pMid, p1 }; var isReversed = SumSignedAngles(arcCenter, points) < 0; diff --git a/OpenNest.Tests/EllipseConverterTests.cs b/OpenNest.Tests/EllipseConverterTests.cs index 8c16165..c48b217 100644 --- a/OpenNest.Tests/EllipseConverterTests.cs +++ b/OpenNest.Tests/EllipseConverterTests.cs @@ -172,15 +172,15 @@ public class EllipseConverterTests var current = (Arc)result[i]; var next = (Arc)result[i + 1]; var gap = current.EndPoint().DistanceTo(next.StartPoint()); - Assert.True(gap < 0.001, - $"Gap of {gap:F6} between arc {i} and arc {i + 1}"); + Assert.True(gap < 1e-6, + $"Gap of {gap:E4} between arc {i} and arc {i + 1}"); } var lastArc = (Arc)result[^1]; var firstArc = (Arc)result[0]; var closingGap = lastArc.EndPoint().DistanceTo(firstArc.StartPoint()); - Assert.True(closingGap < 0.001, - $"Closing gap of {closingGap:F6}"); + Assert.True(closingGap < 1e-6, + $"Closing gap of {closingGap:E4}"); } [Fact]