From cbca7beefde6c69bc21be5bf5d0eb8919278a057 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Wed, 6 Jun 2018 20:53:27 +0200 Subject: [PATCH 1/7] Initial --- .../Media/PathMarkupParser.cs | 805 +++++++++++------- src/Avalonia.Visuals/Media/StreamGeometry.cs | 10 +- .../Media/PathMarkupParserTests.cs | 127 ++- 3 files changed, 610 insertions(+), 332 deletions(-) diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index 9e4a3cbeae9..0307701e827 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -5,50 +5,46 @@ using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; using System.Text; +using System.Text.RegularExpressions; namespace Avalonia.Media { /// /// Parses a path markup string. /// - public class PathMarkupParser + public class PathMarkupParser : IDisposable { - private static readonly Dictionary Commands = new Dictionary - { - { '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 FillRules = new Dictionary - { - {'0', FillRule.EvenOdd }, - {'1', FillRule.NonZero } - }; + private static readonly string s_separatorPattern; - private readonly StreamGeometryContext _context; + private Point _currentPoint; + private Point? _previousControlPoint; + private PathGeometry _currentGeometry; + private PathFigure _currentFigure; + private bool _isDisposed; - /// - /// Initializes a new instance of the class. - /// - /// The context for the geometry. - public PathMarkupParser(StreamGeometryContext context) + private static readonly Dictionary s_commands = + new Dictionary + { + { '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 }, + }; + + static PathMarkupParser() { - _context = context; + s_separatorPattern = CreatesSeparatorPattern(); } - /// - /// Defines the command currently being processed. - /// private enum Command { None, @@ -62,358 +58,581 @@ private enum Command SmoothCubicBezierCurve, SmoothQuadraticBezierCurve, Arc, - Close, + Close } - /// - /// Parses the specified markup string. - /// - /// The markup string. - public void Parse(string s) + public PathGeometry Parse(string s) { - bool openFigure = false; + _currentGeometry = new PathGeometry(); + + var tokens = ParseTokens(s); - using (StringReader reader = new StringReader(s)) + return CreateGeometry(tokens); + } + + void IDisposable.Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (_isDisposed) { - Command command = Command.None; - Point point = new Point(); - bool relative = false; - Point? previousControlPoint = null; + return; + } + + if (disposing) + { + _currentFigure = null; + + _currentGeometry = null; + } + + _isDisposed = true; + } + + private static string CreatesSeparatorPattern() + { + var stringBuilder = new StringBuilder(); + + foreach (var command in s_commands.Keys) + { + stringBuilder.Append(command); + + stringBuilder.Append(char.ToLower(command)); + } + + return @"(?=[" + stringBuilder + "])"; + } + + private static IEnumerable 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; + } - while (ReadCommand(reader, ref command, ref relative)) + private PathGeometry CreateGeometry(IEnumerable commandTokens) + { + _currentGeometry = new PathGeometry(); + + _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.CubicBezierCurve: + AddCubicBezierCurve(commandToken); + break; + case Command.QuadraticBezierCurve: + AddQuadraticBezierCurve(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.SmoothCubicBezierCurve: + AddSmoothCubicBezierCurve(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.SmoothQuadraticBezierCurve: + AddSmoothQuadraticBezierCurve(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.Arc: + AddArc(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.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) { - _context.EndFigure(false); + break; + } + catch (NotSupportedException) + { + break; } } + + return _currentGeometry; } - private Point MirrorControlPoint(Point controlPoint, Point center) + private void SetFillRule(CommandToken commandToken) { - Point dir = (controlPoint - center); - return center + -dir; + _currentGeometry.FillRule = commandToken.ReadFillRule(); } - private static bool ReadCommand( - StringReader reader, - ref Command command, - ref bool relative) + private void CloseFigure() { - ReadWhitespace(reader); + if (_currentFigure != null && !_currentFigure.IsClosed) + { + _currentFigure.IsClosed = true; + } - int i = reader.Peek(); + _previousControlPoint = null; - if (i == -1) + _currentFigure = null; + } + + private void CreateFigure() + { + _currentFigure = new PathFigure { - return false; + StartPoint = _currentPoint, + IsClosed = false + }; + + _currentGeometry.Figures.Add(_currentFigure); + } + + private void AddSegment(PathSegment segment) + { + if (_currentFigure == null) + { + CreateFigure(); } - else + + _currentFigure.Segments.Add(segment); + } + + private void AddMove(CommandToken commandToken) + { + var currentPoint = commandToken.ReadPoint(); + + _currentPoint = currentPoint; + + CreateFigure(); + + if (!commandToken.HasImplicitCommands) { - char c = (char)i; - Command next = Command.None; + return; + } - if (!Commands.TryGetValue(char.ToUpperInvariant(c), out next)) + while (commandToken.HasImplicitCommands) + { + AddLine(commandToken); + + 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) - { - throw new InvalidDataException("Invalid fill rule"); - } - char c = (char)i; - FillRule rule; + _currentPoint = commandToken.IsRelative + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); - if (!FillRules.TryGetValue(c, out rule)) + var lineSegment = new LineSegment { - throw new InvalidDataException("Invalid fill rule"); - } + Point = _currentPoint + }; - return rule; + AddSegment(lineSegment); } - private static double ReadDouble(StringReader reader) + private void AddHorizontalLine(CommandToken commandToken) { - ReadWhitespace(reader); + _currentPoint = commandToken.IsRelative + ? new Point(_currentPoint.X + commandToken.ReadDouble(), _currentPoint.Y) + : _currentPoint.WithX(commandToken.ReadDouble()); - // TODO: Handle Infinity, NaN and scientific notation. - StringBuilder b = new StringBuilder(); - bool readSign = false; - bool readPoint = false; - bool readExponent = false; - int i; + var lineSegment = new LineSegment + { + Point = _currentPoint + }; - while ((i = reader.Peek()) != -1) + AddSegment(lineSegment); + } + + private void AddVerticalLine(CommandToken commandToken) + { + _currentPoint = commandToken.IsRelative + ? new Point(_currentPoint.X, _currentPoint.Y + commandToken.ReadDouble()) + : _currentPoint.WithY(commandToken.ReadDouble()); + + var lineSegment = new LineSegment { - char c = char.ToUpperInvariant((char)i); + Point = _currentPoint + }; - if (((c == '+' || c == '-') && !readSign) || - (c == '.' && !readPoint) || - (c == 'E' && !readExponent) || - char.IsDigit(c)) - { - if (b.Length != 0 && !readExponent && c == '-') - break; - - b.Append(c); - reader.Read(); + AddSegment(lineSegment); + } - 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(); + + var bezierSegment = new BezierSegment + { + Point1 = point1, + Point2 = point2, + Point3 = point3 + }; + + AddSegment(bezierSegment); - return double.Parse(b.ToString(), CultureInfo.InvariantCulture); + _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(); + + var quadraticBezierSegment = new QuadraticBezierSegment { - current = new Point(); - } + Point1 = start, + Point2 = end + }; + + AddSegment(quadraticBezierSegment); - ReadWhitespace(reader); - double x = current.X + ReadDouble(reader); - ReadSeparator(reader); - double y = current.Y + ReadDouble(reader); - return new Point(x, y); + _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); + } + + var bezierSegment = + new BezierSegment { Point1 = _previousControlPoint ?? _currentPoint, Point2 = point2, Point3 = end }; + + AddSegment(bezierSegment); + + _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); + } + + var quadraticBezierSegment = new QuadraticBezierSegment + { + Point1 = _previousControlPoint ?? _currentPoint, + Point2 = end + }; + + AddSegment(quadraticBezierSegment); + + _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(); + + var arcSegment = new ArcSegment + { + Size = size, + RotationAngle = rotationAngle, + IsLargeArc = isLargeArc, + SweepDirection = sweepDirection, + Point = end + }; + + AddSegment(arcSegment); + + _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 arguments) { - char c = (char)i; + Command = command; + + IsRelative = isRelative; + + Arguments = new List(arguments); + } + + public Command Command { get; } + + public bool IsRelative { get; } - if (char.IsWhiteSpace(c)) + 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 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(); + + foreach (Match match in argumentMatches) + { + arguments.Add(match.Value); + } + + return new CommandToken(command, isRelative, arguments); } - else + } + + public FillRule ReadFillRule() + { + if (CurrentPosition == Arguments.Count) { - break; + throw new InvalidDataException("Invalid fill rule"); + } + + var value = Arguments[CurrentPosition]; + + CurrentPosition++; + + switch (value) + { + 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]; - while ((i = reader.Peek()) != -1) + CurrentPosition++; + + switch (value) + { + case "1": + { + return true; + } + + case "0": + { + return false; + } + + default: + throw new InvalidDataException("Invalid boolean value"); + } + } + + public double ReadDouble() { - char c = (char)i; + if (CurrentPosition == Arguments.Count) + { + throw new InvalidDataException("Invalid double value"); + } + + var value = Arguments[CurrentPosition]; + + CurrentPosition++; + + return double.Parse(value, CultureInfo.InvariantCulture); + } - if (char.IsWhiteSpace(c)) + 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); + } + + private static bool ReadCommand( + TextReader reader, + ref Command command, + ref bool relative) + { + ReadWhitespace(reader); + + 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; + } } } } diff --git a/src/Avalonia.Visuals/Media/StreamGeometry.cs b/src/Avalonia.Visuals/Media/StreamGeometry.cs index 9848a649aab..19837403750 100644 --- a/src/Avalonia.Visuals/Media/StreamGeometry.cs +++ b/src/Avalonia.Visuals/Media/StreamGeometry.cs @@ -35,14 +35,10 @@ private StreamGeometry(IStreamGeometryImpl impl) /// A . public static new StreamGeometry Parse(string s) { - StreamGeometry result = new StreamGeometry(); - - using (StreamGeometryContext ctx = result.Open()) + using (var parser = new PathMarkupParser()) { - PathMarkupParser parser = new PathMarkupParser(ctx); - parser.Parse(s); - return result; - } + return parser.Parse(s); + } } /// diff --git a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs index e63d23283cf..d7eb6129ac7 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs @@ -8,53 +8,128 @@ namespace Avalonia.Visuals.UnitTests.Media { + using System.Linq; + public class PathMarkupParserTests { [Fact] public void Parses_Move() { - using (AvaloniaLocator.EnterScope()) - { - var result = new Mock(); - - var parser = PrepareParser(result); + using (var parser = new PathMarkupParser()) + { + var geometry = parser.Parse("M10 10"); - parser.Parse("M10 10"); + var figure = geometry.Figures.First(); - result.Verify(x => x.BeginFigure(new Point(10, 10), true)); + Assert.Equal(new Point(10, 10), figure.StartPoint); } } [Fact] public void Parses_Line() { - using (AvaloniaLocator.EnterScope()) + using (var parser = new PathMarkupParser()) { - var result = new Mock(); + var geometry = parser.Parse("M0 0L10 10"); - var parser = PrepareParser(result); + var figure = geometry.Figures.First(); - parser.Parse("M0 0L10 10"); + var segment = figure.Segments.First(); - result.Verify(x => x.LineTo(new Point(10, 10))); + Assert.IsType(segment); + + var lineSegment = (LineSegment)segment; + + Assert.Equal(new Point(10, 10), lineSegment.Point); } } [Fact] public void Parses_Close() { - using (AvaloniaLocator.EnterScope()) + using (var parser = new PathMarkupParser()) { - var result = new Mock(); + var geometry = parser.Parse("M0 0L10 10z"); + + var figure = geometry.Figures.First(); + + Assert.True(figure.IsClosed); + } + } + + [Fact] + public void Parses_FillMode_Before_Move() + { + using (var parser = new PathMarkupParser()) + { + var geometry = parser.Parse("F 1M0,0"); + + Assert.Equal(FillRule.NonZero, geometry.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) + { + using (var parser = new PathMarkupParser()) + { + var geometry = parser.Parse(pathData); + + var figure = geometry.Figures[0]; + + var segment = figure.Segments[0]; + + Assert.IsType(segment); - var parser = PrepareParser(result); + var lineSegment = (LineSegment)segment; - parser.Parse("M0 0L10 10z"); + Assert.Equal(new Point(10, 10), lineSegment.Point); - result.Verify(x => x.EndFigure(true)); + figure = geometry.Figures[1]; + + segment = figure.Segments[0]; + + Assert.IsType(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) + { + using (var parser = new PathMarkupParser()) + { + var geometry = parser.Parse(pathData); + + var figure = geometry.Figures[0]; + + var segment = figure.Segments[0]; + + Assert.IsType(segment); + + var lineSegment = (LineSegment)segment; + + Assert.Equal(new Point(10, 10), lineSegment.Point); + + segment = figure.Segments[1]; + + Assert.IsType(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 +150,17 @@ public void Parses_Close() ".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()) + using (var parser = new PathMarkupParser()) { - var parser = PrepareParser(); - parser.Parse(pathData); Assert.True(true); } } - - private static PathMarkupParser PrepareParser(Mock implMock = null) - { - AvaloniaLocator.CurrentMutable - .Bind() - .ToConstant(Mock.Of()); - - return new PathMarkupParser( - new StreamGeometryContext(implMock != null ? implMock.Object : Mock.Of())); - } } } \ No newline at end of file From 61816fdf92c90b2929d3e48dd4e8bd5808d18654 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Mon, 11 Jun 2018 14:06:48 +0200 Subject: [PATCH 2/7] Relative move command fix --- src/Avalonia.Visuals/Media/PathMarkupParser.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index 0307701e827..c3fcf191482 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -232,7 +232,9 @@ private void AddSegment(PathSegment segment) private void AddMove(CommandToken commandToken) { - var currentPoint = commandToken.ReadPoint(); + var currentPoint = commandToken.IsRelative + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); _currentPoint = currentPoint; From 219a0da799e954516b9c50eb2ad41c8feae533ba Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 17 Jun 2018 18:56:47 +0200 Subject: [PATCH 3/7] Added benchmarks for PathMarkupParser. --- .../Avalonia.Benchmarks.csproj | 5 ++- .../Visuals/Media/PathMarkupParserTests.cs | 39 +++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 tests/Avalonia.Benchmarks/Visuals/Media/PathMarkupParserTests.cs diff --git a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj index 9e92baf0ff5..a460ab5bf37 100644 --- a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj +++ b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj @@ -1,6 +1,7 @@  - netcoreapp2.0 + netcoreapp2.0 + Exe @@ -15,7 +16,7 @@ - + diff --git a/tests/Avalonia.Benchmarks/Visuals/Media/PathMarkupParserTests.cs b/tests/Avalonia.Benchmarks/Visuals/Media/PathMarkupParserTests.cs new file mode 100644 index 00000000000..6d0027fe230 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Visuals/Media/PathMarkupParserTests.cs @@ -0,0 +1,39 @@ +// 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() + { + var path = "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 parser = new PathMarkupParser(); + var result = parser.Parse(path); + } + } +} From b8f8c1ef0f90f8ce7a12446484a6c7dd34e51d4e Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Sun, 17 Jun 2018 22:26:03 +0200 Subject: [PATCH 4/7] IGeometryContext implementation --- src/Avalonia.Visuals/Media/PathGeometry.cs | 22 +- .../Media/PathMarkupParser.cs | 198 +++++++++--------- src/Avalonia.Visuals/Media/StreamGeometry.cs | 15 +- .../Media/StreamGeometryContext.cs | 2 +- .../Platform/IGeometryContext.cs | 66 ++++++ .../Platform/IStreamGeometryContextImpl.cs | 54 +---- .../Platform/PathGeometryContext.cs | 85 ++++++++ .../Visuals/Media/PathMarkupParserTests.cs | 22 +- .../Media/PathMarkupParserTests.cs | 61 +++--- 9 files changed, 330 insertions(+), 195 deletions(-) create mode 100644 src/Avalonia.Visuals/Platform/IGeometryContext.cs create mode 100644 src/Avalonia.Visuals/Platform/PathGeometryContext.cs diff --git a/src/Avalonia.Visuals/Media/PathGeometry.cs b/src/Avalonia.Visuals/Media/PathGeometry.cs index ecda07ada14..5cf98aa5e72 100644 --- a/src/Avalonia.Visuals/Media/PathGeometry.cs +++ b/src/Avalonia.Visuals/Media/PathGeometry.cs @@ -8,6 +8,8 @@ namespace Avalonia.Media { + using Avalonia.Visuals.Platform; + public class PathGeometry : StreamGeometry { /// @@ -28,7 +30,7 @@ public class PathGeometry : StreamGeometry static PathGeometry() { - FiguresProperty.Changed.AddClassHandler((s, e) => + FiguresProperty.Changed.AddClassHandler((s, e) => s.OnFiguresChanged(e.NewValue as PathFigures)); } @@ -40,6 +42,24 @@ public PathGeometry() Figures = new PathFigures(); } + /// + /// Parses the specified path data to a . + /// + /// The s. + /// + 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; + } + /// /// Gets or sets the figures. /// diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index c3fcf191482..2ffff75de37 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; +using Avalonia.Platform; namespace Avalonia.Media { @@ -17,13 +18,6 @@ namespace Avalonia.Media public class PathMarkupParser : IDisposable { private static readonly string s_separatorPattern; - - private Point _currentPoint; - private Point? _previousControlPoint; - private PathGeometry _currentGeometry; - private PathFigure _currentFigure; - private bool _isDisposed; - private static readonly Dictionary s_commands = new Dictionary { @@ -40,11 +34,32 @@ public class PathMarkupParser : IDisposable { 'Z', Command.Close }, }; + private IGeometryContext _geometryContext; + private Point _currentPoint; + private Point? _previousControlPoint; + private bool? _isOpen; + private bool _isDisposed; + static PathMarkupParser() { s_separatorPattern = CreatesSeparatorPattern(); } + /// + /// Initializes a new instance of the class. + /// + /// The geometry context. + /// geometryContext + public PathMarkupParser(IGeometryContext geometryContext) + { + if (geometryContext == null) + { + throw new ArgumentNullException(nameof(geometryContext)); + } + + _geometryContext = geometryContext; + } + private enum Command { None, @@ -61,13 +76,15 @@ private enum Command Close } - public PathGeometry Parse(string s) + /// + /// Parses the specified path data and writes the result to the geometryContext of this instance. + /// + /// The path data. + public void Parse(string pathData) { - _currentGeometry = new PathGeometry(); - - var tokens = ParseTokens(s); + var tokens = ParseTokens(pathData); - return CreateGeometry(tokens); + CreateGeometry(tokens); } void IDisposable.Dispose() @@ -84,9 +101,7 @@ protected virtual void Dispose(bool disposing) if (disposing) { - _currentFigure = null; - - _currentGeometry = null; + _geometryContext = null; } _isDisposed = true; @@ -118,11 +133,9 @@ private static Point MirrorControlPoint(Point controlPoint, Point center) return center + -dir; } - private PathGeometry CreateGeometry(IEnumerable commandTokens) + private void CreateGeometry(IEnumerable commandTokens) { - _currentGeometry = new PathGeometry(); - - _currentPoint = new Point(); + _currentPoint = new Point(); foreach (var commandToken in commandTokens) { @@ -188,46 +201,32 @@ private PathGeometry CreateGeometry(IEnumerable commandTokens) break; } } - - return _currentGeometry; - } - - private void SetFillRule(CommandToken commandToken) - { - _currentGeometry.FillRule = commandToken.ReadFillRule(); } - private void CloseFigure() + private void CreateFigure() { - if (_currentFigure != null && !_currentFigure.IsClosed) - { - _currentFigure.IsClosed = true; - } - - _previousControlPoint = null; + _geometryContext.BeginFigure(_currentPoint); - _currentFigure = null; + this._isOpen = true; } - private void CreateFigure() + private void SetFillRule(CommandToken commandToken) { - _currentFigure = new PathFigure - { - StartPoint = _currentPoint, - IsClosed = false - }; + var fillRule = commandToken.ReadFillRule(); - _currentGeometry.Figures.Add(_currentFigure); + _geometryContext.SetFillRule(fillRule); } - private void AddSegment(PathSegment segment) + private void CloseFigure() { - if (_currentFigure == null) + if (_isOpen == true) { - CreateFigure(); + _geometryContext.EndFigure(true); } - _currentFigure.Segments.Add(segment); + _previousControlPoint = null; + + _isOpen = null; } private void AddMove(CommandToken commandToken) @@ -266,66 +265,64 @@ private void AddLine(CommandToken commandToken) ? commandToken.ReadRelativePoint(_currentPoint) : commandToken.ReadPoint(); - var lineSegment = new LineSegment + if (_isOpen == null) { - Point = _currentPoint - }; + CreateFigure(); + } - AddSegment(lineSegment); + _geometryContext.LineTo(_currentPoint); } private void AddHorizontalLine(CommandToken commandToken) { _currentPoint = commandToken.IsRelative - ? new Point(_currentPoint.X + commandToken.ReadDouble(), _currentPoint.Y) - : _currentPoint.WithX(commandToken.ReadDouble()); + ? new Point(_currentPoint.X + commandToken.ReadDouble(), _currentPoint.Y) + : _currentPoint.WithX(commandToken.ReadDouble()); - var lineSegment = new LineSegment + if (_isOpen == null) { - Point = _currentPoint - }; + CreateFigure(); + } - AddSegment(lineSegment); + _geometryContext.LineTo(_currentPoint); } private void AddVerticalLine(CommandToken commandToken) { _currentPoint = commandToken.IsRelative - ? new Point(_currentPoint.X, _currentPoint.Y + commandToken.ReadDouble()) - : _currentPoint.WithY(commandToken.ReadDouble()); + ? new Point(_currentPoint.X, _currentPoint.Y + commandToken.ReadDouble()) + : _currentPoint.WithY(commandToken.ReadDouble()); - var lineSegment = new LineSegment + if (_isOpen == null) { - Point = _currentPoint - }; + CreateFigure(); + } - AddSegment(lineSegment); + _geometryContext.LineTo(_currentPoint); } private void AddCubicBezierCurve(CommandToken commandToken) { var point1 = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); var point2 = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); _previousControlPoint = point2; var point3 = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); - var bezierSegment = new BezierSegment + if (_isOpen == null) { - Point1 = point1, - Point2 = point2, - Point3 = point3 - }; + CreateFigure(); + } - AddSegment(bezierSegment); + _geometryContext.CubicBezierTo(point1, point2, point3); _currentPoint = point3; } @@ -333,22 +330,21 @@ private void AddCubicBezierCurve(CommandToken commandToken) private void AddQuadraticBezierCurve(CommandToken commandToken) { var start = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); _previousControlPoint = start; var end = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); - var quadraticBezierSegment = new QuadraticBezierSegment + if (_isOpen == null) { - Point1 = start, - Point2 = end - }; + CreateFigure(); + } - AddSegment(quadraticBezierSegment); + _geometryContext.QuadraticBezierTo(start, end); _currentPoint = end; } @@ -356,8 +352,8 @@ private void AddQuadraticBezierCurve(CommandToken commandToken) private void AddSmoothCubicBezierCurve(CommandToken commandToken) { var point2 = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); var end = commandToken.IsRelative ? commandToken.ReadRelativePoint(_currentPoint) @@ -368,10 +364,12 @@ private void AddSmoothCubicBezierCurve(CommandToken commandToken) _previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint); } - var bezierSegment = - new BezierSegment { Point1 = _previousControlPoint ?? _currentPoint, Point2 = point2, Point3 = end }; + if (_isOpen == null) + { + CreateFigure(); + } - AddSegment(bezierSegment); + _geometryContext.CubicBezierTo(_previousControlPoint ?? _currentPoint, point2, end); _previousControlPoint = point2; @@ -389,13 +387,12 @@ private void AddSmoothQuadraticBezierCurve(CommandToken commandToken) _previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint); } - var quadraticBezierSegment = new QuadraticBezierSegment + if (_isOpen == null) { - Point1 = _previousControlPoint ?? _currentPoint, - Point2 = end - }; + CreateFigure(); + } - AddSegment(quadraticBezierSegment); + _geometryContext.QuadraticBezierTo(_previousControlPoint ?? _currentPoint, end); _currentPoint = end; } @@ -414,16 +411,12 @@ private void AddArc(CommandToken commandToken) ? commandToken.ReadRelativePoint(_currentPoint) : commandToken.ReadPoint(); - var arcSegment = new ArcSegment + if (_isOpen == null) { - Size = size, - RotationAngle = rotationAngle, - IsLargeArc = isLargeArc, - SweepDirection = sweepDirection, - Point = end - }; + CreateFigure(); + } - AddSegment(arcSegment); + _geometryContext.ArcTo(end, size, rotationAngle, isLargeArc, sweepDirection); _currentPoint = end; @@ -589,10 +582,7 @@ public Point ReadRelativePoint(Point origin) return new Point(origin.X + x, origin.Y + y); } - private static bool ReadCommand( - TextReader reader, - ref Command command, - ref bool relative) + private static bool ReadCommand(TextReader reader, ref Command command, ref bool relative) { ReadWhitespace(reader); diff --git a/src/Avalonia.Visuals/Media/StreamGeometry.cs b/src/Avalonia.Visuals/Media/StreamGeometry.cs index 19837403750..2622175d605 100644 --- a/src/Avalonia.Visuals/Media/StreamGeometry.cs +++ b/src/Avalonia.Visuals/Media/StreamGeometry.cs @@ -5,6 +5,8 @@ namespace Avalonia.Media { + using Avalonia.Visuals.Platform; + /// /// Represents the geometry of an arbitrarily complex shape. /// @@ -35,10 +37,15 @@ private StreamGeometry(IStreamGeometryImpl impl) /// A . public static new StreamGeometry Parse(string s) { - using (var parser = new PathMarkupParser()) - { - return parser.Parse(s); - } + var streamGeometry = new StreamGeometry(); + + using (var context = streamGeometry.Open()) + using (var parser = new PathMarkupParser(context)) + { + parser.Parse(s); + } + + return streamGeometry; } /// diff --git a/src/Avalonia.Visuals/Media/StreamGeometryContext.cs b/src/Avalonia.Visuals/Media/StreamGeometryContext.cs index 70b889f817c..7521582067e 100644 --- a/src/Avalonia.Visuals/Media/StreamGeometryContext.cs +++ b/src/Avalonia.Visuals/Media/StreamGeometryContext.cs @@ -15,7 +15,7 @@ namespace Avalonia.Media /// . /// /// TODO: This class is just a wrapper around IStreamGeometryContextImpl: is it needed? - public class StreamGeometryContext : IDisposable + public class StreamGeometryContext : IGeometryContext { private readonly IStreamGeometryContextImpl _impl; diff --git a/src/Avalonia.Visuals/Platform/IGeometryContext.cs b/src/Avalonia.Visuals/Platform/IGeometryContext.cs new file mode 100644 index 00000000000..ac638374285 --- /dev/null +++ b/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 +{ + /// + /// Describes a geometry using drawing commands. + /// + public interface IGeometryContext : IDisposable + { + /// + /// Draws an arc to the specified point. + /// + /// 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. + /// + void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection); + + /// + /// Begins a new figure. + /// + /// The starting point for the figure. + /// Whether the figure is filled. + void BeginFigure(Point startPoint, bool isFilled = true); + + /// + /// Draws a Bezier curve to the specified point. + /// + /// The first control point used to specify the shape of the curve. + /// The second control point used to specify the shape of the curve. + /// The destination point for the end of the curve. + void CubicBezierTo(Point point1, Point point2, Point point3); + + /// + /// Draws a quadratic Bezier curve to the specified point + /// + /// Control point + /// DestinationPoint + void QuadraticBezierTo(Point control, Point endPoint); + + /// + /// Draws a line to the specified point. + /// + /// The destination point. + void LineTo(Point point); + + /// + /// Ends the figure started by . + /// + /// Whether the figure is closed. + void EndFigure(bool isClosed); + + /// + /// Sets the fill rule. + /// + /// The fill rule. + void SetFillRule(FillRule fillRule); + } +} \ No newline at end of file diff --git a/src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs b/src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs index 386560c6b65..da9505cd2d2 100644 --- a/src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs +++ b/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 { /// /// Describes a geometry using drawing commands. /// - public interface IStreamGeometryContextImpl : IDisposable - { - /// - /// Draws an arc to the specified point. - /// - /// 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. - /// - void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection); - - /// - /// Begins a new figure. - /// - /// The starting point for the figure. - /// Whether the figure is filled. - void BeginFigure(Point startPoint, bool isFilled); - - /// - /// Draws a Bezier curve to the specified point. - /// - /// The first control point used to specify the shape of the curve. - /// The second control point used to specify the shape of the curve. - /// The destination point for the end of the curve. - void CubicBezierTo(Point point1, Point point2, Point point3); - - /// - /// Draws a quadratic Bezier curve to the specified point - /// - /// Control point - /// DestinationPoint - void QuadraticBezierTo(Point control, Point endPoint); - - /// - /// Draws a line to the specified point. - /// - /// The destination point. - void LineTo(Point point); - - /// - /// Ends the figure started by . - /// - /// Whether the figure is closed. - void EndFigure(bool isClosed); - - void SetFillRule(FillRule fillRule); + public interface IStreamGeometryContextImpl : IGeometryContext + { } } diff --git a/src/Avalonia.Visuals/Platform/PathGeometryContext.cs b/src/Avalonia.Visuals/Platform/PathGeometryContext.cs new file mode 100644 index 00000000000..cc881094fd9 --- /dev/null +++ b/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; + } + } +} diff --git a/tests/Avalonia.Benchmarks/Visuals/Media/PathMarkupParserTests.cs b/tests/Avalonia.Benchmarks/Visuals/Media/PathMarkupParserTests.cs index 6d0027fe230..c4106340b2f 100644 --- a/tests/Avalonia.Benchmarks/Visuals/Media/PathMarkupParserTests.cs +++ b/tests/Avalonia.Benchmarks/Visuals/Media/PathMarkupParserTests.cs @@ -26,14 +26,20 @@ public void Dispose() [Benchmark] public void Parse_Large_Path() { - var path = "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 parser = new PathMarkupParser(); - var result = parser.Parse(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); + } } } } diff --git a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs index d7eb6129ac7..c8bea43fb02 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs @@ -2,24 +2,23 @@ // 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 { - using System.Linq; - public class PathMarkupParserTests { [Fact] public void Parses_Move() { - using (var parser = new PathMarkupParser()) + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) { - var geometry = parser.Parse("M10 10"); + parser.Parse("M10 10"); - var figure = geometry.Figures.First(); + var figure = pathGeometry.Figures[0]; Assert.Equal(new Point(10, 10), figure.StartPoint); } @@ -28,13 +27,15 @@ public void Parses_Move() [Fact] public void Parses_Line() { - using (var parser = new PathMarkupParser()) + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) { - var geometry = parser.Parse("M0 0L10 10"); + parser.Parse("M0 0L10 10"); - var figure = geometry.Figures.First(); + var figure = pathGeometry.Figures[0]; - var segment = figure.Segments.First(); + var segment = figure.Segments[0]; Assert.IsType(segment); @@ -47,11 +48,13 @@ public void Parses_Line() [Fact] public void Parses_Close() { - using (var parser = new PathMarkupParser()) + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) { - var geometry = parser.Parse("M0 0L10 10z"); + parser.Parse("M0 0L10 10z"); - var figure = geometry.Figures.First(); + var figure = pathGeometry.Figures[0]; Assert.True(figure.IsClosed); } @@ -60,11 +63,13 @@ public void Parses_Close() [Fact] public void Parses_FillMode_Before_Move() { - using (var parser = new PathMarkupParser()) + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) { - var geometry = parser.Parse("F 1M0,0"); + parser.Parse("F 1M0,0"); - Assert.Equal(FillRule.NonZero, geometry.FillRule); + Assert.Equal(FillRule.NonZero, pathGeometry.FillRule); } } @@ -74,11 +79,13 @@ public void Parses_FillMode_Before_Move() [InlineData("M0,0,10,10,20,20")] public void Parses_Implicit_Line_Command_After_Move(string pathData) { - using (var parser = new PathMarkupParser()) + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) { - var geometry = parser.Parse(pathData); + parser.Parse(pathData); - var figure = geometry.Figures[0]; + var figure = pathGeometry.Figures[0]; var segment = figure.Segments[0]; @@ -88,7 +95,7 @@ public void Parses_Implicit_Line_Command_After_Move(string pathData) Assert.Equal(new Point(10, 10), lineSegment.Point); - figure = geometry.Figures[1]; + figure = pathGeometry.Figures[1]; segment = figure.Segments[0]; @@ -106,11 +113,13 @@ public void Parses_Implicit_Line_Command_After_Move(string pathData) [InlineData("m0,0,10,10,20,20")] public void Parses_Implicit_Line_Command_After_Relative_Move(string pathData) { - using (var parser = new PathMarkupParser()) + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) { - var geometry = parser.Parse(pathData); + parser.Parse(pathData); - var figure = geometry.Figures[0]; + var figure = pathGeometry.Figures[0]; var segment = figure.Segments[0]; @@ -155,7 +164,9 @@ public void Parses_Implicit_Line_Command_After_Relative_Move(string pathData) "12.461,8.046C14.45,8.278,16,9.949,16,12")] public void Should_Parse(string pathData) { - using (var parser = new PathMarkupParser()) + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) { parser.Parse(pathData); From cd39703645308fe64ed26e43c034971a07d1a947 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Sun, 17 Jun 2018 23:52:49 +0200 Subject: [PATCH 5/7] Direct2D1 EndFigure fix --- src/Avalonia.Visuals/Media/PathMarkupParser.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index 2ffff75de37..e8824753b59 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -201,13 +201,23 @@ private void CreateGeometry(IEnumerable commandTokens) break; } } + + if (_isOpen != null) + { + _geometryContext.EndFigure(false); + } } private void CreateFigure() { + if (_isOpen != null) + { + _geometryContext.EndFigure(false); + } + _geometryContext.BeginFigure(_currentPoint); - this._isOpen = true; + _isOpen = true; } private void SetFillRule(CommandToken commandToken) From 41fd37151f6e2cc7a947f5c3bea551ddc3f20034 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Sun, 17 Jun 2018 22:26:03 +0200 Subject: [PATCH 6/7] IGeometryContext implementation --- src/Avalonia.Visuals/Media/PathGeometry.cs | 23 +- .../Media/PathMarkupParser.cs | 200 +++++++++--------- src/Avalonia.Visuals/Media/StreamGeometry.cs | 15 +- .../Media/StreamGeometryContext.cs | 2 +- .../Platform/IGeometryContext.cs | 66 ++++++ .../Platform/IStreamGeometryContextImpl.cs | 54 +---- .../Platform/PathGeometryContext.cs | 85 ++++++++ .../Visuals/Media/PathMarkupParserTests.cs | 22 +- .../Media/PathMarkupParserTests.cs | 61 +++--- 9 files changed, 336 insertions(+), 192 deletions(-) create mode 100644 src/Avalonia.Visuals/Platform/IGeometryContext.cs create mode 100644 src/Avalonia.Visuals/Platform/PathGeometryContext.cs diff --git a/src/Avalonia.Visuals/Media/PathGeometry.cs b/src/Avalonia.Visuals/Media/PathGeometry.cs index ecda07ada14..c5cd8036d21 100644 --- a/src/Avalonia.Visuals/Media/PathGeometry.cs +++ b/src/Avalonia.Visuals/Media/PathGeometry.cs @@ -5,9 +5,10 @@ using Avalonia.Collections; using Avalonia.Metadata; using Avalonia.Platform; +using Avalonia.Visuals.Platform; namespace Avalonia.Media -{ +{ public class PathGeometry : StreamGeometry { /// @@ -28,7 +29,7 @@ public class PathGeometry : StreamGeometry static PathGeometry() { - FiguresProperty.Changed.AddClassHandler((s, e) => + FiguresProperty.Changed.AddClassHandler((s, e) => s.OnFiguresChanged(e.NewValue as PathFigures)); } @@ -40,6 +41,24 @@ public PathGeometry() Figures = new PathFigures(); } + /// + /// Parses the specified path data to a . + /// + /// The s. + /// + 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; + } + /// /// Gets or sets the figures. /// diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index c3fcf191482..e8824753b59 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; +using Avalonia.Platform; namespace Avalonia.Media { @@ -17,13 +18,6 @@ namespace Avalonia.Media public class PathMarkupParser : IDisposable { private static readonly string s_separatorPattern; - - private Point _currentPoint; - private Point? _previousControlPoint; - private PathGeometry _currentGeometry; - private PathFigure _currentFigure; - private bool _isDisposed; - private static readonly Dictionary s_commands = new Dictionary { @@ -40,11 +34,32 @@ public class PathMarkupParser : IDisposable { 'Z', Command.Close }, }; + private IGeometryContext _geometryContext; + private Point _currentPoint; + private Point? _previousControlPoint; + private bool? _isOpen; + private bool _isDisposed; + static PathMarkupParser() { s_separatorPattern = CreatesSeparatorPattern(); } + /// + /// Initializes a new instance of the class. + /// + /// The geometry context. + /// geometryContext + public PathMarkupParser(IGeometryContext geometryContext) + { + if (geometryContext == null) + { + throw new ArgumentNullException(nameof(geometryContext)); + } + + _geometryContext = geometryContext; + } + private enum Command { None, @@ -61,13 +76,15 @@ private enum Command Close } - public PathGeometry Parse(string s) + /// + /// Parses the specified path data and writes the result to the geometryContext of this instance. + /// + /// The path data. + public void Parse(string pathData) { - _currentGeometry = new PathGeometry(); - - var tokens = ParseTokens(s); + var tokens = ParseTokens(pathData); - return CreateGeometry(tokens); + CreateGeometry(tokens); } void IDisposable.Dispose() @@ -84,9 +101,7 @@ protected virtual void Dispose(bool disposing) if (disposing) { - _currentFigure = null; - - _currentGeometry = null; + _geometryContext = null; } _isDisposed = true; @@ -118,11 +133,9 @@ private static Point MirrorControlPoint(Point controlPoint, Point center) return center + -dir; } - private PathGeometry CreateGeometry(IEnumerable commandTokens) + private void CreateGeometry(IEnumerable commandTokens) { - _currentGeometry = new PathGeometry(); - - _currentPoint = new Point(); + _currentPoint = new Point(); foreach (var commandToken in commandTokens) { @@ -189,45 +202,41 @@ private PathGeometry CreateGeometry(IEnumerable commandTokens) } } - return _currentGeometry; - } - - private void SetFillRule(CommandToken commandToken) - { - _currentGeometry.FillRule = commandToken.ReadFillRule(); + if (_isOpen != null) + { + _geometryContext.EndFigure(false); + } } - private void CloseFigure() + private void CreateFigure() { - if (_currentFigure != null && !_currentFigure.IsClosed) + if (_isOpen != null) { - _currentFigure.IsClosed = true; + _geometryContext.EndFigure(false); } - _previousControlPoint = null; + _geometryContext.BeginFigure(_currentPoint); - _currentFigure = null; + _isOpen = true; } - private void CreateFigure() + private void SetFillRule(CommandToken commandToken) { - _currentFigure = new PathFigure - { - StartPoint = _currentPoint, - IsClosed = false - }; + var fillRule = commandToken.ReadFillRule(); - _currentGeometry.Figures.Add(_currentFigure); + _geometryContext.SetFillRule(fillRule); } - private void AddSegment(PathSegment segment) + private void CloseFigure() { - if (_currentFigure == null) + if (_isOpen == true) { - CreateFigure(); + _geometryContext.EndFigure(true); } - _currentFigure.Segments.Add(segment); + _previousControlPoint = null; + + _isOpen = null; } private void AddMove(CommandToken commandToken) @@ -266,66 +275,64 @@ private void AddLine(CommandToken commandToken) ? commandToken.ReadRelativePoint(_currentPoint) : commandToken.ReadPoint(); - var lineSegment = new LineSegment + if (_isOpen == null) { - Point = _currentPoint - }; + CreateFigure(); + } - AddSegment(lineSegment); + _geometryContext.LineTo(_currentPoint); } private void AddHorizontalLine(CommandToken commandToken) { _currentPoint = commandToken.IsRelative - ? new Point(_currentPoint.X + commandToken.ReadDouble(), _currentPoint.Y) - : _currentPoint.WithX(commandToken.ReadDouble()); + ? new Point(_currentPoint.X + commandToken.ReadDouble(), _currentPoint.Y) + : _currentPoint.WithX(commandToken.ReadDouble()); - var lineSegment = new LineSegment + if (_isOpen == null) { - Point = _currentPoint - }; + CreateFigure(); + } - AddSegment(lineSegment); + _geometryContext.LineTo(_currentPoint); } private void AddVerticalLine(CommandToken commandToken) { _currentPoint = commandToken.IsRelative - ? new Point(_currentPoint.X, _currentPoint.Y + commandToken.ReadDouble()) - : _currentPoint.WithY(commandToken.ReadDouble()); + ? new Point(_currentPoint.X, _currentPoint.Y + commandToken.ReadDouble()) + : _currentPoint.WithY(commandToken.ReadDouble()); - var lineSegment = new LineSegment + if (_isOpen == null) { - Point = _currentPoint - }; + CreateFigure(); + } - AddSegment(lineSegment); + _geometryContext.LineTo(_currentPoint); } private void AddCubicBezierCurve(CommandToken commandToken) { var point1 = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); var point2 = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); _previousControlPoint = point2; var point3 = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); - var bezierSegment = new BezierSegment + if (_isOpen == null) { - Point1 = point1, - Point2 = point2, - Point3 = point3 - }; + CreateFigure(); + } - AddSegment(bezierSegment); + _geometryContext.CubicBezierTo(point1, point2, point3); _currentPoint = point3; } @@ -333,22 +340,21 @@ private void AddCubicBezierCurve(CommandToken commandToken) private void AddQuadraticBezierCurve(CommandToken commandToken) { var start = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); _previousControlPoint = start; var end = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); - var quadraticBezierSegment = new QuadraticBezierSegment + if (_isOpen == null) { - Point1 = start, - Point2 = end - }; + CreateFigure(); + } - AddSegment(quadraticBezierSegment); + _geometryContext.QuadraticBezierTo(start, end); _currentPoint = end; } @@ -356,8 +362,8 @@ private void AddQuadraticBezierCurve(CommandToken commandToken) private void AddSmoothCubicBezierCurve(CommandToken commandToken) { var point2 = commandToken.IsRelative - ? commandToken.ReadRelativePoint(_currentPoint) - : commandToken.ReadPoint(); + ? commandToken.ReadRelativePoint(_currentPoint) + : commandToken.ReadPoint(); var end = commandToken.IsRelative ? commandToken.ReadRelativePoint(_currentPoint) @@ -368,10 +374,12 @@ private void AddSmoothCubicBezierCurve(CommandToken commandToken) _previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint); } - var bezierSegment = - new BezierSegment { Point1 = _previousControlPoint ?? _currentPoint, Point2 = point2, Point3 = end }; + if (_isOpen == null) + { + CreateFigure(); + } - AddSegment(bezierSegment); + _geometryContext.CubicBezierTo(_previousControlPoint ?? _currentPoint, point2, end); _previousControlPoint = point2; @@ -389,13 +397,12 @@ private void AddSmoothQuadraticBezierCurve(CommandToken commandToken) _previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint); } - var quadraticBezierSegment = new QuadraticBezierSegment + if (_isOpen == null) { - Point1 = _previousControlPoint ?? _currentPoint, - Point2 = end - }; + CreateFigure(); + } - AddSegment(quadraticBezierSegment); + _geometryContext.QuadraticBezierTo(_previousControlPoint ?? _currentPoint, end); _currentPoint = end; } @@ -414,16 +421,12 @@ private void AddArc(CommandToken commandToken) ? commandToken.ReadRelativePoint(_currentPoint) : commandToken.ReadPoint(); - var arcSegment = new ArcSegment + if (_isOpen == null) { - Size = size, - RotationAngle = rotationAngle, - IsLargeArc = isLargeArc, - SweepDirection = sweepDirection, - Point = end - }; + CreateFigure(); + } - AddSegment(arcSegment); + _geometryContext.ArcTo(end, size, rotationAngle, isLargeArc, sweepDirection); _currentPoint = end; @@ -589,10 +592,7 @@ public Point ReadRelativePoint(Point origin) return new Point(origin.X + x, origin.Y + y); } - private static bool ReadCommand( - TextReader reader, - ref Command command, - ref bool relative) + private static bool ReadCommand(TextReader reader, ref Command command, ref bool relative) { ReadWhitespace(reader); diff --git a/src/Avalonia.Visuals/Media/StreamGeometry.cs b/src/Avalonia.Visuals/Media/StreamGeometry.cs index 19837403750..2622175d605 100644 --- a/src/Avalonia.Visuals/Media/StreamGeometry.cs +++ b/src/Avalonia.Visuals/Media/StreamGeometry.cs @@ -5,6 +5,8 @@ namespace Avalonia.Media { + using Avalonia.Visuals.Platform; + /// /// Represents the geometry of an arbitrarily complex shape. /// @@ -35,10 +37,15 @@ private StreamGeometry(IStreamGeometryImpl impl) /// A . public static new StreamGeometry Parse(string s) { - using (var parser = new PathMarkupParser()) - { - return parser.Parse(s); - } + var streamGeometry = new StreamGeometry(); + + using (var context = streamGeometry.Open()) + using (var parser = new PathMarkupParser(context)) + { + parser.Parse(s); + } + + return streamGeometry; } /// diff --git a/src/Avalonia.Visuals/Media/StreamGeometryContext.cs b/src/Avalonia.Visuals/Media/StreamGeometryContext.cs index 70b889f817c..7521582067e 100644 --- a/src/Avalonia.Visuals/Media/StreamGeometryContext.cs +++ b/src/Avalonia.Visuals/Media/StreamGeometryContext.cs @@ -15,7 +15,7 @@ namespace Avalonia.Media /// . /// /// TODO: This class is just a wrapper around IStreamGeometryContextImpl: is it needed? - public class StreamGeometryContext : IDisposable + public class StreamGeometryContext : IGeometryContext { private readonly IStreamGeometryContextImpl _impl; diff --git a/src/Avalonia.Visuals/Platform/IGeometryContext.cs b/src/Avalonia.Visuals/Platform/IGeometryContext.cs new file mode 100644 index 00000000000..ac638374285 --- /dev/null +++ b/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 +{ + /// + /// Describes a geometry using drawing commands. + /// + public interface IGeometryContext : IDisposable + { + /// + /// Draws an arc to the specified point. + /// + /// 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. + /// + void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection); + + /// + /// Begins a new figure. + /// + /// The starting point for the figure. + /// Whether the figure is filled. + void BeginFigure(Point startPoint, bool isFilled = true); + + /// + /// Draws a Bezier curve to the specified point. + /// + /// The first control point used to specify the shape of the curve. + /// The second control point used to specify the shape of the curve. + /// The destination point for the end of the curve. + void CubicBezierTo(Point point1, Point point2, Point point3); + + /// + /// Draws a quadratic Bezier curve to the specified point + /// + /// Control point + /// DestinationPoint + void QuadraticBezierTo(Point control, Point endPoint); + + /// + /// Draws a line to the specified point. + /// + /// The destination point. + void LineTo(Point point); + + /// + /// Ends the figure started by . + /// + /// Whether the figure is closed. + void EndFigure(bool isClosed); + + /// + /// Sets the fill rule. + /// + /// The fill rule. + void SetFillRule(FillRule fillRule); + } +} \ No newline at end of file diff --git a/src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs b/src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs index 386560c6b65..da9505cd2d2 100644 --- a/src/Avalonia.Visuals/Platform/IStreamGeometryContextImpl.cs +++ b/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 { /// /// Describes a geometry using drawing commands. /// - public interface IStreamGeometryContextImpl : IDisposable - { - /// - /// Draws an arc to the specified point. - /// - /// 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. - /// - void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection); - - /// - /// Begins a new figure. - /// - /// The starting point for the figure. - /// Whether the figure is filled. - void BeginFigure(Point startPoint, bool isFilled); - - /// - /// Draws a Bezier curve to the specified point. - /// - /// The first control point used to specify the shape of the curve. - /// The second control point used to specify the shape of the curve. - /// The destination point for the end of the curve. - void CubicBezierTo(Point point1, Point point2, Point point3); - - /// - /// Draws a quadratic Bezier curve to the specified point - /// - /// Control point - /// DestinationPoint - void QuadraticBezierTo(Point control, Point endPoint); - - /// - /// Draws a line to the specified point. - /// - /// The destination point. - void LineTo(Point point); - - /// - /// Ends the figure started by . - /// - /// Whether the figure is closed. - void EndFigure(bool isClosed); - - void SetFillRule(FillRule fillRule); + public interface IStreamGeometryContextImpl : IGeometryContext + { } } diff --git a/src/Avalonia.Visuals/Platform/PathGeometryContext.cs b/src/Avalonia.Visuals/Platform/PathGeometryContext.cs new file mode 100644 index 00000000000..cc881094fd9 --- /dev/null +++ b/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; + } + } +} diff --git a/tests/Avalonia.Benchmarks/Visuals/Media/PathMarkupParserTests.cs b/tests/Avalonia.Benchmarks/Visuals/Media/PathMarkupParserTests.cs index 6d0027fe230..c4106340b2f 100644 --- a/tests/Avalonia.Benchmarks/Visuals/Media/PathMarkupParserTests.cs +++ b/tests/Avalonia.Benchmarks/Visuals/Media/PathMarkupParserTests.cs @@ -26,14 +26,20 @@ public void Dispose() [Benchmark] public void Parse_Large_Path() { - var path = "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 parser = new PathMarkupParser(); - var result = parser.Parse(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); + } } } } diff --git a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs index d7eb6129ac7..c8bea43fb02 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs @@ -2,24 +2,23 @@ // 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 { - using System.Linq; - public class PathMarkupParserTests { [Fact] public void Parses_Move() { - using (var parser = new PathMarkupParser()) + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) { - var geometry = parser.Parse("M10 10"); + parser.Parse("M10 10"); - var figure = geometry.Figures.First(); + var figure = pathGeometry.Figures[0]; Assert.Equal(new Point(10, 10), figure.StartPoint); } @@ -28,13 +27,15 @@ public void Parses_Move() [Fact] public void Parses_Line() { - using (var parser = new PathMarkupParser()) + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) { - var geometry = parser.Parse("M0 0L10 10"); + parser.Parse("M0 0L10 10"); - var figure = geometry.Figures.First(); + var figure = pathGeometry.Figures[0]; - var segment = figure.Segments.First(); + var segment = figure.Segments[0]; Assert.IsType(segment); @@ -47,11 +48,13 @@ public void Parses_Line() [Fact] public void Parses_Close() { - using (var parser = new PathMarkupParser()) + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) { - var geometry = parser.Parse("M0 0L10 10z"); + parser.Parse("M0 0L10 10z"); - var figure = geometry.Figures.First(); + var figure = pathGeometry.Figures[0]; Assert.True(figure.IsClosed); } @@ -60,11 +63,13 @@ public void Parses_Close() [Fact] public void Parses_FillMode_Before_Move() { - using (var parser = new PathMarkupParser()) + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) { - var geometry = parser.Parse("F 1M0,0"); + parser.Parse("F 1M0,0"); - Assert.Equal(FillRule.NonZero, geometry.FillRule); + Assert.Equal(FillRule.NonZero, pathGeometry.FillRule); } } @@ -74,11 +79,13 @@ public void Parses_FillMode_Before_Move() [InlineData("M0,0,10,10,20,20")] public void Parses_Implicit_Line_Command_After_Move(string pathData) { - using (var parser = new PathMarkupParser()) + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) { - var geometry = parser.Parse(pathData); + parser.Parse(pathData); - var figure = geometry.Figures[0]; + var figure = pathGeometry.Figures[0]; var segment = figure.Segments[0]; @@ -88,7 +95,7 @@ public void Parses_Implicit_Line_Command_After_Move(string pathData) Assert.Equal(new Point(10, 10), lineSegment.Point); - figure = geometry.Figures[1]; + figure = pathGeometry.Figures[1]; segment = figure.Segments[0]; @@ -106,11 +113,13 @@ public void Parses_Implicit_Line_Command_After_Move(string pathData) [InlineData("m0,0,10,10,20,20")] public void Parses_Implicit_Line_Command_After_Relative_Move(string pathData) { - using (var parser = new PathMarkupParser()) + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) { - var geometry = parser.Parse(pathData); + parser.Parse(pathData); - var figure = geometry.Figures[0]; + var figure = pathGeometry.Figures[0]; var segment = figure.Segments[0]; @@ -155,7 +164,9 @@ public void Parses_Implicit_Line_Command_After_Relative_Move(string pathData) "12.461,8.046C14.45,8.278,16,9.949,16,12")] public void Should_Parse(string pathData) { - using (var parser = new PathMarkupParser()) + var pathGeometry = new PathGeometry(); + using (var context = new PathGeometryContext(pathGeometry)) + using (var parser = new PathMarkupParser(context)) { parser.Parse(pathData); From c36e2e422251e938c044665b88579fbef94929f1 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Mon, 18 Jun 2018 00:49:38 +0200 Subject: [PATCH 7/7] removed using --- src/Avalonia.Visuals/Media/StreamGeometry.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Avalonia.Visuals/Media/StreamGeometry.cs b/src/Avalonia.Visuals/Media/StreamGeometry.cs index 2622175d605..d3cb7884866 100644 --- a/src/Avalonia.Visuals/Media/StreamGeometry.cs +++ b/src/Avalonia.Visuals/Media/StreamGeometry.cs @@ -5,8 +5,6 @@ namespace Avalonia.Media { - using Avalonia.Visuals.Platform; - /// /// Represents the geometry of an arbitrarily complex shape. ///