File: PathBuilder.cs
Web Access
Project: src\src\Graphics\src\Graphics\Graphics.csproj (Microsoft.Maui.Graphics)
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
 
namespace Microsoft.Maui.Graphics
{
	public class PathBuilder
	{
		public static PathF Build(string definition)
		{
			if (string.IsNullOrEmpty(definition))
				return new PathF();
 
			var pathBuilder = new PathBuilder();
			var path = pathBuilder.BuildPath(definition);
			return path;
		}
 
		private readonly Stack<string> _commandStack = new Stack<string>();
		private bool _closeWhenDone;
		private char _lastCommand = '~';
		private PointF? _lastCurveControlPoint;
		private PointF? _lastMoveTo;
 
		private PathF _path;
		private PointF? _relativePoint;
 
		private bool NextBoolValue
		{
			get
			{
				string vValueAsString = _commandStack.Pop();
 
				if ("1".Equals(vValueAsString, StringComparison.Ordinal))
				{
					return true;
				}
 
				return false;
			}
		}
 
		private float NextValue
		{
			get
			{
				string vValueAsString = _commandStack.Pop();
				try
				{
					return ParseFloat(vValueAsString);
				}
				catch (Exception exc)
				{
					throw new Exception("Error parsing a path value.", exc);
				}
			}
		}
 
		public static float ParseFloat(string value)
		{
			if (float.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var number))
			{
				return number;
			}
 
			// Note: Illustrator will sometimes export numbers that look like "5.96.88", so we need to be able to handle them
			var split = value.Split(new[] { '.' });
			if (split.Length > 2)
			{
				if (float.TryParse($"{split[0]}.{split[1]}", NumberStyles.Any, CultureInfo.InvariantCulture, out number))
				{
					return number;
				}
			}
 
			string stringValue = GetNumbersOnly(value);
			if (float.TryParse(stringValue, NumberStyles.Any, CultureInfo.InvariantCulture, out number))
			{
				return number;
			}
 
			throw new Exception($"Error parsing {value} as a float.");
		}
 
		private static string GetNumbersOnly(string value)
		{
			var builder = new StringBuilder(value.Length);
			foreach (char c in value)
			{
				if (char.IsDigit(c) || c == '.' || c == '-')
				{
					builder.Append(c);
				}
			}
 
			return builder.ToString();
		}
 
		public PathF BuildPath(string pathAsString)
		{
			try
			{
				_lastCommand = '~';
				_lastCurveControlPoint = null;
				_path = null;
				_commandStack.Clear();
				_relativePoint = new PointF(0, 0);
				_closeWhenDone = false;
 
#if DEBUG_PATH
				System.Diagnostics.Debug.WriteLine(aPathString);
#endif
#if NETSTANDARD2_0
				pathAsString = pathAsString.Replace("Infinity", "0");
#else
				pathAsString = pathAsString.Replace("Infinity", "0", StringComparison.Ordinal);
#endif
				pathAsString = SeparateLetterCharsWithSpaces(pathAsString);
#if NETSTANDARD2_0
				pathAsString = pathAsString.Replace("-", " -");
				pathAsString = pathAsString.Replace(" E  -", "E-");
				pathAsString = pathAsString.Replace(" e  -", "e-");
#else
				pathAsString = pathAsString.Replace("-", " -", StringComparison.Ordinal);
				pathAsString = pathAsString.Replace(" E  -", "E-", StringComparison.Ordinal);
				pathAsString = pathAsString.Replace(" e  -", "e-", StringComparison.Ordinal);
#endif
#if DEBUG_PATH
				System.Diagnostics.Debug.WriteLine(aPathString);
#endif
				string[] args = pathAsString.Split(new[] { ' ', '\r', '\n', '\t', ',' }, StringSplitOptions.RemoveEmptyEntries);
				for (int i = args.Length - 1; i >= 0; i--)
				{
					string entry = args[i];
					char c = entry[0];
					if (char.IsLetter(c))
					{
						if (entry.Length > 1)
						{
							entry = entry.Substring(1);
							if (char.IsLetter(entry[0]))
							{
								if (entry.Length > 1)
								{
									_commandStack.Push(entry.Substring(1));
#if DEBUG_PATH
										System.Diagnostics.Debug.WriteLine(vEntry.Substring(1));
#endif
								}
 
								_commandStack.Push(entry[0].ToInvariantString());
#if DEBUG_PATH
								 System.Diagnostics.Debug.WriteLine(vEntry[0].ToString());
#endif
							}
							else
							{
								_commandStack.Push(entry);
#if DEBUG_PATH
								System.Diagnostics.Debug.WriteLine(vEntry);
#endif
							}
						}
 
						_commandStack.Push(c.ToInvariantString());
#if DEBUG_PATH
						System.Diagnostics.Debug.WriteLine(vChar.ToString());
#endif
					}
					else
					{
						_commandStack.Push(entry);
#if DEBUG_PATH
						System.Diagnostics.Debug.WriteLine(vEntry);
#endif
					}
				}
 
				while (_commandStack.Count > 0)
				{
					if (_path == null)
					{
						_path = new PathF();
					}
 
					string topCommand = _commandStack.Pop();
					var firstLetter = topCommand[0];
 
					if (IsCommand(firstLetter))
						HandleCommand(topCommand);
					else
					{
						_commandStack.Push(topCommand);
						HandleCommand(_lastCommand.ToString());
					}
				}
 
				if (_path != null && !_path.Closed)
				{
					if (_closeWhenDone)
					{
						_path.Close();
					}
				}
			}
			catch (Exception exc)
			{
				System.Diagnostics.Debug.WriteLine("=== An error occurred parsing the path. ===", exc);
				System.Diagnostics.Debug.WriteLine(pathAsString);
#if DEBUG
				throw;
#endif
			}
 
			static string SeparateLetterCharsWithSpaces(string input)
			{
				var sb = new StringBuilder(input.Length, maxCapacity: 3 * input.Length);
				foreach (var character in input)
				{
					if (char.IsLetter(character))
					{
						sb.Append(' ');
						sb.Append(character);
						sb.Append(' ');
					}
					else
					{
						sb.Append(character);
					}
				}
				return sb.ToString();
			}
 
			return _path;
		}
 
		private bool IsCommand(char firstLetter)
		{
			if (char.IsDigit(firstLetter))
				return false;
 
			if (firstLetter == '.')
				return false;
 
			if (firstLetter == '-')
				return false;
 
			if (firstLetter == 'e' || firstLetter == 'E')
				return false;
 
			return true;
		}
 
		private void HandleCommand(string command)
		{
			char c = command[0];
 
			if (_lastCommand != '~' && (char.IsDigit(c) || c == '-'))
			{
				var previousCommand = _commandStack.Peek()?[0];
 
				if (_lastCommand == 'M')
				{
					_commandStack.Push(command);
					HandleCommand('L');
				}
				else if (_lastCommand == 'm')
				{
					_commandStack.Push(command);
					HandleCommand('l');
				}
				else if (_lastCommand == 'L')
				{
					_commandStack.Push(command);
					HandleCommand('L');
				}
				else if (_lastCommand == 'l')
				{
					_commandStack.Push(command);
					HandleCommand('l');
				}
				else if (_lastCommand == 'H')
				{
					_commandStack.Push(command);
					HandleCommand('H');
				}
				else if (_lastCommand == 'h')
				{
					_commandStack.Push(command);
					HandleCommand('h');
				}
				else if (_lastCommand == 'V')
				{
					_commandStack.Push(command);
					HandleCommand('V');
				}
				else if (_lastCommand == 'v')
				{
					_commandStack.Push(command);
					HandleCommand('v');
				}
				else if (_lastCommand == 'C')
				{
					_commandStack.Push(command);
					HandleCommand('C');
				}
				else if (_lastCommand == 'c')
				{
					_commandStack.Push(command);
					HandleCommand('c');
				}
				else if (_lastCommand == 'S')
				{
					_commandStack.Push(command);
					HandleCommand('S');
				}
				else if (_lastCommand == 's')
				{
					_commandStack.Push(command);
					HandleCommand('s');
				}
				else if (_lastCommand == 'Q')
				{
					_commandStack.Push(command);
					HandleCommand('Q');
				}
				else if (_lastCommand == 'q')
				{
					_commandStack.Push(command);
					HandleCommand('q');
				}
				else if (_lastCommand == 'T')
				{
					_commandStack.Push(command);
					HandleCommand('T', previousCommand);
				}
				else if (_lastCommand == 't')
				{
					_commandStack.Push(command);
					HandleCommand('t', previousCommand);
				}
				else if (_lastCommand == 'A')
				{
					_commandStack.Push(command);
					HandleCommand('A');
				}
				else if (_lastCommand == 'a')
				{
					_commandStack.Push(command);
					HandleCommand('a');
				}
				else
				{
					System.Diagnostics.Debug.WriteLine("Don't know how to handle the path command: " + command);
				}
			}
			else
			{
				HandleCommand(c);
			}
		}
 
		private void HandleCommand(char command, char? previousCommand = null)
		{
			if (command == 'M')
			{
				MoveTo(false);
			}
			else if (command == 'm')
			{
				MoveTo(true);
				if (_lastCommand == '~')
				{
					command = 'm';
				}
			}
			else if (command == 'z' || command == 'Z')
			{
				ClosePath();
			}
			else if (command == 'L')
			{
				LineTo(false);
			}
			else if (command == 'l')
			{
				LineTo(true);
			}
			else if (command == 'Q')
			{
				QuadTo(false);
			}
			else if (command == 'q')
			{
				QuadTo(true);
			}
			else if (command == 'T')
			{
				ReflectiveQuadTo(false, previousCommand);
			}
			else if (command == 't')
			{
				ReflectiveQuadTo(true, previousCommand);
			}
			else if (command == 'C')
			{
				CurveTo(false);
			}
			else if (command == 'c')
			{
				CurveTo(true);
			}
			else if (command == 'S')
			{
				SmoothCurveTo(false);
			}
			else if (command == 's')
			{
				SmoothCurveTo(true);
			}
			else if (command == 'A')
			{
				ArcTo(false);
			}
			else if (command == 'a')
			{
				ArcTo(true);
			}
			else if (command == 'H')
			{
				HorizontalLineTo(false);
			}
			else if (command == 'h')
			{
				HorizontalLineTo(true);
			}
			else if (command == 'V')
			{
				VerticalLineTo(false);
			}
			else if (command == 'v')
			{
				VerticalLineTo(true);
			}
			else
			{
				System.Diagnostics.Debug.WriteLine("Don't know how to handle the path command: " + command);
			}
 
			if (!(command == 'C' || command == 'c' || command == 's' || command == 'S'))
			{
				_lastCurveControlPoint = null;
			}
 
			_lastCommand = command;
		}
 
		private void ClosePath()
		{
			_path.Close();
			_relativePoint = _lastMoveTo;
 
			/*int vSegments = path.SegmentCount;
		 if (vSegments >= 2)
		 {
			if (path.GetSegmentType(vSegments-2) == PathOperation.MOVE_TO && path.GetSegmentType(vSegments-1) == PathOperation.CLOSE)
			{
			   path.RemoveAllSegmentsAfter(vSegments-2);
			}
		 }*/
		}
 
		private void MoveTo(bool isRelative)
		{
			if (_path.SubPathCount == 1)
			{
				if (_path.FirstPoint.Equals(_path.LastPoint))
				{
					_closeWhenDone = true;
				}
			}
 
			var xOffset = NextValue;
			var yOffset = NextValue;
			var point = NewPoint(xOffset, yOffset, isRelative, true);
			_path.MoveTo(point);
			_lastMoveTo = point;
		}
 
		private void LineTo(bool isRelative)
		{
			var point = NewPoint(NextValue, NextValue, isRelative, true);
			_path.LineTo(point);
		}
 
		private void HorizontalLineTo(bool isRelative)
		{
			var point = NewHorizontalPoint(NextValue, isRelative, true);
			_path.LineTo(point);
		}
 
		private void VerticalLineTo(bool isRelative)
		{
			var point = NewVerticalPoint(NextValue, isRelative, true);
			_path.LineTo(point);
		}
 
		private void CurveTo(bool isRelative)
		{
			var point1 = NewPoint(NextValue, NextValue, isRelative, false);
			float x = NextValue;
			float y = NextValue;
 
			bool isQuad = char.IsLetter(_commandStack.Peek()[0]);
			var point2 = NewPoint(x, y, isRelative, isQuad);
 
			if (isQuad)
			{
				_path.QuadTo(point1, point2);
				_lastCurveControlPoint = point1;
			}
			else
			{
				var point3 = NewPoint(NextValue, NextValue, isRelative, true);
				_path.CurveTo(point1, point2, point3);
				_lastCurveControlPoint = point2;
				//System.Diagnostics.Debug.WriteLine($"CurveTo({point1.X},{point1.Y},{point2.X},{point2.Y},{point3.X},{point3.Y})");
			}
		}
 
		private void QuadTo(bool isRelative)
		{
			var point1 = NewPoint(NextValue, NextValue, isRelative, false);
			var point2 = NewPoint(NextValue, NextValue, isRelative, true);
			_lastCurveControlPoint = point1;
			_path.QuadTo(point1, point2);
		}
 
		private void ReflectiveQuadTo(bool isRelative, char? previousCommand)
		{
			var lastPoint = _path.LastPoint;
			var point1 = lastPoint;
			var lastCurveControlPoint = _lastCurveControlPoint ?? default;
			switch (previousCommand)
			{
				case 'Q':
				case 'q':
				case 'T':
				case 't':
					var dx = lastPoint.X - lastCurveControlPoint.X;
					var dy = lastPoint.Y - lastCurveControlPoint.Y;
					point1 = point1.Offset(dx, dy);
					break;
			}
			var point2 = NewPoint(NextValue, NextValue, isRelative, true);
			_lastCurveControlPoint = point1;
			_path.QuadTo(point1, point2);
		}
 
		private void SmoothCurveTo(bool isRelative)
		{
			PointF? point1 = null;
			var point2 = NewPoint(NextValue, NextValue, isRelative, false);
 
			// ReSharper disable ConvertIfStatementToNullCoalescingExpression
			if (_lastCurveControlPoint == null && _relativePoint != null)
			{
				// ReSharper restore ConvertIfStatementToNullCoalescingExpression
				point1 = GeometryUtil.GetOppositePoint((PointF)_relativePoint, point2);
			}
			else if (_relativePoint != null && _lastCurveControlPoint != null)
			{
				point1 = GeometryUtil.GetOppositePoint((PointF)_relativePoint, (PointF)_lastCurveControlPoint);
			}
 
			var point3 = NewPoint(NextValue, NextValue, isRelative, true);
			if (point1 != null)
				_path.CurveTo((PointF)point1, point2, point3);
			_lastCurveControlPoint = point2;
		}
 
		private void ArcTo(bool isRelative)
		{
			var startPoint = _relativePoint ?? default;
 
			var rx = NextValue;
			var ry = NextValue;
 
			var r = NextValue;
			var largeArcFlag = NextBoolValue;
			var sweepFlag = NextBoolValue;
			var endPoint = NewPoint(NextValue, NextValue, isRelative, false);
 
			var arcPath = new PathF(startPoint);
			arcPath.SVGArcTo(rx, ry, r, largeArcFlag, sweepFlag, endPoint.X, endPoint.Y, startPoint.X, startPoint.Y);
 
			for (int s = 0; s < arcPath.OperationCount; s++)
			{
				var segmentType = arcPath.GetSegmentType(s);
				var pointsInSegment = arcPath.GetPointsForSegment(s);
 
				if (segmentType == PathOperation.Move)
				{
					// do nothing
				}
				else if (segmentType == PathOperation.Line)
				{
					_path.LineTo(pointsInSegment[0]);
				}
				else if (segmentType == PathOperation.Cubic)
				{
					_path.CurveTo(pointsInSegment[0], pointsInSegment[1], pointsInSegment[2]);
				}
				else if (segmentType == PathOperation.Quad)
				{
					_path.QuadTo(pointsInSegment[0], pointsInSegment[1]);
				}
			}
 
			_relativePoint = _path.LastPoint;
		}
 
		private PointF NewPoint(float x, float y, bool isRelative, bool isReference)
		{
			PointF point = default;
 
			if (isRelative && _relativePoint != null)
			{
				point = new PointF(((PointF)_relativePoint).X + x, ((PointF)_relativePoint).Y + y);
			}
			else
			{
				point = new PointF(x, y);
			}
 
			// If this is the reference point, we want to store the location before
			// we translate it into the final coordinates.  This way, future relative
			// points will start from an un-translated position.
			if (isReference)
			{
				_relativePoint = point;
			}
 
			return point;
		}
 
		private PointF NewVerticalPoint(float y, bool isRelative, bool isReference)
		{
			PointF point = default;
 
			if (isRelative && _relativePoint != null)
			{
				point = new PointF(((PointF)_relativePoint).X, ((PointF)_relativePoint).Y + y);
			}
			else if (_relativePoint != null)
			{
				point = new PointF(((PointF)_relativePoint).X, y);
			}
 
			// If this is the reference point, we want to store the location before
			// we translate it into the final coordinates.  This way, future relative
			// points will start from an un-translated position.
			if (isReference)
			{
				_relativePoint = point;
			}
 
			return point;
		}
 
		private PointF NewHorizontalPoint(float x, bool isRelative, bool isReference)
		{
			PointF point = default;
 
			if (isRelative && _relativePoint != null)
			{
				point = new PointF(((PointF)_relativePoint).X + x, ((PointF)_relativePoint).Y);
			}
			else if (_relativePoint != null)
			{
				point = new PointF(x, ((PointF)_relativePoint).Y);
			}
 
			// If this is the reference point, we want to store the location before
			// we translate it into the final coordinates.  This way, future relative
			// points will start from an un-translated position.
			if (isReference)
			{
				_relativePoint = point;
			}
 
			return point;
		}
	}
}