diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issues15151.xaml b/src/Controls/tests/TestCases.HostApp/Issues/Issues15151.xaml
new file mode 100644
index 000000000000..f4641e9cc8c6
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issues15151.xaml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issues15151.xaml.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issues15151.xaml.cs
new file mode 100644
index 000000000000..0e8657448b9d
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/Issues/Issues15151.xaml.cs
@@ -0,0 +1,158 @@
+using System;
+using Microsoft.Maui.Graphics;
+
+namespace Maui.Controls.Sample.Issues
+{
+ [Issue(IssueTracker.Github, 15151, "PathF.Bounds returns too big boxes", PlatformAffected.All)]
+ public partial class Issues15151 : ContentPage
+ {
+ public Issues15151()
+ {
+ InitializeComponent();
+ DrawRectangleExample();
+ DrawBezierExample();
+ DrawOvalExample();
+ }
+
+ private void DrawRectangleExample()
+ {
+ // Create a simple rectangle path
+ var path = new PathF();
+ path.AppendRectangle(50, 50, 150, 100);
+
+ var tightBounds = path.CalculateTightBounds();
+ var flattenedBounds = path.GetBoundsByFlattening();
+
+ RectangleTightBounds.Text = $"Tight bounds: {tightBounds}";
+ RectangleFlattenedBounds.Text = $"Flattened bounds: {flattenedBounds}";
+
+ RectangleView.Drawable = new PathDrawable(path, tightBounds, flattenedBounds);
+ }
+
+ private void DrawBezierExample()
+ {
+ // Create a bezier curve with control points far outside
+ var path = new PathF();
+ path.MoveTo(50, 100);
+ path.CurveTo(50, 300, 300, 300, 300, 100);
+
+ var tightBounds = path.CalculateTightBounds();
+ var flattenedBounds = path.GetBoundsByFlattening();
+
+ BezierTightBounds.Text = $"Tight bounds: {tightBounds}";
+ BezierFlattenedBounds.Text = $"Flattened bounds: {flattenedBounds}";
+
+ BezierView.Drawable = new PathDrawable(path, tightBounds, flattenedBounds, true);
+ }
+
+ private void DrawOvalExample()
+ {
+ // Create an oval using the method from the issue
+ float n = (float)(4 * (Math.Sqrt(2) - 1) / 3);
+ var path = GetOval(150, 125, 100, 75, n * 100, n * 75, 1);
+
+ var tightBounds = path.CalculateTightBounds();
+ var flattenedBounds = path.GetBoundsByFlattening();
+
+ OvalTightBounds.Text = $"Tight bounds: {tightBounds}";
+ OvalFlattenedBounds.Text = $"Flattened bounds: {flattenedBounds}";
+
+ OvalView.Drawable = new PathDrawable(path, tightBounds, flattenedBounds, true);
+ }
+
+ private PathF GetOval(float x, float y, float radiusX, float radiusY, float cDx, float cDy, float deviation)
+ {
+ PathF path = new PathF();
+
+ float x1 = 0;
+ float xm = radiusX;
+ float x2 = radiusX * 2;
+
+ float y1 = 0;
+ float ym = radiusY;
+ float y2 = radiusY * 2;
+
+ x -= radiusX;
+ y -= radiusY;
+
+ float cX1 = xm - cDx;
+ float cX2 = xm + cDx;
+
+ float cY1 = ym - cDy;
+ float cY2 = ym + cDy;
+
+ path.MoveTo(x + xm, y + y2);
+ path.CurveTo(x + cX1 + deviation * 2, y + y2, x + x1, y + cY2, x + x1, y + ym);
+ path.CurveTo(x + x1, y + cY1, x + cX1 - deviation, y + y1, x + xm, y + y1);
+ path.CurveTo(x + cX2, y + y1, x + x2, y + cY1, x + x2, y + ym);
+ path.CurveTo(x + x2, y + cY2, x + cX2, y + y2, x + xm, y + y2);
+ path.Close();
+
+ return path;
+ }
+ }
+
+ // Class to draw paths and their bounds
+ public class PathDrawable : IDrawable
+ {
+ private readonly PathF _path;
+ private readonly RectF _tightBounds;
+ private readonly RectF _flattenedBounds;
+ private readonly bool _showControlPoints;
+
+ public PathDrawable(PathF path, RectF tightBounds, RectF flattenedBounds, bool showControlPoints = false)
+ {
+ _path = path;
+ _tightBounds = tightBounds;
+ _flattenedBounds = flattenedBounds;
+ _showControlPoints = showControlPoints;
+ }
+
+ public void Draw(ICanvas canvas, RectF dirtyRect)
+ {
+ // Draw the flattened bounds
+ canvas.StrokeColor = Colors.Red;
+ canvas.StrokeSize = 1;
+ canvas.DrawRectangle(_flattenedBounds);
+
+ // Draw the tight bounds
+ canvas.StrokeColor = Colors.Blue;
+ canvas.StrokeSize = 1;
+ canvas.DrawRectangle(_tightBounds);
+
+ // Draw the path
+ canvas.StrokeColor = Colors.Black;
+ canvas.StrokeSize = 2;
+ canvas.DrawPath(_path);
+
+ // Draw control points if requested (for bezier curves)
+ if (_showControlPoints)
+ {
+ canvas.StrokeColor = Colors.Red;
+ canvas.FillColor = Colors.Red;
+
+ // For simplicity, we're just showing some potential control points
+ // In a real implementation, we'd extract the actual control points from the path
+ if (_path.OperationCount > 0)
+ {
+ foreach (var point in _path.Points)
+ {
+ canvas.FillCircle(point, 3);
+ }
+ }
+ }
+
+ // Add a legend
+ canvas.FontSize = 10;
+ canvas.StrokeSize = 1;
+
+ canvas.StrokeColor = Colors.Blue;
+ canvas.DrawLine(10, 10, 30, 10);
+ canvas.DrawString("Tight Bounds", 35, 13, HorizontalAlignment.Left);
+
+ canvas.StrokeColor = Colors.Red;
+ canvas.DrawLine(10, 25, 30, 25);
+ canvas.DrawString("Flattened Bounds", 35, 28, HorizontalAlignment.Left);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue15151.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue15151.cs
new file mode 100644
index 000000000000..0742a1973461
--- /dev/null
+++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue15151.cs
@@ -0,0 +1,81 @@
+using NUnit.Framework;
+using UITest.Appium;
+using UITest.Core;
+
+namespace Microsoft.Maui.TestCases.Tests.Issues
+{
+ public class Issue15151 : _IssuesUITest
+ {
+ public override string Issue => "15151";
+
+ public Issue15151(TestDevice device) : base(device)
+ {
+ }
+
+ [Test]
+ [Category(UITestCategories.Graphics)]
+ public void PathFBoundsShouldReturnAccurateBounds()
+ {
+ // Wait for the test page to load and elements to be displayed
+ var rectangleTightBounds = App.WaitForElement("RectangleTightBounds");
+ var rectangleFlattenedBounds = App.WaitForElement("RectangleFlattenedBounds");
+ var bezierTightBounds = App.WaitForElement("BezierTightBounds");
+ var bezierFlattenedBounds = App.WaitForElement("BezierFlattenedBounds");
+ var ovalTightBounds = App.WaitForElement("OvalTightBounds");
+ var ovalFlattenedBounds = App.WaitForElement("OvalFlattenedBounds");
+
+ // Verify the bounds elements exist and have text
+ Assert.IsNotEmpty(rectangleTightBounds);
+ Assert.IsNotEmpty(rectangleFlattenedBounds);
+ Assert.IsNotEmpty(bezierTightBounds);
+ Assert.IsNotEmpty(bezierFlattenedBounds);
+ Assert.IsNotEmpty(ovalTightBounds);
+ Assert.IsNotEmpty(ovalFlattenedBounds);
+
+ // Get the text from the bounds labels
+ var rectTightText = App.FindElement("RectangleTightBounds").GetText();
+ var rectFlatText = App.FindElement("RectangleFlattenedBounds").GetText();
+ var bezierTightText = App.FindElement("BezierTightBounds").GetText();
+ var bezierFlatText = App.FindElement("BezierFlattenedBounds").GetText();
+ var ovalTightText = App.FindElement("OvalTightBounds").GetText();
+ var ovalFlatText = App.FindElement("OvalFlattenedBounds").GetText();
+
+ // Save a screenshot for verification
+ VerifyScreenshot("PathFBoundsTightVsFlattened");
+
+ // Basic value verifications
+ // 1. For rectangle, tight and flattened bounds should be the same or very close
+ Assert.AreEqual(rectTightText, rectFlatText);
+
+ // 2. For bezier curve, tight bounds should be smaller than flattened bounds
+ // Extract the height values for comparison
+ var bezierTightHeight = ExtractHeightFromBounds(bezierTightText);
+ var bezierFlatHeight = ExtractHeightFromBounds(bezierFlatText);
+ Assert.Less(bezierTightHeight, bezierFlatHeight, "Bezier tight bounds height should be less than flattened bounds height");
+
+ // 3. For oval, tight bounds should also be smaller than flattened bounds
+ var ovalTightHeight = ExtractHeightFromBounds(ovalTightText);
+ var ovalFlatHeight = ExtractHeightFromBounds(ovalFlatText);
+ Assert.Less(ovalTightHeight, ovalFlatHeight, "Oval tight bounds height should be less than flattened bounds height");
+ }
+
+ // Helper method to extract the height value from a bounds text string
+ private float ExtractHeightFromBounds(string boundsText)
+ {
+ // Example input: "Tight bounds: {X=50 Y=50 Width=150 Height=100}"
+ // Extract the value after "Height=" and before "}"
+ var heightStart = boundsText.IndexOf("Height=") + 7;
+ var heightEnd = boundsText.IndexOf("}", heightStart);
+ var heightString = boundsText.Substring(heightStart, heightEnd - heightStart);
+
+ // Handle potential spaces or commas
+ heightString = heightString.Trim();
+ if (heightString.Contains(" "))
+ {
+ heightString = heightString.Substring(0, heightString.IndexOf(" "));
+ }
+
+ return float.Parse(heightString);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Graphics/samples/GraphicsTester.Portable/Scenarios/PathBoundsTester.cs b/src/Graphics/samples/GraphicsTester.Portable/Scenarios/PathBoundsTester.cs
new file mode 100644
index 000000000000..0c35441de02c
--- /dev/null
+++ b/src/Graphics/samples/GraphicsTester.Portable/Scenarios/PathBoundsTester.cs
@@ -0,0 +1,85 @@
+using Microsoft.Maui.Graphics;
+using System;
+
+namespace GraphicsTester.Scenarios
+{
+ public class PathBoundsTester : AbstractScenario
+ {
+ public PathBoundsTester() : base(720, 1024)
+ {
+ }
+
+ public override void Draw(ICanvas canvas)
+ {
+ TestSimpleRectangle(canvas);
+ TestCubicBezierCurve(canvas);
+ TestComplexPath(canvas);
+ }
+
+ private void TestSimpleRectangle(ICanvas canvas)
+ {
+ var path = new PathF();
+ path.MoveTo(50, 50);
+ path.LineTo(250, 50);
+ path.LineTo(250, 250);
+ path.LineTo(50, 250);
+ path.Close();
+
+ var tightBounds = path.CalculateTightBounds();
+ var flattenedBounds = path.GetBoundsByFlattening();
+
+ canvas.StrokeColor = Colors.Black;
+ canvas.DrawPath(path);
+
+ canvas.FontSize = 12;
+ canvas.DrawString($"Simple Rectangle", 50, 280);
+ canvas.DrawString($"Tight bounds: {tightBounds}", 50, 300);
+ canvas.DrawString($"Flattened bounds: {flattenedBounds}", 50, 320);
+ }
+
+ private void TestCubicBezierCurve(ICanvas canvas)
+ {
+ var path = new PathF();
+ path.MoveTo(50, 400);
+ path.CurveTo(50, 900, 494, 900, 494, 400);
+
+ var tightBounds = path.CalculateTightBounds();
+ var flattenedBounds = path.GetBoundsByFlattening();
+
+ canvas.StrokeColor = Colors.Black;
+ canvas.DrawPath(path);
+
+ // Draw control points to show how they're outside the actual curve
+ canvas.StrokeColor = Colors.Red;
+ canvas.DrawCircle(50, 900, 5);
+ canvas.DrawCircle(494, 900, 5);
+
+ canvas.FontSize = 12;
+ canvas.DrawString($"Cubic Bezier Curve with Far Control Points", 50, 600);
+ canvas.DrawString($"Tight bounds: {tightBounds}", 50, 620);
+ canvas.DrawString($"Flattened bounds: {flattenedBounds}", 50, 640);
+ canvas.DrawString($"Red dots show control points outside curve", 50, 660);
+ }
+
+ private void TestComplexPath(ICanvas canvas)
+ {
+ var path = new PathF();
+ path.MoveTo(50, 700);
+ path.LineTo(150, 700);
+ path.QuadTo(200, 750, 150, 800);
+ path.CurveTo(100, 850, 0, 850, 50, 800);
+ path.Close();
+
+ var tightBounds = path.CalculateTightBounds();
+ var flattenedBounds = path.GetBoundsByFlattening();
+
+ canvas.StrokeColor = Colors.Black;
+ canvas.DrawPath(path);
+
+ canvas.FontSize = 12;
+ canvas.DrawString($"Complex Path with Multiple Segment Types", 50, 900);
+ canvas.DrawString($"Tight bounds: {tightBounds}", 50, 920);
+ canvas.DrawString($"Flattened bounds: {flattenedBounds}", 50, 940);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Graphics/samples/GraphicsTester.Portable/Scenarios/ScenarioList.cs b/src/Graphics/samples/GraphicsTester.Portable/Scenarios/ScenarioList.cs
index 983f7da39e54..dcd0aa043198 100644
--- a/src/Graphics/samples/GraphicsTester.Portable/Scenarios/ScenarioList.cs
+++ b/src/Graphics/samples/GraphicsTester.Portable/Scenarios/ScenarioList.cs
@@ -62,6 +62,7 @@ public static List Scenarios
new SubtractFromClip(),
new DimensionTest(),
new ScaleCanvas(),
+ new PathBoundsTester(),
};
}
diff --git a/src/Graphics/src/Graphics/PathF.cs b/src/Graphics/src/Graphics/PathF.cs
index 748ed21b1a5b..dacb3285fc6f 100644
--- a/src/Graphics/src/Graphics/PathF.cs
+++ b/src/Graphics/src/Graphics/PathF.cs
@@ -1408,7 +1408,7 @@ public RectF Bounds
_cachedBounds = Platform.GraphicsExtensions.AsRectangleF(cgPath.PathBoundingBox);
#else
- _cachedBounds = GetBoundsByFlattening();
+ _cachedBounds = CalculateTightBounds();
#endif
return (RectF)_cachedBounds;
@@ -1452,6 +1452,247 @@ public RectF GetBoundsByFlattening(float flatness = 0.001f)
_cachedBounds = new RectF(l, t, r - l, b - t);
return (RectF)_cachedBounds;
}
+ private RectF CalculateTightBounds()
+ {
+ if (_points == null || _points.Count == 0)
+ {
+ return new RectF(0, 0, 0, 0);
+ }
+
+ float minX = float.MaxValue;
+ float minY = float.MaxValue;
+ float maxX = float.MinValue;
+ float maxY = float.MinValue;
+
+ int pointIndex = 0;
+ int arcAngleIndex = 0;
+ int arcClockwiseIndex = 0;
+
+ // Process each operation in the path
+ for (int i = 0; i < _operations.Count; i++)
+ {
+ PathOperation operation = _operations[i];
+ switch (operation)
+ {
+ case PathOperation.Move:
+ case PathOperation.Line:
+ {
+ // For move and line operations, simply include the point
+ PointF point = _points[pointIndex++];
+ minX = Math.Min(minX, point.X);
+ minY = Math.Min(minY, point.Y);
+ maxX = Math.Max(maxX, point.X);
+ maxY = Math.Max(maxY, point.Y);
+ break;
+ }
+
+ case PathOperation.Quad:
+ {
+ // For quadratic bezier curves, we need the extreme points
+ PointF startPoint = (i > 0 && _operations[i - 1] != PathOperation.Close) ? _points[pointIndex - 1] : _points[pointIndex - 1]; // Use previous point as start
+ PointF controlPoint = _points[pointIndex++];
+ PointF endPoint = _points[pointIndex++];
+
+ // Include start and end points in bounds
+ minX = Math.Min(minX, Math.Min(startPoint.X, endPoint.X));
+ minY = Math.Min(minY, Math.Min(startPoint.Y, endPoint.Y));
+ maxX = Math.Max(maxX, Math.Max(startPoint.X, endPoint.X));
+ maxY = Math.Max(maxY, Math.Max(startPoint.Y, endPoint.Y));
+
+ // Find extreme points for quadratic bezier curve
+ FindQuadBezierExtremePoints(startPoint, controlPoint, endPoint, ref minX, ref minY, ref maxX, ref maxY);
+ break;
+ }
+
+ case PathOperation.Cubic:
+ {
+ // For cubic bezier curves, we need the extreme points
+ PointF startPoint = (i > 0 && _operations[i - 1] != PathOperation.Close) ? _points[pointIndex - 1] : _points[pointIndex - 1]; // Use previous point as start
+ PointF controlPoint1 = _points[pointIndex++];
+ PointF controlPoint2 = _points[pointIndex++];
+ PointF endPoint = _points[pointIndex++];
+
+ // Include start and end points in bounds
+ minX = Math.Min(minX, Math.Min(startPoint.X, endPoint.X));
+ minY = Math.Min(minY, Math.Min(startPoint.Y, endPoint.Y));
+ maxX = Math.Max(maxX, Math.Max(startPoint.X, endPoint.X));
+ maxY = Math.Max(maxY, Math.Max(startPoint.Y, endPoint.Y));
+
+ // Find extreme points for cubic bezier curve
+ FindCubicBezierExtremePoints(startPoint, controlPoint1, controlPoint2, endPoint, ref minX, ref minY, ref maxX, ref maxY);
+ break;
+ }
+
+ case PathOperation.Arc:
+ {
+ // For arc operations, we need to find the bounds
+ PointF topLeft = _points[pointIndex++];
+ PointF bottomRight = _points[pointIndex++];
+ float startAngle = _arcAngles[arcAngleIndex++];
+ float endAngle = _arcAngles[arcAngleIndex++];
+ bool clockwise = _arcClockwise[arcClockwiseIndex++];
+
+ // For an arc, the points are the rectangle that bounds the ellipse
+ // Just ensure these points are included
+ minX = Math.Min(minX, Math.Min(topLeft.X, bottomRight.X));
+ minY = Math.Min(minY, Math.Min(topLeft.Y, bottomRight.Y));
+ maxX = Math.Max(maxX, Math.Max(topLeft.X, bottomRight.X));
+ maxY = Math.Max(maxY, Math.Max(topLeft.Y, bottomRight.Y));
+ break;
+ }
+
+ case PathOperation.Close:
+ // Close doesn't affect bounds
+ break;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(operation), operation, $"Unexpected path operation: {operation}");
+ }
+ }
+
+ // If no bounds were found, return empty rect
+ if (minX == float.MaxValue)
+ {
+ return new RectF(0, 0, 0, 0);
+ }
+
+ return new RectF(minX, minY, maxX - minX, maxY - minY);
+ }
+
+ private void FindQuadBezierExtremePoints(PointF p0, PointF p1, PointF p2, ref float minX, ref float minY, ref float maxX, ref float maxY)
+ {
+ // For a quadratic bezier curve, extreme points can occur when t = 0, t = 1, or when the derivative is zero
+ // p(t) = (1-t)²p0 + 2(1-t)tp1 + t²p2
+ // p'(t) = 2(1-t)(p1-p0) + 2t(p2-p1)
+ // p'(t) = 0 when t = (p1-p0)/((p1-p0)+(p2-p1))
+
+ // Check X-coordinate
+ if (p1.X != p0.X && p1.X != p2.X)
+ {
+ float tx = (p1.X - p0.X) / (2 * p1.X - p0.X - p2.X);
+ if (tx > 0 && tx < 1)
+ {
+ float x = (1 - tx) * (1 - tx) * p0.X + 2 * (1 - tx) * tx * p1.X + tx * tx * p2.X;
+ minX = Math.Min(minX, x);
+ maxX = Math.Max(maxX, x);
+ }
+ }
+
+ // Check Y-coordinate
+ if (p1.Y != p0.Y && p1.Y != p2.Y)
+ {
+ float ty = (p1.Y - p0.Y) / (2 * p1.Y - p0.Y - p2.Y);
+ if (ty > 0 && ty < 1)
+ {
+ float y = (1 - ty) * (1 - ty) * p0.Y + 2 * (1 - ty) * ty * p1.Y + ty * ty * p2.Y;
+ minY = Math.Min(minY, y);
+ maxY = Math.Max(maxY, y);
+ }
+ }
+ }
+
+ private void FindCubicBezierExtremePoints(PointF p0, PointF p1, PointF p2, PointF p3, ref float minX, ref float minY, ref float maxX, ref float maxY)
+ {
+ // For a cubic bezier curve, extreme points can occur when t = 0, t = 1, or when the derivative is zero
+ // We need to solve a quadratic equation for each dimension
+
+ // For X-coordinate: Find roots of derivative equation
+ float a = 3 * (-p0.X + 3 * p1.X - 3 * p2.X + p3.X);
+ float b = 6 * (p0.X - 2 * p1.X + p2.X);
+ float c = 3 * (p1.X - p0.X);
+
+ if (Math.Abs(a) > 0.0001f) // Not a degenerate case
+ {
+ // Quadratic formula: (-b ± sqrt(b² - 4ac))/(2a)
+ float discriminant = b * b - 4 * a * c;
+ if (discriminant >= 0)
+ {
+ float sqrtDisc = (float)Math.Sqrt(discriminant);
+ float t1 = (-b + sqrtDisc) / (2 * a);
+ float t2 = (-b - sqrtDisc) / (2 * a);
+
+ // Check if t1 is in [0,1]
+ if (t1 >= 0 && t1 <= 1)
+ {
+ float x = EvaluateCubicBezier(t1, p0.X, p1.X, p2.X, p3.X);
+ minX = Math.Min(minX, x);
+ maxX = Math.Max(maxX, x);
+ }
+
+ // Check if t2 is in [0,1]
+ if (t2 >= 0 && t2 <= 1)
+ {
+ float x = EvaluateCubicBezier(t2, p0.X, p1.X, p2.X, p3.X);
+ minX = Math.Min(minX, x);
+ maxX = Math.Max(maxX, x);
+ }
+ }
+ }
+ else if (Math.Abs(b) > 0.0001f) // Linear case
+ {
+ float t = -c / b;
+ if (t >= 0 && t <= 1)
+ {
+ float x = EvaluateCubicBezier(t, p0.X, p1.X, p2.X, p3.X);
+ minX = Math.Min(minX, x);
+ maxX = Math.Max(maxX, x);
+ }
+ }
+
+ // For Y-coordinate: Find roots of derivative equation
+ a = 3 * (-p0.Y + 3 * p1.Y - 3 * p2.Y + p3.Y);
+ b = 6 * (p0.Y - 2 * p1.Y + p2.Y);
+ c = 3 * (p1.Y - p0.Y);
+
+ if (Math.Abs(a) > 0.0001f) // Not a degenerate case
+ {
+ // Quadratic formula: (-b ± sqrt(b² - 4ac))/(2a)
+ float discriminant = b * b - 4 * a * c;
+ if (discriminant >= 0)
+ {
+ float sqrtDisc = (float)Math.Sqrt(discriminant);
+ float t1 = (-b + sqrtDisc) / (2 * a);
+ float t2 = (-b - sqrtDisc) / (2 * a);
+
+ // Check if t1 is in [0,1]
+ if (t1 >= 0 && t1 <= 1)
+ {
+ float y = EvaluateCubicBezier(t1, p0.Y, p1.Y, p2.Y, p3.Y);
+ minY = Math.Min(minY, y);
+ maxY = Math.Max(maxY, y);
+ }
+
+ // Check if t2 is in [0,1]
+ if (t2 >= 0 && t2 <= 1)
+ {
+ float y = EvaluateCubicBezier(t2, p0.Y, p1.Y, p2.Y, p3.Y);
+ minY = Math.Min(minY, y);
+ maxY = Math.Max(maxY, y);
+ }
+ }
+ }
+ else if (Math.Abs(b) > 0.0001f) // Linear case
+ {
+ float t = -c / b;
+ if (t >= 0 && t <= 1)
+ {
+ float y = EvaluateCubicBezier(t, p0.Y, p1.Y, p2.Y, p3.Y);
+ minY = Math.Min(minY, y);
+ maxY = Math.Max(maxY, y);
+ }
+ }
+ }
+
+ private float EvaluateCubicBezier(float t, float p0, float p1, float p2, float p3)
+ {
+ float oneMinusT = 1 - t;
+ return oneMinusT * oneMinusT * oneMinusT * p0 +
+ 3 * oneMinusT * oneMinusT * t * p1 +
+ 3 * oneMinusT * t * t * p2 +
+ t * t * t * p3;
+ }
+
+
public PathF GetFlattenedPath(float flatness = .001f, bool includeSubPaths = false)
{
diff --git a/src/Graphics/tests/Graphics.Tests/PathBoundsTests.cs b/src/Graphics/tests/Graphics.Tests/PathBoundsTests.cs
new file mode 100644
index 000000000000..31e47d5dc64a
--- /dev/null
+++ b/src/Graphics/tests/Graphics.Tests/PathBoundsTests.cs
@@ -0,0 +1,237 @@
+using System;
+using Microsoft.Maui.Graphics;
+using Xunit;
+
+namespace Graphics.Tests
+{
+ public class PathBoundsTests
+ {
+ private const float FloatComparisonDelta = 0.001f;
+
+ [Fact]
+ public void EmptyPath_ShouldReturnZeroBounds()
+ {
+ // Arrange
+ var path = new PathF();
+
+ // Act
+ var bounds = path.CalculateTightBounds();
+
+ // Assert
+ Assert.Equal(0, bounds.X);
+ Assert.Equal(0, bounds.Y);
+ Assert.Equal(0, bounds.Width);
+ Assert.Equal(0, bounds.Height);
+ }
+
+ [Fact]
+ public void SinglePoint_ShouldReturnZeroSizeBounds()
+ {
+ // Arrange
+ var path = new PathF();
+ path.MoveTo(10, 20);
+
+ // Act
+ var bounds = path.CalculateTightBounds();
+
+ // Assert
+ Assert.Equal(10, bounds.X);
+ Assert.Equal(20, bounds.Y);
+ Assert.Equal(0, bounds.Width);
+ Assert.Equal(0, bounds.Height);
+ }
+
+ [Fact]
+ public void LinePath_ShouldReturnCorrectBounds()
+ {
+ // Arrange
+ var path = new PathF();
+ path.MoveTo(10, 20);
+ path.LineTo(100, 200);
+
+ // Act
+ var bounds = path.CalculateTightBounds();
+
+ // Assert
+ Assert.Equal(10, bounds.X);
+ Assert.Equal(20, bounds.Y);
+ Assert.Equal(90, bounds.Width);
+ Assert.Equal(180, bounds.Height);
+ }
+
+ [Fact]
+ public void RectanglePath_ShouldReturnCorrectBounds()
+ {
+ // Arrange
+ var path = new PathF();
+ path.AppendRectangle(0, 0, 200, 200);
+
+ // Act
+ var bounds = path.CalculateTightBounds();
+
+ // Assert
+ Assert.Equal(0, bounds.X);
+ Assert.Equal(0, bounds.Y);
+ Assert.Equal(200, bounds.Width);
+ Assert.Equal(200, bounds.Height);
+ }
+
+ [Fact]
+ public void QuadBezierPath_ShouldReturnCorrectBounds_WithControlPointInside()
+ {
+ // Arrange
+ var path = new PathF();
+ path.MoveTo(0, 0);
+ path.QuadTo(50, 50, 100, 0); // Control point inside the start and end points' box
+
+ // Act
+ var bounds = path.CalculateTightBounds();
+
+ // Assert
+ Assert.Equal(0, bounds.X);
+ Assert.Equal(0, bounds.Y);
+ Assert.Equal(100, bounds.Width);
+ Assert.Equal(25, bounds.Height, FloatComparisonDelta); // Highest point is at y=25 when t=0.5
+ }
+
+ [Fact]
+ public void QuadBezierPath_ShouldReturnCorrectBounds_WithControlPointOutside()
+ {
+ // Arrange
+ var path = new PathF();
+ path.MoveTo(0, 0);
+ path.QuadTo(50, 100, 100, 0); // Control point well outside the start and end points' box
+
+ // Act
+ var bounds = path.CalculateTightBounds();
+
+ // Assert
+ Assert.Equal(0, bounds.X);
+ Assert.Equal(0, bounds.Y, FloatComparisonDelta);
+ Assert.Equal(100, bounds.Width);
+ Assert.Equal(50, bounds.Height, FloatComparisonDelta); // Highest point is at y=50 when t=0.5
+ }
+
+ [Fact]
+ public void CubicBezierPath_ShouldReturnCorrectBounds_WithControlPointsInside()
+ {
+ // Arrange
+ var path = new PathF();
+ path.MoveTo(0, 0);
+ path.CurveTo(25, 25, 75, 25, 100, 0); // Control points inside
+
+ // Act
+ var bounds = path.CalculateTightBounds();
+
+ // Assert
+ Assert.Equal(0, bounds.X);
+ Assert.Equal(0, bounds.Y);
+ Assert.Equal(100, bounds.Width);
+ // Maximum height occurs somewhere around t=0.5, but exact value depends on control points
+ Assert.True(bounds.Height > 0 && bounds.Height < 25);
+ }
+
+ [Fact]
+ public void CubicBezierPath_ShouldReturnCorrectBounds_WithControlPointsOutside()
+ {
+ // Arrange
+ var path = new PathF();
+ path.MoveTo(0, 0);
+ path.CurveTo(25, 100, 75, 100, 100, 0); // Control points far outside
+
+ // Act
+ var bounds = path.CalculateTightBounds();
+
+ // Assert
+ Assert.Equal(0, bounds.X);
+ Assert.Equal(0, bounds.Y);
+ Assert.Equal(100, bounds.Width);
+ Assert.True(bounds.Height > 50 && bounds.Height < 100); // Highest point depends on control points
+ }
+
+ [Fact]
+ public void CubicBezierPath_CompareWithFlattening_ShouldBeMoreAccurate()
+ {
+ // Arrange - Create a path with a cubic bezier curve with control points far outside the actual curve
+ var path = new PathF();
+ path.MoveTo(0, 0);
+ path.CurveTo(0, 500, 444, 500, 444, 0);
+
+ // Act
+ var tightBounds = path.CalculateTightBounds();
+ var flattenedBounds = path.GetBoundsByFlattening();
+
+ // Assert
+ // The tight bounds should be within the expected range
+ Assert.Equal(0, tightBounds.X);
+ Assert.Equal(0, tightBounds.Y);
+ Assert.Equal(444, tightBounds.Width);
+ Assert.True(tightBounds.Height < flattenedBounds.Height);
+
+ // The height of tight bounds should be significantly less than the
+ // height of flattened bounds which would include control points
+ Assert.True(tightBounds.Height < 400);
+ }
+
+ [Fact]
+ public void ComplexPath_WithMultipleSegments_ShouldReturnCorrectBounds()
+ {
+ // Arrange
+ var path = new PathF();
+ path.MoveTo(100, 100);
+ path.LineTo(200, 100);
+ path.QuadTo(300, 50, 300, 200);
+ path.CurveTo(300, 300, 200, 350, 100, 300);
+ path.Close();
+
+ // Act
+ var bounds = path.CalculateTightBounds();
+
+ // Assert
+ Assert.True(bounds.X >= 100);
+ Assert.True(bounds.Y >= 50);
+ Assert.True(bounds.Width <= 200);
+ Assert.True(bounds.Height <= 300);
+
+ // Compare with flattened bounds which might be less accurate
+ var flattenedBounds = path.GetBoundsByFlattening();
+ Assert.True(bounds.Width <= flattenedBounds.Width);
+ Assert.True(bounds.Height <= flattenedBounds.Height);
+ }
+
+ [Fact]
+ public void Arc_ShouldReturnCorrectBounds()
+ {
+ // Arrange
+ var path = new PathF();
+ path.MoveTo(100, 100);
+ path.AddArc(50, 50, 150, 150, 0, 90, true);
+
+ // Act
+ var bounds = path.CalculateTightBounds();
+
+ // Assert
+ Assert.True(bounds.X >= 50);
+ Assert.True(bounds.Y >= 50);
+ Assert.True(bounds.Width <= 100);
+ Assert.True(bounds.Height <= 100);
+ }
+
+ [Fact]
+ public void CirclePath_ShouldReturnCorrectBounds()
+ {
+ // Arrange
+ var path = new PathF();
+ path.AppendCircle(100, 100, 50);
+
+ // Act
+ var bounds = path.CalculateTightBounds();
+
+ // Assert
+ Assert.Equal(50, bounds.X);
+ Assert.Equal(50, bounds.Y);
+ Assert.Equal(100, bounds.Width);
+ Assert.Equal(100, bounds.Height);
+ }
+ }
+}
\ No newline at end of file