csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
606 lines
18 KiB
606 lines
18 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using Avalonia.Platform;
|
|
|
|
namespace Avalonia.Media
|
|
{
|
|
/// <summary>
|
|
/// Parses a path markup string.
|
|
/// </summary>
|
|
public class PathMarkupParser : IDisposable
|
|
{
|
|
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? _beginFigurePoint;
|
|
private Point? _previousControlPoint;
|
|
private bool _isOpen;
|
|
private bool _isDisposed;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="PathMarkupParser"/> class.
|
|
/// </summary>
|
|
/// <param name="geometryContext">The geometry context.</param>
|
|
/// <exception cref="ArgumentNullException">geometryContext</exception>
|
|
public PathMarkupParser(IGeometryContext geometryContext)
|
|
{
|
|
if (geometryContext == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(geometryContext));
|
|
}
|
|
|
|
_geometryContext = geometryContext;
|
|
}
|
|
|
|
private enum Command
|
|
{
|
|
None,
|
|
FillRule,
|
|
Move,
|
|
Line,
|
|
HorizontalLine,
|
|
VerticalLine,
|
|
CubicBezierCurve,
|
|
QuadraticBezierCurve,
|
|
SmoothCubicBezierCurve,
|
|
SmoothQuadraticBezierCurve,
|
|
Arc,
|
|
Close
|
|
}
|
|
|
|
void IDisposable.Dispose()
|
|
{
|
|
Dispose(true);
|
|
}
|
|
|
|
protected virtual void Dispose(bool disposing)
|
|
{
|
|
if (_isDisposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (disposing)
|
|
{
|
|
_geometryContext = null;
|
|
}
|
|
|
|
_isDisposed = true;
|
|
}
|
|
|
|
private static Point MirrorControlPoint(Point controlPoint, Point center)
|
|
{
|
|
var dir = controlPoint - center;
|
|
|
|
return center + -dir;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses the specified path data and writes the result to the geometryContext of this instance.
|
|
/// </summary>
|
|
/// <param name="pathData">The path data.</param>
|
|
public void Parse(string pathData)
|
|
{
|
|
ThrowIfDisposed();
|
|
|
|
var span = pathData.AsSpan();
|
|
_currentPoint = new Point();
|
|
|
|
while(!span.IsEmpty)
|
|
{
|
|
if(!ReadCommand(ref span, out var command, out var relative))
|
|
{
|
|
break;
|
|
}
|
|
|
|
bool initialCommand = true;
|
|
|
|
do
|
|
{
|
|
if (!initialCommand)
|
|
{
|
|
span = ReadSeparator(span);
|
|
}
|
|
|
|
switch (command)
|
|
{
|
|
case Command.None:
|
|
break;
|
|
case Command.FillRule:
|
|
SetFillRule(ref span);
|
|
break;
|
|
case Command.Move:
|
|
AddMove(ref span, relative);
|
|
break;
|
|
case Command.Line:
|
|
AddLine(ref span, relative);
|
|
break;
|
|
case Command.HorizontalLine:
|
|
AddHorizontalLine(ref span, relative);
|
|
break;
|
|
case Command.VerticalLine:
|
|
AddVerticalLine(ref span, relative);
|
|
break;
|
|
case Command.CubicBezierCurve:
|
|
AddCubicBezierCurve(ref span, relative);
|
|
break;
|
|
case Command.QuadraticBezierCurve:
|
|
AddQuadraticBezierCurve(ref span, relative);
|
|
break;
|
|
case Command.SmoothCubicBezierCurve:
|
|
AddSmoothCubicBezierCurve(ref span, relative);
|
|
break;
|
|
case Command.SmoothQuadraticBezierCurve:
|
|
AddSmoothQuadraticBezierCurve(ref span, relative);
|
|
break;
|
|
case Command.Arc:
|
|
AddArc(ref span, relative);
|
|
break;
|
|
case Command.Close:
|
|
CloseFigure();
|
|
break;
|
|
default:
|
|
throw new NotSupportedException("Unsupported command");
|
|
}
|
|
|
|
initialCommand = false;
|
|
} while (PeekArgument(span));
|
|
|
|
}
|
|
|
|
if (_isOpen)
|
|
{
|
|
_geometryContext.EndFigure(false);
|
|
}
|
|
}
|
|
|
|
private void CreateFigure()
|
|
{
|
|
ThrowIfDisposed();
|
|
|
|
if (_isOpen)
|
|
{
|
|
_geometryContext.EndFigure(false);
|
|
}
|
|
|
|
_geometryContext.BeginFigure(_currentPoint);
|
|
|
|
_beginFigurePoint = _currentPoint;
|
|
|
|
_isOpen = true;
|
|
}
|
|
|
|
private void SetFillRule(ref ReadOnlySpan<char> span)
|
|
{
|
|
ThrowIfDisposed();
|
|
|
|
if (!ReadArgument(ref span, out var fillRule) || fillRule.Length != 1)
|
|
{
|
|
throw new InvalidDataException("Invalid fill rule.");
|
|
}
|
|
|
|
FillRule rule;
|
|
|
|
switch (fillRule[0])
|
|
{
|
|
case '0':
|
|
rule = FillRule.EvenOdd;
|
|
break;
|
|
case '1':
|
|
rule = FillRule.NonZero;
|
|
break;
|
|
default:
|
|
throw new InvalidDataException("Invalid fill rule");
|
|
}
|
|
|
|
_geometryContext.SetFillRule(rule);
|
|
}
|
|
|
|
private void CloseFigure()
|
|
{
|
|
ThrowIfDisposed();
|
|
|
|
if (_isOpen)
|
|
{
|
|
_geometryContext.EndFigure(true);
|
|
|
|
if (_beginFigurePoint != null)
|
|
{
|
|
_currentPoint = _beginFigurePoint.Value;
|
|
_beginFigurePoint = null;
|
|
}
|
|
}
|
|
|
|
_previousControlPoint = null;
|
|
|
|
_isOpen = false;
|
|
}
|
|
|
|
private void AddMove(ref ReadOnlySpan<char> span, bool relative)
|
|
{
|
|
var currentPoint = relative
|
|
? ReadRelativePoint(ref span, _currentPoint)
|
|
: ReadPoint(ref span);
|
|
|
|
_currentPoint = currentPoint;
|
|
|
|
CreateFigure();
|
|
|
|
while (PeekArgument(span))
|
|
{
|
|
span = ReadSeparator(span);
|
|
AddLine(ref span, relative);
|
|
}
|
|
}
|
|
|
|
private void AddLine(ref ReadOnlySpan<char> span, bool relative)
|
|
{
|
|
ThrowIfDisposed();
|
|
|
|
_currentPoint = relative
|
|
? ReadRelativePoint(ref span, _currentPoint)
|
|
: ReadPoint(ref span);
|
|
|
|
if (!_isOpen)
|
|
{
|
|
CreateFigure();
|
|
}
|
|
|
|
_geometryContext.LineTo(_currentPoint);
|
|
}
|
|
|
|
private void AddHorizontalLine(ref ReadOnlySpan<char> span, bool relative)
|
|
{
|
|
ThrowIfDisposed();
|
|
|
|
_currentPoint = relative
|
|
? new Point(_currentPoint.X + ReadDouble(ref span), _currentPoint.Y)
|
|
: _currentPoint.WithX(ReadDouble(ref span));
|
|
|
|
if (!_isOpen)
|
|
{
|
|
CreateFigure();
|
|
}
|
|
|
|
_geometryContext.LineTo(_currentPoint);
|
|
}
|
|
|
|
private void AddVerticalLine(ref ReadOnlySpan<char> span, bool relative)
|
|
{
|
|
ThrowIfDisposed();
|
|
|
|
_currentPoint = relative
|
|
? new Point(_currentPoint.X, _currentPoint.Y + ReadDouble(ref span))
|
|
: _currentPoint.WithY(ReadDouble(ref span));
|
|
|
|
if (!_isOpen)
|
|
{
|
|
CreateFigure();
|
|
}
|
|
|
|
_geometryContext.LineTo(_currentPoint);
|
|
}
|
|
|
|
private void AddCubicBezierCurve(ref ReadOnlySpan<char> span, bool relative)
|
|
{
|
|
ThrowIfDisposed();
|
|
|
|
var point1 = relative
|
|
? ReadRelativePoint(ref span, _currentPoint)
|
|
: ReadPoint(ref span);
|
|
|
|
span = ReadSeparator(span);
|
|
|
|
var point2 = relative
|
|
? ReadRelativePoint(ref span, _currentPoint)
|
|
: ReadPoint(ref span);
|
|
|
|
_previousControlPoint = point2;
|
|
|
|
span = ReadSeparator(span);
|
|
|
|
var point3 = relative
|
|
? ReadRelativePoint(ref span, _currentPoint)
|
|
: ReadPoint(ref span);
|
|
|
|
if (!_isOpen)
|
|
{
|
|
CreateFigure();
|
|
}
|
|
|
|
_geometryContext.CubicBezierTo(point1, point2, point3);
|
|
|
|
_currentPoint = point3;
|
|
}
|
|
|
|
private void AddQuadraticBezierCurve(ref ReadOnlySpan<char> span, bool relative)
|
|
{
|
|
ThrowIfDisposed();
|
|
|
|
var start = relative
|
|
? ReadRelativePoint(ref span, _currentPoint)
|
|
: ReadPoint(ref span);
|
|
|
|
_previousControlPoint = start;
|
|
|
|
span = ReadSeparator(span);
|
|
|
|
var end = relative
|
|
? ReadRelativePoint(ref span, _currentPoint)
|
|
: ReadPoint(ref span);
|
|
|
|
if (!_isOpen)
|
|
{
|
|
CreateFigure();
|
|
}
|
|
|
|
_geometryContext.QuadraticBezierTo(start, end);
|
|
|
|
_currentPoint = end;
|
|
}
|
|
|
|
private void AddSmoothCubicBezierCurve(ref ReadOnlySpan<char> span, bool relative)
|
|
{
|
|
ThrowIfDisposed();
|
|
|
|
var point2 = relative
|
|
? ReadRelativePoint(ref span, _currentPoint)
|
|
: ReadPoint(ref span);
|
|
|
|
span = ReadSeparator(span);
|
|
|
|
var end = relative
|
|
? ReadRelativePoint(ref span, _currentPoint)
|
|
: ReadPoint(ref span);
|
|
|
|
if (_previousControlPoint != null)
|
|
{
|
|
_previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint);
|
|
}
|
|
|
|
if (!_isOpen)
|
|
{
|
|
CreateFigure();
|
|
}
|
|
|
|
_geometryContext.CubicBezierTo(_previousControlPoint ?? _currentPoint, point2, end);
|
|
|
|
_previousControlPoint = point2;
|
|
|
|
_currentPoint = end;
|
|
}
|
|
|
|
private void AddSmoothQuadraticBezierCurve(ref ReadOnlySpan<char> span, bool relative)
|
|
{
|
|
ThrowIfDisposed();
|
|
|
|
var end = relative
|
|
? ReadRelativePoint(ref span, _currentPoint)
|
|
: ReadPoint(ref span);
|
|
|
|
if (_previousControlPoint != null)
|
|
{
|
|
_previousControlPoint = MirrorControlPoint((Point)_previousControlPoint, _currentPoint);
|
|
}
|
|
|
|
if (!_isOpen)
|
|
{
|
|
CreateFigure();
|
|
}
|
|
|
|
_geometryContext.QuadraticBezierTo(_previousControlPoint ?? _currentPoint, end);
|
|
|
|
_currentPoint = end;
|
|
}
|
|
|
|
private void AddArc(ref ReadOnlySpan<char> span, bool relative)
|
|
{
|
|
ThrowIfDisposed();
|
|
|
|
var size = ReadSize(ref span);
|
|
|
|
span = ReadSeparator(span);
|
|
|
|
var rotationAngle = ReadDouble(ref span);
|
|
span = ReadSeparator(span);
|
|
var isLargeArc = ReadBool(ref span);
|
|
|
|
span = ReadSeparator(span);
|
|
|
|
var sweepDirection = ReadBool(ref span) ? SweepDirection.Clockwise : SweepDirection.CounterClockwise;
|
|
|
|
span = ReadSeparator(span);
|
|
|
|
var end = relative
|
|
? ReadRelativePoint(ref span, _currentPoint)
|
|
: ReadPoint(ref span);
|
|
|
|
if (!_isOpen)
|
|
{
|
|
CreateFigure();
|
|
}
|
|
|
|
_geometryContext.ArcTo(end, size, rotationAngle, isLargeArc, sweepDirection);
|
|
|
|
_currentPoint = end;
|
|
|
|
_previousControlPoint = null;
|
|
}
|
|
|
|
private static bool PeekArgument(ReadOnlySpan<char> span)
|
|
{
|
|
span = SkipWhitespace(span);
|
|
|
|
return !span.IsEmpty && (span[0] == ',' || span[0] == '-' || span[0] == '.' || char.IsDigit(span[0]));
|
|
}
|
|
|
|
private static bool ReadArgument(ref ReadOnlySpan<char> remaining, out ReadOnlySpan<char> argument)
|
|
{
|
|
remaining = SkipWhitespace(remaining);
|
|
if (remaining.IsEmpty)
|
|
{
|
|
argument = ReadOnlySpan<char>.Empty;
|
|
return false;
|
|
}
|
|
|
|
var valid = false;
|
|
int i = 0;
|
|
if (remaining[i] == '-')
|
|
{
|
|
i++;
|
|
}
|
|
for (; i < remaining.Length && char.IsNumber(remaining[i]); i++) valid = true;
|
|
|
|
if (i < remaining.Length && remaining[i] == '.')
|
|
{
|
|
valid = false;
|
|
i++;
|
|
}
|
|
for (; i < remaining.Length && char.IsNumber(remaining[i]); i++) valid = true;
|
|
|
|
if (i < remaining.Length)
|
|
{
|
|
// scientific notation
|
|
if (remaining[i] == 'E' || remaining[i] == 'e')
|
|
{
|
|
valid = false;
|
|
i++;
|
|
if (remaining[i] == '-' || remaining[i] == '+')
|
|
{
|
|
i++;
|
|
for (; i < remaining.Length && char.IsNumber(remaining[i]); i++) valid = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!valid)
|
|
{
|
|
argument = ReadOnlySpan<char>.Empty;
|
|
return false;
|
|
}
|
|
argument = remaining.Slice(0, i);
|
|
remaining = remaining.Slice(i);
|
|
return true;
|
|
}
|
|
|
|
|
|
private static ReadOnlySpan<char> ReadSeparator(ReadOnlySpan<char> span)
|
|
{
|
|
span = SkipWhitespace(span);
|
|
if (!span.IsEmpty && span[0] == ',')
|
|
{
|
|
span = span.Slice(1);
|
|
}
|
|
return span;
|
|
}
|
|
|
|
private static ReadOnlySpan<char> SkipWhitespace(ReadOnlySpan<char> span)
|
|
{
|
|
int i = 0;
|
|
for (; i < span.Length && char.IsWhiteSpace(span[i]); i++) ;
|
|
return span.Slice(i);
|
|
}
|
|
|
|
private bool ReadBool(ref ReadOnlySpan<char> span)
|
|
{
|
|
span = SkipWhitespace(span);
|
|
|
|
if (span.IsEmpty)
|
|
{
|
|
throw new InvalidDataException("Invalid bool rule.");
|
|
}
|
|
|
|
var c = span[0];
|
|
|
|
span = span.Slice(1);
|
|
|
|
switch (c)
|
|
{
|
|
case '0':
|
|
return false;
|
|
case '1':
|
|
return true;
|
|
default:
|
|
throw new InvalidDataException("Invalid bool rule");
|
|
}
|
|
}
|
|
|
|
private double ReadDouble(ref ReadOnlySpan<char> span)
|
|
{
|
|
if (!ReadArgument(ref span, out var doubleValue))
|
|
{
|
|
throw new InvalidDataException("Invalid double value");
|
|
}
|
|
|
|
return double.Parse(doubleValue.ToString(), CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
private Size ReadSize(ref ReadOnlySpan<char> span)
|
|
{
|
|
var width = ReadDouble(ref span);
|
|
span = ReadSeparator(span);
|
|
var height = ReadDouble(ref span);
|
|
return new Size(width, height);
|
|
}
|
|
|
|
private Point ReadPoint(ref ReadOnlySpan<char> span)
|
|
{
|
|
var x = ReadDouble(ref span);
|
|
span = ReadSeparator(span);
|
|
var y = ReadDouble(ref span);
|
|
return new Point(x, y);
|
|
}
|
|
|
|
private Point ReadRelativePoint(ref ReadOnlySpan<char> span, Point origin)
|
|
{
|
|
var x = ReadDouble(ref span);
|
|
span = ReadSeparator(span);
|
|
var y = ReadDouble(ref span);
|
|
return new Point(origin.X + x, origin.Y + y);
|
|
}
|
|
|
|
private bool ReadCommand(ref ReadOnlySpan<char> span, out Command command, out bool relative)
|
|
{
|
|
span = SkipWhitespace(span);
|
|
if (span.IsEmpty)
|
|
{
|
|
command = default;
|
|
relative = false;
|
|
return false;
|
|
}
|
|
var c = span[0];
|
|
if (!s_commands.TryGetValue(char.ToUpperInvariant(c), out command))
|
|
{
|
|
throw new InvalidDataException("Unexpected path command '" + c + "'.");
|
|
}
|
|
relative = char.IsLower(c);
|
|
span = span.Slice(1);
|
|
return true;
|
|
}
|
|
|
|
[MemberNotNull(nameof(_geometryContext))]
|
|
private void ThrowIfDisposed()
|
|
{
|
|
if (_isDisposed || _geometryContext is null)
|
|
throw new ObjectDisposedException(nameof(PathMarkupParser));
|
|
}
|
|
}
|
|
}
|
|
|