Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ void processTransform(
}

auto result = BaseViewProps::resolveTransform(
Size(viewWidth, viewHeight), transform, transformOrigin);
Size{.width = viewWidth, .height = viewHeight},
transform,
transformOrigin);

// Convert from matrix of floats to double matrix
constexpr size_t MatrixSize = std::tuple_size_v<decltype(result.matrix)>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -558,9 +558,6 @@ Transform BaseViewProps::resolveTransform(
const Transform& transform,
const TransformOrigin& transformOrigin) {
auto transformMatrix = Transform{};
if (frameSize.width == 0 && frameSize.height == 0) {
return transformMatrix;
}

// transform is matrix
if (transform.operations.size() == 1 &&
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,377 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#include <gtest/gtest.h>

#include <react/renderer/components/view/BaseViewProps.h>

namespace facebook::react {

namespace {

// For transforms involving rotations, use this helper to fix floating point
// accuracies
void expectTransformsEqual(const Transform& t1, const Transform& t2) {
for (int i = 0; i < 16; i++) {
EXPECT_NEAR(t1.matrix[i], t2.matrix[i], 0.0001);
}
}

} // namespace

class ResolveTransformTest : public ::testing::Test {
protected:
TransformOrigin createTransformOriginPoints(float x, float y, float z = 0) {
TransformOrigin origin;
origin.xy[0] = ValueUnit(x, UnitType::Point);
origin.xy[1] = ValueUnit(y, UnitType::Point);
origin.z = z;
return origin;
}

TransformOrigin createTransformOriginPercent(float x, float y, float z = 0) {
TransformOrigin origin;
origin.xy[0] = ValueUnit(x, UnitType::Percent);
origin.xy[1] = ValueUnit(y, UnitType::Percent);
origin.z = z;
return origin;
}
};

TEST_F(ResolveTransformTest, EmptyFrameNoTransformOrigin) {
Size frameSize{.width = 0, .height = 0};
Transform transform = Transform::Translate(10.0, 20.0, 0.0);
TransformOrigin transformOrigin; // Default (not set)

auto result =
BaseViewProps::resolveTransform(frameSize, transform, transformOrigin);

// With empty frame size and no transform origin, should just apply the
// transform directly
EXPECT_EQ(result.matrix, transform.matrix);
}

TEST_F(ResolveTransformTest, EmptyFrameTransformOriginPoints) {
Size frameSize{.width = 0, .height = 0};
Transform transform = Transform::Translate(10.0, 20.0, 0.0);
TransformOrigin transformOrigin = createTransformOriginPoints(5, 8);

auto result =
BaseViewProps::resolveTransform(frameSize, transform, transformOrigin);

// Should handle transform origin even with empty frame size
EXPECT_EQ(result.matrix, Transform::Translate(10.0, 20.0, 0.0).matrix);
}

TEST_F(ResolveTransformTest, EmptyFrameTransformOriginPercent) {
Size frameSize{.width = 0, .height = 0};
Transform transform = Transform::Translate(10.0, 20.0, 0.0);
TransformOrigin transformOrigin = createTransformOriginPercent(50, 50);

auto result =
BaseViewProps::resolveTransform(frameSize, transform, transformOrigin);

// Transform origin does not affect translate transform
EXPECT_EQ(result.matrix, Transform::Translate(10.0, 20.0, 0.0).matrix);
}

TEST_F(ResolveTransformTest, NonEmptyFrameNoTransformOrigin) {
Size frameSize{.width = 100, .height = 200};
Transform transform = Transform::Translate(10.0, 20.0, 0.0);
TransformOrigin transformOrigin; // Default (not set)

auto result =
BaseViewProps::resolveTransform(frameSize, transform, transformOrigin);

// Transform origin does not affect translate transform
EXPECT_EQ(result.matrix, Transform::Translate(10.0, 20.0, 0.0).matrix);
}

TEST_F(ResolveTransformTest, NonEmptyFrameTransformOriginPoints) {
Size frameSize{.width = 100, .height = 200};
Transform transform = Transform::Scale(2.0, 1.5, 0.);
TransformOrigin transformOrigin = createTransformOriginPoints(25, 50);

auto result =
BaseViewProps::resolveTransform(frameSize, transform, transformOrigin);

auto expected = Transform::Translate(25.0, 25.0, 0.0) * transform;
EXPECT_EQ(result.matrix, expected.matrix);
}

TEST_F(ResolveTransformTest, NonEmptyFrameTransformOriginPercent) {
Size frameSize{.width = 100, .height = 200};
Transform transform = Transform::Scale(2.0, 1.5, 0.);
TransformOrigin transformOrigin =
createTransformOriginPercent(25, 75); // 25% width, 75% height

auto result =
BaseViewProps::resolveTransform(frameSize, transform, transformOrigin);

// Should resolve percentages: 25% of 100 = 25, 75% of 200 = 150
auto expected = Transform::Translate(25.0, -25.0, 0.0) * transform;
EXPECT_EQ(result.matrix, expected.matrix);
}

TEST_F(ResolveTransformTest, IdentityTransformWithOrigin) {
Size frameSize{.width = 100, .height = 200};
Transform transform = Transform::Identity();
TransformOrigin transformOrigin = createTransformOriginPoints(25, 50);

auto result =
BaseViewProps::resolveTransform(frameSize, transform, transformOrigin);

// Even with identity transform, transform origin should still apply
// translations but they should cancel out, resulting in identity
EXPECT_EQ(result.matrix, transform.matrix);
}

TEST_F(ResolveTransformTest, MultipleTransformOperations) {
Size frameSize{.width = 100, .height = 200};

Transform transform = Transform::Identity();
transform = transform * Transform::Translate(10.0, 20.0, 0.0);
transform = transform * Transform::Scale(2.0, 1.5, 0.0);

TransformOrigin transformOrigin = createTransformOriginPercent(50, 50);

auto result =
BaseViewProps::resolveTransform(frameSize, transform, transformOrigin);

EXPECT_EQ(result.matrix, transform.matrix);
}

TEST_F(ResolveTransformTest, VariousTransformOriginPositions) {
Size frameSize{.width = 100, .height = 200};
Transform transform = Transform::Scale(2.0, 2.0, 0.);

// Test origin at top-left (0, 0)
TransformOrigin topLeft = createTransformOriginPoints(0, 0);
auto resultTopLeft =
BaseViewProps::resolveTransform(frameSize, transform, topLeft);
auto expected = Transform::Translate(50.0, 100.0, 0.0) * transform;
EXPECT_EQ(resultTopLeft.matrix, expected.matrix);

// Test origin at center (50%, 50%)
TransformOrigin center = createTransformOriginPercent(50, 50);
auto resultCenter =
BaseViewProps::resolveTransform(frameSize, transform, center);
EXPECT_EQ(resultCenter.matrix, transform.matrix);

// Test origin at bottom-right (100%, 100%)
TransformOrigin bottomRight = createTransformOriginPercent(100, 100);
auto resultBottomRight =
BaseViewProps::resolveTransform(frameSize, transform, bottomRight);
expected = Transform::Translate(-50.0, -100.0, 0.0) * transform;
EXPECT_EQ(resultBottomRight.matrix, expected.matrix);
}

// Test with z-component in transform origin
TEST_F(ResolveTransformTest, TransformOriginWithZComponent) {
Size frameSize{.width = 100, .height = 200};
Transform transform = Transform::Scale(1.5, 1.5, 0.);

TransformOrigin transformOrigin;
transformOrigin.xy[0] = ValueUnit(50, UnitType::Point);
transformOrigin.xy[1] = ValueUnit(100, UnitType::Point);
transformOrigin.z = 10.0f;

auto result =
BaseViewProps::resolveTransform(frameSize, transform, transformOrigin);
auto expected = Transform::Translate(0.0, 0.0, 10.0) * transform;
EXPECT_EQ(result.matrix, expected.matrix);
}

TEST_F(ResolveTransformTest, ArbitraryTransformMatrix) {
Size frameSize{.width = 100, .height = 200};

Transform transform;
transform.operations.push_back({
.type = TransformOperationType::Arbitrary,
.x = ValueUnit(0, UnitType::Point),
.y = ValueUnit(0, UnitType::Point),
.z = ValueUnit(0, UnitType::Point),
});
// Set custom matrix
transform.matrix = {{2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1, 0, 10, 20, 0, 1}};

TransformOrigin transformOrigin = createTransformOriginPoints(25, 50);

auto result =
BaseViewProps::resolveTransform(frameSize, transform, transformOrigin);

auto expected = Transform::Translate(25.0, 50.0, 0.0) * transform;
EXPECT_EQ(result.matrix, expected.matrix);
}

// Test rotation with empty frame size and no transform origin
TEST_F(ResolveTransformTest, RotationEmptyFrameNoTransformOrigin) {
Size frameSize{.width = 0, .height = 0};
Transform transform = Transform::RotateZ(M_PI / 4.0); // 45 degrees
TransformOrigin transformOrigin; // Default (not set)

auto result =
BaseViewProps::resolveTransform(frameSize, transform, transformOrigin);

// With empty frame size and no transform origin, should just apply the
// rotation directly
expectTransformsEqual(result, transform);
}

// Test rotation with empty frame size and transform origin in points
TEST_F(ResolveTransformTest, RotationEmptyFrameTransformOriginPoints) {
Size frameSize{.width = 0, .height = 0};
Transform transform = Transform::RotateZ(M_PI / 4.0); // 45 degrees
TransformOrigin transformOrigin = createTransformOriginPoints(10, 20);

auto result =
BaseViewProps::resolveTransform(frameSize, transform, transformOrigin);

// With empty frame size, center is (0, 0), so origin offset is (10, 20)
auto expected = Transform::Translate(10.0, 20.0, 0.0) * transform *
Transform::Translate(-10.0, -20.0, 0.0);
expectTransformsEqual(result, expected);
}

// Test rotation with empty frame size and transform origin in percentages
TEST_F(ResolveTransformTest, RotationEmptyFrameTransformOriginPercent) {
Size frameSize{.width = 0, .height = 0};
Transform transform = Transform::RotateZ(M_PI / 6.0); // 30 degrees
TransformOrigin transformOrigin = createTransformOriginPercent(50, 50);

auto result =
BaseViewProps::resolveTransform(frameSize, transform, transformOrigin);

// With 0 frame size, percentages resolve to 0, so no origin offset
expectTransformsEqual(result, transform);
}

// Test rotation with non-empty frame size and no transform origin
TEST_F(ResolveTransformTest, RotationNonEmptyFrameNoTransformOrigin) {
Size frameSize{.width = 100, .height = 200};
Transform transform = Transform::RotateZ(M_PI / 3.0); // 60 degrees
TransformOrigin transformOrigin; // Default (not set)

auto result =
BaseViewProps::resolveTransform(frameSize, transform, transformOrigin);

// Without transform origin, rotation should happen around default center
expectTransformsEqual(result, transform);
}

// Test rotation with non-empty frame size and transform origin in points
TEST_F(ResolveTransformTest, RotationNonEmptyFrameTransformOriginPoints) {
Size frameSize{.width = 100, .height = 200};
Transform transform = Transform::RotateZ(M_PI / 4.0); // 45 degrees
TransformOrigin transformOrigin = createTransformOriginPoints(25, 50);

auto result =
BaseViewProps::resolveTransform(frameSize, transform, transformOrigin);

// Center of 100x200 frame is (50, 100), origin at (25, 50) means offset of
// (-25, -50)
auto expected = Transform::Translate(-25.0, -50.0, 0.0) * transform *
Transform::Translate(25.0, 50.0, 0.0);
expectTransformsEqual(result, expected);
}

// Test rotation with non-empty frame size and transform origin in percentages
TEST_F(ResolveTransformTest, RotationNonEmptyFrameTransformOriginPercent) {
Size frameSize{.width = 100, .height = 200};
Transform transform = Transform::RotateZ(M_PI / 2.0); // 90 degrees
TransformOrigin transformOrigin =
createTransformOriginPercent(25, 75); // 25% width, 75% height

auto result =
BaseViewProps::resolveTransform(frameSize, transform, transformOrigin);

// Should resolve percentages: 25% of 100 = 25, 75% of 200 = 150
// Center is (50, 100), so origin offset is (25-50, 150-100) = (-25, 50)
auto expected = Transform::Translate(-25.0, 50.0, 0.0) * transform *
Transform::Translate(25.0, -50.0, 0.0);
expectTransformsEqual(result, expected);
}

// Test rotation with mixed transform origin units
TEST_F(ResolveTransformTest, RotationMixedTransformOriginUnits) {
Size frameSize{.width = 100, .height = 200};
Transform transform = Transform::RotateZ(M_PI); // 180 degrees

TransformOrigin transformOrigin;
transformOrigin.xy[0] = ValueUnit(30, UnitType::Point); // 30 points
transformOrigin.xy[1] = ValueUnit(25, UnitType::Percent); // 25% of 200 = 50
transformOrigin.z = 0;

auto result =
BaseViewProps::resolveTransform(frameSize, transform, transformOrigin);

// Center is (50, 100), origin is (30, 50), so offset is (-20, -50)
auto expected = Transform::Translate(-20.0, -50.0, 0.0) * transform *
Transform::Translate(20.0, 50.0, 0.0);
expectTransformsEqual(result, expected);
}

// Test multiple rotations (RotateX, RotateY, RotateZ)
TEST_F(ResolveTransformTest, MultipleRotationsWithTransformOrigin) {
Size frameSize{.width = 100, .height = 100};

Transform transform = Transform::Rotate(M_PI / 6.0, M_PI / 4.0, M_PI / 3.0);
TransformOrigin transformOrigin = createTransformOriginPercent(50, 50);

auto result =
BaseViewProps::resolveTransform(frameSize, transform, transformOrigin);
expectTransformsEqual(result, transform);
}

// Test rotation with z-component in transform origin
TEST_F(ResolveTransformTest, RotationWithZTransformOrigin) {
Size frameSize{.width = 100, .height = 200};
Transform transform = Transform::RotateZ(M_PI / 4.0); // 45 degrees

TransformOrigin transformOrigin;
transformOrigin.xy[0] = ValueUnit(50, UnitType::Point);
transformOrigin.xy[1] = ValueUnit(100, UnitType::Point);
transformOrigin.z = 15.0f;

auto result =
BaseViewProps::resolveTransform(frameSize, transform, transformOrigin);

// Center is (50, 100), origin is (50, 100, 15), so offset is (0, 0, 15)
auto expected = Transform::Translate(0.0, 0.0, 15.0) * transform *
Transform::Translate(0.0, 0.0, -15.0);
expectTransformsEqual(result, expected);
}

// Test rotation at different origin positions (corners vs center)
TEST_F(ResolveTransformTest, RotationDifferentOriginPositions) {
Size frameSize{.width = 100, .height = 100};
Transform transform = Transform::RotateZ(M_PI / 2.0); // 90 degrees

// Test rotation around top-left corner (0, 0)
TransformOrigin topLeft = createTransformOriginPoints(0, 0);
auto resultTopLeft =
BaseViewProps::resolveTransform(frameSize, transform, topLeft);
auto expectedTopLeft = Transform::Translate(-50.0, -50.0, 0.0) * transform *
Transform::Translate(50.0, 50.0, 0.0);
expectTransformsEqual(resultTopLeft, expectedTopLeft);

// Test rotation around center (50%, 50%)
TransformOrigin center = createTransformOriginPercent(50, 50);
auto resultCenter =
BaseViewProps::resolveTransform(frameSize, transform, center);
expectTransformsEqual(resultCenter, transform);

// Test rotation around bottom-right corner (100%, 100%)
TransformOrigin bottomRight = createTransformOriginPercent(100, 100);
auto resultBottomRight =
BaseViewProps::resolveTransform(frameSize, transform, bottomRight);
auto expectedBottomRight = Transform::Translate(50.0, 50.0, 0.0) * transform *
Transform::Translate(-50.0, -50.0, 0.0);
expectTransformsEqual(resultBottomRight, expectedBottomRight);
}

} // namespace facebook::react
Loading