Skip to content

Commit

Permalink
[VisualDiagnostics] Fix RectangleAdorner with updated math (#8728)
Browse files Browse the repository at this point in the history
  • Loading branch information
drasticactions authored Jul 15, 2022
1 parent 951f65e commit 0779d0f
Show file tree
Hide file tree
Showing 2 changed files with 263 additions and 12 deletions.
212 changes: 212 additions & 0 deletions src/Core/src/VisualDiagnostics/AdornerModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Platform;

namespace Microsoft.Maui
{
internal class AdornerModel
{
Rect boundingBox = new Rect();
List<Rect> marginZones = new List<Rect>();

public Rect BoundingBox => boundingBox;
public IReadOnlyList<Rect> MarginZones => marginZones;


public void Update(Rect rect, Thickness margin, Matrix4x4 transformToRoot, double density)
{
double unitsPerPixel = 1 / density;
boundingBox = DpiHelper.RoundToPixel(rect, unitsPerPixel);

marginZones.Clear();

if (margin == new Thickness())
{
return;
}

// If transform to root is more than just an offset (i.e. element or
// some of its ancestors are scaled/rotated/etc.) then no margins to
// render. This matches WPF, UWP and WinUI adorners.
if (!Tolerances.AreClose(transformToRoot.M11, 1) ||
!Tolerances.AreClose(transformToRoot.M22, 1) ||
!Tolerances.AreClose(transformToRoot.M12, 0) ||
!Tolerances.AreClose(transformToRoot.M21, 0))
{
return;
}

// Create up to 4 rectangles for margins. Keep in mind that some of
// margin values can be negative, e.g. Margin="-10, 20, 30, -40".

// Left
if (!Tolerances.NearZero(margin.Left))
{
Rect rc = new Rect();
rc.Left = Math.Min(rect.Left - margin.Left, rect.Left);
rc.Width = Math.Abs(margin.Left);
rc.Top = rect.Top;
rc.Bottom = rect.Bottom;
TryAddMarginZone(rc, unitsPerPixel);
}

// Right
if (!Tolerances.NearZero(margin.Right))
{
Rect rc = new Rect();
rc.Left = Math.Min(rect.Right, rect.Right + margin.Right);
rc.Width = Math.Abs(margin.Right);
rc.Top = rect.Top;
rc.Bottom = rect.Bottom;
TryAddMarginZone(rc, unitsPerPixel);
}

// Top
if (!Tolerances.NearZero(margin.Top))
{
Rect rc = new Rect();
rc.Left = rect.Left - Math.Max(0, margin.Left);
rc.Right = rect.Right + Math.Max(0, margin.Right);
rc.Top = Math.Min(rect.Top - margin.Top, rect.Top);
rc.Height = Math.Abs(margin.Top);
TryAddMarginZone(rc, unitsPerPixel);
}

// Bottom
if (!Tolerances.NearZero(margin.Bottom))
{
Rect rc = new Rect();
rc.Left = rect.Left - Math.Max(0, margin.Left);
rc.Right = rect.Right + Math.Max(0, margin.Right);
rc.Top = Math.Min(rect.Bottom + margin.Bottom, rect.Bottom);
rc.Height = Math.Abs(margin.Bottom);
TryAddMarginZone(rc, unitsPerPixel);
}
}

void TryAddMarginZone(Rect rect, double unitsPerPixel)
{
Rect rc = DpiHelper.RoundToPixel(rect, unitsPerPixel);
if (!rc.IsEmpty)
marginZones.Add(rc);
}

/// <summary>
/// DPI related utilities.
/// </summary>
private static class DpiHelper
{
/// <summary>
/// Rounds unit value to nearest pixel.
/// </summary>
/// <returns>
/// Rounded value in units.
/// </returns>
public static double RoundToPixel(double units, double unitsPerPixel)
{
double pixels = units / unitsPerPixel;
double floorPixels = (double)Math.Floor(pixels);
pixels = Tolerances.LessThan(pixels, floorPixels + 0.5) ?
floorPixels : (double)Math.Ceiling(pixels);
return pixels * unitsPerPixel;
}

/// <summary>
/// Rounds point X and Y coordinates to nearest pixel.
/// </summary>
public static Point RoundToPixel(Point point, double unitsPerPixel)
{
Point pixelPoint = new Point(
DpiHelper.RoundToPixel(point.X, unitsPerPixel),
DpiHelper.RoundToPixel(point.Y, unitsPerPixel));
return pixelPoint;
}

/// <summary>
/// Rounds rectangle corner coordinates to nearest pixel.
/// </summary>
public static Rect RoundToPixel(Rect rect, double unitsPerPixel)
{
double left = DpiHelper.RoundToPixel(rect.Left, unitsPerPixel);
double top = DpiHelper.RoundToPixel(rect.Top, unitsPerPixel);
double right = DpiHelper.RoundToPixel(rect.Right, unitsPerPixel);
double bottom = DpiHelper.RoundToPixel(rect.Bottom, unitsPerPixel);
return new Rect(left, top, right - left, bottom - top);
}
}

/// <summary>
/// Helper utilities to compare double values.
/// </summary>
private static class Tolerances
{
const double Epsilon = 2.2204460492503131e-016;
const double ZeroThreshold = 2.2204460492503131e-015;

public static bool AreClose(Point point1, Point point2)
{
if (Tolerances.AreClose(point1.X, point2.X) && Tolerances.AreClose(point1.Y, point2.Y))
{
return true;
}

return false;
}

public static bool NearZero(double value)
{
return Math.Abs(value) < Tolerances.ZeroThreshold;
}

public static bool AreClose(double value1, double value2)
{
//in case they are Infinities (then epsilon check does not work)
if (value1 == value2) return true;
// This computes (|value1-value2| / (|value1| + |value2| + 10.0)) < Tolerances.Epsilon
double eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * Tolerances.Epsilon;
double delta = value1 - value2;
return (-eps < delta) && (eps > delta);
}

public static bool GreaterThan(double value1, double value2)
{
if (value1 > value2)
{
return !Tolerances.AreClose(value1, value2);
}
return false;
}

public static bool GreaterThanOrClose(double value1, double value2)
{
if (value1 <= value2)
{
return Tolerances.AreClose(value1, value2);
}
return true;
}

public static bool LessThan(double value1, double value2)
{
if (value1 < value2)
{
return !Tolerances.AreClose(value1, value2);
}
return false;
}

public static bool LessThanOrClose(double value1, double value2)
{
if (value1 >= value2)
{
return Tolerances.AreClose(value1, value2);
}
return true;
}
}
}
}

63 changes: 51 additions & 12 deletions src/Core/src/VisualDiagnostics/RectangleAdorner.cs
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using Microsoft.Maui.Graphics;
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using Microsoft.Maui.Graphics;

namespace Microsoft.Maui
{
Expand All @@ -7,6 +10,9 @@ namespace Microsoft.Maui
/// </summary>
public class RectangleAdorner : IAdorner
{

readonly AdornerModel model = new();

/// <summary>
/// Initializes a new instance of the <see cref="RectangleAdorner"/> class.
/// </summary>
Expand All @@ -22,7 +28,13 @@ public RectangleAdorner(IView view, float density = 1, Point? offset = null, Col
Offset = offset ?? Point.Zero;
VisualView = view;
Density = density;
DrawnRectangle = Rect.Zero;

// Sanity check
if (Density < 0.1)
{
Density = 1;
Debug.Fail($"Invalid density {density}");
}
}

/// <inheritdoc/>
Expand All @@ -37,27 +49,54 @@ public RectangleAdorner(IView view, float density = 1, Point? offset = null, Col

public Color StrokeColor { get; }

public Rect DrawnRectangle { get; private set; }
public Rect DrawnRectangle => model.BoundingBox;

/// <inheritdoc/>
public virtual bool Contains(Point point) =>
DrawnRectangle.Contains(point);
public virtual bool Contains(Point point)
{
if (model.BoundingBox.Contains(point))
{
return true;
}

if (model.MarginZones.Any(r => r.Contains(point)))
{
return true;
}

return false;
}

/// <inheritdoc/>
public virtual void Draw(ICanvas canvas, RectF dirtyRect)
{
UpdateModel();

// Draw highlight rectangle
canvas.FillColor = FillColor;
canvas.StrokeColor = StrokeColor;
canvas.FillRectangle(model.BoundingBox);
}

void UpdateModel()
{
Rect box = VisualView.GetBoundingBox();

var boundingBox = VisualView.GetBoundingBox();
var x = (boundingBox.X / Density) + Offset.X;
var y = (boundingBox.Y / Density) + Offset.Y;
var width = boundingBox.Width / Density;
var height = boundingBox.Height / Density;
box.X = box.X + Offset.X;
box.Y = box.Y + Offset.Y;

DrawnRectangle = new Rect(x, y, width, height);
Matrix4x4 transform = VisualView.GetViewTransform();
Thickness margin;
if (VisualView is IView view)
{
margin = view.Margin;
}
else
{
margin = new Thickness();
}

canvas.FillRectangle(DrawnRectangle);
model.Update(box, margin, transform, Density);
}
}
}

0 comments on commit 0779d0f

Please sign in to comment.