File: Shapes\GeometryHelper.cs
Web Access
Project: src\src\Controls\src\Core\Controls.Core.csproj (Microsoft.Maui.Controls)
#nullable disable
using System;
using System.Collections.Generic;
using Microsoft.Maui.Graphics;
 
namespace Microsoft.Maui.Controls.Shapes
{
	/// <include file="../../../docs/Microsoft.Maui.Controls.Shapes/GeometryHelper.xml" path="Type[@FullName='Microsoft.Maui.Controls.Shapes.GeometryHelper']/Docs/*" />
	public static class GeometryHelper
	{
		/// <include file="../../../docs/Microsoft.Maui.Controls.Shapes/GeometryHelper.xml" path="//Member[@MemberName='FlattenGeometry'][1]/Docs/*" />
		public static PathGeometry FlattenGeometry(Geometry geoSrc, double tolerance)
		{
			// Return empty PathGeometry if Geometry is null
			if (geoSrc == null)
				return new PathGeometry();
 
			PathGeometry pathGeoDst = new PathGeometry();
			FlattenGeometry(pathGeoDst, geoSrc, tolerance, Matrix.Identity);
 
			return pathGeoDst;
		}
 
		/// <include file="../../../docs/Microsoft.Maui.Controls.Shapes/GeometryHelper.xml" path="//Member[@MemberName='FlattenGeometry'][2]/Docs/*" />
		public static void FlattenGeometry(PathGeometry pathGeoDst, Geometry geoSrc, double tolerance, Matrix matxPrevious)
		{
			var points = new List<Point>();
 
			Matrix matx = matxPrevious;
 
			if (geoSrc is GeometryGroup)
			{
				foreach (Geometry geoChild in (geoSrc as GeometryGroup).Children)
				{
					FlattenGeometry(pathGeoDst, geoChild, tolerance, matx);
				}
			}
			else if (geoSrc is LineGeometry)
			{
				LineGeometry lineGeoSrc = geoSrc as LineGeometry;
				PathFigure figDst = new PathFigure();
				PolyLineSegment segDst = new PolyLineSegment();
 
				figDst.StartPoint = matx.Transform(lineGeoSrc.StartPoint);
				segDst.Points.Add(matx.Transform(lineGeoSrc.EndPoint));
 
				figDst.Segments.Add(segDst);
				pathGeoDst.Figures.Add(figDst);
			}
			else if (geoSrc is RectangleGeometry)
			{
				RectangleGeometry rectGeoSrc = geoSrc as RectangleGeometry;
				PathFigure figDst = new PathFigure();
				PolyLineSegment segDst = new PolyLineSegment();
 
				figDst.StartPoint = matx.Transform(new Point(rectGeoSrc.Rect.Left, rectGeoSrc.Rect.Top));
				segDst.Points.Add(matx.Transform(new Point(rectGeoSrc.Rect.Right, rectGeoSrc.Rect.Top)));
				segDst.Points.Add(matx.Transform(new Point(rectGeoSrc.Rect.Right, rectGeoSrc.Rect.Bottom)));
				segDst.Points.Add(matx.Transform(new Point(rectGeoSrc.Rect.Left, rectGeoSrc.Rect.Bottom)));
				segDst.Points.Add(matx.Transform(new Point(rectGeoSrc.Rect.Left, rectGeoSrc.Rect.Top)));
 
				figDst.IsClosed = true;
				figDst.Segments.Add(segDst);
				pathGeoDst.Figures.Add(figDst);
			}
			else if (geoSrc is EllipseGeometry)
			{
				EllipseGeometry elipGeoSrc = geoSrc as EllipseGeometry;
				PathFigure figDst = new PathFigure();
				PolyLineSegment segDst = new PolyLineSegment();
 
				int max = (int)(4 * (elipGeoSrc.RadiusX + elipGeoSrc.RadiusY) / tolerance);
 
				for (int i = 0; i < max; i++)
				{
					double x = elipGeoSrc.Center.X + elipGeoSrc.RadiusX * Math.Sin(i * 2 * Math.PI / max);
					double y = elipGeoSrc.Center.Y - elipGeoSrc.RadiusY * Math.Cos(i * 2 * Math.PI / max);
					Point pt = matx.Transform(new Point(x, y));
 
					if (i == 0)
						figDst.StartPoint = pt;
					else
						segDst.Points.Add(pt);
				}
 
				figDst.IsClosed = true;
				figDst.Segments.Add(segDst);
				pathGeoDst.Figures.Add(figDst);
			}
			else if (geoSrc is PathGeometry)
			{
				PathGeometry pathGeoSrc = geoSrc as PathGeometry;
				pathGeoDst.FillRule = pathGeoSrc.FillRule;
 
				foreach (PathFigure figSrc in pathGeoSrc.Figures)
				{
					PathFigure figDst = new PathFigure
					{
						IsFilled = figSrc.IsFilled,
						IsClosed = figSrc.IsClosed,
						StartPoint = matx.Transform(figSrc.StartPoint)
					};
					Point ptLast = figDst.StartPoint;
 
					foreach (PathSegment segSrc in figSrc.Segments)
					{
						PolyLineSegment segDst = new PolyLineSegment();
 
						if (segSrc is LineSegment)
						{
							LineSegment lineSegSrc = segSrc as LineSegment;
							ptLast = matx.Transform(lineSegSrc.Point);
							segDst.Points.Add(ptLast);
						}
						else if (segSrc is PolyLineSegment)
						{
							PolyLineSegment polySegSrc = segSrc as PolyLineSegment;
 
							foreach (Point pt in polySegSrc.Points)
							{
								ptLast = matx.Transform(pt);
								segDst.Points.Add(ptLast);
							}
						}
						else if (segSrc is BezierSegment)
						{
							BezierSegment bezSeg = segSrc as BezierSegment;
							Point pt0 = ptLast;
							Point pt1 = matx.Transform(bezSeg.Point1);
							Point pt2 = matx.Transform(bezSeg.Point2);
							Point pt3 = matx.Transform(bezSeg.Point3);
 
							points.Clear();
							FlattenCubicBezier(points, pt0, pt1, pt2, pt3, tolerance);
 
							for (int i = 1; i < points.Count; i++)
								segDst.Points.Add(points[i]);
 
							ptLast = points[points.Count - 1];
						}
						else if (segSrc is PolyBezierSegment)
						{
							PolyBezierSegment polyBezSeg = segSrc as PolyBezierSegment;
 
							for (int bez = 0; bez < polyBezSeg.Points.Count; bez += 3)
							{
								if (bez + 2 > polyBezSeg.Points.Count - 1)
									break;
 
								Point pt0 = ptLast;
								Point pt1 = matx.Transform(polyBezSeg.Points[bez]);
								Point pt2 = matx.Transform(polyBezSeg.Points[bez + 1]);
								Point pt3 = matx.Transform(polyBezSeg.Points[bez + 2]);
 
								points.Clear();
								FlattenCubicBezier(points, pt0, pt1, pt2, pt3, tolerance);
 
								for (int i = 1; i < points.Count; i++)
									segDst.Points.Add(points[i]);
 
								ptLast = points[points.Count - 1];
							}
						}
						else if (segSrc is QuadraticBezierSegment)
						{
							QuadraticBezierSegment quadBezSeg = segSrc as QuadraticBezierSegment;
							Point pt0 = ptLast;
							Point pt1 = matx.Transform(quadBezSeg.Point1);
							Point pt2 = matx.Transform(quadBezSeg.Point2);
 
							points.Clear();
							FlattenQuadraticBezier(points, pt0, pt1, pt2, tolerance);
 
							for (int i = 1; i < points.Count; i++)
								segDst.Points.Add(points[i]);
 
							ptLast = points[points.Count - 1];
						}
						else if (segSrc is PolyQuadraticBezierSegment)
						{
							PolyQuadraticBezierSegment polyQuadBezSeg = segSrc as PolyQuadraticBezierSegment;
 
							for (int bez = 0; bez < polyQuadBezSeg.Points.Count; bez += 2)
							{
								if (bez + 1 > polyQuadBezSeg.Points.Count - 1)
									break;
 
								Point pt0 = ptLast;
								Point pt1 = matx.Transform(polyQuadBezSeg.Points[bez]);
								Point pt2 = matx.Transform(polyQuadBezSeg.Points[bez + 1]);
 
								points.Clear();
								FlattenQuadraticBezier(points, pt0, pt1, pt2, tolerance);
 
								for (int i = 1; i < points.Count; i++)
									segDst.Points.Add(points[i]);
 
								ptLast = points[points.Count - 1];
							}
						}
						else if (segSrc is ArcSegment)
						{
							ArcSegment arcSeg = segSrc as ArcSegment;
 
							points.Clear();
 
							FlattenArc(
								points,
								ptLast,
								arcSeg.Point,
								arcSeg.Size.Width,
								arcSeg.Size.Height,
								arcSeg.RotationAngle,
								arcSeg.IsLargeArc,
								arcSeg.SweepDirection == SweepDirection.CounterClockwise,
								tolerance);
 
							// Set ptLast while transferring points
							for (int i = 1; i < points.Count; i++)
								segDst.Points.Add(ptLast = points[i]);
						}
 
						figDst.Segments.Add(segDst);
					}
 
					pathGeoDst.Figures.Add(figDst);
				}
			}
		}
 
		/// <include file="../../../docs/Microsoft.Maui.Controls.Shapes/GeometryHelper.xml" path="//Member[@MemberName='FlattenCubicBezier']/Docs/*" />
		public static void FlattenCubicBezier(List<Point> points, Point ptStart, Point ptCtrl1, Point ptCtrl2, Point ptEnd, double tolerance)
		{
			int max = (int)((ptCtrl1.Distance(ptStart) + ptCtrl2.Distance(ptCtrl1) + ptEnd.Distance(ptCtrl2)) / tolerance);
 
			for (int i = 0; i <= max; i++)
			{
				double t = (double)i / max;
 
				double x = (1 - t) * (1 - t) * (1 - t) * ptStart.X +
						   3 * t * (1 - t) * (1 - t) * ptCtrl1.X +
						   3 * t * t * (1 - t) * ptCtrl2.X +
						   t * t * t * ptEnd.X;
 
				double y = (1 - t) * (1 - t) * (1 - t) * ptStart.Y +
						   3 * t * (1 - t) * (1 - t) * ptCtrl1.Y +
						   3 * t * t * (1 - t) * ptCtrl2.Y +
						   t * t * t * ptEnd.Y;
 
				points.Add(new Point(x, y));
			}
		}
 
		/// <include file="../../../docs/Microsoft.Maui.Controls.Shapes/GeometryHelper.xml" path="//Member[@MemberName='FlattenQuadraticBezier']/Docs/*" />
		public static void FlattenQuadraticBezier(List<Point> points, Point ptStart, Point ptCtrl, Point ptEnd, double tolerance)
		{
			int max = (int)((ptCtrl.Distance(ptStart) + ptEnd.Distance(ptCtrl)) / tolerance);
 
			for (int i = 0; i <= max; i++)
			{
				double t = (double)i / max;
 
				double x = (1 - t) * (1 - t) * ptStart.X +
						   2 * t * (1 - t) * ptCtrl.X +
						   t * t * ptEnd.X;
 
				double y = (1 - t) * (1 - t) * ptStart.Y +
						   2 * t * (1 - t) * ptCtrl.Y +
						   t * t * ptEnd.Y;
 
				points.Add(new Point(x, y));
			}
		}
 
		// More information: http://www.charlespetzold.com/blog/2008/01/Mathematics-of-ArcSegment.html
		/// <include file="../../../docs/Microsoft.Maui.Controls.Shapes/GeometryHelper.xml" path="//Member[@MemberName='FlattenArc']/Docs/*" />
		public static void FlattenArc(List<Point> points, Point pt1, Point pt2, double radiusX, double radiusY, double angleRotation,
			bool isLargeArc, bool isCounterclockwise, double tolerance)
		{
			// Adjust for different radii and rotation angle
			Matrix matx = new Matrix();
			matx.Rotate(-angleRotation);
			matx.Scale(radiusY / radiusX, 1);
			pt1 = matx.Transform(pt1);
			pt2 = matx.Transform(pt2);
 
			// Get info about chord that connects both points
			Point midPoint = new Point((pt1.X + pt2.X) / 2, (pt1.Y + pt2.Y) / 2);
			Point vect = new Point(pt2.X - pt1.X, pt2.Y - pt1.Y);
			double vectLength = Math.Sqrt(vect.X * vect.X + vect.Y * vect.Y);
			double halfChord = vectLength / 2;
 
			// Get vector from chord to center
			Point vectRotated;
			if (isLargeArc == isCounterclockwise)
				vectRotated = new Point(-vect.Y, vect.X);
			else
				vectRotated = new Point(vect.Y, -vect.X);
 
			// Normalize vectRotated
			double vectRotatedLength = Math.Sqrt(vectRotated.X * vectRotated.X + vectRotated.Y * vectRotated.Y);
			if (vectRotatedLength != 0)
				vectRotated = new Point(vectRotated.X / vectRotatedLength, vectRotated.Y / vectRotatedLength);
			else
				vectRotated = new Point();
 
			// Distance from chord to center
			double centerDistance = Math.Sqrt(Math.Abs((radiusY * radiusY) - (halfChord * halfChord)));
 
			// Calculate center point
			Point center = new Point(
				(centerDistance * vectRotated.X) + midPoint.X,
				(centerDistance * vectRotated.Y) + midPoint.Y);
 
			// Get angles from center to the two points
			double angle1 = Math.Atan2(pt1.Y - center.Y, pt1.X - center.X);
			double angle2 = Math.Atan2(pt2.Y - center.Y, pt2.X - center.X);
 
			double sweep = Math.Abs(angle2 - angle1);
			bool reverseArc;
 
			if (Math.IEEERemainder(sweep + 0.000005, Math.PI) < 0.000010)
			{
				reverseArc = isCounterclockwise == angle1 < angle2;
			}
			else
			{
				bool isAcute = sweep < Math.PI;
				reverseArc = isLargeArc == isAcute;
			}
 
			if (reverseArc)
			{
				if (angle1 < angle2)
					angle1 += 2 * Math.PI;
				else
					angle2 += 2 * Math.PI;
			}
 
			// Invert matrix for final point calculation
			matx.Invert();
 
			// Calculate number of points for polyline approximation
			int max = (int)(4 * (radiusX + radiusY) * Math.Abs(angle2 - angle1) / (2 * Math.PI) / tolerance);
 
			for (int i = 0; i <= max; i++)
			{
				double angle = ((max - i) * angle1 + i * angle2) / max;
				double x = center.X + radiusY * Math.Cos(angle);
				double y = center.Y + radiusY * Math.Sin(angle);
 
				Point pt = matx.Transform(new Point(x, y));
				points.Add(pt);
			}
		}
	}
}