diff --git a/OpenNest.Core/Geometry/ArcFit.cs b/OpenNest.Core/Geometry/ArcFit.cs index edf3b7b..758af4d 100644 --- a/OpenNest.Core/Geometry/ArcFit.cs +++ b/OpenNest.Core/Geometry/ArcFit.cs @@ -56,6 +56,60 @@ namespace OpenNest.Geometry return (new Vector(cx, cy), radius, MaxRadialDeviation(points, cx, cy, radius)); } + /// + /// Fits a circular arc constrained to be tangent to the given directions at both + /// the first and last points. The center lies at the intersection of the normals + /// at P1 and Pn, guaranteeing the arc departs P1 in the start direction and arrives + /// at Pn in the end direction. Uses the radius from P1 (exact start tangent); + /// deviation includes any endpoint gap at Pn. + /// + internal static (Vector center, double radius, double deviation) FitWithDualTangent( + List points, Vector startTangent, Vector endTangent) + { + if (points.Count < 3) + return (Vector.Invalid, 0, double.MaxValue); + + var p1 = points[0]; + var pn = points[^1]; + + var stLen = System.Math.Sqrt(startTangent.X * startTangent.X + startTangent.Y * startTangent.Y); + var etLen = System.Math.Sqrt(endTangent.X * endTangent.X + endTangent.Y * endTangent.Y); + if (stLen < 1e-10 || etLen < 1e-10) + return (Vector.Invalid, 0, double.MaxValue); + + // Normal to start tangent at P1 (perpendicular) + var n1x = -startTangent.Y / stLen; + var n1y = startTangent.X / stLen; + + // Normal to end tangent at Pn + var n2x = -endTangent.Y / etLen; + var n2y = endTangent.X / etLen; + + // Solve: P1 + t1*N1 = Pn + t2*N2 + var det = n1x * (-n2y) - (-n2x) * n1y; + if (System.Math.Abs(det) < 1e-10) + return (Vector.Invalid, 0, double.MaxValue); + + var dx = pn.X - p1.X; + var dy = pn.Y - p1.Y; + var t1 = (dx * (-n2y) - (-n2x) * dy) / det; + + var cx = p1.X + t1 * n1x; + var cy = p1.Y + t1 * n1y; + + // Use radius from P1 (guarantees exact start tangent and passes through P1) + var r1 = System.Math.Sqrt((cx - p1.X) * (cx - p1.X) + (cy - p1.Y) * (cy - p1.Y)); + if (r1 < 1e-10) + return (Vector.Invalid, 0, double.MaxValue); + + // Measure endpoint gap at Pn + var r2 = System.Math.Sqrt((cx - pn.X) * (cx - pn.X) + (cy - pn.Y) * (cy - pn.Y)); + var endpointDev = System.Math.Abs(r2 - r1); + + var interiorDev = MaxRadialDeviation(points, cx, cy, r1); + return (new Vector(cx, cy), r1, System.Math.Max(endpointDev, interiorDev)); + } + /// /// Computes the maximum radial deviation of interior points from a circle. /// diff --git a/OpenNest.Tests/GeometrySimplifierTests.cs b/OpenNest.Tests/GeometrySimplifierTests.cs index 8f891e8..385e2a4 100644 --- a/OpenNest.Tests/GeometrySimplifierTests.cs +++ b/OpenNest.Tests/GeometrySimplifierTests.cs @@ -1,4 +1,7 @@ using OpenNest.Geometry; +using OpenNest.IO; +using System.IO; +using System.Linq; using Xunit; namespace OpenNest.Tests; @@ -127,4 +130,52 @@ public class GeometrySimplifierTests // Index 6 is the fitted arc replacing the second run Assert.IsType(result.Entities[6]); } + + [Fact] + public void Apply_DynaPanDxf_NoGapsAfterSimplification() + { + var path = @"C:\Users\aisaacs\Desktop\Sullys Q29 DXFs\SULLYS-031 Dyna Pan.dxf"; + if (!File.Exists(path)) + return; // skip if file not available + + var importer = new DxfImporter(); + var result = importer.Import(path); + var shapes = ShapeBuilder.GetShapes(result.Entities); + + var simplifier = new GeometrySimplifier { Tolerance = 0.004 }; + + foreach (var shape in shapes) + { + var candidates = simplifier.Analyze(shape); + if (candidates.Count == 0) continue; + + var simplified = simplifier.Apply(shape, candidates); + + // Check for gaps between consecutive entities + for (var i = 0; i < simplified.Entities.Count - 1; i++) + { + var current = simplified.Entities[i]; + var next = simplified.Entities[i + 1]; + + var currentEnd = current switch + { + Line l => l.EndPoint, + Arc a => a.EndPoint(), + _ => Vector.Invalid + }; + var nextStart = next switch + { + Line l => l.StartPoint, + Arc a => a.StartPoint(), + _ => Vector.Invalid + }; + + if (!currentEnd.IsValid() || !nextStart.IsValid()) continue; + + var gap = currentEnd.DistanceTo(nextStart); + Assert.True(gap < 0.005, + $"Gap of {gap:F4} between entities {i} ({current.GetType().Name}) and {i + 1} ({next.GetType().Name})"); + } + } + } } diff --git a/OpenNest/Forms/CadConverterForm.cs b/OpenNest/Forms/CadConverterForm.cs index 112212b..c104896 100644 --- a/OpenNest/Forms/CadConverterForm.cs +++ b/OpenNest/Forms/CadConverterForm.cs @@ -494,13 +494,30 @@ namespace OpenNest.Forms using var dlg = new SaveFileDialog { - Filter = "DXF Files|*.dxf", + Filter = "DXF 2018 (*.dxf)|*.dxf|" + + "DXF 2013 (*.dxf)|*.dxf|" + + "DXF 2010 (*.dxf)|*.dxf|" + + "DXF 2007 (*.dxf)|*.dxf|" + + "DXF 2004 (*.dxf)|*.dxf|" + + "DXF 2000 (*.dxf)|*.dxf|" + + "DXF R14 (*.dxf)|*.dxf", FileName = Path.ChangeExtension(item.Name, ".dxf"), }; if (dlg.ShowDialog() != DialogResult.OK) return; - var doc = new ACadSharp.CadDocument(); + var version = dlg.FilterIndex switch + { + 2 => ACadSharp.ACadVersion.AC1027, + 3 => ACadSharp.ACadVersion.AC1024, + 4 => ACadSharp.ACadVersion.AC1021, + 5 => ACadSharp.ACadVersion.AC1018, + 6 => ACadSharp.ACadVersion.AC1015, + 7 => ACadSharp.ACadVersion.AC1014, + _ => ACadSharp.ACadVersion.AC1032, + }; + + var doc = new ACadSharp.CadDocument(version); foreach (var entity in item.Entities) { switch (entity)