Browse Source

Merge pull request #7395 from hacklex/feature/PreciseArcTo

Fixed and exposed PreciseArcTo for ellipses with extreme width:height ratios
pull/7405/head
Dan Walmsley 4 years ago
committed by GitHub
parent
commit
fe4197ecd2
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 45
      src/Avalonia.Visuals/Media/PreciseEllipticArcHelper.cs
  2. 24
      src/Avalonia.Visuals/Media/StreamGeometryContext.cs
  3. 1
      src/Shared/RenderHelpers/RenderHelpers.projitems
  4. 56
      tests/Avalonia.RenderTests/Media/StreamGeometryTests.cs
  5. BIN
      tests/TestFiles/Direct2D1/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.deferred.expected.png
  6. BIN
      tests/TestFiles/Direct2D1/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.immediate.expected.png
  7. BIN
      tests/TestFiles/Skia/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.deferred.expected.png
  8. BIN
      tests/TestFiles/Skia/Media/StreamGeometry/PreciseEllipticArc_Produces_Valid_Arcs_In_All_Directions.immediate.expected.png

45
src/Shared/RenderHelpers/ArcToHelper.cs → src/Avalonia.Visuals/Media/PreciseEllipticArcHelper.cs

@ -1,5 +1,6 @@
// Copyright © 2003-2004, Luc Maisonobe
// 2015 - Alexey Rozanov <thehdotx@gmail.com> - Adaptations for Avalonia and oval center computations
// 2022 - Alexey Rozanov <thehdotx@gmail.com> - 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
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>

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
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Loading…
Cancel
Save