Browse Source

Merge pull request #7395 from hacklex/feature/PreciseArcTo

Fixed and exposed PreciseArcTo for ellipses with extreme width:height ratios
Dan Walmsley 3 years ago
parent
commit
fe4197ecd2

+ 32 - 13
src/Shared/RenderHelpers/ArcToHelper.cs → src/Avalonia.Visuals/Media/PreciseEllipticArcHelper.cs

@@ -1,5 +1,6 @@
 // Copyright © 2003-2004, Luc Maisonobe
 // 2015 - Alexey Rozanov <[email protected]> - Adaptations for Avalonia and oval center computations
+// 2022 - Alexey Rozanov <[email protected]> - Fix for arcs sometimes drawn in inverted order.
 // All rights reserved.
 // 
 // Redistribution and use in source and binary forms, with
@@ -49,12 +50,10 @@
 // Adapted from http://www.spaceroots.org/documents/ellipse/EllipticalArc.java
 
 using System;
-using Avalonia.Media;
-using Avalonia.Platform;
 
-namespace Avalonia.RenderHelpers
+namespace Avalonia.Media
 {
-    static class ArcToHelper
+    static class PreciseEllipticArcHelper
     {
         /// <summary>
         /// This class represents an elliptical arc on a 2D plane.
@@ -292,6 +291,8 @@ namespace Avalonia.RenderHelpers
             /// </summary>
             internal double G2;
 
+            public bool DrawInOppositeDirection { get; set; }
+
             /// <summary>
             /// Builds an elliptical arc composed of the full unit circle around (0,0)
             /// </summary>
@@ -850,7 +851,7 @@ namespace Avalonia.RenderHelpers
             /// Builds the arc outline using given StreamGeometryContext and default (max) Bezier curve degree and acceptable error of half a pixel (0.5)
             /// </summary>
             /// <param name="path">A StreamGeometryContext to output the path commands to</param>
-            public void BuildArc(IStreamGeometryContextImpl path)
+            public void BuildArc(StreamGeometryContext path)
             {
                 BuildArc(path, _maxDegree, _defaultFlatness, true);
             }
@@ -862,7 +863,7 @@ namespace Avalonia.RenderHelpers
             /// <param name="degree">degree of the Bezier curve to use</param>
             /// <param name="threshold">acceptable error</param>
             /// <param name="openNewFigure">if true, a new figure will be started in the specified StreamGeometryContext</param>
-            public void BuildArc(IStreamGeometryContextImpl path, int degree, double threshold, bool openNewFigure)
+            public void BuildArc(StreamGeometryContext path, int degree, double threshold, bool openNewFigure)
             {
                 if (degree < 1 || degree > _maxDegree)
                     throw new ArgumentException($"degree should be between {1} and {_maxDegree}", nameof(degree));
@@ -888,8 +889,18 @@ namespace Avalonia.RenderHelpers
                     }
                     n = n << 1;
                 }
-                dEta = (Eta2 - Eta1) / n;
-                etaB = Eta1;
+                if (!DrawInOppositeDirection)
+                {
+                    dEta = (Eta2 - Eta1) / n;
+                    etaB = Eta1;
+                }
+                else
+                {
+                    dEta = (Eta1 - Eta2) / n;
+                    etaB = Eta2;
+                }
+
+
                 double cosEtaB = Math.Cos(etaB);
                 double sinEtaB = Math.Sin(etaB);
                 double aCosEtaB = A * cosEtaB;
@@ -922,6 +933,7 @@ namespace Avalonia.RenderHelpers
                 */
 
                 //otherwise we're supposed to be already at the (xB,yB)
+                 
 
                 double t = Math.Tan(0.5 * dEta);
                 double alpha = Math.Sin(dEta) * (Math.Sqrt(4 + 3 * t * t) - 1) / 3;
@@ -1012,7 +1024,7 @@ namespace Avalonia.RenderHelpers
             /// <param name="theta">Ellipse theta (angle measured from the abscissa)</param>
             /// <param name="isLargeArc">Large Arc Indicator</param>
             /// <param name="clockwise">Clockwise direction flag</param>
-            public static void BuildArc(IStreamGeometryContextImpl path, Point p1, Point p2, Size size, double theta, bool isLargeArc, bool clockwise)
+            public static void BuildArc(StreamGeometryContext path, Point p1, Point p2, Size size, double theta, bool isLargeArc, bool clockwise)
             {
 
                 // var orthogonalizer = new RotateTransform(-theta);
@@ -1058,7 +1070,7 @@ namespace Avalonia.RenderHelpers
 
                 }
 
-                double multiplier = Math.Sqrt(numerator / denominator);
+                double multiplier = Math.Sqrt(Math.Abs(numerator / denominator));
                 Point mulVec = new Point(rx * p1S.Y / ry, -ry * p1S.X / rx);
 
                 int sign = (clockwise != isLargeArc) ? 1 : -1;
@@ -1104,9 +1116,16 @@ namespace Avalonia.RenderHelpers
                 // path.LineTo(c, true, true);
                 // path.LineTo(clockwise ? p1 : p2, true,true);
 
-                path.LineTo(clockwise ? p1 : p2);
                 var arc = new EllipticalArc(c.X, c.Y, rx, ry, theta, thetaStart, thetaEnd, false);
+
+                double ManhattanDistance(Point p1, Point p2) => Math.Abs(p1.X - p2.X) + Math.Abs(p1.Y - p2.Y);
+                if (ManhattanDistance(p2, new Point(arc.X2, arc.Y2)) > ManhattanDistance(p2, new Point(arc.X1, arc.Y1)))
+                {
+                    arc.DrawInOppositeDirection = true;
+                }
+
                 arc.BuildArc(path, arc._maxDegree, arc._defaultFlatness, false);
+                //path.LineTo(p2);
 
                 //uncomment this to draw a pie
                 //path.LineTo(c, true, true);
@@ -1136,9 +1155,9 @@ namespace Avalonia.RenderHelpers
             }
         }
 
-        public static void ArcTo(IStreamGeometryContextImpl streamGeometryContextImpl, Point currentPoint, Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection)
+        public static void ArcTo(StreamGeometryContext streamGeometryContextImpl, Point currentPoint, Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection)
         {
-            EllipticalArc.BuildArc(streamGeometryContextImpl, currentPoint, point, size, rotationAngle*Math.PI/180,
+            EllipticalArc.BuildArc(streamGeometryContextImpl, currentPoint, point, size, rotationAngle*(Math.PI/180),
                 isLargeArc,
                 sweepDirection == SweepDirection.Clockwise);
         }

+ 24 - 0
src/Avalonia.Visuals/Media/StreamGeometryContext.cs

@@ -15,6 +15,8 @@ namespace Avalonia.Media
     {
         private readonly IStreamGeometryContextImpl _impl;
 
+        private Point _currentPoint;
+
         /// <summary>
         /// Initializes a new instance of the <see cref="StreamGeometryContext"/> class.
         /// </summary>
@@ -47,6 +49,24 @@ namespace Avalonia.Media
         public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection)
         {
             _impl.ArcTo(point, size, rotationAngle, isLargeArc, sweepDirection);
+            _currentPoint = point;
+        }
+
+
+        /// <summary>
+        /// Draws an arc to the specified point using polylines, quadratic or cubic Bezier curves
+        /// Significantly more precise when drawing elliptic arcs with extreme width:height ratios.        
+        /// </summary>         
+        /// <param name="point">The destination point.</param>
+        /// <param name="size">The radii of an oval whose perimeter is used to draw the angle.</param>
+        /// <param name="rotationAngle">The rotation angle of the oval that specifies the curve.</param>
+        /// <param name="isLargeArc">true to draw the arc greater than 180 degrees; otherwise, false.</param>
+        /// <param name="sweepDirection">
+        /// A value that indicates whether the arc is drawn in the Clockwise or Counterclockwise direction.
+        /// </param>
+        public void PreciseArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection)
+        {
+            PreciseEllipticArcHelper.ArcTo(this, _currentPoint, point, size, rotationAngle, isLargeArc, sweepDirection);
         }
 
         /// <summary>
@@ -57,6 +77,7 @@ namespace Avalonia.Media
         public void BeginFigure(Point startPoint, bool isFilled)
         {
             _impl.BeginFigure(startPoint, isFilled);
+            _currentPoint = startPoint;
         }
 
         /// <summary>
@@ -68,6 +89,7 @@ namespace Avalonia.Media
         public void CubicBezierTo(Point point1, Point point2, Point point3)
         {
             _impl.CubicBezierTo(point1, point2, point3);
+            _currentPoint = point3;
         }
 
         /// <summary>
@@ -78,6 +100,7 @@ namespace Avalonia.Media
         public void QuadraticBezierTo(Point control, Point endPoint)
         {
             _impl.QuadraticBezierTo(control, endPoint);
+            _currentPoint = endPoint;
         }
 
         /// <summary>
@@ -87,6 +110,7 @@ namespace Avalonia.Media
         public void LineTo(Point point)
         {
             _impl.LineTo(point);
+            _currentPoint = point;
         }
 
         /// <summary>

+ 0 - 1
src/Shared/RenderHelpers/RenderHelpers.projitems

@@ -9,7 +9,6 @@
     <Import_RootNamespace>Avalonia.RenderHelpers</Import_RootNamespace>
   </PropertyGroup>
   <ItemGroup>
-    <Compile Include="$(MSBuildThisFileDirectory)ArcToHelper.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)QuadBezierHelper.cs" />
   </ItemGroup>
 </Project>

+ 56 - 0
tests/Avalonia.RenderTests/Media/StreamGeometryTests.cs

@@ -0,0 +1,56 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Avalonia.Controls;
+using Avalonia.Controls.Shapes;
+using Avalonia.Media;
+using Avalonia.Media.Imaging;
+using Xunit;
+
+#if AVALONIA_SKIA
+namespace Avalonia.Skia.RenderTests
+#else
+namespace Avalonia.Direct2D1.RenderTests.Media
+#endif
+{
+    public class StreamGeometryTests : TestBase
+    {
+        public StreamGeometryTests()
+            : base(@"Media\StreamGeometry")
+        {
+        }
+         
+        [Fact]
+        public async Task PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions()
+        {
+            var grid = new Avalonia.Controls.Primitives.UniformGrid() { Columns = 2, Rows = 4, Width = 320, Height = 400 };
+            foreach (var sweepDirection in new[] { SweepDirection.Clockwise, SweepDirection.CounterClockwise })
+                foreach (var isLargeArc in new[] { false, true })
+                    foreach (var isPrecise in new[] { false, true })
+                    {
+                        Point Pt(double x, double y) => new Point(x, y);
+                        Size Sz(double w, double h) => new Size(w, h);
+                        var streamGeometry = new StreamGeometry();
+                        using (var context = streamGeometry.Open())
+                        {
+                            context.BeginFigure(Pt(20, 20), true);
+
+                            if(isPrecise)
+                                context.PreciseArcTo(Pt(40, 40), Sz(20, 20), 0, isLargeArc, sweepDirection);
+                            else
+                                context.ArcTo(Pt(40, 40), Sz(20, 20), 0, isLargeArc, sweepDirection);
+                            context.LineTo(Pt(40, 20));
+                            context.LineTo(Pt(20, 20));
+                            context.EndFigure(true);
+                        }
+                        var pathShape = new Avalonia.Controls.Shapes.Path();
+                        pathShape.Data = streamGeometry;
+                        pathShape.Stroke = new SolidColorBrush(Colors.CornflowerBlue);
+                        pathShape.Fill = new SolidColorBrush(Colors.Gold);
+                        pathShape.StrokeThickness = 2;
+                        pathShape.Margin = new Thickness(20);
+                        grid.Children.Add(pathShape);
+                    }
+            await RenderToFile(grid);
+        }
+    }
+}

BIN
tests/TestFiles/Direct2D1/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.deferred.expected.png


BIN
tests/TestFiles/Direct2D1/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.immediate.expected.png


BIN
tests/TestFiles/Skia/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.deferred.expected.png


BIN
tests/TestFiles/Skia/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.immediate.expected.png