From ca67b1bd29fcfabb84408470bde24ff02d650de0 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Thu, 23 Apr 2026 08:01:45 -0400 Subject: [PATCH] fix(io): handle flipped OCS normal on DXF ellipse import Ellipses with extrusion direction Z=-1 had their parametric direction reversed, causing the curve to appear mirrored. Negate start/end parameters when Normal.Z < 0 to correct the minor-axis traversal. Co-Authored-By: Claude Opus 4.6 --- OpenNest.IO/Extensions.cs | 15 +++- OpenNest.IO/OpenNest.IO.csproj | 3 + .../Geometry/EllipseConverterTests.cs | 76 +++++++++++++++++++ 3 files changed, 91 insertions(+), 3 deletions(-) diff --git a/OpenNest.IO/Extensions.cs b/OpenNest.IO/Extensions.cs index 34fab3b..5128b1b 100644 --- a/OpenNest.IO/Extensions.cs +++ b/OpenNest.IO/Extensions.cs @@ -181,13 +181,22 @@ namespace OpenNest.IO { var center = new Vector(ellipse.Center.X, ellipse.Center.Y); var majorAxis = new Vector(ellipse.MajorAxisEndPoint.X, ellipse.MajorAxisEndPoint.Y); - var semiMajor = System.Math.Sqrt(majorAxis.X * majorAxis.X + majorAxis.Y * majorAxis.Y); - var semiMinor = semiMajor * ellipse.RadiusRatio; - var rotation = System.Math.Atan2(majorAxis.Y, majorAxis.X); var startParam = ellipse.StartParameter; var endParam = ellipse.EndParameter; + if (ellipse.Normal.Z < 0) + { + var newStart = OpenNest.Math.Angle.TwoPI - endParam; + var newEnd = OpenNest.Math.Angle.TwoPI - startParam; + startParam = newStart; + endParam = newEnd; + } + + var semiMajor = System.Math.Sqrt(majorAxis.X * majorAxis.X + majorAxis.Y * majorAxis.Y); + var semiMinor = semiMajor * ellipse.RadiusRatio; + var rotation = System.Math.Atan2(majorAxis.Y, majorAxis.X); + var layer = ellipse.Layer.ToOpenNest(); var color = ellipse.ResolveColor(); var lineTypeName = ellipse.ResolveLineTypeName(); diff --git a/OpenNest.IO/OpenNest.IO.csproj b/OpenNest.IO/OpenNest.IO.csproj index 1d048f7..436b75f 100644 --- a/OpenNest.IO/OpenNest.IO.csproj +++ b/OpenNest.IO/OpenNest.IO.csproj @@ -4,6 +4,9 @@ OpenNest.IO OpenNest.IO + + + diff --git a/OpenNest.Tests/Geometry/EllipseConverterTests.cs b/OpenNest.Tests/Geometry/EllipseConverterTests.cs index b068bac..7115260 100644 --- a/OpenNest.Tests/Geometry/EllipseConverterTests.cs +++ b/OpenNest.Tests/Geometry/EllipseConverterTests.cs @@ -1,4 +1,5 @@ using OpenNest.Geometry; +using OpenNest.IO; using OpenNest.Math; using Xunit; using System.Linq; @@ -244,6 +245,81 @@ public class EllipseConverterTests } } + [Fact] + public void ToOpenNest_FlippedNormalZ_ProducesCorrectArcs() + { + var normal = new ACadSharp.Entities.Ellipse + { + Center = new CSMath.XYZ(-0.275, -0.245, 0), + MajorAxisEndPoint = new CSMath.XYZ(0.0001, 1.245, 0), + RadiusRatio = 0.28, + StartParameter = 0.017, + EndParameter = 1.571, + Normal = new CSMath.XYZ(0, 0, 1) + }; + + var flipped = new ACadSharp.Entities.Ellipse + { + Center = new CSMath.XYZ(0.275, -0.245, 0), + MajorAxisEndPoint = new CSMath.XYZ(-0.0001, 1.245, 0), + RadiusRatio = 0.28, + StartParameter = 0.017, + EndParameter = 1.571, + Normal = new CSMath.XYZ(0, 0, -1) + }; + + var normalArcs = normal.ToOpenNest(); + var flippedArcs = flipped.ToOpenNest(); + + Assert.True(normalArcs.Count > 0); + Assert.True(flippedArcs.Count > 0); + Assert.True(normalArcs.All(e => e is Arc)); + Assert.True(flippedArcs.All(e => e is Arc)); + + var normalFirst = (Arc)normalArcs.First(); + var flippedFirst = (Arc)flippedArcs.First(); + var normalStart = GetArcStart(normalFirst); + var flippedStart = GetArcStart(flippedFirst); + + Assert.True(normalStart.X < 0, $"Normal ellipse start X should be negative, got {normalStart.X}"); + Assert.True(flippedStart.X > 0, $"Flipped ellipse should bulge right, got {flippedStart.X}"); + + var normalBbox = GetBoundingBox(normalArcs.Cast()); + var flippedBbox = GetBoundingBox(flippedArcs.Cast()); + Assert.True(flippedBbox.minX > 0, $"Flipped ellipse should stay on positive X side, minX={flippedBbox.minX}"); + Assert.True(normalBbox.maxX < 0, $"Normal ellipse should stay on negative X side, maxX={normalBbox.maxX}"); + } + + private static (double minX, double maxX) GetBoundingBox(IEnumerable arcs) + { + var minX = double.MaxValue; + var maxX = double.MinValue; + foreach (var arc in arcs) + { + var s = GetArcStart(arc); + var e = GetArcEnd(arc); + minX = System.Math.Min(minX, System.Math.Min(s.X, e.X)); + maxX = System.Math.Max(maxX, System.Math.Max(s.X, e.X)); + } + return (minX, maxX); + } + + private static Vector GetArcStart(Arc arc) + { + var angle = arc.IsReversed ? arc.EndAngle : arc.StartAngle; + return new Vector( + arc.Center.X + arc.Radius * System.Math.Cos(angle), + arc.Center.Y + arc.Radius * System.Math.Sin(angle)); + } + + private static Vector GetArcEnd(Arc arc) + { + var angle = arc.IsReversed ? arc.StartAngle : arc.EndAngle; + return new Vector( + arc.Center.X + arc.Radius * System.Math.Cos(angle), + arc.Center.Y + arc.Radius * System.Math.Sin(angle)); + } + private static double MaxDeviationFromEllipse(Arc arc, Vector ellipseCenter, double semiMajor, double semiMinor, double rotation, int samples) {