diff --git a/src/IxMilia.Converters.Test/DxfToSvgTests.cs b/src/IxMilia.Converters.Test/DxfToSvgTests.cs index 0bbce14..9c13cb9 100644 --- a/src/IxMilia.Converters.Test/DxfToSvgTests.cs +++ b/src/IxMilia.Converters.Test/DxfToSvgTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Threading.Tasks; using System.Xml.Linq; using IxMilia.Dxf; @@ -170,6 +171,45 @@ public void RenderEllipseTest() AssertXElement(expected, actual); } + [Fact] + public void RenderEllipse2Test() + { + var ellipse = new DxfEllipse(new DxfPoint(1.0, 2.0, 3.0), new DxfVector(1.0, 0.0, 0.0), 0.5) + { + StartParameter = 0.0, + EndParameter = Math.PI / 2.0 // 90 degrees + }; + var path = ellipse.GetSvgPath2(); + Assert.Equal(10, path.Segments.Count); + + var move = (SvgMoveToPath)path.Segments[0]; + AssertClose(2.0, move.LocationX); + AssertClose(2.0, move.LocationY); + + var arcSegment = (SvgEllipseLineToPath)path.Segments.Last(); + AssertClose(1.0, arcSegment.LocationX); + AssertClose(2.5, arcSegment.LocationY); + //Assert.Equal(1.0, arcSegment.RadiusX); -> not available any more + //Assert.Equal(0.5, arcSegment.RadiusY); -> not available any more + //Assert.Equal(0.0, arcSegment.XAxisRotation); -> not available any more + //Assert.False(arcSegment.IsLargeArc); -> not available any more + //Assert.True(arcSegment.IsCounterClockwiseSweep); -> not available any more + + var expected = new XElement(DxfToSvgConverter.Xmlns + "path", + new XAttribute("d", path.ToString()), + new XAttribute("fill-opacity", "0"), + new XAttribute("stroke-width", "1.0px"), + new XAttribute("vector-effect", "non-scaling-stroke")); + var actual = ellipse.ToXElement2(); + + AssertXElement(expected, actual); + + //comment in to see the result + //now it's just an approximation with a resolution of 10 degrees + //expected.SetAttributeValue("stroke", "red"); + //expected.AssertExpected(actual, MethodBase.GetCurrentMethod().Name); + } + [Fact] public void EllipsePathOf180DegreesTest() { diff --git a/src/IxMilia.Converters.Test/MathExtensionTests.cs b/src/IxMilia.Converters.Test/MathExtensionTests.cs new file mode 100644 index 0000000..b51e91f --- /dev/null +++ b/src/IxMilia.Converters.Test/MathExtensionTests.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace IxMilia.Converters.Test +{ + public class MathExtensionTests + { + [Theory] + [MemberData(nameof(CalcXAxisRotation_TestData))] + public void CalcXAxisRotation_Test(double majorAxisX, double majorAxisY, double expected_radian, double expected_degree) + { + double radian = 0; + double degree = 0; + MathExtensions.CalcXAxisRotation(majorAxisX, majorAxisY, out radian); + + Assert.Equal(expected_radian, radian); + Assert.Equal(expected_degree, radian.ToDegree()); + + + + } + + public static IEnumerable CalcXAxisRotation_TestData => GetCalcXAxisRotation_TestData(); + + private static IEnumerable GetCalcXAxisRotation_TestData() + { + + yield return new object[] { 1, 0, 0, 0 }; + yield return new object[] { 1, 1, Math.PI / 4, 45 }; + yield return new object[] { 0, 1, 2 * Math.PI / 4, 90 }; + yield return new object[] { -1, 1, 3 * Math.PI / 4, 135 }; + yield return new object[] { -1, 0, Math.PI, 180 }; + yield return new object[] { -1, -1, 5 * Math.PI / 4, 225 }; + yield return new object[] { 0, -1, 6 * Math.PI / 4, 270 }; + yield return new object[] { 1, -1, 7 * Math.PI / 4, 315 }; + } + } +} diff --git a/src/IxMilia.Converters.Test/TestExtentions.cs b/src/IxMilia.Converters.Test/TestExtentions.cs new file mode 100644 index 0000000..b1a5645 --- /dev/null +++ b/src/IxMilia.Converters.Test/TestExtentions.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Linq; +using Xunit; + +namespace IxMilia.Converters.Test +{ + internal static class TestExtentions + { + public static void WriteToFile(this XElement xElement, string nameOfTest, string? alternativeFilename = null) + { + + var svgXml = new XElement(DxfToSvgConverter.Xmlns + "svg" + //new XAttribute("style", "background: black;") + //new XAttribute("width", "10"), + //new XAttribute("height", "10") + ); + + var svgG = new XElement(DxfToSvgConverter.Xmlns + "g", + new XAttribute("transform", " scale(1)") + ); + + svgG.Add(xElement); + + svgXml.Add(svgG); + + using (MemoryStream stream = new MemoryStream()) + { + svgXml.SaveTo(stream); + + // Optionally, you can convert the MemoryStream to a byte array or perform other actions. + byte[] svgBytes = stream.ToArray(); + string path = Assembly.GetExecutingAssembly().Location; + path = Path.Combine(Path.GetDirectoryName(path), "UnitTests", $"{nameOfTest}"); + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + + alternativeFilename = alternativeFilename != null ? $"{alternativeFilename}_" : ""; + + string filePath = @$"{Path.Combine(path, nameOfTest)}_{alternativeFilename}.svg"; + File.WriteAllBytes(filePath, svgBytes); + } + } + + + + + public static void RemoveAttribute(this XElement element, string attributeName) + { + // Find and remove the attribute by name + XAttribute attributeToRemove = element.Attribute(attributeName); + attributeToRemove?.Remove(); + } + public static void AssertExpected(this XElement expected, XElement actual, string nameOfTest, string? alternativeFilename = null) + { + var g = new XElement(DxfToSvgConverter.Xmlns + "g"); + + actual.SetAttributeValue("id", "actual"); + g.Add(actual); + + expected.SetAttributeValue("id", "expected"); + expected.SetAttributeValue("stroke", "green"); + + g.Add(expected); + + g.WriteToFile(nameOfTest, alternativeFilename); + + var expectedVal = expected.Attributes().ToList().Where(s => s.Name == "d").FirstOrDefault().Value; + var actualVal = actual.Attributes().ToList().Where(s => s.Name == "d").FirstOrDefault().Value; + Assert.Equal(expectedVal, actualVal); + } + } +} diff --git a/src/IxMilia.Converters/DxfExtensions.cs b/src/IxMilia.Converters/DxfExtensions.cs index 1a51fef..018ad5b 100644 --- a/src/IxMilia.Converters/DxfExtensions.cs +++ b/src/IxMilia.Converters/DxfExtensions.cs @@ -1,6 +1,6 @@ using System; -using System.Collections.Generic; using System.Linq; +using System.Xml.Linq; using IxMilia.Dwg; using IxMilia.Dwg.Objects; using IxMilia.Dxf; @@ -11,7 +11,7 @@ namespace IxMilia.Converters public static class DxfExtensions { public const double DegreesToRadians = Math.PI / 180.0; - + public static DxfPoint GetPointFromAngle(this DxfCircle circle, double angleInDegrees) { var angleInRadians = angleInDegrees * DegreesToRadians; @@ -241,5 +241,60 @@ public static DrawingUnits ToDrawingUnits(this DxfDrawingUnits drawingUnits) throw new ArgumentOutOfRangeException(nameof(drawingUnits)); } } + + public static XElement? GetPattern(this DxfHatch arc, string patternId) + { + if (arc.IsPatternDoubled) + { + throw new NotImplementedException(); + } + if (arc.PatternType != DxfHatchPatternType.Predefined) + { + throw new NotImplementedException(); + + } + if (arc.PatternDefinitionLines.Count != 1) + { + //throw new NotImplementedException(); + return null; + } + else + { + + + + + + string colorString = string.Empty; + if (arc.Color.IsIndex) + { + colorString = arc.Color.ToRGBString(); + + } + + var lineDistance = (0.1 * arc.PatternScale * 2).ToDisplayString(); + // Create pattern element + XElement patternElement = new XElement(DxfToSvgConverter.Xmlns + "pattern", + new XAttribute("id", $"{patternId}"), + new XAttribute("patternUnits", "userSpaceOnUse"), + new XAttribute("width", $"{lineDistance}"), + new XAttribute("height", $"{lineDistance}"), + new XAttribute("patternTransform", $"rotate({(arc.PatternDefinitionLines.First().Angle * -1).ToDisplayString()})")); + + // Create line element + XElement lineElement = new XElement(DxfToSvgConverter.Xmlns + "line", + new XAttribute("x1", "0"), + new XAttribute("y1", "0"), + new XAttribute("x2", "0"), + new XAttribute("y2", $"{lineDistance}"), + new XAttribute("stroke", $"{colorString}"), + new XAttribute("stroke-width", "1") + ); + // Append rect elements to pattern element + patternElement.Add(lineElement); + return patternElement; + } + + } } } diff --git a/src/IxMilia.Converters/DxfToSvgConverter.cs b/src/IxMilia.Converters/DxfToSvgConverter.cs index 165f37a..a0d02fd 100644 --- a/src/IxMilia.Converters/DxfToSvgConverter.cs +++ b/src/IxMilia.Converters/DxfToSvgConverter.cs @@ -9,6 +9,7 @@ using System.Xml.Linq; using IxMilia.Dxf; using IxMilia.Dxf.Entities; +using static IxMilia.Dxf.Entities.DxfHatch; namespace IxMilia.Converters { @@ -299,7 +300,7 @@ public static async Task ToXElement(this DxfEntity entity, DxfToSvgCon case DxfDimensionBase dim: return dim.ToXElement(dimStyles, drawingUnits, unitFormat, unitPrecision); case DxfEllipse ellipse: - return ellipse.ToXElement(); + return ellipse.ToXElement2(); case DxfImage image: return await image.ToXElement(options); case DxfLine line: @@ -316,6 +317,8 @@ public static async Task ToXElement(this DxfEntity entity, DxfToSvgCon return spline.ToXElement(); case DxfText text: return text.ToXElement(); + case DxfHatch hatch: + return hatch.ToXElement(); default: return null; } @@ -345,6 +348,56 @@ public static XElement ToXElement(this DxfCircle circle) .AddVectorEffect(); } + static int randomId = 1; + public static XElement ToXElement(this DxfHatch arc, bool drawStroke = false) + { + List hatchElements = new List(); + + var hatches = new XElement(DxfToSvgConverter.Xmlns + "g"); + foreach (var boundaryPath in arc.BoundaryPaths) + { + switch (boundaryPath) + { + case NonPolylineBoundaryPath nonPolylineBoundaryPath: + + var paths = nonPolylineBoundaryPath.Edges.GetSvgPath(); + string patternId = $"{arc.PatternName.ToUpper()}_{randomId}"; + var patternElement = arc.GetPattern(patternId); + + if (patternElement is not null) + { + + var boundary = new XElement(DxfToSvgConverter.Xmlns + "path", + new XAttribute("d", paths.ToString())); + boundary.Add(new XAttribute("fill-opacity", 1)); + + if (!drawStroke) + { + boundary.Add(new XAttribute("stroke-width", 0)); + } + boundary.Add(new XAttribute("fill", $"url(#{patternId})")); + hatchElements.Add(patternElement); + hatchElements.Add(boundary); + randomId++; + } + break; + + case PolylineBoundaryPath polylineBoundaryPath: + throw new NotImplementedException(); + break; + + default: + throw new NotImplementedException(); + + break; + } + + } + hatches.Add(hatchElements); + + return hatches; + } + public static XElement ToXElement(this DxfDimensionBase dim, Dictionary dimStyles, DxfDrawingUnits drawingUnits, DxfUnitFormat unitFormat, int unitPrecision) { if (!dimStyles.TryGetValue(dim.DimensionStyleName, out var dimStyle)) @@ -473,6 +526,20 @@ public static XElement ToXElement(this DxfEllipse ellipse) .AddVectorEffect(); } + public static XElement ToXElement2(this DxfEllipse ellipse, int angleResolutionInDegree = 10) + { + var path = ellipse.GetSvgPath2(angleResolutionInDegree); + + XElement baseShape = new XElement(DxfToSvgConverter.Xmlns + "path", + new XAttribute("d", path.ToString())); + + baseShape.Add(new XAttribute("fill-opacity", 0)); + return baseShape + .AddStroke(ellipse.Color) + .AddStrokeWidth(0) + .AddVectorEffect(); + } + public static async Task ToXElement(this DxfImage image, DxfToSvgConverterOptions options) { var imageHref = await options.ResolveImageHrefAsync(image.ImageDefinition.FilePath); @@ -649,6 +716,15 @@ internal static SvgPath GetSvgPath(this DxfEllipse ellipse) { return SvgPath.FromEllipse(ellipse.Center.X, ellipse.Center.Y, ellipse.MajorAxis.X, ellipse.MajorAxis.Y, ellipse.MinorAxisRatio, ellipse.StartParameter, ellipse.EndParameter); } + internal static SvgPath GetSvgPath2(this DxfEllipse ellipse, int angleResolutionInDegree = 10) + { + return SvgPath.FromEllipse2(ellipse.Center.X, ellipse.Center.Y, ellipse.MajorAxis.X, ellipse.MajorAxis.Y, ellipse.MinorAxisRatio, ellipse.StartParameter, ellipse.EndParameter,angleResolutionInDegree: angleResolutionInDegree); + } + + internal static SvgPath GetSvgPath(this IList edges) + { + return SvgPath.FromHatch(edges); + } internal static SvgPath GetSvgPath(this DxfLwPolyline poly) { @@ -710,6 +786,20 @@ internal static SvgPath GetSvgPath(this IList beziers) return new SvgPath(segments); } + public static SvgEllipseLineToPath TransformAngle(this SvgEllipseLineToPath pOriginal, double centerX, double centerY, double alpha) + { + var corrLocationX = pOriginal.LocationX - centerX; + var corrLocationY = pOriginal.LocationY - centerY; + + // X-Koordinate + double transX = corrLocationX * Math.Cos(alpha) - corrLocationY * Math.Sin(alpha) + centerX; + + // Y-Koordinate + double transY = corrLocationX * Math.Sin(alpha) + corrLocationY * Math.Cos(alpha) + centerY; + + return new SvgEllipseLineToPath(pOriginal.AngleRadian, transX, transY); + } + private static XElement AddFill(this XElement element, DxfColor color) { if (color.IsIndex) diff --git a/src/IxMilia.Converters/MathExtensions.cs b/src/IxMilia.Converters/MathExtensions.cs new file mode 100644 index 0000000..eea489d --- /dev/null +++ b/src/IxMilia.Converters/MathExtensions.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Text; + + +namespace IxMilia.Converters +{ + public static class MathExtensions + { + public static double ToDegree(this double radian) + { + return radian * 180 / Math.PI; + } + + public static double ToRadian(this double degree) + { + return degree * Math.PI / 180; + } + + public static double Magnitude(double x, double y) + { + return Math.Sqrt(x * x + y * y); + } + + public static int GetQuadrant(double x, double y) + { + if (x >= 0 && y >= 0) + { + return 1; // Quadrant 1 + } + else if (x < 0 && y >= 0) + { + return 2; // Quadrant 2 + } + else if (x < 0 && y < 0) + { + return 3; // Quadrant 3 + } + else + { + return 4; // Quadrant 4 + } + } + + public static void CalcXAxisRotation(double majorAxisX, double majorAxisY, out double axisAngle, bool considerQuadrants = true) + { + int quadrant = MathExtensions.GetQuadrant(majorAxisX, majorAxisY); + axisAngle = Math.Atan2(majorAxisY, majorAxisX); + + if (considerQuadrants) + { + axisAngle = quadrant <= 2 ? axisAngle : axisAngle + Math.PI * 2; + } + } + + } +} diff --git a/src/IxMilia.Converters/SvgPath.cs b/src/IxMilia.Converters/SvgPath.cs index 4cd7a88..ebda07d 100644 --- a/src/IxMilia.Converters/SvgPath.cs +++ b/src/IxMilia.Converters/SvgPath.cs @@ -1,6 +1,8 @@ -using System; +using IxMilia.Converters; +using System; using System.Collections.Generic; using System.Linq; +using static IxMilia.Dxf.Entities.DxfHatch; namespace IxMilia.Converters { @@ -66,6 +68,138 @@ public static SvgPath FromEllipse(double centerX, double centerY, double majorAx return new SvgPath(segments); } + + public static SvgPath FromEllipse2(double centerX, double centerY, double majorAxisX, double majorAxisY, double minorAxisRatio, double startAngle, double endAngle, bool firstIsMove = true, int angleResolutionInDegree = 10) + { + while (endAngle < startAngle) + { + endAngle += Math.PI * 2.0; + } + MathExtensions.CalcXAxisRotation(majorAxisX, majorAxisY, out var axisAngle, true); + + var majorAxisLength = MathExtensions.Magnitude(majorAxisX, majorAxisY); + var minorAxisLength = majorAxisLength * minorAxisRatio; + + // calc line points + double angleResolution = ((double)angleResolutionInDegree).ToRadian(); + double angleSpan = Math.Abs(endAngle - startAngle); + int calculatedPoints = (int)Math.Ceiling(angleSpan / angleResolution); + + List points = new List(); + for (int i = 0; i < calculatedPoints; i++) + { + var angle = startAngle + i * angleResolution; + var startX = centerX + Math.Cos(angle) * majorAxisLength; + var startY = centerY + Math.Sin(angle) * minorAxisLength; + points.Add(new SvgEllipseLineToPath(angle, startX, startY)); + } + + //add end point + var endX = centerX + Math.Cos(endAngle) * majorAxisLength; + var endY = centerY + Math.Sin(endAngle) * minorAxisLength; + points.Add(new SvgEllipseLineToPath(endAngle, endX, endY)); + + + // transform points to given axis angle of the majorAxis + List transfPoints = new List(); + for (int i = 0; i < points.Count; i++) + { + transfPoints.Add(points[i].TransformAngle(centerX, centerY, axisAngle)); + } + + + // in some cases the ellipse is a part of a path + var segments = new List(); + if (firstIsMove) + { + segments.Add(new SvgMoveToPath(transfPoints.First().LocationX, transfPoints.First().LocationY)); + segments.AddRange(transfPoints.Skip(1)); + } + else + { + segments.AddRange(transfPoints); + } + + return new SvgPath(segments); + } + + public static SvgPath FromHatch(IList edges) + { + List transfPoints = new List(); + + for (int i = 0; i < edges.Count(); i++) + { + switch (edges[i]) + { + case LineBoundaryPathEdge lineBoundaryPathEdge: + // Code for LineBoundaryPathEdge case + transfPoints.Add(new SvgLineToPath(lineBoundaryPathEdge.StartPoint.X, lineBoundaryPathEdge.StartPoint.Y)); + transfPoints.Add(new SvgLineToPath(lineBoundaryPathEdge.EndPoint.X, lineBoundaryPathEdge.EndPoint.Y)); + break; + + case SplineBoundaryPathEdge splineBoundaryPathEdge: + // Code for OtherType case + foreach (var controlPoint in splineBoundaryPathEdge.ControlPoints) + { + transfPoints.Add(new SvgLineToPath(controlPoint.Point.X, controlPoint.Point.Y)); + } + + break; + + case EllipticArcBoundaryPathEdge ellipticArcBoundaryPathEdge: + // Code for OtherType case + //throw new NotImplementedException(); + var ellips = FromEllipse2(ellipticArcBoundaryPathEdge.Center.X, ellipticArcBoundaryPathEdge.Center.Y, + ellipticArcBoundaryPathEdge.MajorAxis.X, ellipticArcBoundaryPathEdge.MajorAxis.Y, + ellipticArcBoundaryPathEdge.MinorAxisRatio, + ellipticArcBoundaryPathEdge.StartAngle.ToRadian(), ellipticArcBoundaryPathEdge.EndAngle.ToRadian(), false); + + transfPoints.AddRange(ellips.Segments); + + break; + + case BoundaryPathEdgeBase boundaryPathEdgeBase: + // Code for OtherType case + // openpoint - implement case + //throw new NotImplementedException(); + break; + + default: + // Default case + // openpoint - implement case + //throw new NotImplementedException(); + + break; + } + } + + var segments = new List(); + if (transfPoints.Count > 0) + { + + if (transfPoints.First().GetType() == typeof(SvgLineToPath)) + { + var temp = (SvgLineToPath)transfPoints.First(); + segments.Add(new SvgMoveToPath(temp.LocationX, temp.LocationY)); + } + else if (transfPoints.First().GetType() == typeof(SvgEllipseLineToPath)) + { + var temp = (SvgEllipseLineToPath)transfPoints.First(); + segments.Add(new SvgMoveToPath(temp.LocationX, temp.LocationY)); + } + else + { + throw new NotImplementedException(); + } + segments.AddRange(transfPoints.Skip(1)); + } + else + { + bool stop = false; + } + + return new SvgPath(segments); + } } public abstract class SvgPathSegment @@ -116,6 +250,17 @@ public override string ToString() } } + public class SvgEllipseLineToPath : SvgLineToPath + { + public double AngleRadian { get; } + public double AngleDegree { get; } + public SvgEllipseLineToPath(double angleRadian, double locationX, double locationY) : base(locationX, locationY) + { + AngleRadian = angleRadian; + AngleDegree = angleRadian.ToDegree(); + } + } + public class SvgArcToPath : SvgPathSegment { public double RadiusX { get; }