diff --git a/src/Shared/RenderHelpers/ArcToHelper.cs b/src/Avalonia.Visuals/Media/PreciseEllipticArcHelper.cs similarity index 97% rename from src/Shared/RenderHelpers/ArcToHelper.cs rename to src/Avalonia.Visuals/Media/PreciseEllipticArcHelper.cs index 0bbf451970..5dd647e8ca 100644 --- a/src/Shared/RenderHelpers/ArcToHelper.cs +++ b/src/Avalonia.Visuals/Media/PreciseEllipticArcHelper.cs @@ -1,5 +1,6 @@ // Copyright © 2003-2004, Luc Maisonobe // 2015 - Alexey Rozanov - Adaptations for Avalonia and oval center computations +// 2022 - Alexey Rozanov - 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 { /// /// This class represents an elliptical arc on a 2D plane. @@ -292,6 +291,8 @@ namespace Avalonia.RenderHelpers /// internal double G2; + public bool DrawInOppositeDirection { get; set; } + /// /// Builds an elliptical arc composed of the full unit circle around (0,0) /// @@ -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) /// /// A StreamGeometryContext to output the path commands to - public void BuildArc(IStreamGeometryContextImpl path) + public void BuildArc(StreamGeometryContext path) { BuildArc(path, _maxDegree, _defaultFlatness, true); } @@ -862,7 +863,7 @@ namespace Avalonia.RenderHelpers /// degree of the Bezier curve to use /// acceptable error /// if true, a new figure will be started in the specified StreamGeometryContext - 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 /// Ellipse theta (angle measured from the abscissa) /// Large Arc Indicator /// Clockwise direction flag - 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); } diff --git a/src/Avalonia.Visuals/Media/StreamGeometryContext.cs b/src/Avalonia.Visuals/Media/StreamGeometryContext.cs index 0bfd774c79..88aba8365e 100644 --- a/src/Avalonia.Visuals/Media/StreamGeometryContext.cs +++ b/src/Avalonia.Visuals/Media/StreamGeometryContext.cs @@ -15,6 +15,8 @@ namespace Avalonia.Media { private readonly IStreamGeometryContextImpl _impl; + private Point _currentPoint; + /// /// Initializes a new instance of the class. /// @@ -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; + } + + + /// + /// 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. + /// + /// The destination point. + /// The radii of an oval whose perimeter is used to draw the angle. + /// The rotation angle of the oval that specifies the curve. + /// true to draw the arc greater than 180 degrees; otherwise, false. + /// + /// A value that indicates whether the arc is drawn in the Clockwise or Counterclockwise direction. + /// + public void PreciseArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) + { + PreciseEllipticArcHelper.ArcTo(this, _currentPoint, point, size, rotationAngle, isLargeArc, sweepDirection); } /// @@ -57,6 +77,7 @@ namespace Avalonia.Media public void BeginFigure(Point startPoint, bool isFilled) { _impl.BeginFigure(startPoint, isFilled); + _currentPoint = startPoint; } /// @@ -68,6 +89,7 @@ namespace Avalonia.Media public void CubicBezierTo(Point point1, Point point2, Point point3) { _impl.CubicBezierTo(point1, point2, point3); + _currentPoint = point3; } /// @@ -78,6 +100,7 @@ namespace Avalonia.Media public void QuadraticBezierTo(Point control, Point endPoint) { _impl.QuadraticBezierTo(control, endPoint); + _currentPoint = endPoint; } /// @@ -87,6 +110,7 @@ namespace Avalonia.Media public void LineTo(Point point) { _impl.LineTo(point); + _currentPoint = point; } /// diff --git a/src/Shared/RenderHelpers/RenderHelpers.projitems b/src/Shared/RenderHelpers/RenderHelpers.projitems index c088097a9f..4c80ec50c4 100644 --- a/src/Shared/RenderHelpers/RenderHelpers.projitems +++ b/src/Shared/RenderHelpers/RenderHelpers.projitems @@ -9,7 +9,6 @@ Avalonia.RenderHelpers - \ No newline at end of file diff --git a/tests/Avalonia.RenderTests/Media/StreamGeometryTests.cs b/tests/Avalonia.RenderTests/Media/StreamGeometryTests.cs new file mode 100644 index 0000000000..fc9cbf6a7f --- /dev/null +++ b/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); + } + } +} diff --git a/tests/TestFiles/Direct2D1/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.deferred.expected.png b/tests/TestFiles/Direct2D1/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.deferred.expected.png new file mode 100644 index 0000000000..825b1d7ea7 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.deferred.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.immediate.expected.png b/tests/TestFiles/Direct2D1/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.immediate.expected.png new file mode 100644 index 0000000000..825b1d7ea7 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.immediate.expected.png differ diff --git a/tests/TestFiles/Skia/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.deferred.expected.png b/tests/TestFiles/Skia/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.deferred.expected.png new file mode 100644 index 0000000000..f7cc90678a Binary files /dev/null and b/tests/TestFiles/Skia/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.deferred.expected.png differ diff --git a/tests/TestFiles/Skia/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.immediate.expected.png b/tests/TestFiles/Skia/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.immediate.expected.png new file mode 100644 index 0000000000..f7cc90678a Binary files /dev/null and b/tests/TestFiles/Skia/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.immediate.expected.png differ