blob: fbcaaf74d9bd3601e6012886d66883a42b241383 [file] [log] [blame]
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/gfx/geometry/transform_util.h"
#include <stddef.h>
#include <limits>
#include "base/cxx17_backports.h"
#include "base/numerics/math_constants.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/point3_f.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/gfx/geometry/test/geometry_util.h"
namespace gfx {
namespace {
#define EXPECT_APPROX_EQ(val1, val2) EXPECT_NEAR(val1, val2, 1e-6);
TEST(TransformUtilTest, GetScaleTransform) {
const Point kAnchor(20, 40);
const float kScale = 0.5f;
Transform scale = GetScaleTransform(kAnchor, kScale);
const int kOffset = 10;
for (int sign_x = -1; sign_x <= 1; ++sign_x) {
for (int sign_y = -1; sign_y <= 1; ++sign_y) {
Point test = scale.MapPoint(Point(kAnchor.x() + sign_x * kOffset,
kAnchor.y() + sign_y * kOffset));
EXPECT_EQ(Point(kAnchor.x() + sign_x * kOffset * kScale,
kAnchor.y() + sign_y * kOffset * kScale),
test);
}
}
}
TEST(TransformUtilTest, TransformAboutPivot) {
Transform transform;
transform.Scale(3, 4);
transform = TransformAboutPivot(PointF(7, 8), transform);
Point point = transform.MapPoint(Point(0, 0));
EXPECT_EQ(Point(-14, -24).ToString(), point.ToString());
point = transform.MapPoint(Point(1, 1));
EXPECT_EQ(Point(-11, -20).ToString(), point.ToString());
}
TEST(TransformUtilTest, BlendOppositeQuaternions) {
DecomposedTransform first;
DecomposedTransform second;
second.quaternion.set_w(-second.quaternion.w());
DecomposedTransform result = BlendDecomposedTransforms(first, second, 0.25);
EXPECT_TRUE(std::isfinite(result.quaternion.x()));
EXPECT_TRUE(std::isfinite(result.quaternion.y()));
EXPECT_TRUE(std::isfinite(result.quaternion.z()));
EXPECT_TRUE(std::isfinite(result.quaternion.w()));
EXPECT_FALSE(std::isnan(result.quaternion.x()));
EXPECT_FALSE(std::isnan(result.quaternion.y()));
EXPECT_FALSE(std::isnan(result.quaternion.z()));
EXPECT_FALSE(std::isnan(result.quaternion.w()));
}
TEST(TransformUtilTest, AccumulateDecomposedTransforms) {
DecomposedTransform a{{2.5, -3.25, 4.75},
{4.5, -5.25, 6.75},
{1.25, -2.5, 3.75},
{5, -4, 3, -2},
{-5, 6, -7, 8}};
DecomposedTransform b{
{-2, 3, 4}, {-4, 5, 6}, {-1, 2, 3}, {6, 7, -8, -9}, {5, 4, -3, -2}};
DecomposedTransform expected{{0.5, -0.25, 8.75},
{-0.5, -1.25, 11.75},
{0.25, -0.5, 6.75},
{11, 3, -5, -12},
{+60, -30, -60, -36}};
EXPECT_DECOMPOSED_TRANSFORM_EQ(expected,
AccumulateDecomposedTransforms(a, b));
}
TEST(TransformUtilTest, TransformBetweenRects) {
auto verify = [](const RectF& src_rect, const RectF& dst_rect) {
const Transform transform = TransformBetweenRects(src_rect, dst_rect);
// Applies |transform| to calculate the target rectangle from |src_rect|.
// Notes that |transform| is in |src_rect|'s local coordinates.
RectF dst_in_parent_coordinates = transform.MapRect(RectF(src_rect.size()));
dst_in_parent_coordinates.Offset(src_rect.OffsetFromOrigin());
// Verifies that the target rectangle is expected.
EXPECT_EQ(dst_rect, dst_in_parent_coordinates);
};
std::vector<std::pair<const RectF, const RectF>> test_cases{
{RectF(0.f, 0.f, 2.f, 3.f), RectF(3.f, 5.f, 4.f, 9.f)},
{RectF(10.f, 7.f, 2.f, 6.f), RectF(4.f, 2.f, 1.f, 12.f)},
{RectF(0.f, 0.f, 3.f, 5.f), RectF(0.f, 0.f, 6.f, 2.5f)}};
for (const auto& test_case : test_cases) {
verify(test_case.first, test_case.second);
verify(test_case.second, test_case.first);
}
// Tests the case where the destination is an empty rectangle.
verify(RectF(0.f, 0.f, 3.f, 5.f), RectF());
}
TEST(TransformUtilTest, OrthoProjectionTransform) {
auto verify = [](float left, float right, float bottom, float top) {
AxisTransform2d t = OrthoProjectionTransform(left, right, bottom, top);
if (right == left || top == bottom) {
EXPECT_EQ(AxisTransform2d(), t);
} else {
EXPECT_EQ(PointF(-1, -1), t.MapPoint(PointF(left, bottom)));
EXPECT_EQ(PointF(1, 1), t.MapPoint(PointF(right, top)));
}
};
verify(0, 0, 0, 0);
verify(10, 20, 10, 30);
verify(10, 30, 20, 30);
verify(0, 0, 10, 20);
verify(-100, 400, 200, -200);
verify(-1.5, 4.25, 2.75, -3.75);
}
TEST(TransformUtilTest, WindowTransform) {
auto verify = [](int x, int y, int width, int height) {
AxisTransform2d t = WindowTransform(x, y, width, height);
EXPECT_EQ(PointF(x, y), t.MapPoint(PointF(-1, -1)));
EXPECT_EQ(PointF(x + width, y + height), t.MapPoint(PointF(1, 1)));
};
verify(0, 0, 0, 0);
verify(10, 20, 0, 30);
verify(10, 30, 20, 0);
verify(0, 0, 10, 20);
verify(-100, -400, 200, 300);
}
TEST(TransformUtilTest, Transform2dScaleComponents) {
// Values to test quiet NaN, infinity, and a denormal float if they're
// present; zero otherwise (since for the case this is used for, it
// should produce the same result).
const float quiet_NaN_or_zero = std::numeric_limits<float>::has_quiet_NaN
? std::numeric_limits<float>::quiet_NaN()
: 0;
const float infinity_or_zero = std::numeric_limits<float>::has_infinity
? std::numeric_limits<float>::infinity()
: 0;
const float denorm_min_or_zero =
(std::numeric_limits<float>::has_denorm == std::denorm_present)
? std::numeric_limits<float>::denorm_min()
: 0;
const struct {
Transform transform;
absl::optional<Vector2dF> expected_scale;
} tests[] = {
// clang-format off
// A matrix with only scale and translation.
{Transform::RowMajor(3, 0, 0, -23,
0, 7, 0, 31,
0, 0, 11, 47,
0, 0, 0, 1),
Vector2dF(3, 7)},
// Matrices like the first, but also with various
// perspective-altering components.
{Transform::RowMajor(3, 0, 0, -23,
0, 7, 0, 31,
0, 0, 11, 47,
0, 0, -0.5, 1),
Vector2dF(3, 7)},
{Transform::RowMajor(3, 0, 0, -23,
0, 7, 0, 31,
0, 0, 11, 47,
0.2f, 0, -0.5f, 1),
absl::nullopt},
{Transform::RowMajor(3, 0, 0, -23,
0, 7, 0, 31,
0, 0, 11, 47,
0.2f, -0.2f, -0.5f, 1),
absl::nullopt},
{Transform::RowMajor(3, 0, 0, -23,
0, 7, 0, 31,
0, 0, 11, 47,
0.2f, -0.2f, -0.5f, 1),
absl::nullopt},
{Transform::RowMajor(3, 0, 0, -23,
0, 7, 0, 31,
0, 0, 11, 47,
0, -0.2f, -0.5f, 1),
absl::nullopt},
{Transform::RowMajor(3, 0, 0, -23,
0, 7, 0, 31,
0, 0, 11, 47,
0, 0, -0.5f, 0.25f),
Vector2dF(12, 28)},
// Matrices like the first, but with some types of rotation.
{Transform::RowMajor(0, 3, 0, -23,
7, 0, 0, 31,
0, 0, 11, 47,
0, 0, 0, 1),
Vector2dF(7, 3)},
{Transform::RowMajor(3, 8, 0, -23,
4, 6, 0, 31,
0, 0, 11, 47,
0, 0, 0, 1),
Vector2dF(5, 10)},
// Combination of rotation and perspective
{Transform::RowMajor(3, 8, 0, -23,
4, 6, 0, 31,
0, 0, 11, 47,
0, 0, 0, 0.25f),
Vector2dF(20, 40)},
// Error handling cases for final perspective component.
{Transform::RowMajor(3, 0, 0, -23,
0, 7, 0, 31,
0, 0, 11, 47,
0, 0, 0, 0),
absl::nullopt},
{Transform::RowMajor(3, 0, 0, -23,
0, 7, 0, 31,
0, 0, 11, 47,
0, 0, 0, quiet_NaN_or_zero),
absl::nullopt},
{Transform::RowMajor(3, 0, 0, -23,
0, 7, 0, 31,
0, 0, 11, 47,
0, 0, 0, infinity_or_zero),
absl::nullopt},
{Transform::RowMajor(3, 0, 0, -23,
0, 7, 0, 31,
0, 0, 11, 47,
0, 0, 0, denorm_min_or_zero),
absl::nullopt},
// clang-format on
};
const float fallback = 1.409718f; // randomly generated in [1,2)
for (const auto& test : tests) {
absl::optional<Vector2dF> try_result =
TryComputeTransform2dScaleComponents(test.transform);
EXPECT_EQ(try_result, test.expected_scale);
Vector2dF result =
ComputeTransform2dScaleComponents(test.transform, fallback);
if (test.expected_scale) {
EXPECT_EQ(result, *test.expected_scale);
} else {
EXPECT_EQ(result, Vector2dF(fallback, fallback));
}
}
}
} // namespace
} // namespace gfx