blob: 27068ae567923ba4dd1c8cea2fa0446968be1d0f [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.
#ifndef UI_GFX_GEOMETRY_TRANSFORM_H_
#define UI_GFX_GEOMETRY_TRANSFORM_H_
#include <iosfwd>
#include <memory>
#include <string>
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "ui/gfx/geometry/axis_transform2d.h"
#include "ui/gfx/geometry/geometry_skia_export.h"
#include "ui/gfx/geometry/matrix44.h"
namespace gfx {
class BoxF;
class Rect;
class RectF;
class Point;
class PointF;
class Point3F;
class QuadF;
class Quaternion;
class Vector2dF;
class Vector3dF;
struct DecomposedTransform;
// 4x4 Transformation matrix. Depending on the complexity of the matrix, it may
// be internally stored as an AxisTransform2d (float precision) or a full
// Matrix44 (4x4 double precision). Which one is used only affects precision and
// performance.
// - On construction (including constructors and static functions returning a
// new Transform object), AxisTransform2d will be used if it the matrix will
// be 2d scale and/or translation, otherwise Matrix44, with some exceptions
// (e.g. ColMajor()) described in the method comments.
// - On mutation, if the matrix has been using AxisTransform2d and the result
// can still be 2d scale and/or translation, AxisTransform2d will still be
// used, otherwise Matrix44, with some exceptions (e.g. set_rc()) described
// in the method comments.
// - On assignment, the new matrix will keep the choice of the rhs matrix.
//
class GEOMETRY_SKIA_EXPORT Transform {
public:
constexpr Transform() : axis_2d_() {}
explicit Transform(const AxisTransform2d& axis_2d) : axis_2d_(axis_2d) {}
// Creates a transform from explicit 16 matrix elements in row-major order.
// Always creates a double precision 4x4 matrix.
// clang-format off
static constexpr Transform RowMajor(
double r0c0, double r0c1, double r0c2, double r0c3,
double r1c0, double r1c1, double r1c2, double r1c3,
double r2c0, double r2c1, double r2c2, double r2c3,
double r3c0, double r3c1, double r3c2, double r3c3) {
return Transform(r0c0, r1c0, r2c0, r3c0, // col 0
r0c1, r1c1, r2c1, r3c1, // col 1
r0c2, r1c2, r2c2, r3c2, // col 2
r0c3, r1c3, r2c3, r3c3); // col 3
}
// Creates a transform from explicit 16 matrix elements in col-major order.
// Always creates a double precision 4x4 matrix.
// See also ColMajor(double[]) and ColMajorF(float[]).
static constexpr Transform ColMajor(
double r0c0, double r1c0, double r2c0, double r3c0,
double r0c1, double r1c1, double r2c1, double r3c1,
double r0c2, double r1c2, double r2c2, double r3c2,
double r0c3, double r1c3, double r2c3, double r3c3) {
return Transform(r0c0, r1c0, r2c0, r3c0, // col 0
r0c1, r1c1, r2c1, r3c1, // col 1
r0c2, r1c2, r2c2, r3c2, // col 2
r0c3, r1c3, r2c3, r3c3); // col 3
}
// clang-format on
// Creates a transform from explicit 2d elements. All other matrix elements
// remain the same as the corresponding elements of an identity matrix.
// Always creates a double precision 4x4 matrix.
// TODO(crbug.com/1359528): Revisit the above statement. Evaluate performance
// and precision requirements of SVG and CSS transform:matrix().
static constexpr Transform Affine(double a, // a.k.a. r0c0 or scale_x
double b, // a.k.a. r1c0 or tan(skew_y)
double c, // a.k.a. r0c1 or tan(skew_x)
double d, // a.k.a r1c1 or scale_y
double e, // a.k.a r0c3 or translation_x
double f) { // a.k.a r1c3 or translaiton_y
return ColMajor(a, b, 0, 0, c, d, 0, 0, 0, 0, 1, 0, e, f, 0, 1);
}
// Constructs a transform corresponding to the given quaternion.
explicit Transform(const Quaternion& q);
// Creates a transform as a 2d translation.
static constexpr Transform MakeTranslation(float tx, float ty) {
return Transform(1, 1, tx, ty);
}
static Transform MakeTranslation(const Vector2dF& v) {
return MakeTranslation(v.x(), v.y());
}
// Creates a transform as a 2d scale.
static constexpr Transform MakeScale(float scale) {
return MakeScale(scale, scale);
}
static constexpr Transform MakeScale(float sx, float sy) {
return Transform(sx, sy, 0, 0);
}
// Accurately rotate by 90, 180 or 270 degrees about the z axis.
static constexpr Transform Make90degRotation() {
return Affine(0, 1, -1, 0, 0, 0);
}
static constexpr Transform Make180degRotation() { return MakeScale(-1); }
static constexpr Transform Make270degRotation() {
return Affine(0, -1, 1, 0, 0, 0);
}
// Resets this transform to the identity transform.
void MakeIdentity() {
full_matrix_ = false;
axis_2d_ = AxisTransform2d();
}
bool operator==(const Transform& rhs) const {
if (LIKELY(!full_matrix_ && !rhs.full_matrix_))
return axis_2d_ == rhs.axis_2d_;
if (full_matrix_ && rhs.full_matrix_)
return matrix_ == rhs.matrix_;
return GetFullMatrix() == rhs.GetFullMatrix();
}
bool operator!=(const Transform& rhs) const { return !(*this == rhs); }
// Gets a value at |row|, |col| from the matrix.
constexpr double rc(int row, int col) const {
DCHECK_LE(static_cast<unsigned>(row), 3u);
DCHECK_LE(static_cast<unsigned>(col), 3u);
if (LIKELY(!full_matrix_)) {
float m[4][4] = {{axis_2d_.scale().x(), 0, 0, axis_2d_.translation().x()},
{0, axis_2d_.scale().y(), 0, axis_2d_.translation().y()},
{0, 0, 1, 0},
{0, 0, 0, 1}};
return m[row][col];
}
return matrix_.rc(row, col);
}
// Sets a value in the matrix at |row|, |col|. It forces full double precision
// 4x4 matrix.
void set_rc(int row, int col, double v) {
DCHECK_LE(static_cast<unsigned>(row), 3u);
DCHECK_LE(static_cast<unsigned>(col), 3u);
EnsureFullMatrix().set_rc(row, col, v);
}
// Constructs Transform from a double col-major array.
// Always creates a double precision 4x4 matrix.
static Transform ColMajor(const double a[16]);
// Constructs Transform from a float col-major array. Creates an
// AxisTransform2d or a Matrix44 depending on the values. GetColMajorF() and
// ColMajorF() are used when passing a Transform through mojo.
static Transform ColMajorF(const float a[16]);
// Gets col-major data.
void GetColMajor(double a[16]) const;
void GetColMajorF(float a[16]) const;
double ColMajorData(int index) const { return rc(index % 4, index / 4); }
// Applies a transformation on the current transformation,
// i.e. this = this * transform.
// "Pre" here means |this| is before the operator in the expression.
// Corresponds to DOMMatrix.multiplySelf().
void PreConcat(const Transform& transform);
// Applies a transformation on the current transformation,
// i.e. this = transform * this.
// Corresponds to DOMMatrix.preMultiplySelf() (note the difference about
// "Pre" and "Post"). "Post" here means |this| is after the operator in the
// expression.
void PostConcat(const Transform& transform);
// Applies a 2d-axis transform on the current transformation,
// i.e. this = this * transform.
void PreConcat(const AxisTransform2d& transform);
// Applies a transformation on the current transformation,
// i.e. this = transform * this.
void PostConcat(const AxisTransform2d& transform);
// Applies the current transformation on a scaling and assigns the result
// to |this|, i.e. this = this * scaling.
void Scale(float scale) { Scale(scale, scale); }
void Scale(float x, float y);
void Scale3d(float x, float y, float z);
// Applies a scale to the current transformation and assigns the result to
// |this|, i.e. this = scaling * this.
void PostScale(float scale) { PostScale(scale, scale); }
void PostScale(float x, float y);
void PostScale3d(float x, float y, float z);
// Applies the current transformation on a translation and assigns the result
// to |this|, i.e. this = this * translation.
void Translate(const Vector2dF& offset);
void Translate(float x, float y);
void Translate3d(const Vector3dF& offset);
void Translate3d(float x, float y, float z);
// Applies a translation to the current transformation and assigns the result
// to |this|, i.e. this = translation * this.
void PostTranslate(const Vector2dF& offset);
void PostTranslate(float x, float y);
void PostTranslate3d(const Vector3dF& offset);
void PostTranslate3d(float x, float y, float z);
// The following methods have the "Pre" semantics,
// i.e. this = this * operation.
// Applies the current transformation on a 2d rotation and assigns the result
// to |this|, i.e. this = this * rotation.
void Rotate(double degrees) { RotateAboutZAxis(degrees); }
// Applies the current transformation on an axis-angle rotation and assigns
// the result to |this|.
void RotateAboutXAxis(double degrees);
void RotateAboutYAxis(double degrees);
void RotateAboutZAxis(double degrees);
void RotateAbout(double x, double y, double z, double degrees);
void RotateAbout(const Vector3dF& axis, double degrees);
// Applies the current transformation on a skew and assigns the result
// to |this|, i.e. this = this * skew.
void Skew(double degrees_x, double degrees_y);
void SkewX(double degrees) { Skew(degrees, 0); }
void SkewY(double degrees) { Skew(0, degrees); }
// Applies the current transformation on a perspective transform and assigns
// the result to |this|.
void ApplyPerspectiveDepth(double depth);
// Returns true if this is the identity matrix.
// This function modifies a mutable variable in |matrix_|.
bool IsIdentity() const {
return LIKELY(!full_matrix_) ? axis_2d_ == AxisTransform2d()
: matrix_.IsIdentity();
}
// Returns true if the matrix is either identity or pure translation.
bool IsIdentityOrTranslation() const {
return LIKELY(!full_matrix_) ? axis_2d_.scale() == Vector2dF(1, 1)
: matrix_.IsIdentityOrTranslation();
}
// Returns true if the matrix is either the identity or a 2d translation.
bool IsIdentityOr2dTranslation() const {
return LIKELY(!full_matrix_)
? axis_2d_.scale() == Vector2dF(1, 1)
: matrix_.IsIdentityOrTranslation() && matrix_.rc(2, 3) == 0;
}
// Returns true if the matrix is either identity or pure translation,
// allowing for an amount of inaccuracy as specified by the parameter.
bool IsApproximatelyIdentityOrTranslation(double tolerance) const;
bool IsApproximatelyIdentityOrIntegerTranslation(double tolerance) const;
// Returns true if the matrix is either a positive scale and/or a translation.
bool IsPositiveScaleOrTranslation() const {
if (LIKELY(!full_matrix_))
return axis_2d_.scale().x() > 0.0 && axis_2d_.scale().y() > 0.0;
if (!matrix_.IsScaleOrTranslation())
return false;
return matrix_.rc(0, 0) > 0.0 && matrix_.rc(1, 1) > 0.0 &&
matrix_.rc(2, 2) > 0.0;
}
// Returns true if the matrix is 2d scale or translation, and if it has scale,
// the scale proportionally scales up in x and y directions. This function
// allows rare false-negatives.
bool Is2dProportionalUpscaleAndOr2dTranslation() const;
// Returns true if the matrix is identity or, if the matrix consists only
// of a translation whose components can be represented as integers. Returns
// false if the translation contains a fractional component or is too large to
// fit in an integer.
bool IsIdentityOrIntegerTranslation() const;
bool IsIdentityOrInteger2dTranslation() const;
// Returns whether this matrix can transform a z=0 plane to something
// containing points where z != 0. This is primarily intended for metrics.
bool Creates3d() const;
// Returns true if the matrix has only x and y scaling components, including
// identity.
bool IsScale2d() const {
return LIKELY(!full_matrix_) ? axis_2d_.translation().IsZero()
: matrix_.IsScale() && matrix_.rc(2, 2) == 1;
}
// Returns true if the matrix is has only scaling and translation components,
// including identity.
bool IsScaleOrTranslation() const {
return LIKELY(!full_matrix_) || matrix_.IsScaleOrTranslation();
}
// Returns true if axis-aligned 2d rects will remain axis-aligned after being
// transformed by this matrix.
bool Preserves2dAxisAlignment() const;
// Returns true if axis-aligned 2d rects will remain axis-aligned and not
// clipped by perspective (w > 0) after being transformed by this matrix,
// and distinct points in the x/y plane will remain distinct after being
// transformed by this matrix and mapped back to the x/y plane.
bool NonDegeneratePreserves2dAxisAlignment() const;
// Returns true if the matrix has any perspective component that would
// change the w-component of a homogeneous point.
bool HasPerspective() const {
return UNLIKELY(full_matrix_) && matrix_.HasPerspective();
}
// Returns true if this transform is non-singular.
bool IsInvertible() const {
return LIKELY(!full_matrix_) ? axis_2d_.IsInvertible()
: matrix_.IsInvertible();
}
// If |this| is invertible, inverts |this| and stores the result in
// |*transform|, and returns true. Otherwise sets |*transform| to identity
// and returns false.
[[nodiscard]] bool GetInverse(Transform* transform) const;
// Same as above except that it assumes success, otherwise DCHECK fails.
// This is suitable when the transform is known to be invertible.
[[nodiscard]] Transform GetCheckedInverse() const;
// Same as GetInverse() except that it returns the the inverse of |this| or
// identity, instead of a bool. This is suitable when it's good to fallback
// to identity silently.
[[nodiscard]] Transform InverseOrIdentity() const;
// Returns true if a layer with a forward-facing normal of (0, 0, 1) would
// have its back side facing frontwards after applying the transform.
bool IsBackFaceVisible() const;
// Transposes this transform in place.
void Transpose();
// Changes the transform to apply as if the origin were at (x, y, z).
void ApplyTransformOrigin(float x, float y, float z);
// Changes the transform to:
// scale3d(z, z, z) * mat * scale3d(1/z, 1/z, 1/z)
// Useful for mapping zoomed points to their zoomed transformed result:
// new_mat * (scale3d(z, z, z) * x) == scale3d(z, z, z) * (mat * x)
void Zoom(float zoom_factor);
// Set 3rd row and 3rd column to (0, 0, 1, 0). Note that this flattening
// operation is not quite the same as an orthographic projection and is
// technically not a linear operation.
//
// One useful interpretation of doing this operation:
// - For x and y values, the new transform behaves effectively like an
// orthographic projection was added to the matrix sequence.
// - For z values, the new transform overrides any effect that the transform
// had on z, and instead it preserves the z value for any points that are
// transformed.
// - Because of linearity of transforms, this flattened transform also
// preserves the effect that any subsequent (multiplied from the right)
// transforms would have on z values.
//
void Flatten();
// Returns true if the 3rd row and 3rd column are both (0, 0, 1, 0).
bool IsFlat() const;
// Returns true if the transform is flat and doesn't have perspective.
bool Is2dTransform() const;
// Returns the x and y translation components of the matrix, clamped with
// ClampFloatGeometry().
Vector2dF To2dTranslation() const;
// Returns the x, y and z translation components of the matrix, clampe with
// ClampFloatGeometry().
Vector3dF To3dTranslation() const;
// Returns the x and y scale components of the matrix, clamped with
// ClampFloatGeometry().
Vector2dF To2dScale() const;
// Returns the point with the transformation applied to |point|, clamped
// with ClampFloatGeometry().
[[nodiscard]] Point3F MapPoint(const Point3F& point) const;
// Maps [point.x(), point.y(), 0] to [result.x(), result.y(), discarded_z].
[[nodiscard]] PointF MapPoint(const PointF& point) const;
[[nodiscard]] Point MapPoint(const Point& point) const;
// Returns the vector with the transformation applied to |vector|, clamped
// with ClampFloatGeometry(). It differs from MapPoint() by that the
// translation and perspective components of the matrix are ignored.
[[nodiscard]] Vector3dF MapVector(const Vector3dF& vector) const;
// Applies the transformation to the vector. The results are clamped with
// ClampFloatGeometry().
void TransformVector4(float vector[4]) const;
// Returns the point with reverse transformation applied to `point`, clamped
// with ClampFloatGeometry(), or `absl::nullopt` if the transformation cannot
// be inverted.
[[nodiscard]] absl::optional<PointF> InverseMapPoint(
const PointF& point) const;
[[nodiscard]] absl::optional<Point3F> InverseMapPoint(
const Point3F& point) const;
// Applies the reverse transformation on `point`. Returns `absl::nullopt` if
// the transformation cannot be inverted. Rounds the result to the nearest
// point.
[[nodiscard]] absl::optional<Point> InverseMapPoint(const Point& point) const;
// Returns the rect that is the smallest axis aligned bounding rect
// containing the transformed rect, clamped with ClampFloatGeometry().
[[nodiscard]] RectF MapRect(const RectF& rect) const;
[[nodiscard]] Rect MapRect(const Rect& rect) const;
// Applies the reverse transformation on the given rect. Returns
// `absl::nullopt` if the transformation cannot be inverted, or the rect that
// is the smallest axis aligned bounding rect containing the transformed rect,
// clamped with ClampFloatGeometry().
[[nodiscard]] absl::optional<RectF> InverseMapRect(const RectF& rect) const;
[[nodiscard]] absl::optional<Rect> InverseMapRect(const Rect& rect) const;
// Returns the box with transformation applied on the given box. The returned
// box will be the smallest axis aligned bounding box containing the
// transformed box, clamped with ClampFloatGeometry().
[[nodiscard]] BoxF MapBox(const BoxF& box) const;
// Applies transformation on the given quad by applying the transformation
// on each point of the quad.
[[nodiscard]] QuadF MapQuad(const QuadF& quad) const;
// Maps a point on the z=0 plane into a point on the plane with which the
// transform applied, by extending a ray perpendicular to the source plane and
// computing the local x,y position of the point where that ray intersects
// with the destination plane. If such a point exists, sets |*clamped| (if
// provided) to false and returns the point. Otherwise sets |*clamped| (if
// provided) to true and:
// - If the ray is parallel with the destination plane, returns PointF().
// - If the opposite ray intersects with the destination plane, returns
// a point containing signed big values (simulating infinities).
//
// See https://bit.ly/perspective-projection-clamping for an illustration of
// clamping with perspective.
//
// When |this| is invertible and the result |*clamped| is false, this
// function is equivalent to:
// inverse(flatten(inverse(this))).MapPoint(point)
// and
// MapPoint(Point3F(point.x(), point.y(), unknown_z)) to
// Point3F(result.x(), result.y(), 0).
[[nodiscard]] PointF ProjectPoint(const PointF& point,
bool* clamped = nullptr) const;
// Projects the four corners of the quad with ProjectPoint(). Returns an
// empty quad if all of the vertices are clamped.
[[nodiscard]] QuadF ProjectQuad(const QuadF& quad) const;
// Decomposes |this| into |decomp|. Returns nullopt if |this| can't be
// decomposed. |decomp| must be identity on input.
//
// Uses routines described in the following specs:
// 2d: https://www.w3.org/TR/css-transforms-1/#decomposing-a-2d-matrix
// 3d: https://www.w3.org/TR/css-transforms-2/#decomposing-a-3d-matrix
//
// Note: when the determinant is negative, the 2d spec calls for flipping one
// of the axis, while the general 3d spec calls for flipping all of the
// scales. The latter not only introduces rotation in the case of a trivial
// scale inversion, but causes transformed objects to needlessly shrink and
// grow as they transform through scale = 0 along multiple axes. Thus 2d
// transforms should follow the 2d spec regarding matrix decomposition.
absl::optional<DecomposedTransform> Decompose() const;
// Composes a transform from the given |decomp|, following the routines
// detailed in this specs:
// https://www.w3.org/TR/css-transforms-2/#recomposing-to-a-3d-matrix
static Transform Compose(const DecomposedTransform& decomp);
// Decomposes |this| and |from|, interpolates the decomposed values, and
// sets |this| to the reconstituted result. Returns false and leaves |this|
// unchanged if either matrix can't be decomposed.
// Uses routines described in this spec:
// https://www.w3.org/TR/css-transforms-2/#matrix-interpolation
//
// Note: this call is expensive for complex transforms since we need to
// decompose the transforms. If you're going to be calling this rapidly
// (e.g., in an animation) for complex transforms, you should decompose once
// using Decompose() and reuse your DecomposedTransform with
// BlendDecomposedTransforms() (see transform_util.h).
bool Blend(const Transform& from, double progress);
// Decomposes |this| and |from|, accumulates the decomposed values, and
// sets |this| to the reconstituted result. Returns false and leaves |this|
// unchanged if either matrix can't be decomposed.
// Uses routines described in this spec:
// https://www.w3.org/TR/css-transforms-2/#combining-transform-lists.
//
// Note: this function has the same performance characteristics as Blend().
// When possible, you should also reuse DecomposedTransform with
// AccumulateDecomposedTransforms() (see transform_util.h).
bool Accumulate(const Transform& other);
double Determinant() const;
// Rounds 2d translation components rc(0, 3), rc(1, 3) to integers.
void Round2dTranslationComponents();
// Rounds translation components to integers, and all other components to
// identity. Normally this function is meaningful only if
// IsApproximatelyIdentityOrIntegerTranslation() is true.
void RoundToIdentityOrIntegerTranslation();
// Returns |this| * |other|.
Transform operator*(const Transform& other) const;
// Sets |this| = |this| * |other|
Transform& operator*=(const Transform& other) {
PreConcat(other);
return *this;
}
// Checks whether `this` approximately equals `transform`.
// Returns true if all following conditions are met:
// - For (x, y) in all translation components of (this, transform):
// abs(x - y) <= abs_translation_tolerance
// - For (x, y) in all other components of (this, transform):
// abs(x - y) <= abs_other_tolerance
// - If rel_scale_tolerance is not zero, for (x, y) in all scale components:
// abs(x - y) <= (abs(x) + abs(y)) * rel_scale_tolerance.
bool ApproximatelyEqual(const gfx::Transform& transform,
float abs_translation_tolerance,
float abs_other_tolerance,
float rel_scale_tolerance) const;
// Checks approximate equality with one tolerance for all components.
bool ApproximatelyEqual(const gfx::Transform& transform,
float abs_tolerance) const {
return ApproximatelyEqual(transform, abs_tolerance, abs_tolerance, 0.0f);
}
// Checks approximate equality with default tolerances. Note that the
// tolerance for translation is big to tolerate scroll components due to
// snapping (floating point error might round the other way).
bool ApproximatelyEqual(const gfx::Transform& transform) const {
return ApproximatelyEqual(transform, 1.0f, 0.1f, 0.0f);
}
void EnsureFullMatrixForTesting() { EnsureFullMatrix(); }
// Returns a string in the format of "[ row0\n, row1\n, row2\n, row3 ]\n".
std::string ToString() const;
// Returns a string containing decomposed components.
std::string ToDecomposedString() const;
private:
// Used internally to construct Transform with parameters in col-major order.
// clang-format off
constexpr Transform(double r0c0, double r1c0, double r2c0, double r3c0,
double r0c1, double r1c1, double r2c1, double r3c1,
double r0c2, double r1c2, double r2c2, double r3c2,
double r0c3, double r1c3, double r2c3, double r3c3)
: full_matrix_(true),
matrix_(r0c0, r1c0, r2c0, r3c0,
r0c1, r1c1, r2c1, r3c1,
r0c2, r1c2, r2c2, r3c2,
r0c3, r1c3, r2c3, r3c3) {}
// clang-format on
constexpr Transform(float scale_x,
float scale_y,
float trans_x,
float trans_y)
: axis_2d_(AxisTransform2d::FromScaleAndTranslation(
Vector2dF(scale_x, scale_y),
Vector2dF(trans_x, trans_y))) {}
// Used internally to construct a Transform with uninitialized full matrix.
explicit Transform(Matrix44::UninitializedTag tag)
: full_matrix_(true), matrix_(tag) {}
PointF MapPointInternal(const Matrix44& matrix, const PointF& point) const;
Point3F MapPointInternal(const Matrix44& matrix, const Point3F& point) const;
Matrix44 GetFullMatrix() const;
Matrix44& EnsureFullMatrix();
// axis_2d_ is used if full_matrix_ is false, otherwise matrix_ is used.
// See the class documentation for more details about how we use them.
bool full_matrix_ = false;
union {
// Each constructor must explicitly initialize one of the following,
// according to the value of full_matrix_.
AxisTransform2d axis_2d_;
Matrix44 matrix_;
};
};
// This is declared here for use in gtest-based unit tests but is defined in
// the //ui/gfx:test_support target. Depend on that to use this in your unit
// test. This should not be used in production code - call ToString() instead.
void PrintTo(const Transform& transform, ::std::ostream* os);
} // namespace gfx
#endif // UI_GFX_GEOMETRY_TRANSFORM_H_