From cb1ebedd0aad3dcd66c177b0204588a8c084d769 Mon Sep 17 00:00:00 2001 From: mediummandoo Date: Wed, 2 Sep 2026 12:54:08 +0900 Subject: [PATCH 1/3] fix: preserve full-sweep hatch boundary angles --- .../IO/DXF/DxfHatchWriterTests.cs | 61 +++++++++++++++++++ .../DxfSectionWriterBase.Entities.cs | 8 +-- 2 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 src/ACadSharp.Tests/IO/DXF/DxfHatchWriterTests.cs diff --git a/src/ACadSharp.Tests/IO/DXF/DxfHatchWriterTests.cs b/src/ACadSharp.Tests/IO/DXF/DxfHatchWriterTests.cs new file mode 100644 index 000000000..6754a3a35 --- /dev/null +++ b/src/ACadSharp.Tests/IO/DXF/DxfHatchWriterTests.cs @@ -0,0 +1,61 @@ +using ACadSharp.Entities; +using ACadSharp.IO; +using CSMath; +using System.IO; +using System.Linq; +using Xunit; + +namespace ACadSharp.Tests.IO.DXF; + +public class DxfHatchWriterTests +{ + [Fact] + public void WriteHatchPreservesFullArcAndEllipseSweeps() + { + Hatch hatch = new Hatch { IsSolid = true }; + hatch.SeedPoints.Add(XY.Zero); + + Hatch.BoundaryPath arcPath = new Hatch.BoundaryPath(); + arcPath.Edges.Add(new Hatch.BoundaryPath.Arc + { + Center = XY.Zero, + Radius = 2, + StartAngle = -MathHelper.HalfPI, + EndAngle = MathHelper.ThreeHalfPI, + CounterClockWise = true, + }); + hatch.Paths.Add(arcPath); + + Hatch.BoundaryPath ellipsePath = new Hatch.BoundaryPath(); + ellipsePath.Edges.Add(new Hatch.BoundaryPath.Ellipse + { + Center = new XY(5, 0), + MajorAxisEndPoint = new XY(2, 0), + RadiusRatio = 0.5, + StartAngle = -MathHelper.HalfPI, + EndAngle = MathHelper.ThreeHalfPI, + CounterClockWise = true, + }); + hatch.Paths.Add(ellipsePath); + + CadDocument document = new CadDocument(); + document.Entities.Add(hatch); + using MemoryStream output = new MemoryStream(); + DxfWriter.Write(output, document); + + using MemoryStream input = new MemoryStream(output.ToArray()); + CadDocument result = DxfReader.Read(input); + Hatch writtenHatch = Assert.Single(result.Entities.OfType()); + Hatch.BoundaryPath.Arc arc = Assert.Single( + writtenHatch.Paths.SelectMany(p => p.Edges).OfType() + ); + Hatch.BoundaryPath.Ellipse ellipse = Assert.Single( + writtenHatch.Paths.SelectMany(p => p.Edges).OfType() + ); + + Assert.Equal(-MathHelper.HalfPI, arc.StartAngle, 12); + Assert.Equal(MathHelper.ThreeHalfPI, arc.EndAngle, 12); + Assert.Equal(-MathHelper.HalfPI, ellipse.StartAngle, 12); + Assert.Equal(MathHelper.ThreeHalfPI, ellipse.EndAngle, 12); + } +} diff --git a/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.Entities.cs b/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.Entities.cs index aac631ade..aad079cc1 100644 --- a/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.Entities.cs +++ b/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.Entities.cs @@ -526,16 +526,16 @@ private void writeHatchBoundaryPathEdge(Hatch.BoundaryPath.Edge edge) case Hatch.BoundaryPath.Arc arc: this._writer.Write(10, arc.Center); this._writer.Write(40, arc.Radius); - this._writer.Write(50, MathHelper.RadToDeg(arc.StartAngle)); - this._writer.Write(51, MathHelper.RadToDeg(arc.EndAngle)); + this._writer.Write(50, arc.StartAngle * MathHelper.RadToDegFactor); + this._writer.Write(51, arc.EndAngle * MathHelper.RadToDegFactor); this._writer.Write(73, arc.CounterClockWise ? (short)1 : (short)0); break; case Hatch.BoundaryPath.Ellipse ellipse: this._writer.Write(10, ellipse.Center); this._writer.Write(11, ellipse.MajorAxisEndPoint); this._writer.Write(40, ellipse.RadiusRatio); - this._writer.Write(50, MathHelper.RadToDeg(ellipse.StartAngle)); - this._writer.Write(51, MathHelper.RadToDeg(ellipse.EndAngle)); + this._writer.Write(50, ellipse.StartAngle * MathHelper.RadToDegFactor); + this._writer.Write(51, ellipse.EndAngle * MathHelper.RadToDegFactor); this._writer.Write(73, ellipse.CounterClockWise ? (short)1 : (short)0); break; case Hatch.BoundaryPath.Line line: From 34bd66b11d232e540f7c7624f98821aa6a2dab9e Mon Sep 17 00:00:00 2001 From: mediummandoo Date: Thu, 10 Sep 2026 14:09:17 +0900 Subject: [PATCH 2/3] fix: use non-normalizing hatch conversion and shared writer fixtures --- .../IO/DXF/DxfHatchWriterTests.cs | 61 ------------------- .../IO/DXF/DxfWriterSingleObjectTests.cs | 40 +++++++++++- .../IO/WriterSingleObjectTests.cs | 34 ++++++++++- .../DxfSectionWriterBase.Entities.cs | 8 +-- 4 files changed, 75 insertions(+), 68 deletions(-) delete mode 100644 src/ACadSharp.Tests/IO/DXF/DxfHatchWriterTests.cs diff --git a/src/ACadSharp.Tests/IO/DXF/DxfHatchWriterTests.cs b/src/ACadSharp.Tests/IO/DXF/DxfHatchWriterTests.cs deleted file mode 100644 index 6754a3a35..000000000 --- a/src/ACadSharp.Tests/IO/DXF/DxfHatchWriterTests.cs +++ /dev/null @@ -1,61 +0,0 @@ -using ACadSharp.Entities; -using ACadSharp.IO; -using CSMath; -using System.IO; -using System.Linq; -using Xunit; - -namespace ACadSharp.Tests.IO.DXF; - -public class DxfHatchWriterTests -{ - [Fact] - public void WriteHatchPreservesFullArcAndEllipseSweeps() - { - Hatch hatch = new Hatch { IsSolid = true }; - hatch.SeedPoints.Add(XY.Zero); - - Hatch.BoundaryPath arcPath = new Hatch.BoundaryPath(); - arcPath.Edges.Add(new Hatch.BoundaryPath.Arc - { - Center = XY.Zero, - Radius = 2, - StartAngle = -MathHelper.HalfPI, - EndAngle = MathHelper.ThreeHalfPI, - CounterClockWise = true, - }); - hatch.Paths.Add(arcPath); - - Hatch.BoundaryPath ellipsePath = new Hatch.BoundaryPath(); - ellipsePath.Edges.Add(new Hatch.BoundaryPath.Ellipse - { - Center = new XY(5, 0), - MajorAxisEndPoint = new XY(2, 0), - RadiusRatio = 0.5, - StartAngle = -MathHelper.HalfPI, - EndAngle = MathHelper.ThreeHalfPI, - CounterClockWise = true, - }); - hatch.Paths.Add(ellipsePath); - - CadDocument document = new CadDocument(); - document.Entities.Add(hatch); - using MemoryStream output = new MemoryStream(); - DxfWriter.Write(output, document); - - using MemoryStream input = new MemoryStream(output.ToArray()); - CadDocument result = DxfReader.Read(input); - Hatch writtenHatch = Assert.Single(result.Entities.OfType()); - Hatch.BoundaryPath.Arc arc = Assert.Single( - writtenHatch.Paths.SelectMany(p => p.Edges).OfType() - ); - Hatch.BoundaryPath.Ellipse ellipse = Assert.Single( - writtenHatch.Paths.SelectMany(p => p.Edges).OfType() - ); - - Assert.Equal(-MathHelper.HalfPI, arc.StartAngle, 12); - Assert.Equal(MathHelper.ThreeHalfPI, arc.EndAngle, 12); - Assert.Equal(-MathHelper.HalfPI, ellipse.StartAngle, 12); - Assert.Equal(MathHelper.ThreeHalfPI, ellipse.EndAngle, 12); - } -} diff --git a/src/ACadSharp.Tests/IO/DXF/DxfWriterSingleObjectTests.cs b/src/ACadSharp.Tests/IO/DXF/DxfWriterSingleObjectTests.cs index a412c49b3..ce7e0e167 100644 --- a/src/ACadSharp.Tests/IO/DXF/DxfWriterSingleObjectTests.cs +++ b/src/ACadSharp.Tests/IO/DXF/DxfWriterSingleObjectTests.cs @@ -1,5 +1,9 @@ -using ACadSharp.IO; +using ACadSharp.Entities; +using ACadSharp.IO; +using CSMath; +using System; using System.IO; +using System.Linq; using Xunit; using Xunit.Abstractions; @@ -51,6 +55,38 @@ public void WriteCasesAC1032(SingleCaseGenerator data) this.writeDxfFile(data, ACadVersion.AC1032); } + [Theory] + [InlineData(-MathHelper.HalfPI, MathHelper.ThreeHalfPI)] + [InlineData(Math.PI, 3 * Math.PI)] + [InlineData(0, MathHelper.HalfPI)] + public void WriteHatchPreservesBoundaryAngles(double startAngle, double endAngle) + { + SingleCaseGenerator data = new SingleCaseGenerator(); + data.CreateHatchFullSweeps(); + + Hatch hatch = Assert.Single(data.Document.Entities.OfType()); + Hatch.BoundaryPath.Arc arc = Assert.Single( + hatch.Paths.SelectMany(p => p.Edges).OfType()); + Hatch.BoundaryPath.Ellipse ellipse = Assert.Single( + hatch.Paths.SelectMany(p => p.Edges).OfType()); + arc.StartAngle = ellipse.StartAngle = startAngle; + arc.EndAngle = ellipse.EndAngle = endAngle; + + using MemoryStream output = new MemoryStream(); + DxfWriter.Write(output, data.Document); + + using MemoryStream input = new MemoryStream(output.ToArray()); + CadDocument result = DxfReader.Read(input); + Hatch writtenHatch = Assert.Single(result.Entities.OfType()); + arc = Assert.Single(writtenHatch.Paths.SelectMany(p => p.Edges).OfType()); + ellipse = Assert.Single(writtenHatch.Paths.SelectMany(p => p.Edges).OfType()); + + Assert.Equal(startAngle, arc.StartAngle, 12); + Assert.Equal(endAngle, arc.EndAngle, 12); + Assert.Equal(startAngle, ellipse.StartAngle, 12); + Assert.Equal(endAngle, ellipse.EndAngle, 12); + } + protected void writeDxfFile(SingleCaseGenerator data, ACadVersion version) { Assert.True(data.HasExecuted, $"The writer has failed during it's execution."); @@ -84,4 +120,4 @@ protected void writeDxfFile(SingleCaseGenerator data, ACadVersion version) } } } -} \ No newline at end of file +} diff --git a/src/ACadSharp.Tests/IO/WriterSingleObjectTests.cs b/src/ACadSharp.Tests/IO/WriterSingleObjectTests.cs index 899e8c31a..53de5e5bd 100644 --- a/src/ACadSharp.Tests/IO/WriterSingleObjectTests.cs +++ b/src/ACadSharp.Tests/IO/WriterSingleObjectTests.cs @@ -68,6 +68,7 @@ static WriterSingleObjectTests() Data.Add(new(nameof(SingleCaseGenerator.CreateHatchPolyline))); Data.Add(new(nameof(SingleCaseGenerator.CreateHatch))); Data.Add(new(nameof(SingleCaseGenerator.CreateCircleHatch))); + Data.Add(new(nameof(SingleCaseGenerator.CreateHatchFullSweeps))); Data.Add(new(nameof(SingleCaseGenerator.HatchWithEntities))); Data.Add(new(nameof(SingleCaseGenerator.ChangedEncoding))); Data.Add(new(nameof(SingleCaseGenerator.AddBlockWithAttributes))); @@ -739,6 +740,37 @@ public void CreateHatch() this.Document.Entities.Add(hatch); } + public void CreateHatchFullSweeps() + { + Hatch hatch = new Hatch { IsSolid = true }; + hatch.SeedPoints.Add(XY.Zero); + + Hatch.BoundaryPath arcPath = new Hatch.BoundaryPath(); + arcPath.Edges.Add(new Hatch.BoundaryPath.Arc + { + Center = XY.Zero, + Radius = 2, + StartAngle = -MathHelper.HalfPI, + EndAngle = MathHelper.ThreeHalfPI, + CounterClockWise = true, + }); + hatch.Paths.Add(arcPath); + + Hatch.BoundaryPath ellipsePath = new Hatch.BoundaryPath(); + ellipsePath.Edges.Add(new Hatch.BoundaryPath.Ellipse + { + Center = new XY(5, 0), + MajorAxisEndPoint = new XY(2, 0), + RadiusRatio = 0.5, + StartAngle = -MathHelper.HalfPI, + EndAngle = MathHelper.ThreeHalfPI, + CounterClockWise = true, + }); + hatch.Paths.Add(ellipsePath); + + this.Document.Entities.Add(hatch); + } + public void CreateHatchPolyline() { Hatch hatch = new Hatch(); @@ -2014,4 +2046,4 @@ public void XRef() this.Document.Entities.Add(new Insert(record)); } } -} \ No newline at end of file +} diff --git a/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.Entities.cs b/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.Entities.cs index 2c0b14e88..04bb3a3fd 100644 --- a/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.Entities.cs +++ b/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.Entities.cs @@ -528,16 +528,16 @@ private void writeHatchBoundaryPathEdge(Hatch.BoundaryPath.Edge edge) case Hatch.BoundaryPath.Arc arc: this._writer.Write(10, arc.Center); this._writer.Write(40, arc.Radius); - this._writer.Write(50, arc.StartAngle * MathHelper.RadToDegFactor); - this._writer.Write(51, arc.EndAngle * MathHelper.RadToDegFactor); + this._writer.Write(50, MathHelper.RadToDeg(arc.StartAngle, normalize: false)); + this._writer.Write(51, MathHelper.RadToDeg(arc.EndAngle, normalize: false)); this._writer.Write(73, arc.CounterClockWise ? (short)1 : (short)0); break; case Hatch.BoundaryPath.Ellipse ellipse: this._writer.Write(10, ellipse.Center); this._writer.Write(11, ellipse.MajorAxisEndPoint); this._writer.Write(40, ellipse.RadiusRatio); - this._writer.Write(50, ellipse.StartAngle * MathHelper.RadToDegFactor); - this._writer.Write(51, ellipse.EndAngle * MathHelper.RadToDegFactor); + this._writer.Write(50, MathHelper.RadToDeg(ellipse.StartAngle, normalize: false)); + this._writer.Write(51, MathHelper.RadToDeg(ellipse.EndAngle, normalize: false)); this._writer.Write(73, ellipse.CounterClockWise ? (short)1 : (short)0); break; case Hatch.BoundaryPath.Line line: From f0bc83aa2ea3bb8868bc711296139314f414cf4f Mon Sep 17 00:00:00 2001 From: mediummandoo Date: Thu, 10 Sep 2026 15:27:32 +0900 Subject: [PATCH 3/3] fix: normalize hatch angles within signed bounds --- .../IO/DXF/DxfWriterSingleObjectTests.cs | 38 ++++++++++++++----- .../IO/WriterSingleObjectTests.cs | 4 +- .../DxfSectionWriterBase.Entities.cs | 26 +++++++++++-- 3 files changed, 53 insertions(+), 15 deletions(-) diff --git a/src/ACadSharp.Tests/IO/DXF/DxfWriterSingleObjectTests.cs b/src/ACadSharp.Tests/IO/DXF/DxfWriterSingleObjectTests.cs index ce7e0e167..e24381f90 100644 --- a/src/ACadSharp.Tests/IO/DXF/DxfWriterSingleObjectTests.cs +++ b/src/ACadSharp.Tests/IO/DXF/DxfWriterSingleObjectTests.cs @@ -1,7 +1,6 @@ using ACadSharp.Entities; using ACadSharp.IO; using CSMath; -using System; using System.IO; using System.Linq; using Xunit; @@ -56,11 +55,25 @@ public void WriteCasesAC1032(SingleCaseGenerator data) } [Theory] - [InlineData(-MathHelper.HalfPI, MathHelper.ThreeHalfPI)] - [InlineData(Math.PI, 3 * Math.PI)] - [InlineData(0, MathHelper.HalfPI)] - public void WriteHatchPreservesBoundaryAngles(double startAngle, double endAngle) + [InlineData(-90, 270, -90, 270)] + [InlineData(180, 540, -180, 180)] + [InlineData(-540, -180, -180, 180)] + [InlineData(540, 180, 180, -180)] + [InlineData(-180, -540, 180, -180)] + [InlineData(0, 360, 0, 360)] + [InlineData(0, -360, 0, -360)] + [InlineData(360, 720, 0, 360)] + [InlineData(90, 90, 90, 90)] + [InlineData(0, 90, 0, 90)] + [InlineData(450, 540, 90, 180)] + [InlineData(-540, -450, -180, -90)] + public void WriteHatchNormalizesBoundaryAngles(double startDegrees, double endDegrees, double expectedStartDegrees, double expectedEndDegrees) { + double startAngle = MathHelper.DegToRad(startDegrees); + double endAngle = MathHelper.DegToRad(endDegrees); + double expectedStartAngle = MathHelper.DegToRad(expectedStartDegrees); + double expectedEndAngle = MathHelper.DegToRad(expectedEndDegrees); + SingleCaseGenerator data = new SingleCaseGenerator(); data.CreateHatchFullSweeps(); @@ -71,6 +84,7 @@ public void WriteHatchPreservesBoundaryAngles(double startAngle, double endAngle hatch.Paths.SelectMany(p => p.Edges).OfType()); arc.StartAngle = ellipse.StartAngle = startAngle; arc.EndAngle = ellipse.EndAngle = endAngle; + ellipse.CounterClockWise = false; using MemoryStream output = new MemoryStream(); DxfWriter.Write(output, data.Document); @@ -81,10 +95,16 @@ public void WriteHatchPreservesBoundaryAngles(double startAngle, double endAngle arc = Assert.Single(writtenHatch.Paths.SelectMany(p => p.Edges).OfType()); ellipse = Assert.Single(writtenHatch.Paths.SelectMany(p => p.Edges).OfType()); - Assert.Equal(startAngle, arc.StartAngle, 12); - Assert.Equal(endAngle, arc.EndAngle, 12); - Assert.Equal(startAngle, ellipse.StartAngle, 12); - Assert.Equal(endAngle, ellipse.EndAngle, 12); + Assert.Equal(expectedStartAngle, arc.StartAngle, 12); + Assert.Equal(expectedEndAngle, arc.EndAngle, 12); + Assert.Equal(expectedStartAngle, ellipse.StartAngle, 12); + Assert.Equal(expectedEndAngle, ellipse.EndAngle, 12); + Assert.InRange(arc.StartAngle, -MathHelper.TwoPI, MathHelper.TwoPI); + Assert.InRange(arc.EndAngle, -MathHelper.TwoPI, MathHelper.TwoPI); + Assert.InRange(ellipse.StartAngle, -MathHelper.TwoPI, MathHelper.TwoPI); + Assert.InRange(ellipse.EndAngle, -MathHelper.TwoPI, MathHelper.TwoPI); + Assert.True(arc.CounterClockWise); + Assert.False(ellipse.CounterClockWise); } protected void writeDxfFile(SingleCaseGenerator data, ACadVersion version) diff --git a/src/ACadSharp.Tests/IO/WriterSingleObjectTests.cs b/src/ACadSharp.Tests/IO/WriterSingleObjectTests.cs index 53de5e5bd..35250bcbc 100644 --- a/src/ACadSharp.Tests/IO/WriterSingleObjectTests.cs +++ b/src/ACadSharp.Tests/IO/WriterSingleObjectTests.cs @@ -762,8 +762,8 @@ public void CreateHatchFullSweeps() Center = new XY(5, 0), MajorAxisEndPoint = new XY(2, 0), RadiusRatio = 0.5, - StartAngle = -MathHelper.HalfPI, - EndAngle = MathHelper.ThreeHalfPI, + StartAngle = Math.PI, + EndAngle = 3 * Math.PI, CounterClockWise = true, }); hatch.Paths.Add(ellipsePath); diff --git a/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.Entities.cs b/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.Entities.cs index 04bb3a3fd..aca6880dc 100644 --- a/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.Entities.cs +++ b/src/ACadSharp/IO/DXF/DxfStreamWriter/DxfSectionWriterBase.Entities.cs @@ -516,6 +516,26 @@ private void writeHatch(Hatch hatch) //TODO: Implement HatchGradientPattern } + private void writeHatchBoundaryAngles(double startAngle, double endAngle) + { + double start = MathHelper.RadToDeg(startAngle, normalize: true, absolute: false); + double end = MathHelper.RadToDeg(endAngle, normalize: true, absolute: false); + + if (MathHelper.IsEqual(Math.Abs(endAngle - startAngle), MathHelper.TwoPI)) + { + // Keep a full sweep distinct without writing angles outside -360 to 360. + double sweep = endAngle > startAngle ? 360.0 : -360.0; + if (Math.Abs(start + sweep) > 360.0) + { + start -= sweep; + } + end = start + sweep; + } + + this._writer.Write(50, start); + this._writer.Write(51, end); + } + private void writeHatchBoundaryPathEdge(Hatch.BoundaryPath.Edge edge) { if (edge is not Hatch.BoundaryPath.Polyline) @@ -528,16 +548,14 @@ private void writeHatchBoundaryPathEdge(Hatch.BoundaryPath.Edge edge) case Hatch.BoundaryPath.Arc arc: this._writer.Write(10, arc.Center); this._writer.Write(40, arc.Radius); - this._writer.Write(50, MathHelper.RadToDeg(arc.StartAngle, normalize: false)); - this._writer.Write(51, MathHelper.RadToDeg(arc.EndAngle, normalize: false)); + this.writeHatchBoundaryAngles(arc.StartAngle, arc.EndAngle); this._writer.Write(73, arc.CounterClockWise ? (short)1 : (short)0); break; case Hatch.BoundaryPath.Ellipse ellipse: this._writer.Write(10, ellipse.Center); this._writer.Write(11, ellipse.MajorAxisEndPoint); this._writer.Write(40, ellipse.RadiusRatio); - this._writer.Write(50, MathHelper.RadToDeg(ellipse.StartAngle, normalize: false)); - this._writer.Write(51, MathHelper.RadToDeg(ellipse.EndAngle, normalize: false)); + this.writeHatchBoundaryAngles(ellipse.StartAngle, ellipse.EndAngle); this._writer.Write(73, ellipse.CounterClockWise ? (short)1 : (short)0); break; case Hatch.BoundaryPath.Line line: