| // 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 "gtest/gtest.h" | |
| #include "impeller/geometry/geometry_asserts.h" | |
| #include <limits> | |
| #include <sstream> | |
| #include <type_traits> | |
| #include "flutter/fml/build_config.h" | |
| #include "flutter/testing/testing.h" | |
| #include "impeller/geometry/color.h" | |
| #include "impeller/geometry/constants.h" | |
| #include "impeller/geometry/gradient.h" | |
| #include "impeller/geometry/half.h" | |
| #include "impeller/geometry/point.h" | |
| #include "impeller/geometry/rect.h" | |
| #include "impeller/geometry/scalar.h" | |
| #include "impeller/geometry/size.h" | |
| // TODO(zanderso): https://github.com/flutter/flutter/issues/127701 | |
| // NOLINTBEGIN(bugprone-unchecked-optional-access) | |
| namespace impeller { | |
| namespace testing { | |
| TEST(GeometryTest, ScalarNearlyEqual) { | |
| ASSERT_FALSE(ScalarNearlyEqual(0.0021f, 0.001f)); | |
| ASSERT_TRUE(ScalarNearlyEqual(0.0019f, 0.001f)); | |
| ASSERT_TRUE(ScalarNearlyEqual(0.002f, 0.001f, 0.0011f)); | |
| ASSERT_FALSE(ScalarNearlyEqual(0.002f, 0.001f, 0.0009f)); | |
| ASSERT_TRUE(ScalarNearlyEqual( | |
| 1.0f, 1.0f + std::numeric_limits<float>::epsilon() * 4)); | |
| } | |
| TEST(GeometryTest, MakeColumn) { | |
| auto matrix = Matrix::MakeColumn(1, 2, 3, 4, // | |
| 5, 6, 7, 8, // | |
| 9, 10, 11, 12, // | |
| 13, 14, 15, 16); | |
| auto expect = Matrix{1, 2, 3, 4, // | |
| 5, 6, 7, 8, // | |
| 9, 10, 11, 12, // | |
| 13, 14, 15, 16}; | |
| ASSERT_TRUE(matrix == expect); | |
| } | |
| TEST(GeometryTest, MakeRow) { | |
| auto matrix = Matrix::MakeRow(1, 2, 3, 4, // | |
| 5, 6, 7, 8, // | |
| 9, 10, 11, 12, // | |
| 13, 14, 15, 16); | |
| auto expect = Matrix{1, 5, 9, 13, // | |
| 2, 6, 10, 14, // | |
| 3, 7, 11, 15, // | |
| 4, 8, 12, 16}; | |
| ASSERT_TRUE(matrix == expect); | |
| } | |
| TEST(GeometryTest, RotationMatrix) { | |
| auto rotation = Matrix::MakeRotationZ(Radians{kPiOver4}); | |
| auto expect = Matrix{0.707, 0.707, 0, 0, // | |
| -0.707, 0.707, 0, 0, // | |
| 0, 0, 1, 0, // | |
| 0, 0, 0, 1}; | |
| ASSERT_MATRIX_NEAR(rotation, expect); | |
| } | |
| TEST(GeometryTest, InvertMultMatrix) { | |
| { | |
| auto rotation = Matrix::MakeRotationZ(Radians{kPiOver4}); | |
| auto invert = rotation.Invert(); | |
| auto expect = Matrix{0.707, -0.707, 0, 0, // | |
| 0.707, 0.707, 0, 0, // | |
| 0, 0, 1, 0, // | |
| 0, 0, 0, 1}; | |
| ASSERT_MATRIX_NEAR(invert, expect); | |
| } | |
| { | |
| auto scale = Matrix::MakeScale(Vector2{2, 4}); | |
| auto invert = scale.Invert(); | |
| auto expect = Matrix{0.5, 0, 0, 0, // | |
| 0, 0.25, 0, 0, // | |
| 0, 0, 1, 0, // | |
| 0, 0, 0, 1}; | |
| ASSERT_MATRIX_NEAR(invert, expect); | |
| } | |
| } | |
| TEST(GeometryTest, MatrixBasis) { | |
| auto matrix = Matrix{1, 2, 3, 4, // | |
| 5, 6, 7, 8, // | |
| 9, 10, 11, 12, // | |
| 13, 14, 15, 16}; | |
| auto basis = matrix.Basis(); | |
| auto expect = Matrix{1, 2, 3, 0, // | |
| 5, 6, 7, 0, // | |
| 9, 10, 11, 0, // | |
| 0, 0, 0, 1}; | |
| ASSERT_MATRIX_NEAR(basis, expect); | |
| } | |
| TEST(GeometryTest, MutliplicationMatrix) { | |
| auto rotation = Matrix::MakeRotationZ(Radians{kPiOver4}); | |
| auto invert = rotation.Invert(); | |
| ASSERT_MATRIX_NEAR(rotation * invert, Matrix{}); | |
| } | |
| TEST(GeometryTest, DeterminantTest) { | |
| auto matrix = Matrix{3, 4, 14, 155, 2, 1, 3, 4, 2, 3, 2, 1, 1, 2, 4, 2}; | |
| ASSERT_EQ(matrix.GetDeterminant(), -1889); | |
| } | |
| TEST(GeometryTest, InvertMatrix) { | |
| auto inverted = Matrix{10, -9, -12, 8, // | |
| 7, -12, 11, 22, // | |
| -10, 10, 3, 6, // | |
| -2, 22, 2, 1} | |
| .Invert(); | |
| auto result = Matrix{ | |
| 438.0 / 85123.0, 1751.0 / 85123.0, -7783.0 / 85123.0, 4672.0 / 85123.0, | |
| 393.0 / 85123.0, -178.0 / 85123.0, -570.0 / 85123.0, 4192 / 85123.0, | |
| -5230.0 / 85123.0, 2802.0 / 85123.0, -3461.0 / 85123.0, 962.0 / 85123.0, | |
| 2690.0 / 85123.0, 1814.0 / 85123.0, 3896.0 / 85123.0, 319.0 / 85123.0}; | |
| ASSERT_MATRIX_NEAR(inverted, result); | |
| } | |
| TEST(GeometryTest, TestDecomposition) { | |
| auto rotated = Matrix::MakeRotationZ(Radians{kPiOver4}); | |
| auto result = rotated.Decompose(); | |
| ASSERT_TRUE(result.has_value()); | |
| MatrixDecomposition res = result.value(); | |
| auto quaternion = Quaternion{{0.0, 0.0, 1.0}, kPiOver4}; | |
| ASSERT_QUATERNION_NEAR(res.rotation, quaternion); | |
| } | |
| TEST(GeometryTest, TestDecomposition2) { | |
| auto rotated = Matrix::MakeRotationZ(Radians{kPiOver4}); | |
| auto scaled = Matrix::MakeScale({2.0, 3.0, 1.0}); | |
| auto translated = Matrix::MakeTranslation({-200, 750, 20}); | |
| auto result = (translated * rotated * scaled).Decompose(); | |
| ASSERT_TRUE(result.has_value()); | |
| MatrixDecomposition res = result.value(); | |
| auto quaternion = Quaternion{{0.0, 0.0, 1.0}, kPiOver4}; | |
| ASSERT_QUATERNION_NEAR(res.rotation, quaternion); | |
| ASSERT_FLOAT_EQ(res.translation.x, -200); | |
| ASSERT_FLOAT_EQ(res.translation.y, 750); | |
| ASSERT_FLOAT_EQ(res.translation.z, 20); | |
| ASSERT_FLOAT_EQ(res.scale.x, 2); | |
| ASSERT_FLOAT_EQ(res.scale.y, 3); | |
| ASSERT_FLOAT_EQ(res.scale.z, 1); | |
| } | |
| TEST(GeometryTest, TestRecomposition) { | |
| /* | |
| * Decomposition. | |
| */ | |
| auto rotated = Matrix::MakeRotationZ(Radians{kPiOver4}); | |
| auto result = rotated.Decompose(); | |
| ASSERT_TRUE(result.has_value()); | |
| MatrixDecomposition res = result.value(); | |
| auto quaternion = Quaternion{{0.0, 0.0, 1.0}, kPiOver4}; | |
| ASSERT_QUATERNION_NEAR(res.rotation, quaternion); | |
| /* | |
| * Recomposition. | |
| */ | |
| ASSERT_MATRIX_NEAR(rotated, Matrix{res}); | |
| } | |
| TEST(GeometryTest, TestRecomposition2) { | |
| auto matrix = Matrix::MakeTranslation({100, 100, 100}) * | |
| Matrix::MakeRotationZ(Radians{kPiOver4}) * | |
| Matrix::MakeScale({2.0, 2.0, 2.0}); | |
| auto result = matrix.Decompose(); | |
| ASSERT_TRUE(result.has_value()); | |
| ASSERT_MATRIX_NEAR(matrix, Matrix{result.value()}); | |
| } | |
| TEST(GeometryTest, MatrixVectorMultiplication) { | |
| { | |
| auto matrix = Matrix::MakeTranslation({100, 100, 100}) * | |
| Matrix::MakeRotationZ(Radians{kPiOver2}) * | |
| Matrix::MakeScale({2.0, 2.0, 2.0}); | |
| auto vector = Vector4(10, 20, 30, 2); | |
| Vector4 result = matrix * vector; | |
| auto expected = Vector4(160, 220, 260, 2); | |
| ASSERT_VECTOR4_NEAR(result, expected); | |
| } | |
| { | |
| auto matrix = Matrix::MakeTranslation({100, 100, 100}) * | |
| Matrix::MakeRotationZ(Radians{kPiOver2}) * | |
| Matrix::MakeScale({2.0, 2.0, 2.0}); | |
| auto vector = Vector3(10, 20, 30); | |
| Vector3 result = matrix * vector; | |
| auto expected = Vector3(60, 120, 160); | |
| ASSERT_VECTOR3_NEAR(result, expected); | |
| } | |
| { | |
| auto matrix = Matrix::MakeTranslation({100, 100, 100}) * | |
| Matrix::MakeRotationZ(Radians{kPiOver2}) * | |
| Matrix::MakeScale({2.0, 2.0, 2.0}); | |
| auto vector = Point(10, 20); | |
| Point result = matrix * vector; | |
| auto expected = Point(60, 120); | |
| ASSERT_POINT_NEAR(result, expected); | |
| } | |
| // Matrix Vector ops should respect perspective transforms. | |
| { | |
| auto matrix = Matrix::MakePerspective(Radians(kPiOver2), 1, 1, 100); | |
| auto vector = Vector3(3, 3, -3); | |
| Vector3 result = matrix * vector; | |
| auto expected = Vector3(-1, -1, 1.3468); | |
| ASSERT_VECTOR3_NEAR(result, expected); | |
| } | |
| { | |
| auto matrix = Matrix::MakePerspective(Radians(kPiOver2), 1, 1, 100) * | |
| Matrix::MakeTranslation(Vector3(0, 0, -3)); | |
| auto point = Point(3, 3); | |
| Point result = matrix * point; | |
| auto expected = Point(-1, -1); | |
| ASSERT_POINT_NEAR(result, expected); | |
| } | |
| // Resolves to 0 on perspective singularity. | |
| { | |
| auto matrix = Matrix::MakePerspective(Radians(kPiOver2), 1, 1, 100); | |
| auto point = Point(3, 3); | |
| Point result = matrix * point; | |
| auto expected = Point(0, 0); | |
| ASSERT_POINT_NEAR(result, expected); | |
| } | |
| } | |
| TEST(GeometryTest, MatrixMakeRotationFromQuaternion) { | |
| { | |
| auto matrix = Matrix::MakeRotation(Quaternion({1, 0, 0}, kPiOver2)); | |
| auto expected = Matrix::MakeRotationX(Radians(kPiOver2)); | |
| ASSERT_MATRIX_NEAR(matrix, expected); | |
| } | |
| { | |
| auto matrix = Matrix::MakeRotation(Quaternion({0, 1, 0}, kPiOver2)); | |
| auto expected = Matrix::MakeRotationY(Radians(kPiOver2)); | |
| ASSERT_MATRIX_NEAR(matrix, expected); | |
| } | |
| { | |
| auto matrix = Matrix::MakeRotation(Quaternion({0, 0, 1}, kPiOver2)); | |
| auto expected = Matrix::MakeRotationZ(Radians(kPiOver2)); | |
| ASSERT_MATRIX_NEAR(matrix, expected); | |
| } | |
| } | |
| TEST(GeometryTest, MatrixTransformDirection) { | |
| { | |
| auto matrix = Matrix::MakeTranslation({100, 100, 100}) * | |
| Matrix::MakeRotationZ(Radians{kPiOver2}) * | |
| Matrix::MakeScale({2.0, 2.0, 2.0}); | |
| auto vector = Vector4(10, 20, 30, 2); | |
| Vector4 result = matrix.TransformDirection(vector); | |
| auto expected = Vector4(-40, 20, 60, 2); | |
| ASSERT_VECTOR4_NEAR(result, expected); | |
| } | |
| { | |
| auto matrix = Matrix::MakeTranslation({100, 100, 100}) * | |
| Matrix::MakeRotationZ(Radians{kPiOver2}) * | |
| Matrix::MakeScale({2.0, 2.0, 2.0}); | |
| auto vector = Vector3(10, 20, 30); | |
| Vector3 result = matrix.TransformDirection(vector); | |
| auto expected = Vector3(-40, 20, 60); | |
| ASSERT_VECTOR3_NEAR(result, expected); | |
| } | |
| { | |
| auto matrix = Matrix::MakeTranslation({0, -0.4, 100}) * | |
| Matrix::MakeRotationZ(Radians{kPiOver2}) * | |
| Matrix::MakeScale({2.0, 2.0, 2.0}); | |
| auto vector = Point(10, 20); | |
| Point result = matrix.TransformDirection(vector); | |
| auto expected = Point(-40, 20); | |
| ASSERT_POINT_NEAR(result, expected); | |
| } | |
| } | |
| TEST(GeometryTest, MatrixGetMaxBasisLength) { | |
| { | |
| auto m = Matrix::MakeScale({3, 1, 1}); | |
| ASSERT_EQ(m.GetMaxBasisLength(), 3); | |
| m = m * Matrix::MakeSkew(0, 4); | |
| ASSERT_EQ(m.GetMaxBasisLength(), 5); | |
| } | |
| { | |
| auto m = Matrix::MakeScale({-3, 4, 2}); | |
| ASSERT_EQ(m.GetMaxBasisLength(), 4); | |
| } | |
| } | |
| TEST(GeometryTest, MatrixGetMaxBasisLengthXY) { | |
| { | |
| auto m = Matrix::MakeScale({3, 1, 1}); | |
| ASSERT_EQ(m.GetMaxBasisLengthXY(), 3); | |
| m = m * Matrix::MakeSkew(0, 4); | |
| ASSERT_EQ(m.GetMaxBasisLengthXY(), 5); | |
| } | |
| { | |
| auto m = Matrix::MakeScale({-3, 4, 7}); | |
| ASSERT_EQ(m.GetMaxBasisLengthXY(), 4); | |
| } | |
| } | |
| TEST(GeometryTest, MatrixMakeOrthographic) { | |
| { | |
| auto m = Matrix::MakeOrthographic(Size(100, 200)); | |
| auto expect = Matrix{ | |
| 0.02, 0, 0, 0, // | |
| 0, -0.01, 0, 0, // | |
| 0, 0, 0, 0, // | |
| -1, 1, 0.5, 1, // | |
| }; | |
| ASSERT_MATRIX_NEAR(m, expect); | |
| } | |
| { | |
| auto m = Matrix::MakeOrthographic(Size(400, 100)); | |
| auto expect = Matrix{ | |
| 0.005, 0, 0, 0, // | |
| 0, -0.02, 0, 0, // | |
| 0, 0, 0, 0, // | |
| -1, 1, 0.5, 1, // | |
| }; | |
| ASSERT_MATRIX_NEAR(m, expect); | |
| } | |
| } | |
| TEST(GeometryTest, MatrixMakePerspective) { | |
| { | |
| auto m = Matrix::MakePerspective(Degrees(60), Size(100, 200), 1, 10); | |
| auto expect = Matrix{ | |
| 3.4641, 0, 0, 0, // | |
| 0, 1.73205, 0, 0, // | |
| 0, 0, 1.11111, 1, // | |
| 0, 0, -1.11111, 0, // | |
| }; | |
| ASSERT_MATRIX_NEAR(m, expect); | |
| } | |
| { | |
| auto m = Matrix::MakePerspective(Radians(1), 2, 10, 20); | |
| auto expect = Matrix{ | |
| 0.915244, 0, 0, 0, // | |
| 0, 1.83049, 0, 0, // | |
| 0, 0, 2, 1, // | |
| 0, 0, -20, 0, // | |
| }; | |
| ASSERT_MATRIX_NEAR(m, expect); | |
| } | |
| } | |
| TEST(GeometryTest, MatrixGetBasisVectors) { | |
| { | |
| auto m = Matrix(); | |
| Vector3 x = m.GetBasisX(); | |
| Vector3 y = m.GetBasisY(); | |
| Vector3 z = m.GetBasisZ(); | |
| ASSERT_VECTOR3_NEAR(x, Vector3(1, 0, 0)); | |
| ASSERT_VECTOR3_NEAR(y, Vector3(0, 1, 0)); | |
| ASSERT_VECTOR3_NEAR(z, Vector3(0, 0, 1)); | |
| } | |
| { | |
| auto m = Matrix::MakeRotationZ(Radians{kPiOver2}) * | |
| Matrix::MakeRotationX(Radians{kPiOver2}) * | |
| Matrix::MakeScale(Vector3(2, 3, 4)); | |
| Vector3 x = m.GetBasisX(); | |
| Vector3 y = m.GetBasisY(); | |
| Vector3 z = m.GetBasisZ(); | |
| ASSERT_VECTOR3_NEAR(x, Vector3(0, 2, 0)); | |
| ASSERT_VECTOR3_NEAR(y, Vector3(0, 0, 3)); | |
| ASSERT_VECTOR3_NEAR(z, Vector3(4, 0, 0)); | |
| } | |
| } | |
| TEST(GeometryTest, MatrixGetDirectionScale) { | |
| { | |
| auto m = Matrix(); | |
| Scalar result = m.GetDirectionScale(Vector3{1, 0, 0}); | |
| ASSERT_FLOAT_EQ(result, 1); | |
| } | |
| { | |
| auto m = Matrix::MakeRotationX(Degrees{10}) * | |
| Matrix::MakeRotationY(Degrees{83}) * | |
| Matrix::MakeRotationZ(Degrees{172}); | |
| Scalar result = m.GetDirectionScale(Vector3{0, 1, 0}); | |
| ASSERT_FLOAT_EQ(result, 1); | |
| } | |
| { | |
| auto m = Matrix::MakeRotationZ(Radians{kPiOver2}) * | |
| Matrix::MakeScale(Vector3(3, 4, 5)); | |
| Scalar result = m.GetDirectionScale(Vector3{2, 0, 0}); | |
| ASSERT_FLOAT_EQ(result, 8); | |
| } | |
| } | |
| TEST(GeometryTest, MatrixIsAligned) { | |
| { | |
| auto m = Matrix::MakeTranslation({1, 2, 3}); | |
| bool result = m.IsAligned(); | |
| ASSERT_TRUE(result); | |
| } | |
| { | |
| auto m = Matrix::MakeRotationZ(Degrees{123}); | |
| bool result = m.IsAligned(); | |
| ASSERT_FALSE(result); | |
| } | |
| } | |
| TEST(GeometryTest, MatrixTranslationScaleOnly) { | |
| { | |
| auto m = Matrix(); | |
| bool result = m.IsTranslationScaleOnly(); | |
| ASSERT_TRUE(result); | |
| } | |
| { | |
| auto m = Matrix::MakeScale(Vector3(2, 3, 4)); | |
| bool result = m.IsTranslationScaleOnly(); | |
| ASSERT_TRUE(result); | |
| } | |
| { | |
| auto m = Matrix::MakeTranslation(Vector3(2, 3, 4)); | |
| bool result = m.IsTranslationScaleOnly(); | |
| ASSERT_TRUE(result); | |
| } | |
| { | |
| auto m = Matrix::MakeRotationZ(Degrees(10)); | |
| bool result = m.IsTranslationScaleOnly(); | |
| ASSERT_FALSE(result); | |
| } | |
| } | |
| TEST(GeometryTest, MatrixLookAt) { | |
| { | |
| auto m = Matrix::MakeLookAt(Vector3(0, 0, -1), Vector3(0, 0, 1), | |
| Vector3(0, 1, 0)); | |
| auto expected = Matrix{ | |
| 1, 0, 0, 0, // | |
| 0, 1, 0, 0, // | |
| 0, 0, 1, 0, // | |
| 0, 0, 1, 1, // | |
| }; | |
| ASSERT_MATRIX_NEAR(m, expected); | |
| } | |
| // Sideways tilt. | |
| { | |
| auto m = Matrix::MakeLookAt(Vector3(0, 0, -1), Vector3(0, 0, 1), | |
| Vector3(1, 1, 0).Normalize()); | |
| // clang-format off | |
| auto expected = Matrix{ | |
| k1OverSqrt2, k1OverSqrt2, 0, 0, | |
| -k1OverSqrt2, k1OverSqrt2, 0, 0, | |
| 0, 0, 1, 0, | |
| 0, 0, 1, 1, | |
| }; | |
| // clang-format on | |
| ASSERT_MATRIX_NEAR(m, expected); | |
| } | |
| // Half way between +x and -y, yaw 90 | |
| { | |
| auto m = | |
| Matrix::MakeLookAt(Vector3(), Vector3(10, -10, 0), Vector3(0, 0, -1)); | |
| // clang-format off | |
| auto expected = Matrix{ | |
| -k1OverSqrt2, 0, k1OverSqrt2, 0, | |
| -k1OverSqrt2, 0, -k1OverSqrt2, 0, | |
| 0, -1, 0, 0, | |
| 0, 0, 0, 1, | |
| }; | |
| // clang-format on | |
| ASSERT_MATRIX_NEAR(m, expected); | |
| } | |
| } | |
| TEST(GeometryTest, QuaternionLerp) { | |
| auto q1 = Quaternion{{0.0, 0.0, 1.0}, 0.0}; | |
| auto q2 = Quaternion{{0.0, 0.0, 1.0}, kPiOver4}; | |
| auto q3 = q1.Slerp(q2, 0.5); | |
| auto expected = Quaternion{{0.0, 0.0, 1.0}, kPiOver4 / 2.0}; | |
| ASSERT_QUATERNION_NEAR(q3, expected); | |
| } | |
| TEST(GeometryTest, QuaternionVectorMultiply) { | |
| { | |
| Quaternion q({0, 0, 1}, 0); | |
| Vector3 v(0, 1, 0); | |
| Vector3 result = q * v; | |
| Vector3 expected(0, 1, 0); | |
| ASSERT_VECTOR3_NEAR(result, expected); | |
| } | |
| { | |
| Quaternion q({0, 0, 1}, k2Pi); | |
| Vector3 v(1, 0, 0); | |
| Vector3 result = q * v; | |
| Vector3 expected(1, 0, 0); | |
| ASSERT_VECTOR3_NEAR(result, expected); | |
| } | |
| { | |
| Quaternion q({0, 0, 1}, kPiOver4); | |
| Vector3 v(0, 1, 0); | |
| Vector3 result = q * v; | |
| Vector3 expected(-k1OverSqrt2, k1OverSqrt2, 0); | |
| ASSERT_VECTOR3_NEAR(result, expected); | |
| } | |
| { | |
| Quaternion q(Vector3(1, 0, 1).Normalize(), kPi); | |
| Vector3 v(0, 0, -1); | |
| Vector3 result = q * v; | |
| Vector3 expected(-1, 0, 0); | |
| ASSERT_VECTOR3_NEAR(result, expected); | |
| } | |
| } | |
| TEST(GeometryTest, CanGenerateMipCounts) { | |
| ASSERT_EQ((Size{128, 128}.MipCount()), 7u); | |
| ASSERT_EQ((Size{128, 256}.MipCount()), 8u); | |
| ASSERT_EQ((Size{128, 130}.MipCount()), 8u); | |
| ASSERT_EQ((Size{128, 257}.MipCount()), 9u); | |
| ASSERT_EQ((Size{257, 128}.MipCount()), 9u); | |
| ASSERT_EQ((Size{128, 0}.MipCount()), 1u); | |
| ASSERT_EQ((Size{128, -25}.MipCount()), 1u); | |
| ASSERT_EQ((Size{-128, 25}.MipCount()), 1u); | |
| ASSERT_EQ((Size{1, 1}.MipCount()), 1u); | |
| ASSERT_EQ((Size{0, 0}.MipCount()), 1u); | |
| } | |
| TEST(GeometryTest, CanConvertTTypesExplicitly) { | |
| { | |
| Point p1(1.0, 2.0); | |
| IPoint p2 = static_cast<IPoint>(p1); | |
| ASSERT_EQ(p2.x, 1u); | |
| ASSERT_EQ(p2.y, 2u); | |
| } | |
| { | |
| Size s1(1.0, 2.0); | |
| ISize s2 = static_cast<ISize>(s1); | |
| ASSERT_EQ(s2.width, 1u); | |
| ASSERT_EQ(s2.height, 2u); | |
| } | |
| { | |
| Size s1(1.0, 2.0); | |
| Point p1 = static_cast<Point>(s1); | |
| ASSERT_EQ(p1.x, 1u); | |
| ASSERT_EQ(p1.y, 2u); | |
| } | |
| } | |
| TEST(GeometryTest, CanPerformAlgebraicPointOps) { | |
| { | |
| IPoint p1(1, 2); | |
| IPoint p2 = p1 + IPoint(1, 2); | |
| ASSERT_EQ(p2.x, 2u); | |
| ASSERT_EQ(p2.y, 4u); | |
| } | |
| { | |
| IPoint p1(3, 6); | |
| IPoint p2 = p1 - IPoint(1, 2); | |
| ASSERT_EQ(p2.x, 2u); | |
| ASSERT_EQ(p2.y, 4u); | |
| } | |
| { | |
| IPoint p1(1, 2); | |
| IPoint p2 = p1 * IPoint(2, 3); | |
| ASSERT_EQ(p2.x, 2u); | |
| ASSERT_EQ(p2.y, 6u); | |
| } | |
| { | |
| IPoint p1(2, 6); | |
| IPoint p2 = p1 / IPoint(2, 3); | |
| ASSERT_EQ(p2.x, 1u); | |
| ASSERT_EQ(p2.y, 2u); | |
| } | |
| } | |
| TEST(GeometryTest, CanPerformAlgebraicPointOpsWithArithmeticTypes) { | |
| // LHS | |
| { | |
| IPoint p1(1, 2); | |
| IPoint p2 = p1 * 2.0f; | |
| ASSERT_EQ(p2.x, 2u); | |
| ASSERT_EQ(p2.y, 4u); | |
| } | |
| { | |
| IPoint p1(2, 6); | |
| IPoint p2 = p1 / 2.0f; | |
| ASSERT_EQ(p2.x, 1u); | |
| ASSERT_EQ(p2.y, 3u); | |
| } | |
| // RHS | |
| { | |
| IPoint p1(1, 2); | |
| IPoint p2 = 2.0f * p1; | |
| ASSERT_EQ(p2.x, 2u); | |
| ASSERT_EQ(p2.y, 4u); | |
| } | |
| { | |
| IPoint p1(2, 6); | |
| IPoint p2 = 12.0f / p1; | |
| ASSERT_EQ(p2.x, 6u); | |
| ASSERT_EQ(p2.y, 2u); | |
| } | |
| } | |
| TEST(GeometryTest, PointIntegerCoercesToFloat) { | |
| // Integer on LHS, float on RHS | |
| { | |
| IPoint p1(1, 2); | |
| Point p2 = p1 + Point(1, 2); | |
| ASSERT_FLOAT_EQ(p2.x, 2u); | |
| ASSERT_FLOAT_EQ(p2.y, 4u); | |
| } | |
| { | |
| IPoint p1(3, 6); | |
| Point p2 = p1 - Point(1, 2); | |
| ASSERT_FLOAT_EQ(p2.x, 2u); | |
| ASSERT_FLOAT_EQ(p2.y, 4u); | |
| } | |
| { | |
| IPoint p1(1, 2); | |
| Point p2 = p1 * Point(2, 3); | |
| ASSERT_FLOAT_EQ(p2.x, 2u); | |
| ASSERT_FLOAT_EQ(p2.y, 6u); | |
| } | |
| { | |
| IPoint p1(2, 6); | |
| Point p2 = p1 / Point(2, 3); | |
| ASSERT_FLOAT_EQ(p2.x, 1u); | |
| ASSERT_FLOAT_EQ(p2.y, 2u); | |
| } | |
| // Float on LHS, integer on RHS | |
| { | |
| Point p1(1, 2); | |
| Point p2 = p1 + IPoint(1, 2); | |
| ASSERT_FLOAT_EQ(p2.x, 2u); | |
| ASSERT_FLOAT_EQ(p2.y, 4u); | |
| } | |
| { | |
| Point p1(3, 6); | |
| Point p2 = p1 - IPoint(1, 2); | |
| ASSERT_FLOAT_EQ(p2.x, 2u); | |
| ASSERT_FLOAT_EQ(p2.y, 4u); | |
| } | |
| { | |
| Point p1(1, 2); | |
| Point p2 = p1 * IPoint(2, 3); | |
| ASSERT_FLOAT_EQ(p2.x, 2u); | |
| ASSERT_FLOAT_EQ(p2.y, 6u); | |
| } | |
| { | |
| Point p1(2, 6); | |
| Point p2 = p1 / IPoint(2, 3); | |
| ASSERT_FLOAT_EQ(p2.x, 1u); | |
| ASSERT_FLOAT_EQ(p2.y, 2u); | |
| } | |
| } | |
| TEST(GeometryTest, SizeCoercesToPoint) { | |
| // Point on LHS, Size on RHS | |
| { | |
| IPoint p1(1, 2); | |
| IPoint p2 = p1 + ISize(1, 2); | |
| ASSERT_EQ(p2.x, 2u); | |
| ASSERT_EQ(p2.y, 4u); | |
| } | |
| { | |
| IPoint p1(3, 6); | |
| IPoint p2 = p1 - ISize(1, 2); | |
| ASSERT_EQ(p2.x, 2u); | |
| ASSERT_EQ(p2.y, 4u); | |
| } | |
| { | |
| IPoint p1(1, 2); | |
| IPoint p2 = p1 * ISize(2, 3); | |
| ASSERT_EQ(p2.x, 2u); | |
| ASSERT_EQ(p2.y, 6u); | |
| } | |
| { | |
| IPoint p1(2, 6); | |
| IPoint p2 = p1 / ISize(2, 3); | |
| ASSERT_EQ(p2.x, 1u); | |
| ASSERT_EQ(p2.y, 2u); | |
| } | |
| // Size on LHS, Point on RHS | |
| { | |
| ISize p1(1, 2); | |
| IPoint p2 = p1 + IPoint(1, 2); | |
| ASSERT_EQ(p2.x, 2u); | |
| ASSERT_EQ(p2.y, 4u); | |
| } | |
| { | |
| ISize p1(3, 6); | |
| IPoint p2 = p1 - IPoint(1, 2); | |
| ASSERT_EQ(p2.x, 2u); | |
| ASSERT_EQ(p2.y, 4u); | |
| } | |
| { | |
| ISize p1(1, 2); | |
| IPoint p2 = p1 * IPoint(2, 3); | |
| ASSERT_EQ(p2.x, 2u); | |
| ASSERT_EQ(p2.y, 6u); | |
| } | |
| { | |
| ISize p1(2, 6); | |
| IPoint p2 = p1 / IPoint(2, 3); | |
| ASSERT_EQ(p2.x, 1u); | |
| ASSERT_EQ(p2.y, 2u); | |
| } | |
| } | |
| TEST(GeometryTest, CanUsePointAssignmentOperators) { | |
| // Point on RHS | |
| { | |
| IPoint p(1, 2); | |
| p += IPoint(1, 2); | |
| ASSERT_EQ(p.x, 2u); | |
| ASSERT_EQ(p.y, 4u); | |
| } | |
| { | |
| IPoint p(3, 6); | |
| p -= IPoint(1, 2); | |
| ASSERT_EQ(p.x, 2u); | |
| ASSERT_EQ(p.y, 4u); | |
| } | |
| { | |
| IPoint p(1, 2); | |
| p *= IPoint(2, 3); | |
| ASSERT_EQ(p.x, 2u); | |
| ASSERT_EQ(p.y, 6u); | |
| } | |
| { | |
| IPoint p(2, 6); | |
| p /= IPoint(2, 3); | |
| ASSERT_EQ(p.x, 1u); | |
| ASSERT_EQ(p.y, 2u); | |
| } | |
| // Size on RHS | |
| { | |
| IPoint p(1, 2); | |
| p += ISize(1, 2); | |
| ASSERT_EQ(p.x, 2u); | |
| ASSERT_EQ(p.y, 4u); | |
| } | |
| { | |
| IPoint p(3, 6); | |
| p -= ISize(1, 2); | |
| ASSERT_EQ(p.x, 2u); | |
| ASSERT_EQ(p.y, 4u); | |
| } | |
| { | |
| IPoint p(1, 2); | |
| p *= ISize(2, 3); | |
| ASSERT_EQ(p.x, 2u); | |
| ASSERT_EQ(p.y, 6u); | |
| } | |
| { | |
| IPoint p(2, 6); | |
| p /= ISize(2, 3); | |
| ASSERT_EQ(p.x, 1u); | |
| ASSERT_EQ(p.y, 2u); | |
| } | |
| // Arithmetic type on RHS | |
| { | |
| IPoint p(1, 2); | |
| p *= 3; | |
| ASSERT_EQ(p.x, 3u); | |
| ASSERT_EQ(p.y, 6u); | |
| } | |
| { | |
| IPoint p(3, 6); | |
| p /= 3; | |
| ASSERT_EQ(p.x, 1u); | |
| ASSERT_EQ(p.y, 2u); | |
| } | |
| } | |
| TEST(GeometryTest, PointDotProduct) { | |
| { | |
| Point p(1, 0); | |
| Scalar s = p.Dot(Point(-1, 0)); | |
| ASSERT_FLOAT_EQ(s, -1); | |
| } | |
| { | |
| Point p(0, -1); | |
| Scalar s = p.Dot(Point(-1, 0)); | |
| ASSERT_FLOAT_EQ(s, 0); | |
| } | |
| { | |
| Point p(1, 2); | |
| Scalar s = p.Dot(Point(3, -4)); | |
| ASSERT_FLOAT_EQ(s, -5); | |
| } | |
| } | |
| TEST(GeometryTest, PointCrossProduct) { | |
| { | |
| Point p(1, 0); | |
| Scalar s = p.Cross(Point(-1, 0)); | |
| ASSERT_FLOAT_EQ(s, 0); | |
| } | |
| { | |
| Point p(0, -1); | |
| Scalar s = p.Cross(Point(-1, 0)); | |
| ASSERT_FLOAT_EQ(s, -1); | |
| } | |
| { | |
| Point p(1, 2); | |
| Scalar s = p.Cross(Point(3, -4)); | |
| ASSERT_FLOAT_EQ(s, -10); | |
| } | |
| } | |
| TEST(GeometryTest, PointReflect) { | |
| { | |
| Point axis = Point(0, 1); | |
| Point a(2, 3); | |
| auto reflected = a.Reflect(axis); | |
| auto expected = Point(2, -3); | |
| ASSERT_POINT_NEAR(reflected, expected); | |
| } | |
| { | |
| Point axis = Point(1, 1).Normalize(); | |
| Point a(1, 0); | |
| auto reflected = a.Reflect(axis); | |
| auto expected = Point(0, -1); | |
| ASSERT_POINT_NEAR(reflected, expected); | |
| } | |
| { | |
| Point axis = Point(1, 1).Normalize(); | |
| Point a(-1, -1); | |
| auto reflected = a.Reflect(axis); | |
| ASSERT_POINT_NEAR(reflected, -a); | |
| } | |
| } | |
| TEST(GeometryTest, PointAbs) { | |
| Point a(-1, -2); | |
| auto a_abs = a.Abs(); | |
| auto expected = Point(1, 2); | |
| ASSERT_POINT_NEAR(a_abs, expected); | |
| } | |
| TEST(GeometryTest, PointAngleTo) { | |
| // Negative result in the CCW (with up = -Y) direction. | |
| { | |
| Point a(1, 1); | |
| Point b(1, -1); | |
| Radians actual = a.AngleTo(b); | |
| Radians expected = Radians{-kPi / 2}; | |
| ASSERT_FLOAT_EQ(actual.radians, expected.radians); | |
| } | |
| // Check the other direction to ensure the result is signed correctly. | |
| { | |
| Point a(1, -1); | |
| Point b(1, 1); | |
| Radians actual = a.AngleTo(b); | |
| Radians expected = Radians{kPi / 2}; | |
| ASSERT_FLOAT_EQ(actual.radians, expected.radians); | |
| } | |
| // Differences in magnitude should have no impact on the result. | |
| { | |
| Point a(100, -100); | |
| Point b(0.01, 0.01); | |
| Radians actual = a.AngleTo(b); | |
| Radians expected = Radians{kPi / 2}; | |
| ASSERT_FLOAT_EQ(actual.radians, expected.radians); | |
| } | |
| } | |
| TEST(GeometryTest, PointMin) { | |
| Point p(1, 2); | |
| Point result = p.Min({0, 10}); | |
| Point expected(0, 2); | |
| ASSERT_POINT_NEAR(result, expected); | |
| } | |
| TEST(GeometryTest, Vector3Min) { | |
| Vector3 p(1, 2, 3); | |
| Vector3 result = p.Min({0, 10, 2}); | |
| Vector3 expected(0, 2, 2); | |
| ASSERT_VECTOR3_NEAR(result, expected); | |
| } | |
| TEST(GeometryTest, Vector4Min) { | |
| Vector4 p(1, 2, 3, 4); | |
| Vector4 result = p.Min({0, 10, 2, 1}); | |
| Vector4 expected(0, 2, 2, 1); | |
| ASSERT_VECTOR4_NEAR(result, expected); | |
| } | |
| TEST(GeometryTest, PointMax) { | |
| Point p(1, 2); | |
| Point result = p.Max({0, 10}); | |
| Point expected(1, 10); | |
| ASSERT_POINT_NEAR(result, expected); | |
| } | |
| TEST(GeometryTest, Vector3Max) { | |
| Vector3 p(1, 2, 3); | |
| Vector3 result = p.Max({0, 10, 2}); | |
| Vector3 expected(1, 10, 3); | |
| ASSERT_VECTOR3_NEAR(result, expected); | |
| } | |
| TEST(GeometryTest, Vector4Max) { | |
| Vector4 p(1, 2, 3, 4); | |
| Vector4 result = p.Max({0, 10, 2, 1}); | |
| Vector4 expected(1, 10, 3, 4); | |
| ASSERT_VECTOR4_NEAR(result, expected); | |
| } | |
| TEST(GeometryTest, PointFloor) { | |
| Point p(1.5, 2.3); | |
| Point result = p.Floor(); | |
| Point expected(1, 2); | |
| ASSERT_POINT_NEAR(result, expected); | |
| } | |
| TEST(GeometryTest, Vector3Floor) { | |
| Vector3 p(1.5, 2.3, 3.9); | |
| Vector3 result = p.Floor(); | |
| Vector3 expected(1, 2, 3); | |
| ASSERT_VECTOR3_NEAR(result, expected); | |
| } | |
| TEST(GeometryTest, Vector4Floor) { | |
| Vector4 p(1.5, 2.3, 3.9, 4.0); | |
| Vector4 result = p.Floor(); | |
| Vector4 expected(1, 2, 3, 4); | |
| ASSERT_VECTOR4_NEAR(result, expected); | |
| } | |
| TEST(GeometryTest, PointCeil) { | |
| Point p(1.5, 2.3); | |
| Point result = p.Ceil(); | |
| Point expected(2, 3); | |
| ASSERT_POINT_NEAR(result, expected); | |
| } | |
| TEST(GeometryTest, Vector3Ceil) { | |
| Vector3 p(1.5, 2.3, 3.9); | |
| Vector3 result = p.Ceil(); | |
| Vector3 expected(2, 3, 4); | |
| ASSERT_VECTOR3_NEAR(result, expected); | |
| } | |
| TEST(GeometryTest, Vector4Ceil) { | |
| Vector4 p(1.5, 2.3, 3.9, 4.0); | |
| Vector4 result = p.Ceil(); | |
| Vector4 expected(2, 3, 4, 4); | |
| ASSERT_VECTOR4_NEAR(result, expected); | |
| } | |
| TEST(GeometryTest, PointRound) { | |
| Point p(1.5, 2.3); | |
| Point result = p.Round(); | |
| Point expected(2, 2); | |
| ASSERT_POINT_NEAR(result, expected); | |
| } | |
| TEST(GeometryTest, Vector3Round) { | |
| Vector3 p(1.5, 2.3, 3.9); | |
| Vector3 result = p.Round(); | |
| Vector3 expected(2, 2, 4); | |
| ASSERT_VECTOR3_NEAR(result, expected); | |
| } | |
| TEST(GeometryTest, Vector4Round) { | |
| Vector4 p(1.5, 2.3, 3.9, 4.0); | |
| Vector4 result = p.Round(); | |
| Vector4 expected(2, 2, 4, 4); | |
| ASSERT_VECTOR4_NEAR(result, expected); | |
| } | |
| TEST(GeometryTest, PointLerp) { | |
| Point p(1, 2); | |
| Point result = p.Lerp({5, 10}, 0.75); | |
| Point expected(4, 8); | |
| ASSERT_POINT_NEAR(result, expected); | |
| } | |
| TEST(GeometryTest, Vector3Lerp) { | |
| Vector3 p(1, 2, 3); | |
| Vector3 result = p.Lerp({5, 10, 15}, 0.75); | |
| Vector3 expected(4, 8, 12); | |
| ASSERT_VECTOR3_NEAR(result, expected); | |
| } | |
| TEST(GeometryTest, Vector4Lerp) { | |
| Vector4 p(1, 2, 3, 4); | |
| Vector4 result = p.Lerp({5, 10, 15, 20}, 0.75); | |
| Vector4 expected(4, 8, 12, 16); | |
| ASSERT_VECTOR4_NEAR(result, expected); | |
| } | |
| TEST(GeometryTest, CanUseVector3AssignmentOperators) { | |
| { | |
| Vector3 p(1, 2, 4); | |
| p += Vector3(1, 2, 4); | |
| ASSERT_EQ(p.x, 2u); | |
| ASSERT_EQ(p.y, 4u); | |
| ASSERT_EQ(p.z, 8u); | |
| } | |
| { | |
| Vector3 p(3, 6, 8); | |
| p -= Vector3(1, 2, 3); | |
| ASSERT_EQ(p.x, 2u); | |
| ASSERT_EQ(p.y, 4u); | |
| ASSERT_EQ(p.z, 5u); | |
| } | |
| { | |
| Vector3 p(1, 2, 3); | |
| p *= Vector3(2, 3, 4); | |
| ASSERT_EQ(p.x, 2u); | |
| ASSERT_EQ(p.y, 6u); | |
| ASSERT_EQ(p.z, 12u); | |
| } | |
| { | |
| Vector3 p(1, 2, 3); | |
| p *= 2; | |
| ASSERT_EQ(p.x, 2u); | |
| ASSERT_EQ(p.y, 4u); | |
| ASSERT_EQ(p.z, 6u); | |
| } | |
| { | |
| Vector3 p(2, 6, 12); | |
| p /= Vector3(2, 3, 4); | |
| ASSERT_EQ(p.x, 1u); | |
| ASSERT_EQ(p.y, 2u); | |
| ASSERT_EQ(p.z, 3u); | |
| } | |
| { | |
| Vector3 p(2, 6, 12); | |
| p /= 2; | |
| ASSERT_EQ(p.x, 1u); | |
| ASSERT_EQ(p.y, 3u); | |
| ASSERT_EQ(p.z, 6u); | |
| } | |
| } | |
| TEST(GeometryTest, CanPerformAlgebraicVector3Ops) { | |
| { | |
| Vector3 p1(1, 2, 3); | |
| Vector3 p2 = p1 + Vector3(1, 2, 3); | |
| ASSERT_EQ(p2.x, 2u); | |
| ASSERT_EQ(p2.y, 4u); | |
| ASSERT_EQ(p2.z, 6u); | |
| } | |
| { | |
| Vector3 p1(3, 6, 9); | |
| Vector3 p2 = p1 - Vector3(1, 2, 3); | |
| ASSERT_EQ(p2.x, 2u); | |
| ASSERT_EQ(p2.y, 4u); | |
| ASSERT_EQ(p2.z, 6u); | |
| } | |
| { | |
| Vector3 p1(1, 2, 3); | |
| Vector3 p2 = p1 * Vector3(2, 3, 4); | |
| ASSERT_EQ(p2.x, 2u); | |
| ASSERT_EQ(p2.y, 6u); | |
| ASSERT_EQ(p2.z, 12u); | |
| } | |
| { | |
| Vector3 p1(2, 6, 12); | |
| Vector3 p2 = p1 / Vector3(2, 3, 4); | |
| ASSERT_EQ(p2.x, 1u); | |
| ASSERT_EQ(p2.y, 2u); | |
| ASSERT_EQ(p2.z, 3u); | |
| } | |
| } | |
| TEST(GeometryTest, CanPerformAlgebraicVector3OpsWithArithmeticTypes) { | |
| // LHS | |
| { | |
| Vector3 p1(1, 2, 3); | |
| Vector3 p2 = p1 + 2.0f; | |
| ASSERT_EQ(p2.x, 3); | |
| ASSERT_EQ(p2.y, 4); | |
| ASSERT_EQ(p2.z, 5); | |
| } | |
| { | |
| Vector3 p1(1, 2, 3); | |
| Vector3 p2 = p1 - 2.0f; | |
| ASSERT_EQ(p2.x, -1); | |
| ASSERT_EQ(p2.y, 0); | |
| ASSERT_EQ(p2.z, 1); | |
| } | |
| { | |
| Vector3 p1(1, 2, 3); | |
| Vector3 p2 = p1 * 2.0f; | |
| ASSERT_EQ(p2.x, 2); | |
| ASSERT_EQ(p2.y, 4); | |
| ASSERT_EQ(p2.z, 6); | |
| } | |
| { | |
| Vector3 p1(2, 6, 12); | |
| Vector3 p2 = p1 / 2.0f; | |
| ASSERT_EQ(p2.x, 1); | |
| ASSERT_EQ(p2.y, 3); | |
| ASSERT_EQ(p2.z, 6); | |
| } | |
| // RHS | |
| { | |
| Vector3 p1(1, 2, 3); | |
| Vector3 p2 = 2.0f + p1; | |
| ASSERT_EQ(p2.x, 3); | |
| ASSERT_EQ(p2.y, 4); | |
| ASSERT_EQ(p2.z, 5); | |
| } | |
| { | |
| Vector3 p1(1, 2, 3); | |
| Vector3 p2 = 2.0f - p1; | |
| ASSERT_EQ(p2.x, 1); | |
| ASSERT_EQ(p2.y, 0); | |
| ASSERT_EQ(p2.z, -1); | |
| } | |
| { | |
| Vector3 p1(1, 2, 3); | |
| Vector3 p2 = 2.0f * p1; | |
| ASSERT_EQ(p2.x, 2); | |
| ASSERT_EQ(p2.y, 4); | |
| ASSERT_EQ(p2.z, 6); | |
| } | |
| { | |
| Vector3 p1(2, 6, 12); | |
| Vector3 p2 = 12.0f / p1; | |
| ASSERT_EQ(p2.x, 6); | |
| ASSERT_EQ(p2.y, 2); | |
| ASSERT_EQ(p2.z, 1); | |
| } | |
| } | |
| TEST(GeometryTest, ColorPremultiply) { | |
| { | |
| Color a(1.0, 0.5, 0.2, 0.5); | |
| Color premultiplied = a.Premultiply(); | |
| Color expected = Color(0.5, 0.25, 0.1, 0.5); | |
| ASSERT_COLOR_NEAR(premultiplied, expected); | |
| } | |
| { | |
| Color a(0.5, 0.25, 0.1, 0.5); | |
| Color unpremultiplied = a.Unpremultiply(); | |
| Color expected = Color(1.0, 0.5, 0.2, 0.5); | |
| ASSERT_COLOR_NEAR(unpremultiplied, expected); | |
| } | |
| { | |
| Color a(0.5, 0.25, 0.1, 0.0); | |
| Color unpremultiplied = a.Unpremultiply(); | |
| Color expected = Color(0.0, 0.0, 0.0, 0.0); | |
| ASSERT_COLOR_NEAR(unpremultiplied, expected); | |
| } | |
| } | |
| TEST(GeometryTest, ColorR8G8B8A8) { | |
| { | |
| Color a(1.0, 0.5, 0.2, 0.5); | |
| std::array<uint8_t, 4> expected = {255, 128, 51, 128}; | |
| ASSERT_ARRAY_4_NEAR(a.ToR8G8B8A8(), expected); | |
| } | |
| { | |
| Color a(0.0, 0.0, 0.0, 0.0); | |
| std::array<uint8_t, 4> expected = {0, 0, 0, 0}; | |
| ASSERT_ARRAY_4_NEAR(a.ToR8G8B8A8(), expected); | |
| } | |
| { | |
| Color a(1.0, 1.0, 1.0, 1.0); | |
| std::array<uint8_t, 4> expected = {255, 255, 255, 255}; | |
| ASSERT_ARRAY_4_NEAR(a.ToR8G8B8A8(), expected); | |
| } | |
| } | |
| TEST(GeometryTest, ColorLerp) { | |
| { | |
| Color a(0.0, 0.0, 0.0, 0.0); | |
| Color b(1.0, 1.0, 1.0, 1.0); | |
| ASSERT_COLOR_NEAR(Color::Lerp(a, b, 0.5), Color(0.5, 0.5, 0.5, 0.5)); | |
| ASSERT_COLOR_NEAR(Color::Lerp(a, b, 0.0), a); | |
| ASSERT_COLOR_NEAR(Color::Lerp(a, b, 1.0), b); | |
| ASSERT_COLOR_NEAR(Color::Lerp(a, b, 0.2), Color(0.2, 0.2, 0.2, 0.2)); | |
| } | |
| { | |
| Color a(0.2, 0.4, 1.0, 0.5); | |
| Color b(0.4, 1.0, 0.2, 0.3); | |
| ASSERT_COLOR_NEAR(Color::Lerp(a, b, 0.5), Color(0.3, 0.7, 0.6, 0.4)); | |
| ASSERT_COLOR_NEAR(Color::Lerp(a, b, 0.0), a); | |
| ASSERT_COLOR_NEAR(Color::Lerp(a, b, 1.0), b); | |
| ASSERT_COLOR_NEAR(Color::Lerp(a, b, 0.2), Color(0.24, 0.52, 0.84, 0.46)); | |
| } | |
| } | |
| TEST(GeometryTest, ColorClamp01) { | |
| { | |
| Color result = Color(0.5, 0.5, 0.5, 0.5).Clamp01(); | |
| Color expected = Color(0.5, 0.5, 0.5, 0.5); | |
| ASSERT_COLOR_NEAR(result, expected); | |
| } | |
| { | |
| Color result = Color(-1, -1, -1, -1).Clamp01(); | |
| Color expected = Color(0, 0, 0, 0); | |
| ASSERT_COLOR_NEAR(result, expected); | |
| } | |
| { | |
| Color result = Color(2, 2, 2, 2).Clamp01(); | |
| Color expected = Color(1, 1, 1, 1); | |
| ASSERT_COLOR_NEAR(result, expected); | |
| } | |
| } | |
| TEST(GeometryTest, ColorMakeRGBA8) { | |
| { | |
| Color a = Color::MakeRGBA8(0, 0, 0, 0); | |
| Color b = Color::BlackTransparent(); | |
| ASSERT_COLOR_NEAR(a, b); | |
| } | |
| { | |
| Color a = Color::MakeRGBA8(255, 255, 255, 255); | |
| Color b = Color::White(); | |
| ASSERT_COLOR_NEAR(a, b); | |
| } | |
| { | |
| Color a = Color::MakeRGBA8(63, 127, 191, 127); | |
| Color b(0.247059, 0.498039, 0.74902, 0.498039); | |
| ASSERT_COLOR_NEAR(a, b); | |
| } | |
| } | |
| TEST(GeometryTest, ColorApplyColorMatrix) { | |
| { | |
| ColorMatrix color_matrix = { | |
| 1, 1, 1, 1, 1, // | |
| 1, 1, 1, 1, 1, // | |
| 1, 1, 1, 1, 1, // | |
| 1, 1, 1, 1, 1, // | |
| }; | |
| auto result = Color::White().ApplyColorMatrix(color_matrix); | |
| auto expected = Color(1, 1, 1, 1); | |
| ASSERT_COLOR_NEAR(result, expected); | |
| } | |
| { | |
| ColorMatrix color_matrix = { | |
| 0.1, 0, 0, 0, 0.01, // | |
| 0, 0.2, 0, 0, 0.02, // | |
| 0, 0, 0.3, 0, 0.03, // | |
| 0, 0, 0, 0.4, 0.04, // | |
| }; | |
| auto result = Color::White().ApplyColorMatrix(color_matrix); | |
| auto expected = Color(0.11, 0.22, 0.33, 0.44); | |
| ASSERT_COLOR_NEAR(result, expected); | |
| } | |
| } | |
| TEST(GeometryTest, ColorLinearToSRGB) { | |
| { | |
| auto result = Color::White().LinearToSRGB(); | |
| auto expected = Color(1, 1, 1, 1); | |
| ASSERT_COLOR_NEAR(result, expected); | |
| } | |
| { | |
| auto result = Color::BlackTransparent().LinearToSRGB(); | |
| auto expected = Color(0, 0, 0, 0); | |
| ASSERT_COLOR_NEAR(result, expected); | |
| } | |
| { | |
| auto result = Color(0.2, 0.4, 0.6, 0.8).LinearToSRGB(); | |
| auto expected = Color(0.484529, 0.665185, 0.797738, 0.8); | |
| ASSERT_COLOR_NEAR(result, expected); | |
| } | |
| } | |
| TEST(GeometryTest, ColorSRGBToLinear) { | |
| { | |
| auto result = Color::White().SRGBToLinear(); | |
| auto expected = Color(1, 1, 1, 1); | |
| ASSERT_COLOR_NEAR(result, expected); | |
| } | |
| { | |
| auto result = Color::BlackTransparent().SRGBToLinear(); | |
| auto expected = Color(0, 0, 0, 0); | |
| ASSERT_COLOR_NEAR(result, expected); | |
| } | |
| { | |
| auto result = Color(0.2, 0.4, 0.6, 0.8).SRGBToLinear(); | |
| auto expected = Color(0.0331048, 0.132868, 0.318547, 0.8); | |
| ASSERT_COLOR_NEAR(result, expected); | |
| } | |
| } | |
| struct ColorBlendTestData { | |
| static constexpr Color kDestinationColor = | |
| Color::CornflowerBlue().WithAlpha(0.75); | |
| static constexpr Color kSourceColors[] = {Color::White().WithAlpha(0.75), | |
| Color::LimeGreen().WithAlpha(0.75), | |
| Color::Black().WithAlpha(0.75)}; | |
| // THIS RESULT TABLE IS GENERATED! | |
| // | |
| // Uncomment the `GenerateColorBlendResults` test below to print a new table | |
| // after making changes to `Color::Blend`. | |
| static constexpr Color kExpectedResults | |
| [sizeof(kSourceColors)] | |
| [static_cast<std::underlying_type_t<BlendMode>>(BlendMode::kLast) + 1] = { | |
| { | |
| {0, 0, 0, 0}, // Clear | |
| {1, 1, 1, 0.75}, // Source | |
| {0.392157, 0.584314, 0.929412, 0.75}, // Destination | |
| {0.878431, 0.916863, 0.985882, 0.9375}, // SourceOver | |
| {0.513726, 0.667451, 0.943529, 0.9375}, // DestinationOver | |
| {1, 1, 1, 0.5625}, // SourceIn | |
| {0.392157, 0.584314, 0.929412, 0.5625}, // DestinationIn | |
| {1, 1, 1, 0.1875}, // SourceOut | |
| {0.392157, 0.584314, 0.929412, 0.1875}, // DestinationOut | |
| {0.848039, 0.896078, 0.982353, 0.75}, // SourceATop | |
| {0.544118, 0.688235, 0.947059, 0.75}, // DestinationATop | |
| {0.696078, 0.792157, 0.964706, 0.375}, // Xor | |
| {1, 1, 1, 1}, // Plus | |
| {0.392157, 0.584314, 0.929412, 0.5625}, // Modulate | |
| {0.878431, 0.916863, 0.985882, 0.9375}, // Screen | |
| {0.74902, 0.916863, 0.985882, 0.9375}, // Overlay | |
| {0.513726, 0.667451, 0.943529, 0.9375}, // Darken | |
| {0.878431, 0.916863, 0.985882, 0.9375}, // Lighten | |
| {0.878431, 0.916863, 0.985882, 0.9375}, // ColorDodge | |
| {0.513725, 0.667451, 0.943529, 0.9375}, // ColorBurn | |
| {0.878431, 0.916863, 0.985882, 0.9375}, // HardLight | |
| {0.654166, 0.775505, 0.964318, 0.9375}, // SoftLight | |
| {0.643137, 0.566275, 0.428235, 0.9375}, // Difference | |
| {0.643137, 0.566275, 0.428235, 0.9375}, // Exclusion | |
| {0.513726, 0.667451, 0.943529, 0.9375}, // Multiply | |
| {0.617208, 0.655639, 0.724659, 0.9375}, // Hue | |
| {0.617208, 0.655639, 0.724659, 0.9375}, // Saturation | |
| {0.617208, 0.655639, 0.724659, 0.9375}, // Color | |
| {0.878431, 0.916863, 0.985882, 0.9375}, // Luminosity | |
| }, | |
| { | |
| {0, 0, 0, 0}, // Clear | |
| {0.196078, 0.803922, 0.196078, 0.75}, // Source | |
| {0.392157, 0.584314, 0.929412, 0.75}, // Destination | |
| {0.235294, 0.76, 0.342745, 0.9375}, // SourceOver | |
| {0.352941, 0.628235, 0.782745, 0.9375}, // DestinationOver | |
| {0.196078, 0.803922, 0.196078, 0.5625}, // SourceIn | |
| {0.392157, 0.584314, 0.929412, 0.5625}, // DestinationIn | |
| {0.196078, 0.803922, 0.196078, 0.1875}, // SourceOut | |
| {0.392157, 0.584314, 0.929412, 0.1875}, // DestinationOut | |
| {0.245098, 0.74902, 0.379412, 0.75}, // SourceATop | |
| {0.343137, 0.639216, 0.746078, 0.75}, // DestinationATop | |
| {0.294118, 0.694118, 0.562745, 0.375}, // Xor | |
| {0.441176, 1, 0.844118, 1}, // Plus | |
| {0.0768935, 0.469742, 0.182238, 0.5625}, // Modulate | |
| {0.424452, 0.828743, 0.79105, 0.9375}, // Screen | |
| {0.209919, 0.779839, 0.757001, 0.9375}, // Overlay | |
| {0.235294, 0.628235, 0.342745, 0.9375}, // Darken | |
| {0.352941, 0.76, 0.782745, 0.9375}, // Lighten | |
| {0.41033, 0.877647, 0.825098, 0.9375}, // ColorDodge | |
| {0.117647, 0.567403, 0.609098, 0.9375}, // ColorBurn | |
| {0.209919, 0.779839, 0.443783, 0.9375}, // HardLight | |
| {0.266006, 0.693915, 0.758818, 0.9375}, // SoftLight | |
| {0.235294, 0.409412, 0.665098, 0.9375}, // Difference | |
| {0.378316, 0.546897, 0.681707, 0.9375}, // Exclusion | |
| {0.163783, 0.559493, 0.334441, 0.9375}, // Multiply | |
| {0.266235, 0.748588, 0.373686, 0.9375}, // Hue | |
| {0.339345, 0.629787, 0.811502, 0.9375}, // Saturation | |
| {0.241247, 0.765953, 0.348698, 0.9375}, // Color | |
| {0.346988, 0.622282, 0.776792, 0.9375}, // Luminosity | |
| }, | |
| { | |
| {0, 0, 0, 0}, // Clear | |
| {0, 0, 0, 0.75}, // Source | |
| {0.392157, 0.584314, 0.929412, 0.75}, // Destination | |
| {0.0784314, 0.116863, 0.185882, 0.9375}, // SourceOver | |
| {0.313726, 0.467451, 0.743529, 0.9375}, // DestinationOver | |
| {0, 0, 0, 0.5625}, // SourceIn | |
| {0.392157, 0.584314, 0.929412, 0.5625}, // DestinationIn | |
| {0, 0, 0, 0.1875}, // SourceOut | |
| {0.392157, 0.584314, 0.929412, 0.1875}, // DestinationOut | |
| {0.0980392, 0.146078, 0.232353, 0.75}, // SourceATop | |
| {0.294118, 0.438235, 0.697059, 0.75}, // DestinationATop | |
| {0.196078, 0.292157, 0.464706, 0.375}, // Xor | |
| {0.294118, 0.438235, 0.697059, 1}, // Plus | |
| {0, 0, 0, 0.5625}, // Modulate | |
| {0.313726, 0.467451, 0.743529, 0.9375}, // Screen | |
| {0.0784314, 0.218039, 0.701176, 0.9375}, // Overlay | |
| {0.0784314, 0.116863, 0.185882, 0.9375}, // Darken | |
| {0.313726, 0.467451, 0.743529, 0.9375}, // Lighten | |
| {0.313726, 0.467451, 0.743529, 0.9375}, // ColorDodge | |
| {0.0784314, 0.116863, 0.185882, 0.9375}, // ColorBurn | |
| {0.0784314, 0.116863, 0.185882, 0.9375}, // HardLight | |
| {0.170704, 0.321716, 0.704166, 0.9375}, // SoftLight | |
| {0.313726, 0.467451, 0.743529, 0.9375}, // Difference | |
| {0.313726, 0.467451, 0.743529, 0.9375}, // Exclusion | |
| {0.0784314, 0.116863, 0.185882, 0.9375}, // Multiply | |
| {0.417208, 0.455639, 0.524659, 0.9375}, // Hue | |
| {0.417208, 0.455639, 0.524659, 0.9375}, // Saturation | |
| {0.417208, 0.455639, 0.524659, 0.9375}, // Color | |
| {0.0784314, 0.116863, 0.185882, 0.9375}, // Luminosity | |
| }, | |
| }; | |
| }; | |
| /// To print a new ColorBlendTestData::kExpectedResults table, uncomment this | |
| /// test and run with: | |
| /// --gtest_filter="GeometryTest.GenerateColorBlendResults" | |
| /* | |
| TEST(GeometryTest, GenerateColorBlendResults) { | |
| auto& o = std::cout; | |
| using BlendT = std::underlying_type_t<BlendMode>; | |
| o << "{"; | |
| for (const auto& source : ColorBlendTestData::kSourceColors) { | |
| o << "{"; | |
| for (BlendT blend_i = 0; | |
| blend_i < static_cast<BlendT>(BlendMode::kLast) + 1; blend_i++) { | |
| auto blend = static_cast<BlendMode>(blend_i); | |
| Color c = ColorBlendTestData::kDestinationColor.Blend(source, blend); | |
| o << "{" << c.red << "," << c.green << "," << c.blue << "," << c.alpha | |
| << "}, // " << BlendModeToString(blend) << std::endl; | |
| } | |
| o << "},"; | |
| } | |
| o << "};" << std::endl; | |
| } | |
| */ | |
| #define _BLEND_MODE_RESULT_CHECK(blend_mode) \ | |
| blend_i = static_cast<BlendT>(BlendMode::k##blend_mode); \ | |
| expected = ColorBlendTestData::kExpectedResults[source_i][blend_i]; \ | |
| EXPECT_COLOR_NEAR(dst.Blend(src, BlendMode::k##blend_mode), expected); | |
| TEST(GeometryTest, ColorBlendReturnsExpectedResults) { | |
| using BlendT = std::underlying_type_t<BlendMode>; | |
| Color dst = ColorBlendTestData::kDestinationColor; | |
| for (size_t source_i = 0; | |
| source_i < sizeof(ColorBlendTestData::kSourceColors) / sizeof(Color); | |
| source_i++) { | |
| Color src = ColorBlendTestData::kSourceColors[source_i]; | |
| size_t blend_i; | |
| Color expected; | |
| IMPELLER_FOR_EACH_BLEND_MODE(_BLEND_MODE_RESULT_CHECK) | |
| } | |
| } | |
| #define _BLEND_MODE_NAME_CHECK(blend_mode) \ | |
| case BlendMode::k##blend_mode: \ | |
| ASSERT_STREQ(result, #blend_mode); \ | |
| break; | |
| TEST(GeometryTest, BlendModeToString) { | |
| using BlendT = std::underlying_type_t<BlendMode>; | |
| for (BlendT i = 0; i <= static_cast<BlendT>(BlendMode::kLast); i++) { | |
| auto mode = static_cast<BlendMode>(i); | |
| auto result = BlendModeToString(mode); | |
| switch (mode) { IMPELLER_FOR_EACH_BLEND_MODE(_BLEND_MODE_NAME_CHECK) } | |
| } | |
| } | |
| TEST(GeometryTest, CanConvertBetweenDegressAndRadians) { | |
| { | |
| auto deg = Degrees{90.0}; | |
| Radians rad = deg; | |
| ASSERT_FLOAT_EQ(rad.radians, kPiOver2); | |
| } | |
| } | |
| TEST(GeometryTest, MatrixPrinting) { | |
| { | |
| std::stringstream stream; | |
| Matrix m; | |
| stream << m; | |
| ASSERT_EQ(stream.str(), R"(( | |
| 1.000000, 0.000000, 0.000000, 0.000000, | |
| 0.000000, 1.000000, 0.000000, 0.000000, | |
| 0.000000, 0.000000, 1.000000, 0.000000, | |
| 0.000000, 0.000000, 0.000000, 1.000000, | |
| ))"); | |
| } | |
| { | |
| std::stringstream stream; | |
| Matrix m = Matrix::MakeTranslation(Vector3(10, 20, 30)); | |
| stream << m; | |
| ASSERT_EQ(stream.str(), R"(( | |
| 1.000000, 0.000000, 0.000000, 10.000000, | |
| 0.000000, 1.000000, 0.000000, 20.000000, | |
| 0.000000, 0.000000, 1.000000, 30.000000, | |
| 0.000000, 0.000000, 0.000000, 1.000000, | |
| ))"); | |
| } | |
| } | |
| TEST(GeometryTest, PointPrinting) { | |
| { | |
| std::stringstream stream; | |
| Point m; | |
| stream << m; | |
| ASSERT_EQ(stream.str(), "(0, 0)"); | |
| } | |
| { | |
| std::stringstream stream; | |
| Point m(13, 37); | |
| stream << m; | |
| ASSERT_EQ(stream.str(), "(13, 37)"); | |
| } | |
| } | |
| TEST(GeometryTest, Vector3Printing) { | |
| { | |
| std::stringstream stream; | |
| Vector3 m; | |
| stream << m; | |
| ASSERT_EQ(stream.str(), "(0, 0, 0)"); | |
| } | |
| { | |
| std::stringstream stream; | |
| Vector3 m(1, 2, 3); | |
| stream << m; | |
| ASSERT_EQ(stream.str(), "(1, 2, 3)"); | |
| } | |
| } | |
| TEST(GeometryTest, Vector4Printing) { | |
| { | |
| std::stringstream stream; | |
| Vector4 m; | |
| stream << m; | |
| ASSERT_EQ(stream.str(), "(0, 0, 0, 1)"); | |
| } | |
| { | |
| std::stringstream stream; | |
| Vector4 m(1, 2, 3, 4); | |
| stream << m; | |
| ASSERT_EQ(stream.str(), "(1, 2, 3, 4)"); | |
| } | |
| } | |
| TEST(GeometryTest, ColorPrinting) { | |
| { | |
| std::stringstream stream; | |
| Color m; | |
| stream << m; | |
| ASSERT_EQ(stream.str(), "(0, 0, 0, 0)"); | |
| } | |
| { | |
| std::stringstream stream; | |
| Color m(1, 2, 3, 4); | |
| stream << m; | |
| ASSERT_EQ(stream.str(), "(1, 2, 3, 4)"); | |
| } | |
| } | |
| TEST(GeometryTest, ToIColor) { | |
| ASSERT_EQ(Color::ToIColor(Color(0, 0, 0, 0)), 0u); | |
| ASSERT_EQ(Color::ToIColor(Color(1.0, 1.0, 1.0, 1.0)), 0xFFFFFFFF); | |
| ASSERT_EQ(Color::ToIColor(Color(0.5, 0.5, 1.0, 1.0)), 0xFF8080FF); | |
| } | |
| TEST(GeometryTest, Gradient) { | |
| { | |
| // Simple 2 color gradient produces color buffer containing exactly those | |
| // values. | |
| std::vector<Color> colors = {Color::Red(), Color::Blue()}; | |
| std::vector<Scalar> stops = {0.0, 1.0}; | |
| auto gradient = CreateGradientBuffer(colors, stops); | |
| ASSERT_COLOR_BUFFER_NEAR(gradient.color_bytes, colors); | |
| ASSERT_EQ(gradient.texture_size, 2u); | |
| } | |
| { | |
| // Gradient with duplicate stops does not create an empty texture. | |
| std::vector<Color> colors = {Color::Red(), Color::Yellow(), Color::Black(), | |
| Color::Blue()}; | |
| std::vector<Scalar> stops = {0.0, 0.25, 0.25, 1.0}; | |
| auto gradient = CreateGradientBuffer(colors, stops); | |
| ASSERT_EQ(gradient.texture_size, 5u); | |
| } | |
| { | |
| // Simple N color gradient produces color buffer containing exactly those | |
| // values. | |
| std::vector<Color> colors = {Color::Red(), Color::Blue(), Color::Green(), | |
| Color::White()}; | |
| std::vector<Scalar> stops = {0.0, 0.33, 0.66, 1.0}; | |
| auto gradient = CreateGradientBuffer(colors, stops); | |
| ASSERT_COLOR_BUFFER_NEAR(gradient.color_bytes, colors); | |
| ASSERT_EQ(gradient.texture_size, 4u); | |
| } | |
| { | |
| // Gradient with color stops will lerp and scale buffer. | |
| std::vector<Color> colors = {Color::Red(), Color::Blue(), Color::Green()}; | |
| std::vector<Scalar> stops = {0.0, 0.25, 1.0}; | |
| auto gradient = CreateGradientBuffer(colors, stops); | |
| std::vector<Color> lerped_colors = { | |
| Color::Red(), | |
| Color::Blue(), | |
| Color::Lerp(Color::Blue(), Color::Green(), 0.3333), | |
| Color::Lerp(Color::Blue(), Color::Green(), 0.6666), | |
| Color::Green(), | |
| }; | |
| ASSERT_COLOR_BUFFER_NEAR(gradient.color_bytes, lerped_colors); | |
| ASSERT_EQ(gradient.texture_size, 5u); | |
| } | |
| { | |
| // Gradient size is capped at 1024. | |
| std::vector<Color> colors = {}; | |
| std::vector<Scalar> stops = {}; | |
| for (auto i = 0u; i < 1025; i++) { | |
| colors.push_back(Color::Blue()); | |
| stops.push_back(i / 1025.0); | |
| } | |
| auto gradient = CreateGradientBuffer(colors, stops); | |
| ASSERT_EQ(gradient.texture_size, 1024u); | |
| ASSERT_EQ(gradient.color_bytes.size(), 1024u * 4); | |
| } | |
| } | |
| TEST(GeometryTest, HalfConversions) { | |
| #if defined(FML_OS_MACOSX) || defined(FML_OS_IOS) || \ | |
| defined(FML_OS_IOS_SIMULATOR) | |
| ASSERT_EQ(ScalarToHalf(0.0), 0.0f16); | |
| ASSERT_EQ(ScalarToHalf(0.05), 0.05f16); | |
| ASSERT_EQ(ScalarToHalf(2.43), 2.43f16); | |
| ASSERT_EQ(ScalarToHalf(-1.45), -1.45f16); | |
| // 65504 is the largest possible half. | |
| ASSERT_EQ(ScalarToHalf(65504.0f), 65504.0f16); | |
| ASSERT_EQ(ScalarToHalf(65504.0f + 1), 65504.0f16); | |
| // Colors | |
| ASSERT_EQ(HalfVector4(Color::Red()), | |
| HalfVector4(1.0f16, 0.0f16, 0.0f16, 1.0f16)); | |
| ASSERT_EQ(HalfVector4(Color::Green()), | |
| HalfVector4(0.0f16, 1.0f16, 0.0f16, 1.0f16)); | |
| ASSERT_EQ(HalfVector4(Color::Blue()), | |
| HalfVector4(0.0f16, 0.0f16, 1.0f16, 1.0f16)); | |
| ASSERT_EQ(HalfVector4(Color::Black().WithAlpha(0)), | |
| HalfVector4(0.0f16, 0.0f16, 0.0f16, 0.0f16)); | |
| ASSERT_EQ(HalfVector3(Vector3(4.0, 6.0, -1.0)), | |
| HalfVector3(4.0f16, 6.0f16, -1.0f16)); | |
| ASSERT_EQ(HalfVector2(Vector2(4.0, 6.0)), HalfVector2(4.0f16, 6.0f16)); | |
| ASSERT_EQ(Half(0.5f), Half(0.5f16)); | |
| ASSERT_EQ(Half(0.5), Half(0.5f16)); | |
| ASSERT_EQ(Half(5), Half(5.0f16)); | |
| #else | |
| GTEST_SKIP() << "Half-precision floats (IEEE 754) are not portable and " | |
| "only used on Apple platforms."; | |
| #endif // FML_OS_MACOSX || FML_OS_IOS || FML_OS_IOS_SIMULATOR | |
| } | |
| } // namespace testing | |
| } // namespace impeller | |
| // NOLINTEND(bugprone-unchecked-optional-access) |