blob: db027c3c3419b02c0373caf8400610c0fcfc425c [file] [log] [blame]
// 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 "impeller/geometry/geometry_unittests.h"
#include <limits>
#include <sstream>
#include "flutter/testing/testing.h"
#include "impeller/geometry/constants.h"
#include "impeller/geometry/gradient.h"
#include "impeller/geometry/path.h"
#include "impeller/geometry/path_builder.h"
#include "impeller/geometry/path_component.h"
#include "impeller/geometry/point.h"
#include "impeller/geometry/rect.h"
#include "impeller/geometry/scalar.h"
#include "impeller/geometry/size.h"
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, 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);
}
}
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, MatrixMakeOrthographic) {
{
auto m = Matrix::MakeOrthographic(Size(100, 200));
auto expect = Matrix{
0.02, 0, 0, 0, //
0, -0.01, 0, 0, //
0, 0, 1, 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, 1, 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, 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, EmptyPath) {
auto path = PathBuilder{}.TakePath();
ASSERT_EQ(path.GetComponentCount(), 1u);
ContourComponent c;
path.GetContourComponentAtIndex(0, c);
ASSERT_POINT_NEAR(c.destination, Point());
Path::Polyline polyline = path.CreatePolyline();
ASSERT_TRUE(polyline.points.empty());
ASSERT_TRUE(polyline.contours.empty());
}
TEST(GeometryTest, SimplePath) {
Path path;
path.AddLinearComponent({0, 0}, {100, 100})
.AddQuadraticComponent({100, 100}, {200, 200}, {300, 300})
.AddCubicComponent({300, 300}, {400, 400}, {500, 500}, {600, 600});
ASSERT_EQ(path.GetComponentCount(), 4u);
path.EnumerateComponents(
[](size_t index, const LinearPathComponent& linear) {
Point p1(0, 0);
Point p2(100, 100);
ASSERT_EQ(index, 1u);
ASSERT_EQ(linear.p1, p1);
ASSERT_EQ(linear.p2, p2);
},
[](size_t index, const QuadraticPathComponent& quad) {
Point p1(100, 100);
Point cp(200, 200);
Point p2(300, 300);
ASSERT_EQ(index, 2u);
ASSERT_EQ(quad.p1, p1);
ASSERT_EQ(quad.cp, cp);
ASSERT_EQ(quad.p2, p2);
},
[](size_t index, const CubicPathComponent& cubic) {
Point p1(300, 300);
Point cp1(400, 400);
Point cp2(500, 500);
Point p2(600, 600);
ASSERT_EQ(index, 3u);
ASSERT_EQ(cubic.p1, p1);
ASSERT_EQ(cubic.cp1, cp1);
ASSERT_EQ(cubic.cp2, cp2);
ASSERT_EQ(cubic.p2, p2);
},
[](size_t index, const ContourComponent& contour) {
Point p1(0, 0);
ASSERT_EQ(index, 0u);
ASSERT_EQ(contour.destination, p1);
ASSERT_FALSE(contour.is_closed);
});
}
TEST(GeometryTest, BoundingBoxCubic) {
Path path;
path.AddCubicComponent({120, 160}, {25, 200}, {220, 260}, {220, 40});
auto box = path.GetBoundingBox();
Rect expected(93.9101, 40, 126.09, 158.862);
ASSERT_TRUE(box.has_value());
ASSERT_RECT_NEAR(box.value(), expected);
}
TEST(GeometryTest, BoundingBoxOfCompositePathIsCorrect) {
PathBuilder builder;
builder.AddRoundedRect({{10, 10}, {300, 300}}, {50, 50, 50, 50});
auto path = builder.TakePath();
auto actual = path.GetBoundingBox();
Rect expected(10, 10, 300, 300);
ASSERT_TRUE(actual.has_value());
ASSERT_RECT_NEAR(actual.value(), expected);
}
TEST(GeometryTest, PathGetBoundingBoxForCubicWithNoDerivativeRootsIsCorrect) {
PathBuilder builder;
// Straight diagonal line.
builder.AddCubicCurve({0, 1}, {2, 3}, {4, 5}, {6, 7});
auto path = builder.TakePath();
auto actual = path.GetBoundingBox();
auto expected = Rect::MakeLTRB(0, 1, 6, 7);
ASSERT_TRUE(actual.has_value());
ASSERT_RECT_NEAR(actual.value(), 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);
}
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);
}
{
Rect r1(1.0, 2.0, 3.0, 4.0);
IRect r2 = static_cast<IRect>(r1);
ASSERT_EQ(r2.origin.x, 1u);
ASSERT_EQ(r2.origin.y, 2u);
ASSERT_EQ(r2.size.width, 3u);
ASSERT_EQ(r2.size.height, 4u);
}
}
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, 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(2, 6, 12);
p /= Vector3(2, 3, 4);
ASSERT_EQ(p.x, 1u);
ASSERT_EQ(p.y, 2u);
ASSERT_EQ(p.z, 3u);
}
}
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, CanConvertBetweenDegressAndRadians) {
{
auto deg = Degrees{90.0};
Radians rad = deg;
ASSERT_FLOAT_EQ(rad.radians, kPiOver2);
}
}
TEST(GeometryTest, RectMakeSize) {
{
Size s(100, 200);
Rect r = Rect::MakeSize(s);
Rect expected = Rect::MakeLTRB(0, 0, 100, 200);
ASSERT_RECT_NEAR(r, expected);
}
{
ISize s(100, 200);
Rect r = Rect::MakeSize(s);
Rect expected = Rect::MakeLTRB(0, 0, 100, 200);
ASSERT_RECT_NEAR(r, expected);
}
{
Size s(100, 200);
IRect r = IRect::MakeSize(s);
IRect expected = IRect::MakeLTRB(0, 0, 100, 200);
ASSERT_EQ(r, expected);
}
{
ISize s(100, 200);
IRect r = IRect::MakeSize(s);
IRect expected = IRect::MakeLTRB(0, 0, 100, 200);
ASSERT_EQ(r, expected);
}
}
TEST(GeometryTest, RectUnion) {
{
Rect a(100, 100, 100, 100);
Rect b(0, 0, 0, 0);
auto u = a.Union(b);
auto expected = Rect(0, 0, 200, 200);
ASSERT_RECT_NEAR(u, expected);
}
{
Rect a(100, 100, 100, 100);
Rect b(10, 10, 0, 0);
auto u = a.Union(b);
auto expected = Rect(10, 10, 190, 190);
ASSERT_RECT_NEAR(u, expected);
}
{
Rect a(0, 0, 100, 100);
Rect b(10, 10, 100, 100);
auto u = a.Union(b);
auto expected = Rect(0, 0, 110, 110);
ASSERT_RECT_NEAR(u, expected);
}
{
Rect a(0, 0, 100, 100);
Rect b(100, 100, 100, 100);
auto u = a.Union(b);
auto expected = Rect(0, 0, 200, 200);
ASSERT_RECT_NEAR(u, expected);
}
}
TEST(GeometryTest, RectIntersection) {
{
Rect a(100, 100, 100, 100);
Rect b(0, 0, 0, 0);
auto u = a.Intersection(b);
ASSERT_FALSE(u.has_value());
}
{
Rect a(100, 100, 100, 100);
Rect b(10, 10, 0, 0);
auto u = a.Intersection(b);
ASSERT_FALSE(u.has_value());
}
{
Rect a(0, 0, 100, 100);
Rect b(10, 10, 100, 100);
auto u = a.Intersection(b);
ASSERT_TRUE(u.has_value());
auto expected = Rect(10, 10, 90, 90);
ASSERT_RECT_NEAR(u.value(), expected);
}
{
Rect a(0, 0, 100, 100);
Rect b(100, 100, 100, 100);
auto u = a.Intersection(b);
ASSERT_FALSE(u.has_value());
}
}
TEST(GeometryTest, RectIntersectsWithRect) {
{
Rect a(100, 100, 100, 100);
Rect b(0, 0, 0, 0);
ASSERT_FALSE(a.IntersectsWithRect(b));
}
{
Rect a(100, 100, 100, 100);
Rect b(10, 10, 0, 0);
ASSERT_FALSE(a.IntersectsWithRect(b));
}
{
Rect a(0, 0, 100, 100);
Rect b(10, 10, 100, 100);
ASSERT_TRUE(a.IntersectsWithRect(b));
}
{
Rect a(0, 0, 100, 100);
Rect b(100, 100, 100, 100);
ASSERT_FALSE(a.IntersectsWithRect(b));
}
}
TEST(GeometryTest, RectContainsPoint) {
{
// Origin is inclusive
Rect r(100, 100, 100, 100);
Point p(100, 100);
ASSERT_TRUE(r.Contains(p));
}
{
// Size is exclusive
Rect r(100, 100, 100, 100);
Point p(200, 200);
ASSERT_FALSE(r.Contains(p));
}
{
Rect r(100, 100, 100, 100);
Point p(99, 99);
ASSERT_FALSE(r.Contains(p));
}
{
Rect r(100, 100, 100, 100);
Point p(199, 199);
ASSERT_TRUE(r.Contains(p));
}
}
TEST(GeometryTest, RectContainsRect) {
{
Rect a(100, 100, 100, 100);
ASSERT_TRUE(a.Contains(a));
}
{
Rect a(100, 100, 100, 100);
Rect b(0, 0, 0, 0);
ASSERT_FALSE(a.Contains(b));
}
{
Rect a(100, 100, 100, 100);
Rect b(150, 150, 20, 20);
ASSERT_TRUE(a.Contains(b));
}
{
Rect a(100, 100, 100, 100);
Rect b(150, 150, 100, 100);
ASSERT_FALSE(a.Contains(b));
}
{
Rect a(100, 100, 100, 100);
Rect b(50, 50, 100, 100);
ASSERT_FALSE(a.Contains(b));
}
{
Rect a(100, 100, 100, 100);
Rect b(0, 0, 300, 300);
ASSERT_FALSE(a.Contains(b));
}
}
TEST(GeometryTest, RectGetPoints) {
Rect r(100, 200, 300, 400);
auto points = r.GetPoints();
ASSERT_POINT_NEAR(points[0], Point(100, 200));
ASSERT_POINT_NEAR(points[1], Point(400, 200));
ASSERT_POINT_NEAR(points[2], Point(100, 600));
ASSERT_POINT_NEAR(points[3], Point(400, 600));
}
TEST(GeometryTest, RectGetTransformedPoints) {
Rect r(100, 200, 300, 400);
auto points = r.GetTransformedPoints(Matrix::MakeTranslation({10, 20}));
ASSERT_POINT_NEAR(points[0], Point(110, 220));
ASSERT_POINT_NEAR(points[1], Point(410, 220));
ASSERT_POINT_NEAR(points[2], Point(110, 620));
ASSERT_POINT_NEAR(points[3], Point(410, 620));
}
TEST(GeometryTest, RectMakePointBounds) {
{
Rect r =
Rect::MakePointBounds({Point(1, 5), Point(4, -1), Point(0, 6)}).value();
auto expected = Rect(0, -1, 4, 7);
ASSERT_RECT_NEAR(r, expected);
}
{
std::optional<Rect> r = Rect::MakePointBounds({});
ASSERT_FALSE(r.has_value());
}
}
TEST(GeometryTest, RectGetPositive) {
{
Rect r{100, 200, 300, 400};
auto actual = r.GetPositive();
ASSERT_RECT_NEAR(r, actual);
}
{
Rect r{100, 200, -100, -100};
auto actual = r.GetPositive();
Rect expected(0, 100, 100, 100);
ASSERT_RECT_NEAR(expected, actual);
}
}
TEST(GeometryTest, CubicPathComponentPolylineDoesNotIncludePointOne) {
CubicPathComponent component({10, 10}, {20, 35}, {35, 20}, {40, 40});
SmoothingApproximation approximation;
auto polyline = component.CreatePolyline(approximation);
ASSERT_NE(polyline.front().x, 10);
ASSERT_NE(polyline.front().y, 10);
ASSERT_EQ(polyline.back().x, 40);
ASSERT_EQ(polyline.back().y, 40);
}
TEST(GeometryTest, PathCreatePolyLineDoesNotDuplicatePoints) {
Path path;
path.AddContourComponent({10, 10});
path.AddLinearComponent({10, 10}, {20, 20});
path.AddLinearComponent({20, 20}, {30, 30});
path.AddContourComponent({40, 40});
path.AddLinearComponent({40, 40}, {50, 50});
auto polyline = path.CreatePolyline();
ASSERT_EQ(polyline.contours.size(), 2u);
ASSERT_EQ(polyline.points.size(), 5u);
ASSERT_EQ(polyline.points[0].x, 10);
ASSERT_EQ(polyline.points[1].x, 20);
ASSERT_EQ(polyline.points[2].x, 30);
ASSERT_EQ(polyline.points[3].x, 40);
ASSERT_EQ(polyline.points[4].x, 50);
}
TEST(GeometryTest, PathBuilderSetsCorrectContourPropertiesForAddCommands) {
// Closed shapes.
{
Path path = PathBuilder{}.AddCircle({100, 100}, 50).TakePath();
ContourComponent contour;
path.GetContourComponentAtIndex(0, contour);
ASSERT_POINT_NEAR(contour.destination, Point(100, 50));
ASSERT_TRUE(contour.is_closed);
}
{
Path path = PathBuilder{}.AddOval(Rect(100, 100, 100, 100)).TakePath();
ContourComponent contour;
path.GetContourComponentAtIndex(0, contour);
ASSERT_POINT_NEAR(contour.destination, Point(150, 100));
ASSERT_TRUE(contour.is_closed);
}
{
Path path = PathBuilder{}.AddRect(Rect(100, 100, 100, 100)).TakePath();
ContourComponent contour;
path.GetContourComponentAtIndex(0, contour);
ASSERT_POINT_NEAR(contour.destination, Point(100, 100));
ASSERT_TRUE(contour.is_closed);
}
{
Path path =
PathBuilder{}.AddRoundedRect(Rect(100, 100, 100, 100), 10).TakePath();
ContourComponent contour;
path.GetContourComponentAtIndex(0, contour);
ASSERT_POINT_NEAR(contour.destination, Point(110, 100));
ASSERT_TRUE(contour.is_closed);
}
// Open shapes.
{
Point p(100, 100);
Path path = PathBuilder{}.AddLine(p, {200, 100}).TakePath();
ContourComponent contour;
path.GetContourComponentAtIndex(0, contour);
ASSERT_POINT_NEAR(contour.destination, p);
ASSERT_FALSE(contour.is_closed);
}
{
Path path =
PathBuilder{}
.AddCubicCurve({100, 100}, {100, 50}, {100, 150}, {200, 100})
.TakePath();
ContourComponent contour;
path.GetContourComponentAtIndex(0, contour);
ASSERT_POINT_NEAR(contour.destination, Point(100, 100));
ASSERT_FALSE(contour.is_closed);
}
{
Path path = PathBuilder{}
.AddQuadraticCurve({100, 100}, {100, 50}, {200, 100})
.TakePath();
ContourComponent contour;
path.GetContourComponentAtIndex(0, contour);
ASSERT_POINT_NEAR(contour.destination, Point(100, 100));
ASSERT_FALSE(contour.is_closed);
}
}
TEST(GeometryTest, PathCreatePolylineGeneratesCorrectContourData) {
Path::Polyline polyline = PathBuilder{}
.AddLine({100, 100}, {200, 100})
.MoveTo({100, 200})
.LineTo({150, 250})
.LineTo({200, 200})
.Close()
.TakePath()
.CreatePolyline();
ASSERT_EQ(polyline.points.size(), 6u);
ASSERT_EQ(polyline.contours.size(), 2u);
ASSERT_EQ(polyline.contours[0].is_closed, false);
ASSERT_EQ(polyline.contours[0].start_index, 0u);
ASSERT_EQ(polyline.contours[1].is_closed, true);
ASSERT_EQ(polyline.contours[1].start_index, 2u);
}
TEST(GeometryTest, PolylineGetContourPointBoundsReturnsCorrectRanges) {
Path::Polyline polyline = PathBuilder{}
.AddLine({100, 100}, {200, 100})
.MoveTo({100, 200})
.LineTo({150, 250})
.LineTo({200, 200})
.Close()
.TakePath()
.CreatePolyline();
size_t a1, a2, b1, b2;
std::tie(a1, a2) = polyline.GetContourPointBounds(0);
std::tie(b1, b2) = polyline.GetContourPointBounds(1);
ASSERT_EQ(a1, 0u);
ASSERT_EQ(a2, 2u);
ASSERT_EQ(b1, 2u);
ASSERT_EQ(b2, 6u);
}
TEST(GeometryTest, PathAddRectPolylineHasCorrectContourData) {
Path::Polyline polyline = PathBuilder{}
.AddRect(Rect::MakeLTRB(50, 60, 70, 80))
.TakePath()
.CreatePolyline();
ASSERT_EQ(polyline.contours.size(), 1u);
ASSERT_TRUE(polyline.contours[0].is_closed);
ASSERT_EQ(polyline.contours[0].start_index, 0u);
ASSERT_EQ(polyline.points.size(), 5u);
ASSERT_EQ(polyline.points[0], Point(50, 60));
ASSERT_EQ(polyline.points[1], Point(70, 60));
ASSERT_EQ(polyline.points[2], Point(70, 80));
ASSERT_EQ(polyline.points[3], Point(50, 80));
ASSERT_EQ(polyline.points[4], Point(50, 60));
}
TEST(GeometryTest, PathPolylineDuplicatesAreRemovedForSameContour) {
Path::Polyline polyline =
PathBuilder{}
.MoveTo({50, 50})
.LineTo({50, 50}) // Insert duplicate at beginning of contour.
.LineTo({100, 50})
.LineTo({100, 50}) // Insert duplicate at contour join.
.LineTo({100, 100})
.Close() // Implicitly insert duplicate {50, 50} across contours.
.LineTo({0, 50})
.LineTo({0, 100})
.LineTo({0, 100}) // Insert duplicate at end of contour.
.TakePath()
.CreatePolyline();
ASSERT_EQ(polyline.contours.size(), 2u);
ASSERT_EQ(polyline.contours[0].start_index, 0u);
ASSERT_TRUE(polyline.contours[0].is_closed);
ASSERT_EQ(polyline.contours[1].start_index, 4u);
ASSERT_FALSE(polyline.contours[1].is_closed);
ASSERT_EQ(polyline.points.size(), 7u);
ASSERT_EQ(polyline.points[0], Point(50, 50));
ASSERT_EQ(polyline.points[1], Point(100, 50));
ASSERT_EQ(polyline.points[2], Point(100, 100));
ASSERT_EQ(polyline.points[3], Point(50, 50));
ASSERT_EQ(polyline.points[4], Point(50, 50));
ASSERT_EQ(polyline.points[5], Point(0, 50));
ASSERT_EQ(polyline.points[6], Point(0, 100));
}
TEST(GeometryTest, VerticesConstructorAndGetters) {
std::vector<Point> points = {Point(1, 2), Point(2, 3), Point(3, 4)};
std::vector<uint16_t> indices = {0, 1, 2};
std::vector<Color> colors = {Color::White(), Color::White(), Color::White()};
Vertices vertices = Vertices(points, indices, colors, VertexMode::kTriangle,
Rect(0, 0, 4, 4));
ASSERT_EQ(vertices.GetBoundingBox().value(), Rect(0, 0, 4, 4));
ASSERT_EQ(vertices.GetPositions(), points);
ASSERT_EQ(vertices.GetIndices(), indices);
ASSERT_EQ(vertices.GetColors(), colors);
ASSERT_EQ(vertices.GetMode(), VertexMode::kTriangle);
}
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,
))");
stream.str("");
stream.clear();
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, 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};
uint32_t texture_size;
auto gradient = CreateGradientBuffer(colors, stops, &texture_size);
ASSERT_COLOR_BUFFER_NEAR(gradient, colors);
ASSERT_EQ(texture_size, 2u);
}
{
// 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};
uint32_t texture_size;
auto gradient = CreateGradientBuffer(colors, stops, &texture_size);
ASSERT_COLOR_BUFFER_NEAR(gradient, colors);
ASSERT_EQ(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};
uint32_t texture_size;
auto gradient = CreateGradientBuffer(colors, stops, &texture_size);
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, lerped_colors);
ASSERT_EQ(texture_size, 5u);
}
{
// Gradient size is capped at 1024.
std::vector<Color> colors = {};
std::vector<Scalar> stops = {};
for (auto i = 0u; i < 2000; i++) {
colors.push_back(Color::Blue());
stops.push_back(i / 2000.0);
}
stops[1999] = 1.0;
uint32_t texture_size;
auto gradient = CreateGradientBuffer(colors, stops, &texture_size);
ASSERT_EQ(texture_size, 1024u);
}
}
} // namespace testing
} // namespace impeller