blob: 2ffb9371645d4c096942b2c581de34d9d62ac1cf [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.h"
#include <ostream>
#include "base/check_op.h"
#include "base/notreached.h"
#include "base/strings/stringprintf.h"
#include "ui/gfx/geometry/angle_conversions.h"
#include "ui/gfx/geometry/axis_transform2d.h"
#include "ui/gfx/geometry/box_f.h"
#include "ui/gfx/geometry/clamp_float_geometry.h"
#include "ui/gfx/geometry/decomposed_transform.h"
#include "ui/gfx/geometry/double4.h"
#include "ui/gfx/geometry/point3_f.h"
#include "ui/gfx/geometry/point_conversions.h"
#include "ui/gfx/geometry/quad_f.h"
#include "ui/gfx/geometry/quaternion.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/geometry/transform_util.h"
#include "ui/gfx/geometry/vector3d_f.h"
namespace gfx {
namespace {
const double kEpsilon = std::numeric_limits<float>::epsilon();
double TanDegrees(double degrees) {
return std::tan(DegToRad(degrees));
}
struct SinCos {
double sin;
double cos;
bool IsZeroAngle() const { return sin == 0 && cos == 1; }
};
SinCos SinCosDegrees(double degrees) {
double n90degrees = degrees / 90.0;
int n = static_cast<int>(n90degrees);
if (n == n90degrees) {
n %= 4;
if (n < 0)
n += 4;
constexpr SinCos kSinCosN90[] = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
return kSinCosN90[n];
}
// fmod is to reduce errors of DegToRad() with large |degrees|.
double rad = DegToRad(std::fmod(degrees, 360.0));
return SinCos{std::sin(rad), std::cos(rad)};
}
inline bool ApproximatelyZero(double x, double tolerance) {
return std::abs(x) <= tolerance;
}
inline bool ApproximatelyOne(double x, double tolerance) {
return std::abs(x - 1) <= tolerance;
}
Matrix44 AxisTransform2dToMatrix44(const AxisTransform2d& axis_2d) {
return Matrix44(axis_2d.scale().x(), 0, 0, 0, // col 0
0, axis_2d.scale().y(), 0, 0, // col 1
0, 0, 1, 0, // col 2
axis_2d.translation().x(), axis_2d.translation().y(), 0, 1);
}
template <typename T>
void AxisTransform2dToColMajor(const AxisTransform2d& axis_2d, T a[16]) {
a[0] = axis_2d.scale().x();
a[5] = axis_2d.scale().y();
a[12] = axis_2d.translation().x();
a[13] = axis_2d.translation().y();
a[1] = a[2] = a[3] = a[4] = a[6] = a[7] = a[8] = a[9] = a[11] = a[14] = 0;
a[10] = a[15] = 1;
}
} // namespace
// clang-format off
Transform::Transform(const Quaternion& q)
: Transform(
// Col 0.
1.0 - 2.0 * (q.y() * q.y() + q.z() * q.z()),
2.0 * (q.x() * q.y() + q.z() * q.w()),
2.0 * (q.x() * q.z() - q.y() * q.w()),
0,
// Col 1.
2.0 * (q.x() * q.y() - q.z() * q.w()),
1.0 - 2.0 * (q.x() * q.x() + q.z() * q.z()),
2.0 * (q.y() * q.z() + q.x() * q.w()),
0,
// Col 2.
2.0 * (q.x() * q.z() + q.y() * q.w()),
2.0 * (q.y() * q.z() - q.x() * q.w()),
1.0 - 2.0 * (q.x() * q.x() + q.y() * q.y()),
0,
// Col 3.
0, 0, 0, 1) {}
// clang-format on
Matrix44 Transform::GetFullMatrix() const {
if (LIKELY(!full_matrix_))
return AxisTransform2dToMatrix44(axis_2d_);
return matrix_;
}
Matrix44& Transform::EnsureFullMatrix() {
if (LIKELY(!full_matrix_)) {
full_matrix_ = true;
matrix_ = AxisTransform2dToMatrix44(axis_2d_);
}
return matrix_;
}
// static
Transform Transform::ColMajor(const double a[16]) {
return Transform(a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9],
a[10], a[11], a[12], a[13], a[14], a[15]);
}
// static
Transform Transform::ColMajorF(const float a[16]) {
if (AllTrue(Float4{a[1], a[2], a[3], a[4]} == Float4{0, 0, 0, 0} &
Float4{a[6], a[7], a[8], a[9]} == Float4{0, 0, 0, 0} &
Float4{a[10], a[11], a[14], a[15]} == Float4{1, 0, 0, 1})) {
return Transform(a[0], a[5], a[12], a[13]);
}
return Transform(a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9],
a[10], a[11], a[12], a[13], a[14], a[15]);
}
void Transform::GetColMajor(double a[16]) const {
if (LIKELY(!full_matrix_)) {
AxisTransform2dToColMajor(axis_2d_, a);
} else {
matrix_.GetColMajor(a);
}
}
void Transform::GetColMajorF(float a[16]) const {
if (LIKELY(!full_matrix_)) {
AxisTransform2dToColMajor(axis_2d_, a);
} else {
matrix_.GetColMajorF(a);
}
}
void Transform::RotateAboutXAxis(double degrees) {
SinCos sin_cos = SinCosDegrees(degrees);
if (sin_cos.IsZeroAngle())
return;
EnsureFullMatrix().RotateAboutXAxisSinCos(sin_cos.sin, sin_cos.cos);
}
void Transform::RotateAboutYAxis(double degrees) {
SinCos sin_cos = SinCosDegrees(degrees);
if (sin_cos.IsZeroAngle())
return;
EnsureFullMatrix().RotateAboutYAxisSinCos(sin_cos.sin, sin_cos.cos);
}
void Transform::RotateAboutZAxis(double degrees) {
SinCos sin_cos = SinCosDegrees(degrees);
if (sin_cos.IsZeroAngle())
return;
EnsureFullMatrix().RotateAboutZAxisSinCos(sin_cos.sin, sin_cos.cos);
}
void Transform::RotateAbout(double x, double y, double z, double degrees) {
SinCos sin_cos = SinCosDegrees(degrees);
if (sin_cos.IsZeroAngle())
return;
double square_length = x * x + y * y + z * z;
if (square_length == 0)
return;
if (square_length != 1) {
double scale = 1.0 / sqrt(square_length);
x *= scale;
y *= scale;
z *= scale;
}
EnsureFullMatrix().RotateUnitSinCos(x, y, z, sin_cos.sin, sin_cos.cos);
}
void Transform::RotateAbout(const Vector3dF& axis, double degrees) {
RotateAbout(axis.x(), axis.y(), axis.z(), degrees);
}
double Transform::Determinant() const {
return LIKELY(!full_matrix_) ? axis_2d_.Determinant() : matrix_.Determinant();
}
void Transform::Scale(float x, float y) {
if (LIKELY(!full_matrix_))
axis_2d_.PreScale(Vector2dF(x, y));
else
matrix_.PreScale(x, y);
}
void Transform::PostScale(float x, float y) {
if (LIKELY(!full_matrix_))
axis_2d_.PostScale(Vector2dF(x, y));
else
matrix_.PostScale(x, y);
}
void Transform::Scale3d(float x, float y, float z) {
if (z == 1)
Scale(x, y);
else
EnsureFullMatrix().PreScale3d(x, y, z);
}
void Transform::PostScale3d(float x, float y, float z) {
if (z == 1)
PostScale(x, y);
else
EnsureFullMatrix().PostScale3d(x, y, z);
}
void Transform::Translate(const Vector2dF& offset) {
Translate(offset.x(), offset.y());
}
void Transform::Translate(float x, float y) {
if (LIKELY(!full_matrix_))
axis_2d_.PreTranslate(Vector2dF(x, y));
else
matrix_.PreTranslate(x, y);
}
void Transform::PostTranslate(const Vector2dF& offset) {
PostTranslate(offset.x(), offset.y());
}
void Transform::PostTranslate(float x, float y) {
if (LIKELY(!full_matrix_))
axis_2d_.PostTranslate(Vector2dF(x, y));
else
matrix_.PostTranslate(x, y);
}
void Transform::PostTranslate3d(const Vector3dF& offset) {
PostTranslate3d(offset.x(), offset.y(), offset.z());
}
void Transform::PostTranslate3d(float x, float y, float z) {
if (z == 0)
PostTranslate(x, y);
else
EnsureFullMatrix().PostTranslate3d(x, y, z);
}
void Transform::Translate3d(const Vector3dF& offset) {
Translate3d(offset.x(), offset.y(), offset.z());
}
void Transform::Translate3d(float x, float y, float z) {
if (z == 0)
Translate(x, y);
else
EnsureFullMatrix().PreTranslate3d(x, y, z);
}
void Transform::Skew(double degrees_x, double degrees_y) {
if (!degrees_x && !degrees_y)
return;
EnsureFullMatrix().Skew(TanDegrees(degrees_x), TanDegrees(degrees_y));
}
void Transform::ApplyPerspectiveDepth(double depth) {
if (depth == 0)
return;
EnsureFullMatrix().ApplyPerspectiveDepth(depth);
}
void Transform::PreConcat(const Transform& transform) {
if (LIKELY(!transform.full_matrix_)) {
PreConcat(transform.axis_2d_);
} else if (LIKELY(!full_matrix_)) {
AxisTransform2d self = axis_2d_;
*this = transform;
PostConcat(self);
} else {
matrix_.PreConcat(transform.matrix_);
}
}
void Transform::PostConcat(const Transform& transform) {
if (LIKELY(!transform.full_matrix_)) {
PostConcat(transform.axis_2d_);
} else if (LIKELY(!full_matrix_)) {
AxisTransform2d self = axis_2d_;
*this = transform;
PreConcat(self);
} else {
matrix_.PostConcat(transform.matrix_);
}
}
Transform Transform::operator*(const Transform& transform) const {
if (LIKELY(!transform.full_matrix_)) {
Transform result = *this;
result.PreConcat(transform.axis_2d_);
return result;
}
if (LIKELY(!full_matrix_)) {
Transform result = transform;
result.PostConcat(axis_2d_);
return result;
}
Transform result(Matrix44::kUninitialized);
result.matrix_.SetConcat(matrix_, transform.matrix_);
return result;
}
void Transform::PreConcat(const AxisTransform2d& transform) {
Translate(transform.translation());
Scale(transform.scale().x(), transform.scale().y());
}
void Transform::PostConcat(const AxisTransform2d& transform) {
PostScale(transform.scale().x(), transform.scale().y());
PostTranslate(transform.translation());
}
bool Transform::IsApproximatelyIdentityOrTranslation(double tolerance) const {
DCHECK_GE(tolerance, 0);
if (LIKELY(!full_matrix_)) {
return ApproximatelyOne(axis_2d_.scale().x(), tolerance) &&
ApproximatelyOne(axis_2d_.scale().y(), tolerance);
}
if (!ApproximatelyOne(matrix_.rc(0, 0), tolerance) ||
!ApproximatelyZero(matrix_.rc(1, 0), tolerance) ||
!ApproximatelyZero(matrix_.rc(2, 0), tolerance) ||
!ApproximatelyZero(matrix_.rc(0, 1), tolerance) ||
!ApproximatelyOne(matrix_.rc(1, 1), tolerance) ||
!ApproximatelyZero(matrix_.rc(2, 1), tolerance) ||
!ApproximatelyZero(matrix_.rc(0, 2), tolerance) ||
!ApproximatelyZero(matrix_.rc(1, 2), tolerance) ||
!ApproximatelyOne(matrix_.rc(2, 2), tolerance)) {
return false;
}
// Check perspective components more strictly by using the smaller of float
// epsilon and |tolerance|.
const double perspective_tolerance = std::min(kEpsilon, tolerance);
return ApproximatelyZero(matrix_.rc(3, 0), perspective_tolerance) &&
ApproximatelyZero(matrix_.rc(3, 1), perspective_tolerance) &&
ApproximatelyZero(matrix_.rc(3, 2), perspective_tolerance) &&
ApproximatelyOne(matrix_.rc(3, 3), perspective_tolerance);
}
bool Transform::IsApproximatelyIdentityOrIntegerTranslation(
double tolerance) const {
if (!IsApproximatelyIdentityOrTranslation(tolerance))
return false;
if (LIKELY(!full_matrix_)) {
for (float t : {axis_2d_.translation().x(), axis_2d_.translation().y()}) {
if (!base::IsValueInRangeForNumericType<int>(t) ||
std::abs(std::round(t) - t) > tolerance)
return false;
}
return true;
}
for (double t : {matrix_.rc(0, 3), matrix_.rc(1, 3), matrix_.rc(2, 3)}) {
if (!base::IsValueInRangeForNumericType<int>(t) ||
std::abs(std::round(t) - t) > tolerance)
return false;
}
return true;
}
bool Transform::Is2dProportionalUpscaleAndOr2dTranslation() const {
if (LIKELY(!full_matrix_)) {
return axis_2d_.scale().x() >= 1 &&
axis_2d_.scale().x() == axis_2d_.scale().y();
}
return matrix_.IsScaleOrTranslation() &&
// Check proportional upscale.
matrix_.rc(0, 0) >= 1 && matrix_.rc(1, 1) == matrix_.rc(0, 0) &&
// Check no scale/translation in z axis.
matrix_.rc(2, 2) == 1 && matrix_.rc(2, 3) == 0;
}
bool Transform::IsIdentityOrIntegerTranslation() const {
if (!IsIdentityOrTranslation())
return false;
if (LIKELY(!full_matrix_)) {
for (float t : {axis_2d_.translation().x(), axis_2d_.translation().y()}) {
if (!base::IsValueInRangeForNumericType<int>(t) ||
static_cast<int>(t) != t) {
return false;
}
}
return true;
}
for (double t : {matrix_.rc(0, 3), matrix_.rc(1, 3), matrix_.rc(2, 3)}) {
if (!base::IsValueInRangeForNumericType<int>(t) || static_cast<int>(t) != t)
return false;
}
return true;
}
bool Transform::IsIdentityOrInteger2dTranslation() const {
return IsIdentityOrIntegerTranslation() && rc(2, 3) == 0;
}
bool Transform::Creates3d() const {
if (LIKELY(!full_matrix_))
return false;
return matrix_.rc(2, 0) != 0 || matrix_.rc(2, 1) != 0 ||
matrix_.rc(2, 3) != 0;
}
bool Transform::IsBackFaceVisible() const {
if (LIKELY(!full_matrix_))
return false;
// Compute whether a layer with a forward-facing normal of (0, 0, 1, 0)
// would have its back face visible after applying the transform.
// This is done by transforming the normal and seeing if the resulting z
// value is positive or negative. However, note that transforming a normal
// actually requires using the inverse-transpose of the original transform.
//
// We can avoid inverting and transposing the matrix since we know we want
// to transform only the specific normal vector (0, 0, 1, 0). In this case,
// we only need the 3rd row, 3rd column of the inverse-transpose. We can
// calculate only the 3rd row 3rd column element of the inverse, skipping
// everything else.
//
// For more information, refer to:
// http://en.wikipedia.org/wiki/Invertible_matrix#Analytic_solution
//
double determinant = matrix_.Determinant();
// If matrix was not invertible, then just assume back face is not visible.
if (determinant == 0)
return false;
// Compute the cofactor of the 3rd row, 3rd column.
double cofactor_part_1 =
matrix_.rc(0, 0) * matrix_.rc(1, 1) * matrix_.rc(3, 3);
double cofactor_part_2 =
matrix_.rc(0, 1) * matrix_.rc(1, 3) * matrix_.rc(3, 0);
double cofactor_part_3 =
matrix_.rc(0, 3) * matrix_.rc(1, 0) * matrix_.rc(3, 1);
double cofactor_part_4 =
matrix_.rc(0, 0) * matrix_.rc(1, 3) * matrix_.rc(3, 1);
double cofactor_part_5 =
matrix_.rc(0, 1) * matrix_.rc(1, 0) * matrix_.rc(3, 3);
double cofactor_part_6 =
matrix_.rc(0, 3) * matrix_.rc(1, 1) * matrix_.rc(3, 0);
double cofactor33 = cofactor_part_1 + cofactor_part_2 + cofactor_part_3 -
cofactor_part_4 - cofactor_part_5 - cofactor_part_6;
// Technically the transformed z component is cofactor33 / determinant. But
// we can avoid the costly division because we only care about the resulting
// +/- sign; we can check this equivalently by multiplication.
return cofactor33 * determinant < -kEpsilon;
}
bool Transform::GetInverse(Transform* transform) const {
if (LIKELY(!full_matrix_)) {
transform->full_matrix_ = false;
if (axis_2d_.IsInvertible()) {
transform->axis_2d_ = axis_2d_;
transform->axis_2d_.Invert();
return true;
}
transform->axis_2d_ = AxisTransform2d();
return false;
}
if (matrix_.GetInverse(transform->matrix_)) {
transform->full_matrix_ = true;
return true;
}
// Initialize the return value to identity if this matrix turned
// out to be un-invertible.
transform->MakeIdentity();
return false;
}
Transform Transform::GetCheckedInverse() const {
Transform inverse;
if (!GetInverse(&inverse))
NOTREACHED() << ToString() << " is not invertible";
return inverse;
}
Transform Transform::InverseOrIdentity() const {
Transform inverse;
bool invertible = GetInverse(&inverse);
DCHECK(invertible || inverse.IsIdentity());
return inverse;
}
bool Transform::Preserves2dAxisAlignment() const {
if (LIKELY(!full_matrix_))
return true;
// Check whether an axis aligned 2-dimensional rect would remain axis-aligned
// after being transformed by this matrix (and implicitly projected by
// dropping any non-zero z-values).
//
// The 4th column can be ignored because translations don't affect axis
// alignment. The 3rd column can be ignored because we are assuming 2d
// inputs, where z-values will be zero. The 3rd row can also be ignored
// because we are assuming 2d outputs, and any resulting z-value is dropped
// anyway. For the inner 2x2 portion, the only effects that keep a rect axis
// aligned are (1) swapping axes and (2) scaling axes. This can be checked by
// verifying only 1 element of every column and row is non-zero. Degenerate
// cases that project the x or y dimension to zero are considered to preserve
// axis alignment.
//
// If the matrix does have perspective component that is affected by x or y
// values: The current implementation conservatively assumes that axis
// alignment is not preserved.
bool has_x_or_y_perspective = matrix_.rc(3, 0) != 0 || matrix_.rc(3, 1) != 0;
int num_non_zero_in_row_0 = 0;
int num_non_zero_in_row_1 = 0;
int num_non_zero_in_col_0 = 0;
int num_non_zero_in_col_1 = 0;
if (std::abs(matrix_.rc(0, 0)) > kEpsilon) {
num_non_zero_in_row_0++;
num_non_zero_in_col_0++;
}
if (std::abs(matrix_.rc(0, 1)) > kEpsilon) {
num_non_zero_in_row_0++;
num_non_zero_in_col_1++;
}
if (std::abs(matrix_.rc(1, 0)) > kEpsilon) {
num_non_zero_in_row_1++;
num_non_zero_in_col_0++;
}
if (std::abs(matrix_.rc(1, 1)) > kEpsilon) {
num_non_zero_in_row_1++;
num_non_zero_in_col_1++;
}
return num_non_zero_in_row_0 <= 1 && num_non_zero_in_row_1 <= 1 &&
num_non_zero_in_col_0 <= 1 && num_non_zero_in_col_1 <= 1 &&
!has_x_or_y_perspective;
}
bool Transform::NonDegeneratePreserves2dAxisAlignment() const {
if (LIKELY(!full_matrix_))
return axis_2d_.scale().x() > kEpsilon && axis_2d_.scale().y() > kEpsilon;
// See comments above for Preserves2dAxisAlignment.
// This function differs from it by requiring:
// (1) that there are exactly two nonzero values on a diagonal in
// the upper left 2x2 submatrix, and
// (2) that the w perspective value is positive.
bool has_x_or_y_perspective = matrix_.rc(3, 0) != 0 || matrix_.rc(3, 1) != 0;
bool positive_w_perspective = matrix_.rc(3, 3) > kEpsilon;
bool have_0_0 = std::abs(matrix_.rc(0, 0)) > kEpsilon;
bool have_0_1 = std::abs(matrix_.rc(0, 1)) > kEpsilon;
bool have_1_0 = std::abs(matrix_.rc(1, 0)) > kEpsilon;
bool have_1_1 = std::abs(matrix_.rc(1, 1)) > kEpsilon;
return have_0_0 == have_1_1 && have_0_1 == have_1_0 && have_0_0 != have_0_1 &&
!has_x_or_y_perspective && positive_w_perspective;
}
void Transform::Transpose() {
if (!IsScale2d())
EnsureFullMatrix().Transpose();
}
void Transform::ApplyTransformOrigin(float x, float y, float z) {
PostTranslate3d(x, y, z);
Translate3d(-x, -y, -z);
}
void Transform::Zoom(float zoom_factor) {
if (LIKELY(!full_matrix_)) {
axis_2d_.Zoom(zoom_factor);
} else {
matrix_.Zoom(zoom_factor);
}
}
void Transform::Flatten() {
if (UNLIKELY(full_matrix_))
matrix_.Flatten();
DCHECK(IsFlat());
}
bool Transform::IsFlat() const {
return LIKELY(!full_matrix_) || matrix_.IsFlat();
}
bool Transform::Is2dTransform() const {
return LIKELY(!full_matrix_) || matrix_.Is2dTransform();
}
Vector2dF Transform::To2dTranslation() const {
if (LIKELY(!full_matrix_)) {
return Vector2dF(ClampFloatGeometry(axis_2d_.translation().x()),
ClampFloatGeometry(axis_2d_.translation().y()));
}
return Vector2dF(ClampFloatGeometry(matrix_.rc(0, 3)),
ClampFloatGeometry(matrix_.rc(1, 3)));
}
Vector3dF Transform::To3dTranslation() const {
if (LIKELY(!full_matrix_)) {
return Vector3dF(ClampFloatGeometry(axis_2d_.translation().x()),
ClampFloatGeometry(axis_2d_.translation().y()), 0);
}
return Vector3dF(ClampFloatGeometry(matrix_.rc(0, 3)),
ClampFloatGeometry(matrix_.rc(1, 3)),
ClampFloatGeometry(matrix_.rc(2, 3)));
}
Vector2dF Transform::To2dScale() const {
if (LIKELY(!full_matrix_)) {
return Vector2dF(ClampFloatGeometry(axis_2d_.scale().x()),
ClampFloatGeometry(axis_2d_.scale().y()));
}
return Vector2dF(ClampFloatGeometry(matrix_.rc(0, 0)),
ClampFloatGeometry(matrix_.rc(1, 1)));
}
Point Transform::MapPoint(const Point& point) const {
return gfx::ToRoundedPoint(MapPoint(gfx::PointF(point)));
}
PointF Transform::MapPoint(const PointF& point) const {
return LIKELY(!full_matrix_) ? axis_2d_.MapPoint(point)
: MapPointInternal(matrix_, point);
}
Point3F Transform::MapPoint(const Point3F& point) const {
if (LIKELY(!full_matrix_)) {
PointF result = axis_2d_.MapPoint(point.AsPointF());
return Point3F(result.x(), result.y(), ClampFloatGeometry(point.z()));
}
return MapPointInternal(matrix_, point);
}
Vector3dF Transform::MapVector(const Vector3dF& vector) const {
if (LIKELY(!full_matrix_)) {
return Vector3dF(ClampFloatGeometry(vector.x() * axis_2d_.scale().x()),
ClampFloatGeometry(vector.y() * axis_2d_.scale().y()),
ClampFloatGeometry(vector.z()));
}
double p[4] = {vector.x(), vector.y(), vector.z(), 0};
matrix_.MapVector4(p);
return Vector3dF(ClampFloatGeometry(p[0]), ClampFloatGeometry(p[1]),
ClampFloatGeometry(p[2]));
}
void Transform::TransformVector4(float vector[4]) const {
DCHECK(vector);
if (LIKELY(!full_matrix_)) {
vector[0] = vector[0] * axis_2d_.scale().x() +
vector[3] * axis_2d_.translation().x();
vector[1] = vector[1] * axis_2d_.scale().y() +
vector[3] * axis_2d_.translation().y();
for (int i = 0; i < 4; i++)
vector[i] = ClampFloatGeometry(vector[i]);
} else {
double v[4] = {vector[0], vector[1], vector[2], vector[3]};
matrix_.MapVector4(v);
for (int i = 0; i < 4; i++)
vector[i] = ClampFloatGeometry(v[i]);
}
}
absl::optional<PointF> Transform::InverseMapPoint(const PointF& point) const {
if (LIKELY(!full_matrix_)) {
if (!axis_2d_.IsInvertible())
return absl::nullopt;
return axis_2d_.InverseMapPoint(point);
}
Matrix44 inverse(Matrix44::kUninitialized);
if (!matrix_.GetInverse(inverse))
return absl::nullopt;
return MapPointInternal(inverse, point);
}
absl::optional<Point> Transform::InverseMapPoint(const Point& point) const {
if (absl::optional<PointF> point_f = InverseMapPoint(PointF(point)))
return ToRoundedPoint(*point_f);
return absl::nullopt;
}
absl::optional<Point3F> Transform::InverseMapPoint(const Point3F& point) const {
if (LIKELY(!full_matrix_)) {
if (!axis_2d_.IsInvertible())
return absl::nullopt;
PointF result = axis_2d_.InverseMapPoint(point.AsPointF());
return Point3F(result.x(), result.y(), ClampFloatGeometry(point.z()));
}
Matrix44 inverse(Matrix44::kUninitialized);
if (!matrix_.GetInverse(inverse))
return absl::nullopt;
return absl::make_optional(MapPointInternal(inverse, point));
}
RectF Transform::MapRect(const RectF& rect) const {
if (IsIdentity())
return rect;
if (LIKELY(!full_matrix_) && axis_2d_.scale().x() >= 0 &&
axis_2d_.scale().y() >= 0) {
return axis_2d_.MapRect(rect);
}
return MapQuad(QuadF(rect)).BoundingBox();
}
Rect Transform::MapRect(const Rect& rect) const {
if (IsIdentity())
return rect;
return ToEnclosingRect(MapRect(RectF(rect)));
}
absl::optional<RectF> Transform::InverseMapRect(const RectF& rect) const {
if (IsIdentity())
return rect;
if (LIKELY(!full_matrix_)) {
if (!axis_2d_.IsInvertible())
return absl::nullopt;
if (axis_2d_.scale().x() > 0 && axis_2d_.scale().y() > 0)
return axis_2d_.InverseMapRect(rect);
}
Transform inverse;
if (!GetInverse(&inverse))
return absl::nullopt;
return inverse.MapQuad(QuadF(rect)).BoundingBox();
}
absl::optional<Rect> Transform::InverseMapRect(const Rect& rect) const {
if (IsIdentity())
return rect;
if (absl::optional<RectF> mapped = InverseMapRect(RectF(rect)))
return ToEnclosingRect(mapped.value());
return absl::nullopt;
}
BoxF Transform::MapBox(const BoxF& box) const {
BoxF bounds;
bool first_point = true;
for (int corner = 0; corner < 8; ++corner) {
gfx::Point3F point = box.origin();
point += gfx::Vector3dF(corner & 1 ? box.width() : 0.f,
corner & 2 ? box.height() : 0.f,
corner & 4 ? box.depth() : 0.f);
point = MapPoint(point);
if (first_point) {
bounds.set_origin(point);
first_point = false;
} else {
bounds.ExpandTo(point);
}
}
return bounds;
}
QuadF Transform::MapQuad(const QuadF& quad) const {
return QuadF(MapPoint(quad.p1()), MapPoint(quad.p2()), MapPoint(quad.p3()),
MapPoint(quad.p4()));
}
PointF Transform::ProjectPoint(const PointF& point, bool* clamped) const {
// This is basically ray-tracing. We have a point in the destination plane
// with z=0, and we cast a ray parallel to the z-axis from that point to find
// the z-position at which it intersects the z=0 plane with the transform
// applied. Once we have that point we apply the inverse transform to find
// the corresponding point in the source space.
//
// Given a plane with normal Pn, and a ray starting at point R0 and with
// direction defined by the vector Rd, we can find the intersection point as
// a distance d from R0 in units of Rd by:
//
// d = -dot (Pn', R0) / dot (Pn', Rd)
if (clamped)
*clamped = false;
if (LIKELY(!full_matrix_))
return axis_2d_.MapPoint(point);
if (!std::isnormal(matrix_.rc(2, 2))) {
// In this case, the projection plane is parallel to the ray we are trying
// to trace, and there is no well-defined value for the projection.
if (clamped)
*clamped = true;
return gfx::PointF();
}
double x = point.x();
double y = point.y();
double z = -(matrix_.rc(2, 0) * x + matrix_.rc(2, 1) * y + matrix_.rc(2, 3)) /
matrix_.rc(2, 2);
if (!std::isfinite(z)) {
// Same as the previous condition.
if (clamped)
*clamped = true;
return gfx::PointF();
}
double v[4] = {x, y, z, 1};
matrix_.MapVector4(v);
if (v[3] <= 0) {
// To represent infinity and ensure the bounding box of ProjectQuad() is
// accurate in both float, int and blink::LayoutUnit, we use a large but
// not-too-large number here when clamping.
constexpr double kBigNumber = 1 << (std::numeric_limits<float>::digits - 1);
if (clamped)
*clamped = true;
return PointF(std::copysign(kBigNumber, v[0]),
std::copysign(kBigNumber, v[1]));
}
if (v[3] != 1) {
v[0] /= v[3];
v[1] /= v[3];
}
return PointF(ClampFloatGeometry(v[0]), ClampFloatGeometry(v[1]));
}
QuadF Transform::ProjectQuad(const QuadF& quad) const {
bool clamped1 = false;
bool clamped2 = false;
bool clamped3 = false;
bool clamped4 = false;
QuadF projected_quad(
ProjectPoint(quad.p1(), &clamped1), ProjectPoint(quad.p2(), &clamped2),
ProjectPoint(quad.p3(), &clamped3), ProjectPoint(quad.p4(), &clamped4));
// If all points on the quad had w < 0, then the entire quad would not be
// visible to the projected surface.
if (clamped1 && clamped2 && clamped3 && clamped4)
return QuadF();
return projected_quad;
}
absl::optional<DecomposedTransform> Transform::Decompose() const {
if (LIKELY(!full_matrix_)) {
// Consider letting 2d decomposition always succeed.
if (!axis_2d_.IsInvertible())
return absl::nullopt;
return axis_2d_.Decompose();
}
return matrix_.Decompose();
}
// static
Transform Transform::Compose(const DecomposedTransform& decomp) {
Transform result;
for (int i = 0; i < 3; i++) {
if (decomp.perspective[i] != 0)
result.set_rc(3, i, decomp.perspective[i]);
}
if (decomp.perspective[3] != 1)
result.set_rc(3, 3, decomp.perspective[3]);
result.Translate3d(decomp.translate[0], decomp.translate[1],
decomp.translate[2]);
result.PreConcat(Transform(decomp.quaternion));
if (decomp.skew[0] || decomp.skew[1] || decomp.skew[2])
result.EnsureFullMatrix().ApplyDecomposedSkews(decomp.skew);
result.Scale3d(decomp.scale[0], decomp.scale[1], decomp.scale[2]);
return result;
}
bool Transform::Blend(const Transform& from, double progress) {
absl::optional<DecomposedTransform> to_decomp = Decompose();
if (!to_decomp)
return false;
absl::optional<DecomposedTransform> from_decomp = from.Decompose();
if (!from_decomp)
return false;
*to_decomp = BlendDecomposedTransforms(*to_decomp, *from_decomp, progress);
*this = Compose(*to_decomp);
return true;
}
bool Transform::Accumulate(const Transform& other) {
absl::optional<DecomposedTransform> this_decomp = Decompose();
if (!this_decomp)
return false;
absl::optional<DecomposedTransform> other_decomp = other.Decompose();
if (!other_decomp)
return false;
*this_decomp = AccumulateDecomposedTransforms(*this_decomp, *other_decomp);
*this = Compose(*this_decomp);
return true;
}
void Transform::Round2dTranslationComponents() {
if (LIKELY(!full_matrix_)) {
axis_2d_ = AxisTransform2d::FromScaleAndTranslation(
axis_2d_.scale(), Vector2dF(std::round(axis_2d_.translation().x()),
std::round(axis_2d_.translation().y())));
} else {
matrix_.set_rc(0, 3, std::round(matrix_.rc(0, 3)));
matrix_.set_rc(1, 3, std::round(matrix_.rc(1, 3)));
}
}
void Transform::RoundToIdentityOrIntegerTranslation() {
if (LIKELY(!full_matrix_)) {
axis_2d_ = AxisTransform2d::FromScaleAndTranslation(
Vector2dF(1, 1), Vector2dF(std::round(axis_2d_.translation().x()),
std::round(axis_2d_.translation().y())));
} else {
matrix_ =
Matrix44(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, // col0-2
std::round(matrix_.rc(0, 3)), // col3
std::round(matrix_.rc(1, 3)), std::round(matrix_.rc(2, 3)), 1);
}
}
PointF Transform::MapPointInternal(const Matrix44& matrix,
const PointF& point) const {
DCHECK(full_matrix_);
double p[2] = {point.x(), point.y()};
double w = matrix.MapVector2(p);
if (w != 1.0 && std::isnormal(w)) {
double w_inverse = 1.0 / w;
return PointF(ClampFloatGeometry(p[0] * w_inverse),
ClampFloatGeometry(p[1] * w_inverse));
}
return PointF(ClampFloatGeometry(p[0]), ClampFloatGeometry(p[1]));
}
Point3F Transform::MapPointInternal(const Matrix44& matrix,
const Point3F& point) const {
DCHECK(full_matrix_);
double p[4] = {point.x(), point.y(), point.z(), 1};
matrix.MapVector4(p);
if (p[3] != 1.0 && std::isnormal(p[3])) {
double w_inverse = 1.0 / p[3];
return Point3F(ClampFloatGeometry(p[0] * w_inverse),
ClampFloatGeometry(p[1] * w_inverse),
ClampFloatGeometry(p[2] * w_inverse));
}
return Point3F(ClampFloatGeometry(p[0]), ClampFloatGeometry(p[1]),
ClampFloatGeometry(p[2]));
}
bool Transform::ApproximatelyEqual(const gfx::Transform& transform,
float abs_translation_tolerance,
float abs_other_tolerance,
float rel_scale_tolerance) const {
if (*this == transform)
return true;
if (abs_translation_tolerance == 0 && abs_other_tolerance == 0)
return false;
auto approximately_equal = [abs_other_tolerance](float a, float b) {
return std::abs(a - b) <= abs_other_tolerance;
};
auto translation_approximately_equal = [abs_translation_tolerance](float a,
float b) {
return std::abs(a - b) <= abs_translation_tolerance;
};
auto scale_approximately_equal = [abs_other_tolerance, rel_scale_tolerance](
float a, float b) {
float diff = std::abs(a - b);
return diff <= abs_other_tolerance &&
(rel_scale_tolerance == 0 ||
diff <= (std::abs(a) + std::abs(b)) * rel_scale_tolerance);
};
if (LIKELY(!full_matrix_) && LIKELY(!transform.full_matrix_)) {
return scale_approximately_equal(axis_2d_.scale().x(),
transform.axis_2d_.scale().x()) &&
scale_approximately_equal(axis_2d_.scale().y(),
transform.axis_2d_.scale().y()) &&
translation_approximately_equal(
axis_2d_.translation().x(),
transform.axis_2d_.translation().x()) &&
translation_approximately_equal(
axis_2d_.translation().y(),
transform.axis_2d_.translation().y());
}
for (int row = 0; row < 4; row++) {
for (int col = 0; col < 4; col++) {
float x = rc(row, col);
float y = transform.rc(row, col);
if (row < 3 && col == 3) {
if (!translation_approximately_equal(x, y))
return false;
} else if (row < 3 && col == row) {
if (!scale_approximately_equal(x, y))
return false;
} else if (!approximately_equal(x, y)) {
return false;
}
}
}
return true;
}
std::string Transform::ToString() const {
return base::StringPrintf(
"[ %lg %lg %lg %lg\n"
" %lg %lg %lg %lg\n"
" %lg %lg %lg %lg\n"
" %lg %lg %lg %lg ]\n",
rc(0, 0), rc(0, 1), rc(0, 2), rc(0, 3), rc(1, 0), rc(1, 1), rc(1, 2),
rc(1, 3), rc(2, 0), rc(2, 1), rc(2, 2), rc(2, 3), rc(3, 0), rc(3, 1),
rc(3, 2), rc(3, 3));
}
std::string Transform::ToDecomposedString() const {
absl::optional<gfx::DecomposedTransform> decomp = Decompose();
if (!decomp)
return ToString() + "(degenerate)";
if (IsIdentity())
return "identity";
if (IsIdentityOrTranslation()) {
return base::StringPrintf("translate: %lg,%lg,%lg", decomp->translate[0],
decomp->translate[1], decomp->translate[2]);
}
return decomp->ToString();
}
} // namespace gfx