|
|
@@ -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;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|