[DisplayList] implement shadow bounds without relying on Skia utilities (#172572)

Right now DisplayList defers to Skia to compute the bounds of shadow
operations, but we really need to own this code now that we are doing
our own rendering so I ported it over and simplified it to just deal
with the cases we care about.

Eliminates a bullet item in
https://github.com/flutter/flutter/issues/161456
This commit is contained in:
Jim Graham 2025-07-23 08:20:31 -07:00 committed by GitHub
parent 0a2ee25955
commit 3deedd23cd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 548 additions and 9 deletions

View File

@ -167,6 +167,7 @@ if (enable_unittests) {
sources = [
"benchmarking/dl_complexity_unittests.cc",
"display_list_unittests.cc",
"dl_canvas_unittests.cc",
"dl_color_unittests.cc",
"dl_paint_unittests.cc",
"dl_storage_unittests.cc",
@ -180,6 +181,7 @@ if (enable_unittests) {
"geometry/dl_path_unittests.cc",
"geometry/dl_region_unittests.cc",
"geometry/dl_rtree_unittests.cc",
"skia/dl_sk_canvas_unittests.cc",
"skia/dl_sk_conversions_unittests.cc",
"skia/dl_sk_paint_dispatcher_unittests.cc",
"utils/dl_accumulation_rect_unittests.cc",

View File

@ -4,9 +4,121 @@
#include "flutter/display_list/dl_canvas.h"
#include "flutter/display_list/geometry/dl_geometry_conversions.h"
#include "flutter/third_party/skia/include/core/SkPoint3.h"
#include "flutter/third_party/skia/include/utils/SkShadowUtils.h"
namespace {
// ShadowBounds code adapted from SkShadowUtils using the Directional flag.
using DlScalar = flutter::DlScalar;
using DlVector3 = flutter::DlVector3;
using DlVector2 = flutter::DlVector2;
using DlRect = flutter::DlRect;
using DlMatrix = flutter::DlMatrix;
static constexpr DlScalar kAmbientHeightFactor = 1.0f / 128.0f;
static constexpr DlScalar kAmbientGeomFactor = 64.0f;
// Assuming that we have a light height of 600 for the spot shadow, the spot
// values will reach their maximum at a height of approximately 292.3077.
// We'll round up to 300 to keep it simple.
static constexpr DlScalar kMaxAmbientRadius =
300.0f * kAmbientHeightFactor * kAmbientGeomFactor;
inline DlScalar AmbientBlurRadius(DlScalar height) {
return std::min(height * kAmbientHeightFactor * kAmbientGeomFactor,
kMaxAmbientRadius);
}
struct DrawShadowRec {
DlVector3 light_position;
DlScalar light_radius = 0.0f;
DlScalar occluder_z = 0.0f;
};
static inline float DivideAndClamp(float numer,
float denom,
float min,
float max) {
float result = std::clamp(numer / denom, min, max);
// ensure that clamp handled non-finites correctly
FML_DCHECK(result >= min && result <= max);
return result;
}
inline void GetDirectionalParams(DrawShadowRec params,
DlScalar* blur_radius,
DlScalar* scale,
DlVector2* translate) {
*blur_radius = params.light_radius * params.occluder_z;
*scale = 1.0f;
// Max z-ratio is ("max expected elevation" / "min allowable z").
constexpr DlScalar kMaxZRatio = 64.0f / flutter::kEhCloseEnough;
DlScalar zRatio = DivideAndClamp(params.occluder_z, params.light_position.z,
0.0f, kMaxZRatio);
*translate = DlVector2(-zRatio * params.light_position.x,
-zRatio * params.light_position.y);
}
DlRect GetLocalBounds(DlRect ambient_bounds,
const DlMatrix& matrix,
const DrawShadowRec& params) {
if (!matrix.IsInvertible() || ambient_bounds.IsEmpty()) {
return DlRect();
}
DlScalar ambient_blur;
DlScalar spot_blur;
DlScalar spot_scale;
DlVector2 spot_offset;
if (matrix.HasPerspective2D()) {
// transform ambient and spot bounds into device space
ambient_bounds = ambient_bounds.TransformAndClipBounds(matrix);
// get ambient blur (in device space)
ambient_blur = AmbientBlurRadius(params.occluder_z);
// get spot params (in device space)
GetDirectionalParams(params, &spot_blur, &spot_scale, &spot_offset);
} else {
auto min_scale = matrix.GetMinScale2D();
// We've already checked the matrix for perspective elements.
FML_DCHECK(min_scale.has_value());
DlScalar device_to_local_scale = 1.0f / min_scale.value_or(1.0f);
// get ambient blur (in local space)
DlScalar device_space_ambient_blur = AmbientBlurRadius(params.occluder_z);
ambient_blur = device_space_ambient_blur * device_to_local_scale;
// get spot params (in local space)
GetDirectionalParams(params, &spot_blur, &spot_scale, &spot_offset);
// light dir is in device space, map spot offset back into local space
DlMatrix inverse = matrix.Invert();
spot_offset = inverse.TransformDirection(spot_offset);
// convert spot blur to local space
spot_blur *= device_to_local_scale;
}
// in both cases, adjust ambient and spot bounds
DlRect spot_bounds = ambient_bounds;
ambient_bounds = ambient_bounds.Expand(ambient_blur);
spot_bounds = spot_bounds.Scale(spot_scale);
spot_bounds = spot_bounds.Shift(spot_offset);
spot_bounds = spot_bounds.Expand(spot_blur);
// merge bounds
DlRect result = ambient_bounds.Union(spot_bounds);
// outset a bit to account for floating point error
result = result.Expand(1.0f, 1.0f);
// if perspective, transform back to src space
if (matrix.HasPerspective2D()) {
DlMatrix inverse = matrix.Invert();
result = result.TransformAndClipBounds(inverse);
}
return result;
}
} // namespace
namespace flutter {
@ -14,12 +126,13 @@ DlRect DlCanvas::ComputeShadowBounds(const DlPath& path,
float elevation,
DlScalar dpr,
const DlMatrix& ctm) {
SkRect shadow_bounds(ToSkRect(path.GetBounds()));
SkShadowUtils::GetLocalBounds(
ToSkMatrix(ctm), path.GetSkPath(), SkPoint3::Make(0, 0, dpr * elevation),
SkPoint3::Make(0, -1, 1), kShadowLightRadius / kShadowLightHeight,
SkShadowFlags::kDirectionalLight_ShadowFlag, &shadow_bounds);
return ToDlRect(shadow_bounds);
return GetLocalBounds(
path.GetBounds(), ctm,
{
.light_position = DlVector3(0.0f, -1.0f, 1.0f),
.light_radius = kShadowLightRadius / kShadowLightHeight,
.occluder_z = dpr * elevation,
});
}
} // namespace flutter

View File

@ -195,6 +195,23 @@ class DlCanvas {
DlScalar x,
DlScalar y,
const DlPaint& paint) = 0;
/// @brief Draws the shadow of the given |path| rendered in the provided
/// |color| (which is only consulted for its opacity) as would be
/// produced by a directional light source uniformly shining in
/// the device space direction {0, -1, 1} against a backdrop
/// which is |elevation * dpr| device coordinates below the |path|
/// in the Z direction.
///
/// Normally the renderer might consider omitting the rendering of any
/// of the shadow pixels that fall under the |path| itself, as an
/// optimization, unless the |transparent_occluder| flag is specified
/// which would indicate that the optimization isn't appropriate.
///
/// Note that the |elevation| and |dpr| are unique in the API for being
/// considered in pure device coordinates while the |path| is interpreted
/// relative to the current local-to-device transform.
///
/// @see |ComputeShadowBounds|
virtual void DrawShadow(const DlPath& path,
const DlColor color,
const DlScalar elevation,
@ -206,6 +223,16 @@ class DlCanvas {
static constexpr DlScalar kShadowLightHeight = 600;
static constexpr DlScalar kShadowLightRadius = 800;
/// @brief Compute the local coverage for a |DrawShadow| operation using
/// the given parameters (excluding the color and the transparent
/// occluder parameters which do not affect the bounds).
///
/// Since the elevation is expressed in device coordinates relative to the
/// provided |dpr| value, the |ctm| of the final rendering coordinate
/// system that will be applied to the path must be provided so the two
/// sets of coordinates (path and light source) can be correlated.
///
/// @see |DrawShadow|
static DlRect ComputeShadowBounds(const DlPath& path,
float elevation,
DlScalar dpr,

View File

@ -0,0 +1,57 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "flutter/display_list/dl_canvas.h"
#include "gtest/gtest.h"
namespace flutter {
namespace testing {
TEST(DisplayListCanvas, GetShadowBoundsScaleTranslate) {
DlMatrix matrix =
DlMatrix::MakeTranslateScale({5.0f, 7.0f, 1.0f}, {10.0f, 15.0f, 7.0f});
DlPath path = DlPath::MakeRectLTRB(100, 100, 200, 200);
DlRect shadow_bounds =
DlCanvas::ComputeShadowBounds(path, 5.0f, 2.0f, matrix);
EXPECT_FLOAT_EQ(shadow_bounds.GetLeft(), 96.333336f);
EXPECT_FLOAT_EQ(shadow_bounds.GetTop(), 97.761909f);
EXPECT_FLOAT_EQ(shadow_bounds.GetRight(), 203.66667f);
EXPECT_FLOAT_EQ(shadow_bounds.GetBottom(), 205.09525f);
}
TEST(DisplayListCanvas, GetShadowBoundsScaleTranslateRotate) {
DlMatrix matrix =
DlMatrix::MakeTranslateScale({5.0f, 7.0f, 1.0f}, {10.0f, 15.0f, 7.0f});
matrix = matrix * DlMatrix::MakeRotationZ(DlDegrees(45));
DlPath path = DlPath::MakeRectLTRB(100, 100, 200, 200);
DlRect shadow_bounds =
DlCanvas::ComputeShadowBounds(path, 5.0f, 2.0f, matrix);
EXPECT_FLOAT_EQ(shadow_bounds.GetLeft(), 97.343491f);
EXPECT_FLOAT_EQ(shadow_bounds.GetTop(), 97.343491f);
EXPECT_FLOAT_EQ(shadow_bounds.GetRight(), 204.67682f);
EXPECT_FLOAT_EQ(shadow_bounds.GetBottom(), 204.67682f);
}
TEST(DisplayListCanvas, GetShadowBoundsScaleTranslatePerspective) {
DlMatrix matrix =
DlMatrix::MakeTranslateScale({5.0f, 7.0f, 1.0f}, {10.0f, 15.0f, 7.0f});
matrix.m[3] = 0.001f;
DlPath path = DlPath::MakeRectLTRB(100, 100, 200, 200);
DlRect shadow_bounds =
DlCanvas::ComputeShadowBounds(path, 5.0f, 2.0f, matrix);
EXPECT_FLOAT_EQ(shadow_bounds.GetLeft(), 96.535324f);
EXPECT_FLOAT_EQ(shadow_bounds.GetTop(), 90.253288f);
EXPECT_FLOAT_EQ(shadow_bounds.GetRight(), 204.15054f);
EXPECT_FLOAT_EQ(shadow_bounds.GetBottom(), 223.3252f);
}
} // namespace testing
} // namespace flutter

View File

@ -20,6 +20,7 @@ using DlRadians = impeller::Radians;
using DlPoint = impeller::Point;
using DlVector2 = impeller::Vector2;
using DlVector3 = impeller::Vector3;
using DlIPoint = impeller::IPoint32;
using DlSize = impeller::Size;
using DlISize = impeller::ISize32;

View File

@ -0,0 +1,80 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "flutter/display_list/skia/dl_sk_canvas.h"
#include "flutter/display_list/skia/dl_sk_conversions.h"
#include "flutter/third_party/skia/include/utils/SkShadowUtils.h"
#include "gtest/gtest.h"
namespace flutter {
namespace testing {
namespace {
void TestShadowBounds(bool with_rotate, bool with_perspective) {
const SkVector3 light_position = SkVector3::Make(0.0f, -1.0f, 1.0f);
const DlScalar light_radius =
DlCanvas::kShadowLightRadius / DlCanvas::kShadowLightHeight;
DlPath dl_path = DlPath::MakeRectLTRB(100, 100, 200, 200);
for (int dpr = 1; dpr <= 2; dpr++) {
for (int elevation = 1; elevation <= 5; elevation++) {
SkVector3 z_params = SkVector3::Make(0.0f, 0.0f, elevation * dpr);
for (int i = 1; i <= 10; i++) {
DlScalar xScale = static_cast<DlScalar>(i);
for (int j = 1; j <= 10; j++) {
DlScalar yScale = static_cast<DlScalar>(j);
DlMatrix matrix = DlMatrix::MakeTranslateScale({xScale, yScale, 1.0f},
{10.0f, 15.0f, 7.0f});
if (with_rotate) {
matrix = matrix * DlMatrix::MakeRotationZ(DlDegrees(45));
}
if (with_perspective) {
matrix.m[3] = 0.001f;
}
SkMatrix sk_matrix;
ASSERT_TRUE(ToSk(&matrix, sk_matrix) != nullptr);
SkMatrix sk_inverse = sk_matrix;
ASSERT_TRUE(sk_matrix.invert(&sk_inverse));
auto label = (std::stringstream()
<< "Matrix: " << matrix << ", elevation = " << elevation
<< ", dpr = " << dpr)
.str();
DlRect dl_bounds =
DlCanvas::ComputeShadowBounds(dl_path, elevation, dpr, matrix);
SkRect sk_bounds;
ASSERT_TRUE(SkShadowUtils::GetLocalBounds(
sk_matrix, dl_path.GetSkPath(), z_params, light_position,
light_radius, kDirectionalLight_ShadowFlag, &sk_bounds))
<< label;
EXPECT_FLOAT_EQ(dl_bounds.GetLeft(), sk_bounds.fLeft) << label;
EXPECT_FLOAT_EQ(dl_bounds.GetTop(), sk_bounds.fTop) << label;
EXPECT_FLOAT_EQ(dl_bounds.GetRight(), sk_bounds.fRight) << label;
EXPECT_FLOAT_EQ(dl_bounds.GetBottom(), sk_bounds.fBottom) << label;
}
}
}
}
}
} // namespace
TEST(DlSkCanvas, ShadowBoundsCompatibilityTranslateScale) {
TestShadowBounds(false, false);
}
TEST(DlSkCanvas, ShadowBoundsCompatibilityTranslateScaleRotate) {
TestShadowBounds(true, false);
}
TEST(DlSkCanvas, ShadowBoundsCompatibilityTranslateScalePerspective) {
TestShadowBounds(false, true);
}
} // namespace testing
} // namespace flutter

View File

@ -7,6 +7,8 @@
#include <climits>
#include <sstream>
#include "flutter/fml/logging.h"
namespace impeller {
Matrix::Matrix(const MatrixDecomposition& d) : Matrix() {
@ -358,6 +360,76 @@ std::optional<MatrixDecomposition> Matrix::Decompose() const {
return result;
}
std::optional<std::pair<Scalar, Scalar>> Matrix::GetScales2D() const {
if (HasPerspective2D()) {
return std::nullopt;
}
// We only operate on the uppermost 2x2 matrix since those are the only
// values that can induce a scale on 2D coordinates.
// [ a b ]
// [ c d ]
double a = m[0];
double b = m[1];
double c = m[4];
double d = m[5];
if (b == 0.0f && c == 0.0f) {
return {{std::abs(a), std::abs(d)}};
}
if (a == 0.0f && d == 0.0f) {
return {{std::abs(b), std::abs(c)}};
}
// Compute eigenvalues for the matrix (transpose(A) * A):
// [ a2 b2 ] == [ a b ] [ a c ] == [ aa + bb ac + bd ]
// [ c2 d2 ] [ c d ] [ b d ] [ ac + bd cc + dd ]
// (note the reverse diagonal entries in the answer are identical)
double a2 = a * a + b * b;
double b2 = a * c + b * d;
double c2 = b2;
double d2 = c * c + d * d;
//
// If L is an eigenvalue, then
// det(this - L*Identity) == 0
// det([ a - L b ]
// [ c d - L ]) == 0
// (a - L) * (d - L) - bc == 0
// ad - aL - dL + L^2 - bc == 0
// L^2 + (-a + -d)L + ad - bc == 0
//
// Using quadratic equation for (Ax^2 + Bx + C):
// A == 1
// B == -(a2 + d2)
// C == a2d2 - b2c2
//
// (We use -B for calculations because the square is the same as B and we
// need -B for the final quadratic equation computations anyway.)
double minus_B = a2 + d2;
double C = a2 * d2 - b2 * c2;
double B_squared_minus_4AC = minus_B * minus_B - 4 * 1.0f * C;
double quadratic_sqrt;
if (B_squared_minus_4AC <= 0.0f) {
// This test should never fail, but we might be slightly negative
FML_DCHECK(B_squared_minus_4AC + kEhCloseEnough >= 0.0f);
// Uniform scales (possibly rotated) would tend to end up here
// in which case both eigenvalues are identical
quadratic_sqrt = 0.0f;
} else {
quadratic_sqrt = std::sqrt(B_squared_minus_4AC);
}
// Since this is returning the sqrt of the values, we can guarantee that
// the returned scales are non-negative.
FML_DCHECK(minus_B - quadratic_sqrt >= 0.0f);
FML_DCHECK(minus_B + quadratic_sqrt >= 0.0f);
return {{std::sqrt((minus_B - quadratic_sqrt) / 2.0f),
std::sqrt((minus_B + quadratic_sqrt) / 2.0f)}};
}
uint64_t MatrixDecomposition::GetComponentsMask() const {
uint64_t mask = 0;

View File

@ -320,6 +320,11 @@ struct Matrix {
bool IsInvertible() const { return GetDeterminant() != 0; }
/// @brief Return the maximum scale applied specifically to either the
/// X axis or Y axis unit vectors (the bases). The matrix might
/// lengthen a non-axis-aligned vector by more than this value.
///
/// @see |GetMaxScale2D|
inline Scalar GetMaxBasisLengthXY() const {
// The full basis computation requires computing the squared scaling factor
// for translate/scale only matrices. This substantially limits the range of
@ -332,6 +337,54 @@ struct Matrix {
e[1][0] * e[1][0] + e[1][1] * e[1][1]));
}
/// @brief Return the smaller of the two non-negative scales that will
/// be applied to 2D coordinates by this matrix. If the matrix
/// has perspective components, the method will return a nullopt.
///
/// Note that negative scale factors really represent a positive scale
/// factor with a flip, so the absolute value (the positive scale factor)
/// is returned instead so that the results can be directly applied to
/// rendering calculations to compute the potential size of an operation.
///
/// This method differs from the "basis length" methods in that those
/// methods answer the question "how much does this transform stretch
/// perfectly horizontal or vertical source vectors, whereas this method
/// can answer "what's the smallest scale applied to any vector regardless
/// of direction".
///
/// @see |GetScales2D|
std::optional<Scalar> GetMinScale2D() const {
auto scales = GetScales2D();
if (!scales.has_value()) {
return std::nullopt;
}
return std::min(scales->first, scales->second);
}
/// @brief Return the smaller of the two non-negative scales that will
/// be applied to 2D coordinates by this matrix. If the matrix
/// has perspective components, the method will return a nullopt.
///
/// Note that negative scale factors really represent a positive scale
/// factor with a flip, so the absolute value (the positive scale factor)
/// is returned instead so that the results can be directly applied to
/// rendering calculations to compute the potential size of an operation.
///
/// This method differs from the "basis length" methods in that those
/// methods answer the question "how much does this transform stretch
/// perfectly horizontal or vertical source vectors, whereas this method
/// can answer "what's the largest scale applied to any vector regardless
/// of direction".
///
/// @see |GetScales2D|
std::optional<Scalar> GetMaxScale2D() const {
auto scales = GetScales2D();
if (!scales.has_value()) {
return std::nullopt;
}
return std::max(scales->first, scales->second);
}
constexpr Vector3 GetBasisX() const { return Vector3(m[0], m[1], m[2]); }
constexpr Vector3 GetBasisY() const { return Vector3(m[4], m[5], m[6]); }
@ -450,6 +503,20 @@ struct Matrix {
std::optional<MatrixDecomposition> Decompose() const;
/// @brief Compute the two non-negative scales applied by this matrix to
/// 2D coordinates and return them as an optional pair of Scalar
/// values in any order. If the matrix has perspective elements,
/// this method will return a nullopt.
///
/// Note that negative scale factors really represent a positive scale
/// factor with a flip, so the absolute value (the positive scale factor)
/// is returned instead so that the results can be directly applied to
/// rendering calculations to compute the potential size of an operation.
///
/// @see |GetMinScale2D|
/// @see |GetMaxScale2D|
std::optional<std::pair<Scalar, Scalar>> GetScales2D() const;
bool Equals(const Matrix& matrix, Scalar epsilon = 1e-5f) const {
const Scalar* a = m;
const Scalar* b = matrix.m;

View File

@ -322,5 +322,125 @@ TEST(MatrixTest, To3x3) {
EXPECT_TRUE(MatrixNear(x.To3x3(), Matrix()));
}
TEST(MatrixTest, MinMaxScales2D) {
// The GetScales2D() method is allowed to return the scales in any
// order so we need to take special care in verifying the return
// value to test them in either order.
auto check_pair = [](const Matrix& matrix, Scalar scale1, Scalar scale2) {
auto pair = matrix.GetScales2D();
EXPECT_TRUE(pair.has_value())
<< "Scales: " << scale1 << ", " << scale2 << ", " << matrix;
if (ScalarNearlyEqual(pair->first, scale1)) {
EXPECT_FLOAT_EQ(pair->first, scale1) << matrix;
EXPECT_FLOAT_EQ(pair->second, scale2) << matrix;
} else {
EXPECT_FLOAT_EQ(pair->first, scale2) << matrix;
EXPECT_FLOAT_EQ(pair->second, scale1) << matrix;
}
};
for (int i = 1; i < 10; i++) {
Scalar xScale = static_cast<Scalar>(i);
for (int j = 1; j < 10; j++) {
Scalar yScale = static_cast<Scalar>(j);
Scalar minScale = std::min(xScale, yScale);
Scalar maxScale = std::max(xScale, yScale);
{
// Simple scale
Matrix matrix = Matrix::MakeScale({xScale, yScale, 1.0f});
EXPECT_TRUE(matrix.GetMinScale2D().has_value());
EXPECT_TRUE(matrix.GetMaxScale2D().has_value());
EXPECT_FLOAT_EQ(matrix.GetMinScale2D().value_or(-1.0f), minScale);
EXPECT_FLOAT_EQ(matrix.GetMaxScale2D().value_or(-1.0f), maxScale);
check_pair(matrix, xScale, yScale);
}
{
// Simple scale with Z scale
Matrix matrix = Matrix::MakeScale({xScale, yScale, 5.0f});
EXPECT_TRUE(matrix.GetMinScale2D().has_value());
EXPECT_TRUE(matrix.GetMaxScale2D().has_value());
EXPECT_FLOAT_EQ(matrix.GetMinScale2D().value_or(-1.0f), minScale);
EXPECT_FLOAT_EQ(matrix.GetMaxScale2D().value_or(-1.0f), maxScale);
check_pair(matrix, xScale, yScale);
}
{
// Simple scale + translate
Matrix matrix = Matrix::MakeTranslateScale({xScale, yScale, 1.0f},
{10.0f, 15.0f, 2.0f});
EXPECT_TRUE(matrix.GetMinScale2D().has_value());
EXPECT_TRUE(matrix.GetMaxScale2D().has_value());
EXPECT_FLOAT_EQ(matrix.GetMinScale2D().value_or(-1.0f), minScale);
EXPECT_FLOAT_EQ(matrix.GetMaxScale2D().value_or(-1.0f), maxScale);
check_pair(matrix, xScale, yScale);
}
for (int d = 45; d < 360; d += 45) {
{
// Rotation * Scale
Matrix matrix = Matrix::MakeScale({xScale, yScale, 1.0f}) *
Matrix::MakeRotationZ(Degrees(d));
EXPECT_TRUE(matrix.GetMinScale2D().has_value());
EXPECT_TRUE(matrix.GetMaxScale2D().has_value());
EXPECT_FLOAT_EQ(matrix.GetMinScale2D().value_or(-1.0f), minScale);
EXPECT_FLOAT_EQ(matrix.GetMaxScale2D().value_or(-1.0f), maxScale);
check_pair(matrix, xScale, yScale);
}
{
// Scale * Rotation
Matrix matrix = Matrix::MakeRotationZ(Degrees(d)) *
Matrix::MakeScale({xScale, yScale, 1.0f});
EXPECT_TRUE(matrix.GetMinScale2D().has_value());
EXPECT_TRUE(matrix.GetMaxScale2D().has_value());
EXPECT_FLOAT_EQ(matrix.GetMinScale2D().value_or(-1.0f), minScale);
EXPECT_FLOAT_EQ(matrix.GetMaxScale2D().value_or(-1.0f), maxScale);
check_pair(matrix, xScale, yScale);
}
}
{
// Scale + PerspectiveX (returns invalid values)
Matrix matrix = Matrix::MakeScale({xScale, yScale, 1.0f});
matrix.m[3] = 0.1;
EXPECT_FALSE(matrix.GetMinScale2D().has_value());
EXPECT_FALSE(matrix.GetMaxScale2D().has_value());
EXPECT_FALSE(matrix.GetScales2D().has_value());
}
{
// Scale + PerspectiveY (returns invalid values)
Matrix matrix = Matrix::MakeScale({xScale, yScale, 1.0f});
matrix.m[7] = 0.1;
EXPECT_FALSE(matrix.GetMinScale2D().has_value());
EXPECT_FALSE(matrix.GetMaxScale2D().has_value());
EXPECT_FALSE(matrix.GetScales2D().has_value());
}
{
// Scale + PerspectiveZ (Z ignored; returns actual scales)
Matrix matrix = Matrix::MakeScale({xScale, yScale, 1.0f});
matrix.m[11] = 0.1;
EXPECT_TRUE(matrix.GetMinScale2D().has_value());
EXPECT_TRUE(matrix.GetMaxScale2D().has_value());
EXPECT_FLOAT_EQ(matrix.GetMinScale2D().value_or(-1.0f), minScale);
EXPECT_FLOAT_EQ(matrix.GetMaxScale2D().value_or(-1.0f), maxScale);
check_pair(matrix, xScale, yScale);
}
{
// Scale + PerspectiveW (returns invalid values)
Matrix matrix = Matrix::MakeScale({xScale, yScale, 1.0f});
matrix.m[15] = 0.1;
EXPECT_FALSE(matrix.GetMinScale2D().has_value());
EXPECT_FALSE(matrix.GetMaxScale2D().has_value());
EXPECT_FALSE(matrix.GetScales2D().has_value());
}
}
}
}
} // namespace testing
} // namespace impeller