Skip to content

Fix PathF.Bounds returning boxes that are too large #29583

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issues15151.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Maui.Controls.Sample.Issues.Issues15151"
Title="Issues15151 - PathF.Bounds">
<ScrollView>
<VerticalStackLayout Spacing="10" Padding="20">
<Label Text="PathF.Bounds - Testing accurate bounding box calculation" FontSize="Subtitle" />
<Label Text="Below are examples showing paths with their bounding boxes. Blue is the tight bounds (fixed method), red is old flattening method."
FontSize="Small" />

<Label Text="Simple Rectangle" FontSize="Medium" />
<GraphicsView x:Name="RectangleView" HeightRequest="220" WidthRequest="400" />
<Label x:Name="RectangleTightBounds" AutomationId="RectangleTightBounds" FontSize="Small" />
<Label x:Name="RectangleFlattenedBounds" AutomationId="RectangleFlattenedBounds" FontSize="Small" />

<Label Text="Cubic Bezier with Control Points Far Outside Curve" FontSize="Medium" Margin="0,20,0,0" />
<GraphicsView x:Name="BezierView" HeightRequest="300" WidthRequest="400" />
<Label x:Name="BezierTightBounds" AutomationId="BezierTightBounds" FontSize="Small" />
<Label x:Name="BezierFlattenedBounds" AutomationId="BezierFlattenedBounds" FontSize="Small" />

<Label Text="Oval with Control Points" FontSize="Medium" Margin="0,20,0,0" />
<GraphicsView x:Name="OvalView" HeightRequest="250" WidthRequest="400" />
<Label x:Name="OvalTightBounds" AutomationId="OvalTightBounds" FontSize="Small" />
<Label x:Name="OvalFlattenedBounds" AutomationId="OvalFlattenedBounds" FontSize="Small" />
</VerticalStackLayout>
</ScrollView>
</ContentPage>
159 changes: 159 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issues15151.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
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)
{
// For cubic bezier, control points could be far outside the path
for (int i = 0; i < _path.Points.Count; i++)
{
canvas.FillCircle(_path.Points[i], 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);

canvas.StrokeColor = Colors.Red;
canvas.DrawLine(10, 25, 30, 25);
canvas.DrawString("Flattened Bounds", 35, 28);
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public static List<AbstractScenario> Scenarios
new SubtractFromClip(),
new DimensionTest(),
new ScaleCanvas(),
new PathBoundsTester(),
};
}

Expand Down
Loading