Browse Source

Merge pull request #1661 from Gillibald/fixes/PathMarkupParser

PathMarkupParser rewrite
pull/1673/merge
Steven Kirk 8 years ago
committed by GitHub
parent
commit
0a10a7180a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 21
      src/Avalonia.Visuals/Media/PathGeometry.cs
  2. 789
      src/Avalonia.Visuals/Media/PathMarkupParser.cs
  3. 11
      src/Avalonia.Visuals/Media/StreamGeometry.cs
  4. 2
      src/Avalonia.Visuals/Media/StreamGeometryContext.cs
  5. 66
      src/Avalonia.Visuals/Platform/IGeometryContext.cs
  6. 54
      src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs
  7. 85
      src/Avalonia.Visuals/Platform/PathGeometryContext.cs
  8. 5
      tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
  9. 45
      tests/Avalonia.Benchmarks/Visuals/Media/PathMarkupParserTests.cs
  10. 142
      tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs

21
src/Avalonia.Visuals/Media/PathGeometry.cs

@ -5,6 +5,7 @@ using System;
using Avalonia.Collections;
using Avalonia.Metadata;
using Avalonia.Platform;
using Avalonia.Visuals.Platform;
namespace Avalonia.Media
{
@ -28,7 +29,7 @@ namespace Avalonia.Media
static PathGeometry()
{
FiguresProperty.Changed.AddClassHandler<PathGeometry>((s, e) =>
FiguresProperty.Changed.AddClassHandler<PathGeometry>((s, e) =>
s.OnFiguresChanged(e.NewValue as PathFigures));
}
@ -40,6 +41,24 @@ namespace Avalonia.Media
Figures = new PathFigures();
}
/// <summary>
/// Parses the specified path data to a <see cref="PathGeometry"/>.
/// </summary>
/// <param name="pathData">The s.</param>
/// <returns></returns>
public static new PathGeometry Parse(string pathData)
{
var pathGeometry = new PathGeometry();
using (var context = new PathGeometryContext(pathGeometry))
using (var parser = new PathMarkupParser(context))
{
parser.Parse(pathData);
}
return pathGeometry;
}
/// <summary>
/// Gets or sets the figures.
/// </summary>

789
src/Avalonia.Visuals/Media/PathMarkupParser.cs

@ -5,50 +5,61 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Avalonia.Platform;
namespace Avalonia.Media
{
/// <summary>
/// Parses a path markup string.
/// </summary>
public class PathMarkupParser
public class PathMarkupParser : IDisposable
{
private static readonly Dictionary<char, Command> Commands = new Dictionary<char, Command>
{
{ 'F', Command.FillRule },
{ 'M', Command.Move },
{ 'L', Command.Line },
{ 'H', Command.HorizontalLine },
{ 'V', Command.VerticalLine },
{ 'Q', Command.QuadraticBezierCurve },
{ 'T', Command.SmoothQuadraticBezierCurve },
{ 'C', Command.CubicBezierCurve },
{ 'S', Command.SmoothCubicBezierCurve },
{ 'A', Command.Arc },
{ 'Z', Command.Close },
};
private static readonly Dictionary<char, FillRule> FillRules = new Dictionary<char, FillRule>
private static readonly string s_separatorPattern;
private static readonly Dictionary<char, Command> s_commands =
new Dictionary<char, Command>
{
{ 'F', Command.FillRule },
{ 'M', Command.Move },
{ 'L', Command.Line },
{ 'H', Command.HorizontalLine },
{ 'V', Command.VerticalLine },
{ 'Q', Command.QuadraticBezierCurve },
{ 'T', Command.SmoothQuadraticBezierCurve },
{ 'C', Command.CubicBezierCurve },
{ 'S', Command.SmoothCubicBezierCurve },
{ 'A', Command.Arc },
{ 'Z', Command.Close },
};
private IGeometryContext _geometryContext;
private Point _currentPoint;
private Point? _previousControlPoint;
private bool? _isOpen;
private bool _isDisposed;
static PathMarkupParser()
{
{'0', FillRule.EvenOdd },
{'1', FillRule.NonZero }
};
private readonly StreamGeometryContext _context;
s_separatorPattern = CreatesSeparatorPattern();
}
/// <summary>
/// Initializes a new instance of the <see cref="PathMarkupParser"/> class.
/// </summary>
/// <param name="context">The context for the geometry.</param>
public PathMarkupParser(StreamGeometryContext context)
/// <param name="geometryContext">The geometry context.</param>
/// <exception cref="ArgumentNullException">geometryContext</exception>
public PathMarkupParser(IGeometryContext geometryContext)
{
_context = context;
if (geometryContext == null)
{
throw new ArgumentNullException(nameof(geometryContext));
}
_geometryContext = geometryContext;
}
/// <summary>
/// Defines the command currently being processed.
/// </summary>
private enum Command
{
None,
@ -62,358 +73,568 @@ namespace Avalonia.Media
SmoothCubicBezierCurve,
SmoothQuadraticBezierCurve,
Arc,
Close,
Close
}
/// <summary>
/// Parses the specified markup string.
/// Parses the specified path data and writes the result to the geometryContext of this instance.
/// </summary>
/// <param name="s">The markup string.</param>
public void Parse(string s)
/// <param name="pathData">The path data.</param>
public void Parse(string pathData)
{
var tokens = ParseTokens(pathData);
CreateGeometry(tokens);
}
void IDisposable.Dispose()
{
Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
bool openFigure = false;
if (_isDisposed)
{
return;
}
using (StringReader reader = new StringReader(s))
if (disposing)
{
Command command = Command.None;
Point point = new Point();
bool relative = false;
Point? previousControlPoint = null;
_geometryContext = null;
}
_isDisposed = true;
}
private static string CreatesSeparatorPattern()
{
var stringBuilder = new StringBuilder();
while (ReadCommand(reader, ref command, ref relative))
foreach (var command in s_commands.Keys)
{
stringBuilder.Append(command);
stringBuilder.Append(char.ToLower(command));
}
return @"(?=[" + stringBuilder + "])";
}
private static IEnumerable<CommandToken> ParseTokens(string s)
{
return Regex.Split(s, s_separatorPattern).Where(t => !string.IsNullOrEmpty(t)).Select(CommandToken.Parse);
}
private static Point MirrorControlPoint(Point controlPoint, Point center)
{
var dir = controlPoint - center;
return center + -dir;
}
private void CreateGeometry(IEnumerable<CommandToken> commandTokens)
{
_currentPoint = new Point();
foreach (var commandToken in commandTokens)
{
try
{
switch (command)
while (true)
{
case Command.FillRule:
_context.SetFillRule(ReadFillRule(reader));
previousControlPoint = null;
break;
case Command.Move:
if (openFigure)
{
_context.EndFigure(false);
}
point = ReadPoint(reader, point, relative);
_context.BeginFigure(point, true);
openFigure = true;
previousControlPoint = null;
break;
case Command.Line:
point = ReadPoint(reader, point, relative);
_context.LineTo(point);
previousControlPoint = null;
break;
case Command.HorizontalLine:
if (!relative)
{
point = point.WithX(ReadDouble(reader));
}
else
{
point = new Point(point.X + ReadDouble(reader), point.Y);
}
_context.LineTo(point);
previousControlPoint = null;
break;
case Command.VerticalLine:
if (!relative)
{
point = point.WithY(ReadDouble(reader));
}
else
{
point = new Point(point.X, point.Y + ReadDouble(reader));
}
_context.LineTo(point);
previousControlPoint = null;
break;
case Command.QuadraticBezierCurve:
{
Point handle = ReadPoint(reader, point, relative);
previousControlPoint = handle;
ReadSeparator(reader);
point = ReadPoint(reader, point, relative);
_context.QuadraticBezierTo(handle, point);
switch (commandToken.Command)
{
case Command.None:
break;
case Command.FillRule:
SetFillRule(commandToken);
break;
case Command.Move:
AddMove(commandToken);
break;
case Command.Line:
AddLine(commandToken);
break;
case Command.HorizontalLine:
AddHorizontalLine(commandToken);
break;
case Command.VerticalLine:
AddVerticalLine(commandToken);
break;
}
case Command.SmoothQuadraticBezierCurve:
{
Point end = ReadPoint(reader, point, relative);
if(previousControlPoint != null)
previousControlPoint = MirrorControlPoint((Point)previousControlPoint, point);
_context.QuadraticBezierTo(previousControlPoint ?? point, end);
point = end;
case Command.CubicBezierCurve:
AddCubicBezierCurve(commandToken);
break;
}
case Command.CubicBezierCurve:
{
Point point1 = ReadPoint(reader, point, relative);
ReadSeparator(reader);
Point point2 = ReadPoint(reader, point, relative);
previousControlPoint = point2;
ReadSeparator(reader);
point = ReadPoint(reader, point, relative);
_context.CubicBezierTo(point1, point2, point);
case Command.QuadraticBezierCurve:
AddQuadraticBezierCurve(commandToken);
break;
}
case Command.SmoothCubicBezierCurve:
{
Point point2 = ReadPoint(reader, point, relative);
ReadSeparator(reader);
Point end = ReadPoint(reader, point, relative);
if(previousControlPoint != null)
previousControlPoint = MirrorControlPoint((Point)previousControlPoint, point);
_context.CubicBezierTo(previousControlPoint ?? point, point2, end);
previousControlPoint = point2;
point = end;
case Command.SmoothCubicBezierCurve:
AddSmoothCubicBezierCurve(commandToken);
break;
}
case Command.Arc:
{
Size size = ReadSize(reader);
ReadSeparator(reader);
double rotationAngle = ReadDouble(reader);
ReadSeparator(reader);
bool isLargeArc = ReadBool(reader);
ReadSeparator(reader);
SweepDirection sweepDirection = ReadBool(reader) ? SweepDirection.Clockwise : SweepDirection.CounterClockwise;
ReadSeparator(reader);
point = ReadPoint(reader, point, relative);
_context.ArcTo(point, size, rotationAngle, isLargeArc, sweepDirection);
previousControlPoint = null;
case Command.SmoothQuadraticBezierCurve:
AddSmoothQuadraticBezierCurve(commandToken);
break;
}
case Command.Arc:
AddArc(commandToken);
break;
case Command.Close:
CloseFigure();
break;
default:
throw new NotSupportedException("Unsupported command");
}
case Command.Close:
_context.EndFigure(true);
openFigure = false;
previousControlPoint = null;
break;
if (commandToken.HasImplicitCommands)
{
continue;
}
default:
throw new NotSupportedException("Unsupported command");
break;
}
}
if (openFigure)
catch (InvalidDataException)
{
break;
}
catch (NotSupportedException)
{
_context.EndFigure(false);
break;
}
}
if (_isOpen != null)
{
_geometryContext.EndFigure(false);
}
}
private Point MirrorControlPoint(Point controlPoint, Point center)
private void CreateFigure()
{
Point dir = (controlPoint - center);
return center + -dir;
if (_isOpen != null)
{
_geometryContext.EndFigure(false);
}
_geometryContext.BeginFigure(_currentPoint);
_isOpen = true;
}
private static bool ReadCommand(
StringReader reader,
ref Command command,
ref bool relative)
private void SetFillRule(CommandToken commandToken)
{
ReadWhitespace(reader);
var fillRule = commandToken.ReadFillRule();
int i = reader.Peek();
_geometryContext.SetFillRule(fillRule);
}
if (i == -1)
private void CloseFigure()
{
if (_isOpen == true)
{
return false;
_geometryContext.EndFigure(true);
}
else
_previousControlPoint = null;
_isOpen = null;
}
private void AddMove(CommandToken commandToken)
{
var currentPoint = commandToken.IsRelative
? commandToken.ReadRelativePoint(_currentPoint)
: commandToken.ReadPoint();
_currentPoint = currentPoint;
CreateFigure();
if (!commandToken.HasImplicitCommands)
{
return;
}
while (commandToken.HasImplicitCommands)
{
char c = (char)i;
Command next = Command.None;
AddLine(commandToken);
if (!Commands.TryGetValue(char.ToUpperInvariant(c), out next))
if (commandToken.IsRelative)
{
if ((char.IsDigit(c) || c == '.' || c == '+' || c == '-') &&
(command != Command.None))
{
return true;
}
else
{
throw new InvalidDataException("Unexpected path command '" + c + "'.");
}
continue;
}
command = next;
relative = char.IsLower(c);
reader.Read();
return true;
_currentPoint = currentPoint;
CreateFigure();
}
}
private static FillRule ReadFillRule(StringReader reader)
private void AddLine(CommandToken commandToken)
{
int i = reader.Read();
if (i == -1)
_currentPoint = commandToken.IsRelative
? commandToken.ReadRelativePoint(_currentPoint)
: commandToken.ReadPoint();
if (_isOpen == null)
{
throw new InvalidDataException("Invalid fill rule");
CreateFigure();
}
char c = (char)i;
FillRule rule;
if (!FillRules.TryGetValue(c, out rule))
_geometryContext.LineTo(_currentPoint);
}
private void AddHorizontalLine(CommandToken commandToken)
{
_currentPoint = commandToken.IsRelative
? new Point(_currentPoint.X + commandToken.ReadDouble(), _currentPoint.Y)
: _currentPoint.WithX(commandToken.ReadDouble());
if (_isOpen == null)
{
throw new InvalidDataException("Invalid fill rule");
CreateFigure();
}
return rule;
_geometryContext.LineTo(_currentPoint);
}
private static double ReadDouble(StringReader reader)
private void AddVerticalLine(CommandToken commandToken)
{
ReadWhitespace(reader);
_currentPoint = commandToken.IsRelative
? new Point(_currentPoint.X, _currentPoint.Y + commandToken.ReadDouble())
: _currentPoint.WithY(commandToken.ReadDouble());
// TODO: Handle Infinity, NaN and scientific notation.
StringBuilder b = new StringBuilder();
bool readSign = false;
bool readPoint = false;
bool readExponent = false;
int i;
while ((i = reader.Peek()) != -1)
if (_isOpen == null)
{
char c = char.ToUpperInvariant((char)i);
CreateFigure();
}
if (((c == '+' || c == '-') && !readSign) ||
(c == '.' && !readPoint) ||
(c == 'E' && !readExponent) ||
char.IsDigit(c))
{
if (b.Length != 0 && !readExponent && c == '-')
break;
b.Append(c);
reader.Read();
_geometryContext.LineTo(_currentPoint);
}
if (!readSign)
{
readSign = c == '+' || c == '-';
}
private void AddCubicBezierCurve(CommandToken commandToken)
{
var point1 = commandToken.IsRelative
? commandToken.ReadRelativePoint(_currentPoint)
: commandToken.ReadPoint();
if (!readPoint)
{
readPoint = c == '.';
}
var point2 = commandToken.IsRelative
? commandToken.ReadRelativePoint(_currentPoint)
: commandToken.ReadPoint();
if (c == 'E')
{
readSign = false;
readExponent = true;
}
}
else
{
break;
}
_previousControlPoint = point2;
var point3 = commandToken.IsRelative
? commandToken.ReadRelativePoint(_currentPoint)
: commandToken.ReadPoint();
if (_isOpen == null)
{
CreateFigure();
}
return double.Parse(b.ToString(), CultureInfo.InvariantCulture);
_geometryContext.CubicBezierTo(point1, point2, point3);
_currentPoint = point3;
}
private static Point ReadPoint(StringReader reader, Point current, bool relative)
private void AddQuadraticBezierCurve(CommandToken commandToken)
{
if (!relative)
var start = commandToken.IsRelative
? commandToken.ReadRelativePoint(_currentPoint)
: commandToken.ReadPoint();
_previousControlPoint = start;
var end = commandToken.IsRelative
? commandToken.ReadRelativePoint(_currentPoint)
: commandToken.ReadPoint();
if (_isOpen == null)
{
current = new Point();
CreateFigure();
}
ReadWhitespace(reader);
double x = current.X + ReadDouble(reader);
ReadSeparator(reader);
double y = current.Y + ReadDouble(reader);
return new Point(x, y);
_geometryContext.QuadraticBezierTo(start, end);
_currentPoint = end;
}
private static Size ReadSize(StringReader reader)
private void AddSmoothCubicBezierCurve(CommandToken commandToken)
{
ReadWhitespace(reader);
double x = ReadDouble(reader);
ReadSeparator(reader);
double y = ReadDouble(reader);
return new Size(x, y);
var point2 = commandToken.IsRelative
? commandToken.ReadRelativePoint(_currentPoint)
: commandToken.ReadPoint();
var end = commandToken.IsRelative
? commandToken.ReadRelativePoint(_currentPoint)
: commandToken.ReadPoint();
if (_previousControlPoint != null)
{
_previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint);
}
if (_isOpen == null)
{
CreateFigure();
}
_geometryContext.CubicBezierTo(_previousControlPoint ?? _currentPoint, point2, end);
_previousControlPoint = point2;
_currentPoint = end;
}
private static bool ReadBool(StringReader reader)
private void AddSmoothQuadraticBezierCurve(CommandToken commandToken)
{
return ReadDouble(reader) != 0;
var end = commandToken.IsRelative
? commandToken.ReadRelativePoint(_currentPoint)
: commandToken.ReadPoint();
if (_previousControlPoint != null)
{
_previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint);
}
if (_isOpen == null)
{
CreateFigure();
}
_geometryContext.QuadraticBezierTo(_previousControlPoint ?? _currentPoint, end);
_currentPoint = end;
}
private static Point ReadRelativePoint(StringReader reader, Point lastPoint)
private void AddArc(CommandToken commandToken)
{
ReadWhitespace(reader);
double x = ReadDouble(reader);
ReadSeparator(reader);
double y = ReadDouble(reader);
return new Point(lastPoint.X + x, lastPoint.Y + y);
var size = commandToken.ReadSize();
var rotationAngle = commandToken.ReadDouble();
var isLargeArc = commandToken.ReadBool();
var sweepDirection = commandToken.ReadBool() ? SweepDirection.Clockwise : SweepDirection.CounterClockwise;
var end = commandToken.IsRelative
? commandToken.ReadRelativePoint(_currentPoint)
: commandToken.ReadPoint();
if (_isOpen == null)
{
CreateFigure();
}
_geometryContext.ArcTo(end, size, rotationAngle, isLargeArc, sweepDirection);
_currentPoint = end;
_previousControlPoint = null;
}
private static void ReadSeparator(StringReader reader)
private class CommandToken
{
int i;
bool readComma = false;
private const string ArgumentExpression = @"-?[0-9]*\.?\d+";
while ((i = reader.Peek()) != -1)
private CommandToken(Command command, bool isRelative, IEnumerable<string> arguments)
{
char c = (char)i;
Command = command;
IsRelative = isRelative;
Arguments = new List<string>(arguments);
}
public Command Command { get; }
if (char.IsWhiteSpace(c))
public bool IsRelative { get; }
public bool HasImplicitCommands
{
get
{
reader.Read();
if (CurrentPosition == 0 && Arguments.Count > 0)
{
return true;
}
return CurrentPosition < Arguments.Count - 1;
}
else if (c == ',')
}
private int CurrentPosition { get; set; }
private List<string> Arguments { get; }
public static CommandToken Parse(string s)
{
using (var reader = new StringReader(s))
{
if (readComma)
var command = Command.None;
var isRelative = false;
if (!ReadCommand(reader, ref command, ref isRelative))
{
throw new InvalidDataException("Unexpected ','.");
throw new InvalidDataException("No path command declared.");
}
readComma = true;
reader.Read();
var commandArguments = reader.ReadToEnd();
var argumentMatches = Regex.Matches(commandArguments, ArgumentExpression);
var arguments = new List<string>();
foreach (Match match in argumentMatches)
{
arguments.Add(match.Value);
}
return new CommandToken(command, isRelative, arguments);
}
}
public FillRule ReadFillRule()
{
if (CurrentPosition == Arguments.Count)
{
throw new InvalidDataException("Invalid fill rule");
}
else
var value = Arguments[CurrentPosition];
CurrentPosition++;
switch (value)
{
break;
case "0":
{
return FillRule.EvenOdd;
}
case "1":
{
return FillRule.NonZero;
}
default:
throw new InvalidDataException("Invalid fill rule");
}
}
}
private static void ReadWhitespace(StringReader reader)
{
int i;
public bool ReadBool()
{
if (CurrentPosition == Arguments.Count)
{
throw new InvalidDataException("Invalid boolean value");
}
var value = Arguments[CurrentPosition];
CurrentPosition++;
switch (value)
{
case "1":
{
return true;
}
case "0":
{
return false;
}
default:
throw new InvalidDataException("Invalid boolean value");
}
}
public double ReadDouble()
{
if (CurrentPosition == Arguments.Count)
{
throw new InvalidDataException("Invalid double value");
}
var value = Arguments[CurrentPosition];
CurrentPosition++;
return double.Parse(value, CultureInfo.InvariantCulture);
}
public Size ReadSize()
{
var width = ReadDouble();
var height = ReadDouble();
return new Size(width, height);
}
public Point ReadPoint()
{
var x = ReadDouble();
var y = ReadDouble();
return new Point(x, y);
}
public Point ReadRelativePoint(Point origin)
{
var x = ReadDouble();
var y = ReadDouble();
return new Point(origin.X + x, origin.Y + y);
}
while ((i = reader.Peek()) != -1)
private static bool ReadCommand(TextReader reader, ref Command command, ref bool relative)
{
char c = (char)i;
ReadWhitespace(reader);
if (char.IsWhiteSpace(c))
var i = reader.Peek();
if (i == -1)
{
reader.Read();
return false;
}
else
var c = (char)i;
if (!s_commands.TryGetValue(char.ToUpperInvariant(c), out var next))
{
break;
throw new InvalidDataException("Unexpected path command '" + c + "'.");
}
command = next;
relative = char.IsLower(c);
reader.Read();
return true;
}
private static void ReadWhitespace(TextReader reader)
{
int i;
while ((i = reader.Peek()) != -1)
{
var c = (char)i;
if (char.IsWhiteSpace(c))
{
reader.Read();
}
else
{
break;
}
}
}
}

11
src/Avalonia.Visuals/Media/StreamGeometry.cs

@ -35,14 +35,15 @@ namespace Avalonia.Media
/// <returns>A <see cref="StreamGeometry"/>.</returns>
public static new StreamGeometry Parse(string s)
{
StreamGeometry result = new StreamGeometry();
var streamGeometry = new StreamGeometry();
using (StreamGeometryContext ctx = result.Open())
{
PathMarkupParser parser = new PathMarkupParser(ctx);
using (var context = streamGeometry.Open())
using (var parser = new PathMarkupParser(context))
{
parser.Parse(s);
return result;
}
return streamGeometry;
}
/// <inheritdoc/>

2
src/Avalonia.Visuals/Media/StreamGeometryContext.cs

@ -15,7 +15,7 @@ namespace Avalonia.Media
/// <see cref="StreamGeometry.Open"/>.
/// </remarks>
/// TODO: This class is just a wrapper around IStreamGeometryContextImpl: is it needed?
public class StreamGeometryContext : IDisposable
public class StreamGeometryContext : IGeometryContext
{
private readonly IStreamGeometryContextImpl _impl;

66
src/Avalonia.Visuals/Platform/IGeometryContext.cs

@ -0,0 +1,66 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Media;
namespace Avalonia.Platform
{
/// <summary>
/// Describes a geometry using drawing commands.
/// </summary>
public interface IGeometryContext : IDisposable
{
/// <summary>
/// Draws an arc to the specified point.
/// </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>
void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection);
/// <summary>
/// Begins a new figure.
/// </summary>
/// <param name="startPoint">The starting point for the figure.</param>
/// <param name="isFilled">Whether the figure is filled.</param>
void BeginFigure(Point startPoint, bool isFilled = true);
/// <summary>
/// Draws a Bezier curve to the specified point.
/// </summary>
/// <param name="point1">The first control point used to specify the shape of the curve.</param>
/// <param name="point2">The second control point used to specify the shape of the curve.</param>
/// <param name="point3">The destination point for the end of the curve.</param>
void CubicBezierTo(Point point1, Point point2, Point point3);
/// <summary>
/// Draws a quadratic Bezier curve to the specified point
/// </summary>
/// <param name="control">Control point</param>
/// <param name="endPoint">DestinationPoint</param>
void QuadraticBezierTo(Point control, Point endPoint);
/// <summary>
/// Draws a line to the specified point.
/// </summary>
/// <param name="point">The destination point.</param>
void LineTo(Point point);
/// <summary>
/// Ends the figure started by <see cref="BeginFigure(Point, bool)"/>.
/// </summary>
/// <param name="isClosed">Whether the figure is closed.</param>
void EndFigure(bool isClosed);
/// <summary>
/// Sets the fill rule.
/// </summary>
/// <param name="fillRule">The fill rule.</param>
void SetFillRule(FillRule fillRule);
}
}

54
src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs

@ -1,62 +1,12 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Media;
namespace Avalonia.Platform
{
/// <summary>
/// Describes a geometry using drawing commands.
/// </summary>
public interface IStreamGeometryContextImpl : IDisposable
{
/// <summary>
/// Draws an arc to the specified point.
/// </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>
void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection);
/// <summary>
/// Begins a new figure.
/// </summary>
/// <param name="startPoint">The starting point for the figure.</param>
/// <param name="isFilled">Whether the figure is filled.</param>
void BeginFigure(Point startPoint, bool isFilled);
/// <summary>
/// Draws a Bezier curve to the specified point.
/// </summary>
/// <param name="point1">The first control point used to specify the shape of the curve.</param>
/// <param name="point2">The second control point used to specify the shape of the curve.</param>
/// <param name="point3">The destination point for the end of the curve.</param>
void CubicBezierTo(Point point1, Point point2, Point point3);
/// <summary>
/// Draws a quadratic Bezier curve to the specified point
/// </summary>
/// <param name="control">Control point</param>
/// <param name="endPoint">DestinationPoint</param>
void QuadraticBezierTo(Point control, Point endPoint);
/// <summary>
/// Draws a line to the specified point.
/// </summary>
/// <param name="point">The destination point.</param>
void LineTo(Point point);
/// <summary>
/// Ends the figure started by <see cref="BeginFigure(Point, bool)"/>.
/// </summary>
/// <param name="isClosed">Whether the figure is closed.</param>
void EndFigure(bool isClosed);
void SetFillRule(FillRule fillRule);
public interface IStreamGeometryContextImpl : IGeometryContext
{
}
}

85
src/Avalonia.Visuals/Platform/PathGeometryContext.cs

@ -0,0 +1,85 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Media;
using Avalonia.Platform;
using System;
namespace Avalonia.Visuals.Platform
{
public class PathGeometryContext : IGeometryContext
{
private PathFigure _currentFigure;
private PathGeometry _pathGeometry;
public PathGeometryContext(PathGeometry pathGeometry)
{
_pathGeometry = pathGeometry ?? throw new ArgumentNullException(nameof(pathGeometry));
}
public void Dispose()
{
_pathGeometry = null;
}
public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection)
{
var arcSegment = new ArcSegment
{
Size = size,
RotationAngle = rotationAngle,
IsLargeArc = isLargeArc,
SweepDirection = sweepDirection,
Point = point
};
_currentFigure.Segments.Add(arcSegment);
}
public void BeginFigure(Point startPoint, bool isFilled)
{
_currentFigure = new PathFigure { StartPoint = startPoint, IsClosed = false, IsFilled = isFilled };
_pathGeometry.Figures.Add(_currentFigure);
}
public void CubicBezierTo(Point point1, Point point2, Point point3)
{
var bezierSegment = new BezierSegment { Point1 = point1, Point2 = point2, Point3 = point3 };
_currentFigure.Segments.Add(bezierSegment);
}
public void QuadraticBezierTo(Point control, Point endPoint)
{
var quadraticBezierSegment = new QuadraticBezierSegment { Point1 = control, Point2 = endPoint };
_currentFigure.Segments.Add(quadraticBezierSegment);
}
public void LineTo(Point point)
{
var lineSegment = new LineSegment
{
Point = point
};
_currentFigure.Segments.Add(lineSegment);
}
public void EndFigure(bool isClosed)
{
if (_currentFigure != null)
{
_currentFigure.IsClosed = isClosed;
}
_currentFigure = null;
}
public void SetFillRule(FillRule fillRule)
{
_pathGeometry.FillRule = fillRule;
}
}
}

5
tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj

@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.0</TargetFrameworks>
<TargetFramework>netcoreapp2.0</TargetFramework>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Animation\Avalonia.Animation.csproj" />
@ -15,7 +16,7 @@
<ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.10.8" />
<PackageReference Include="BenchmarkDotNet" Version="0.10.14" />
</ItemGroup>
<ItemGroup>
<Folder Include="Properties\" />

45
tests/Avalonia.Benchmarks/Visuals/Media/PathMarkupParserTests.cs

@ -0,0 +1,45 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Media;
using Avalonia.UnitTests;
using BenchmarkDotNet.Attributes;
namespace Avalonia.Benchmarks.Visuals.Media
{
[MemoryDiagnoser]
public class PathMarkupParserTests : IDisposable
{
private IDisposable _app;
public PathMarkupParserTests()
{
_app = UnitTestApplication.Start(TestServices.StyledWindow);
}
public void Dispose()
{
_app.Dispose();
}
[Benchmark]
public void Parse_Large_Path()
{
const string PathData = "F1 M 16.6309 18.6563C 17.1309 8.15625 29.8809 14.1563 29.8809 14.1563C 30.8809 11.1563 34.1308 11.4063" +
" 34.1308 11.4063C 33.5 12 34.6309 13.1563 34.6309 13.1563C 32.1309 13.1562 31.1309 14.9062 31.1309 14.9" +
"062C 41.1309 23.9062 32.6309 27.9063 32.6309 27.9062C 24.6309 24.9063 21.1309 22.1562 16.6309 18.6563 Z" +
" M 16.6309 19.9063C 21.6309 24.1563 25.1309 26.1562 31.6309 28.6562C 31.6309 28.6562 26.3809 39.1562 18" +
".3809 36.1563C 18.3809 36.1563 18 38 16.3809 36.9063C 15 36 16.3809 34.9063 16.3809 34.9063C 16.3809 34" +
".9063 10.1309 30.9062 16.6309 19.9063 Z ";
var streamGeometry = new StreamGeometry();
using (var context = streamGeometry.Open())
using (var parser = new PathMarkupParser(context))
{
parser.Parse(PathData);
}
}
}
}

142
tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs

@ -2,8 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Media;
using Avalonia.Platform;
using Moq;
using Avalonia.Visuals.Platform;
using Xunit;
namespace Avalonia.Visuals.UnitTests.Media
@ -13,48 +12,133 @@ namespace Avalonia.Visuals.UnitTests.Media
[Fact]
public void Parses_Move()
{
using (AvaloniaLocator.EnterScope())
{
var result = new Mock<IStreamGeometryContextImpl>();
var parser = PrepareParser(result);
var pathGeometry = new PathGeometry();
using (var context = new PathGeometryContext(pathGeometry))
using (var parser = new PathMarkupParser(context))
{
parser.Parse("M10 10");
result.Verify(x => x.BeginFigure(new Point(10, 10), true));
var figure = pathGeometry.Figures[0];
Assert.Equal(new Point(10, 10), figure.StartPoint);
}
}
[Fact]
public void Parses_Line()
{
using (AvaloniaLocator.EnterScope())
var pathGeometry = new PathGeometry();
using (var context = new PathGeometryContext(pathGeometry))
using (var parser = new PathMarkupParser(context))
{
var result = new Mock<IStreamGeometryContextImpl>();
parser.Parse("M0 0L10 10");
var parser = PrepareParser(result);
var figure = pathGeometry.Figures[0];
parser.Parse("M0 0L10 10");
var segment = figure.Segments[0];
Assert.IsType<LineSegment>(segment);
result.Verify(x => x.LineTo(new Point(10, 10)));
var lineSegment = (LineSegment)segment;
Assert.Equal(new Point(10, 10), lineSegment.Point);
}
}
[Fact]
public void Parses_Close()
{
using (AvaloniaLocator.EnterScope())
var pathGeometry = new PathGeometry();
using (var context = new PathGeometryContext(pathGeometry))
using (var parser = new PathMarkupParser(context))
{
var result = new Mock<IStreamGeometryContextImpl>();
parser.Parse("M0 0L10 10z");
var parser = PrepareParser(result);
var figure = pathGeometry.Figures[0];
parser.Parse("M0 0L10 10z");
Assert.True(figure.IsClosed);
}
}
result.Verify(x => x.EndFigure(true));
[Fact]
public void Parses_FillMode_Before_Move()
{
var pathGeometry = new PathGeometry();
using (var context = new PathGeometryContext(pathGeometry))
using (var parser = new PathMarkupParser(context))
{
parser.Parse("F 1M0,0");
Assert.Equal(FillRule.NonZero, pathGeometry.FillRule);
}
}
[Theory]
[InlineData("M0 0 10 10 20 20")]
[InlineData("M0,0 10,10 20,20")]
[InlineData("M0,0,10,10,20,20")]
public void Parses_Implicit_Line_Command_After_Move(string pathData)
{
var pathGeometry = new PathGeometry();
using (var context = new PathGeometryContext(pathGeometry))
using (var parser = new PathMarkupParser(context))
{
parser.Parse(pathData);
var figure = pathGeometry.Figures[0];
var segment = figure.Segments[0];
Assert.IsType<LineSegment>(segment);
var lineSegment = (LineSegment)segment;
Assert.Equal(new Point(10, 10), lineSegment.Point);
figure = pathGeometry.Figures[1];
segment = figure.Segments[0];
Assert.IsType<LineSegment>(segment);
lineSegment = (LineSegment)segment;
Assert.Equal(new Point(20, 20), lineSegment.Point);
}
}
[Theory]
[InlineData("m0 0 10 10 20 20")]
[InlineData("m0,0 10,10 20,20")]
[InlineData("m0,0,10,10,20,20")]
public void Parses_Implicit_Line_Command_After_Relative_Move(string pathData)
{
var pathGeometry = new PathGeometry();
using (var context = new PathGeometryContext(pathGeometry))
using (var parser = new PathMarkupParser(context))
{
parser.Parse(pathData);
var figure = pathGeometry.Figures[0];
var segment = figure.Segments[0];
Assert.IsType<LineSegment>(segment);
var lineSegment = (LineSegment)segment;
Assert.Equal(new Point(10, 10), lineSegment.Point);
segment = figure.Segments[1];
Assert.IsType<LineSegment>(segment);
lineSegment = (LineSegment)segment;
Assert.Equal(new Point(30, 30), lineSegment.Point);
}
}
[Theory]
[InlineData("F1 M24,14 A2,2,0,1,1,20,14 A2,2,0,1,1,24,14 z")] // issue #1107
[InlineData("M0 0L10 10z")]
@ -75,29 +159,19 @@ namespace Avalonia.Visuals.UnitTests.Media
".3809 36.1563C 18.3809 36.1563 18 38 16.3809 36.9063C 15 36 16.3809 34.9063 16.3809 34.9063C 16.3809 34" +
".9063 10.1309 30.9062 16.6309 19.9063 Z ")]
[InlineData(
"F1M16,12C16,14.209 14.209,16 12,16 9.791,16 8,14.209 8,12 8,11.817 8.03,11.644 8.054,11.467L6.585,10 4,10 " +
"4,6.414 2.5,7.914 0,5.414 0,3.586 3.586,0 4.414,0 7.414,3 7.586,3 9,1.586 11.914,4.5 10.414,6 " +
"F1M16,12C16,14.209 14.209,16 12,16 9.791,16 8,14.209 8,12 8,11.817 8.03,11.644 8.054,11.467L6.585,10 4,10 " +
"4,6.414 2.5,7.914 0,5.414 0,3.586 3.586,0 4.414,0 7.414,3 7.586,3 9,1.586 11.914,4.5 10.414,6 " +
"12.461,8.046C14.45,8.278,16,9.949,16,12")]
public void Should_Parse(string pathData)
{
using (AvaloniaLocator.EnterScope())
var pathGeometry = new PathGeometry();
using (var context = new PathGeometryContext(pathGeometry))
using (var parser = new PathMarkupParser(context))
{
var parser = PrepareParser();
parser.Parse(pathData);
Assert.True(true);
}
}
private static PathMarkupParser PrepareParser(Mock<IStreamGeometryContextImpl> implMock = null)
{
AvaloniaLocator.CurrentMutable
.Bind<IPlatformRenderInterface>()
.ToConstant(Mock.Of<IPlatformRenderInterface>());
return new PathMarkupParser(
new StreamGeometryContext(implMock != null ? implMock.Object : Mock.Of<IStreamGeometryContextImpl>()));
}
}
}
Loading…
Cancel
Save