diff --git a/engine/src/flutter/ci/licenses_golden/excluded_files b/engine/src/flutter/ci/licenses_golden/excluded_files index 17e3231f06..98b6d25aec 100644 --- a/engine/src/flutter/ci/licenses_golden/excluded_files +++ b/engine/src/flutter/ci/licenses_golden/excluded_files @@ -161,6 +161,7 @@ ../../../flutter/impeller/geometry/matrix_unittests.cc ../../../flutter/impeller/geometry/path_unittests.cc ../../../flutter/impeller/geometry/rect_unittests.cc +../../../flutter/impeller/geometry/saturated_math_unittests.cc ../../../flutter/impeller/geometry/size_unittests.cc ../../../flutter/impeller/geometry/trig_unittests.cc ../../../flutter/impeller/golden_tests/README.md diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter index 96f271fb68..41ade31557 100644 --- a/engine/src/flutter/ci/licenses_golden/licenses_flutter +++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter @@ -5292,6 +5292,7 @@ ORIGIN: ../../../flutter/impeller/geometry/quaternion.cc + ../../../flutter/LICE ORIGIN: ../../../flutter/impeller/geometry/quaternion.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/geometry/rect.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/geometry/rect.h + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/impeller/geometry/saturated_math.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/geometry/scalar.h + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/geometry/shear.cc + ../../../flutter/LICENSE ORIGIN: ../../../flutter/impeller/geometry/shear.h + ../../../flutter/LICENSE @@ -8129,6 +8130,7 @@ FILE: ../../../flutter/impeller/geometry/quaternion.cc FILE: ../../../flutter/impeller/geometry/quaternion.h FILE: ../../../flutter/impeller/geometry/rect.cc FILE: ../../../flutter/impeller/geometry/rect.h +FILE: ../../../flutter/impeller/geometry/saturated_math.h FILE: ../../../flutter/impeller/geometry/scalar.h FILE: ../../../flutter/impeller/geometry/shear.cc FILE: ../../../flutter/impeller/geometry/shear.h diff --git a/engine/src/flutter/impeller/entity/geometry/stroke_path_geometry.cc b/engine/src/flutter/impeller/entity/geometry/stroke_path_geometry.cc index 87eca5da81..1237845261 100644 --- a/engine/src/flutter/impeller/entity/geometry/stroke_path_geometry.cc +++ b/engine/src/flutter/impeller/entity/geometry/stroke_path_geometry.cc @@ -508,7 +508,6 @@ std::optional StrokePathGeometry::GetCoverage( if (!path_bounds.has_value()) { return std::nullopt; } - auto path_coverage = path_bounds->TransformBounds(transform); Scalar max_radius = 0.5; if (stroke_cap_ == Cap::kSquare) { @@ -522,12 +521,8 @@ std::optional StrokePathGeometry::GetCoverage( return std::nullopt; } Scalar min_size = 1.0f / sqrt(std::abs(determinant)); - Vector2 max_radius_xy = - transform - .TransformDirection(Vector2(max_radius, max_radius) * - std::max(stroke_width_, min_size)) - .Abs(); - return path_coverage.Expand(max_radius_xy); + max_radius *= std::max(stroke_width_, min_size); + return path_bounds->Expand(max_radius).TransformBounds(transform); } } // namespace impeller diff --git a/engine/src/flutter/impeller/geometry/BUILD.gn b/engine/src/flutter/impeller/geometry/BUILD.gn index ce6fe023c9..5129f34662 100644 --- a/engine/src/flutter/impeller/geometry/BUILD.gn +++ b/engine/src/flutter/impeller/geometry/BUILD.gn @@ -29,6 +29,7 @@ impeller_component("geometry") { "quaternion.h", "rect.cc", "rect.h", + "saturated_math.h", "scalar.h", "shear.cc", "shear.h", @@ -66,6 +67,7 @@ impeller_component("geometry_unittests") { "matrix_unittests.cc", "path_unittests.cc", "rect_unittests.cc", + "saturated_math_unittests.cc", "size_unittests.cc", "trig_unittests.cc", ] diff --git a/engine/src/flutter/impeller/geometry/geometry_asserts.h b/engine/src/flutter/impeller/geometry/geometry_asserts.h index f5727c90da..0d7e39768d 100644 --- a/engine/src/flutter/impeller/geometry/geometry_asserts.h +++ b/engine/src/flutter/impeller/geometry/geometry_asserts.h @@ -54,10 +54,10 @@ inline ::testing::AssertionResult QuaternionNear(impeller::Quaternion a, } inline ::testing::AssertionResult RectNear(impeller::Rect a, impeller::Rect b) { - auto equal = NumberNear(a.GetX(), b.GetX()) && - NumberNear(a.GetY(), b.GetY()) && - NumberNear(a.GetWidth(), b.GetWidth()) && - NumberNear(a.GetHeight(), b.GetHeight()); + auto equal = NumberNear(a.GetLeft(), b.GetLeft()) && + NumberNear(a.GetTop(), b.GetTop()) && + NumberNear(a.GetRight(), b.GetRight()) && + NumberNear(a.GetBottom(), b.GetBottom()); return equal ? ::testing::AssertionSuccess() : ::testing::AssertionFailure() diff --git a/engine/src/flutter/impeller/geometry/geometry_unittests.cc b/engine/src/flutter/impeller/geometry/geometry_unittests.cc index cb3629a9c5..6b7f6951c8 100644 --- a/engine/src/flutter/impeller/geometry/geometry_unittests.cc +++ b/engine/src/flutter/impeller/geometry/geometry_unittests.cc @@ -622,19 +622,6 @@ TEST(GeometryTest, CanConvertTTypesExplicitly) { ASSERT_EQ(p1.x, 1u); ASSERT_EQ(p1.y, 2u); } - - { - Rect r1 = Rect::MakeXYWH(1.0, 2.0, 3.0, 4.0); - IRect r2 = static_cast(r1); - ASSERT_EQ(r2.GetOrigin().x, 1u); - ASSERT_EQ(r2.GetOrigin().y, 2u); - ASSERT_EQ(r2.GetSize().width, 3u); - ASSERT_EQ(r2.GetSize().height, 4u); - ASSERT_EQ(r2.GetX(), 1u); - ASSERT_EQ(r2.GetY(), 2u); - ASSERT_EQ(r2.GetWidth(), 3u); - ASSERT_EQ(r2.GetHeight(), 4u); - } } TEST(GeometryTest, CanPerformAlgebraicPointOps) { @@ -1466,523 +1453,6 @@ TEST(GeometryTest, CanConvertBetweenDegressAndRadians) { } } -TEST(GeometryTest, RectUnion) { - { - Rect a = Rect::MakeXYWH(100, 100, 100, 100); - Rect b = Rect::MakeXYWH(0, 0, 0, 0); - auto u = a.Union(b); - auto expected = Rect::MakeXYWH(0, 0, 200, 200); - ASSERT_RECT_NEAR(u, expected); - } - - { - Rect a = Rect::MakeXYWH(100, 100, 100, 100); - Rect b = Rect::MakeXYWH(10, 10, 0, 0); - auto u = a.Union(b); - auto expected = Rect::MakeXYWH(10, 10, 190, 190); - ASSERT_RECT_NEAR(u, expected); - } - - { - Rect a = Rect::MakeXYWH(0, 0, 100, 100); - Rect b = Rect::MakeXYWH(10, 10, 100, 100); - auto u = a.Union(b); - auto expected = Rect::MakeXYWH(0, 0, 110, 110); - ASSERT_RECT_NEAR(u, expected); - } - - { - Rect a = Rect::MakeXYWH(0, 0, 100, 100); - Rect b = Rect::MakeXYWH(100, 100, 100, 100); - auto u = a.Union(b); - auto expected = Rect::MakeXYWH(0, 0, 200, 200); - ASSERT_RECT_NEAR(u, expected); - } -} - -TEST(GeometryTest, OptRectUnion) { - Rect a = Rect::MakeLTRB(0, 0, 100, 100); - Rect b = Rect::MakeLTRB(100, 100, 200, 200); - Rect c = Rect::MakeLTRB(100, 0, 200, 100); - - // NullOpt, NullOpt - EXPECT_FALSE(Rect::Union(std::nullopt, std::nullopt).has_value()); - EXPECT_EQ(Rect::Union(std::nullopt, std::nullopt), std::nullopt); - - auto test1 = [](const Rect& r) { - // Rect, NullOpt - EXPECT_TRUE(Rect::Union(r, std::nullopt).has_value()); - EXPECT_EQ(Rect::Union(r, std::nullopt).value(), r); - - // OptRect, NullOpt - EXPECT_TRUE(Rect::Union(std::optional(r), std::nullopt).has_value()); - EXPECT_EQ(Rect::Union(std::optional(r), std::nullopt).value(), r); - - // NullOpt, Rect - EXPECT_TRUE(Rect::Union(std::nullopt, r).has_value()); - EXPECT_EQ(Rect::Union(std::nullopt, r).value(), r); - - // NullOpt, OptRect - EXPECT_TRUE(Rect::Union(std::nullopt, std::optional(r)).has_value()); - EXPECT_EQ(Rect::Union(std::nullopt, std::optional(r)).value(), r); - }; - - test1(a); - test1(b); - test1(c); - - auto test2 = [](const Rect& a, const Rect& b, const Rect& u) { - ASSERT_EQ(a.Union(b), u); - - // Rect, OptRect - EXPECT_TRUE(Rect::Union(a, std::optional(b)).has_value()); - EXPECT_EQ(Rect::Union(a, std::optional(b)).value(), u); - - // OptRect, Rect - EXPECT_TRUE(Rect::Union(std::optional(a), b).has_value()); - EXPECT_EQ(Rect::Union(std::optional(a), b).value(), u); - - // OptRect, OptRect - EXPECT_TRUE(Rect::Union(std::optional(a), std::optional(b)).has_value()); - EXPECT_EQ(Rect::Union(std::optional(a), std::optional(b)).value(), u); - }; - - test2(a, b, Rect::MakeLTRB(0, 0, 200, 200)); - test2(a, c, Rect::MakeLTRB(0, 0, 200, 100)); - test2(b, c, Rect::MakeLTRB(100, 0, 200, 200)); -} - -TEST(GeometryTest, RectIntersection) { - { - Rect a = Rect::MakeXYWH(100, 100, 100, 100); - Rect b = Rect::MakeXYWH(0, 0, 0, 0); - - auto u = a.Intersection(b); - ASSERT_FALSE(u.has_value()); - } - - { - Rect a = Rect::MakeXYWH(100, 100, 100, 100); - Rect b = Rect::MakeXYWH(10, 10, 0, 0); - auto u = a.Intersection(b); - ASSERT_FALSE(u.has_value()); - } - - { - Rect a = Rect::MakeXYWH(0, 0, 100, 100); - Rect b = Rect::MakeXYWH(10, 10, 100, 100); - auto u = a.Intersection(b); - ASSERT_TRUE(u.has_value()); - auto expected = Rect::MakeXYWH(10, 10, 90, 90); - ASSERT_RECT_NEAR(u.value(), expected); - } - - { - Rect a = Rect::MakeXYWH(0, 0, 100, 100); - Rect b = Rect::MakeXYWH(100, 100, 100, 100); - auto u = a.Intersection(b); - ASSERT_FALSE(u.has_value()); - } - - { - Rect a = Rect::MakeMaximum(); - Rect b = Rect::MakeXYWH(10, 10, 300, 300); - auto u = a.Intersection(b); - ASSERT_TRUE(u); - ASSERT_RECT_NEAR(u.value(), b); - } - - { - Rect a = Rect::MakeMaximum(); - Rect b = Rect::MakeMaximum(); - auto u = a.Intersection(b); - ASSERT_TRUE(u); - ASSERT_EQ(u, Rect::MakeMaximum()); - } -} - -TEST(GeometryTest, OptRectIntersection) { - Rect a = Rect::MakeLTRB(0, 0, 110, 110); - Rect b = Rect::MakeLTRB(100, 100, 200, 200); - Rect c = Rect::MakeLTRB(100, 0, 200, 110); - - // NullOpt, NullOpt - EXPECT_FALSE(Rect::Intersection(std::nullopt, std::nullopt).has_value()); - EXPECT_EQ(Rect::Intersection(std::nullopt, std::nullopt), std::nullopt); - - auto test1 = [](const Rect& r) { - // Rect, NullOpt - EXPECT_TRUE(Rect::Intersection(r, std::nullopt).has_value()); - EXPECT_EQ(Rect::Intersection(r, std::nullopt).value(), r); - - // OptRect, NullOpt - EXPECT_TRUE(Rect::Intersection(std::optional(r), std::nullopt).has_value()); - EXPECT_EQ(Rect::Intersection(std::optional(r), std::nullopt).value(), r); - - // NullOpt, Rect - EXPECT_TRUE(Rect::Intersection(std::nullopt, r).has_value()); - EXPECT_EQ(Rect::Intersection(std::nullopt, r).value(), r); - - // NullOpt, OptRect - EXPECT_TRUE(Rect::Intersection(std::nullopt, std::optional(r)).has_value()); - EXPECT_EQ(Rect::Intersection(std::nullopt, std::optional(r)).value(), r); - }; - - test1(a); - test1(b); - test1(c); - - auto test2 = [](const Rect& a, const Rect& b, const Rect& i) { - ASSERT_EQ(a.Intersection(b), i); - - // Rect, OptRect - EXPECT_TRUE(Rect::Intersection(a, std::optional(b)).has_value()); - EXPECT_EQ(Rect::Intersection(a, std::optional(b)).value(), i); - - // OptRect, Rect - EXPECT_TRUE(Rect::Intersection(std::optional(a), b).has_value()); - EXPECT_EQ(Rect::Intersection(std::optional(a), b).value(), i); - - // OptRect, OptRect - EXPECT_TRUE( - Rect::Intersection(std::optional(a), std::optional(b)).has_value()); - EXPECT_EQ(Rect::Intersection(std::optional(a), std::optional(b)).value(), - i); - }; - - test2(a, b, Rect::MakeLTRB(100, 100, 110, 110)); - test2(a, c, Rect::MakeLTRB(100, 0, 110, 110)); - test2(b, c, Rect::MakeLTRB(100, 100, 200, 110)); -} - -TEST(GeometryTest, RectIntersectsWithRect) { - { - Rect a = Rect::MakeXYWH(100, 100, 100, 100); - Rect b = Rect::MakeXYWH(0, 0, 0, 0); - ASSERT_FALSE(a.IntersectsWithRect(b)); - } - - { - Rect a = Rect::MakeXYWH(100, 100, 100, 100); - Rect b = Rect::MakeXYWH(10, 10, 0, 0); - ASSERT_FALSE(a.IntersectsWithRect(b)); - } - - { - Rect a = Rect::MakeXYWH(0, 0, 100, 100); - Rect b = Rect::MakeXYWH(10, 10, 100, 100); - ASSERT_TRUE(a.IntersectsWithRect(b)); - } - - { - Rect a = Rect::MakeXYWH(0, 0, 100, 100); - Rect b = Rect::MakeXYWH(100, 100, 100, 100); - ASSERT_FALSE(a.IntersectsWithRect(b)); - } - - { - Rect a = Rect::MakeMaximum(); - Rect b = Rect::MakeXYWH(10, 10, 100, 100); - ASSERT_TRUE(a.IntersectsWithRect(b)); - } - - { - Rect a = Rect::MakeMaximum(); - Rect b = Rect::MakeMaximum(); - ASSERT_TRUE(a.IntersectsWithRect(b)); - } -} - -TEST(GeometryTest, RectCutout) { - // No cutout. - { - Rect a = Rect::MakeXYWH(0, 0, 100, 100); - Rect b = Rect::MakeXYWH(0, 0, 50, 50); - auto u = a.Cutout(b); - ASSERT_TRUE(u.has_value()); - ASSERT_RECT_NEAR(u.value(), a); - } - - // Full cutout. - { - Rect a = Rect::MakeXYWH(0, 0, 100, 100); - Rect b = Rect::MakeXYWH(-10, -10, 120, 120); - auto u = a.Cutout(b); - ASSERT_FALSE(u.has_value()); - } - - // Cutout from top. - { - auto a = Rect::MakeLTRB(0, 0, 100, 100); - auto b = Rect::MakeLTRB(-10, -10, 110, 90); - auto u = a.Cutout(b); - auto expected = Rect::MakeLTRB(0, 90, 100, 100); - ASSERT_TRUE(u.has_value()); - ASSERT_RECT_NEAR(u.value(), expected); - } - - // Cutout from bottom. - { - auto a = Rect::MakeLTRB(0, 0, 100, 100); - auto b = Rect::MakeLTRB(-10, 10, 110, 110); - auto u = a.Cutout(b); - auto expected = Rect::MakeLTRB(0, 0, 100, 10); - ASSERT_TRUE(u.has_value()); - ASSERT_RECT_NEAR(u.value(), expected); - } - - // Cutout from left. - { - auto a = Rect::MakeLTRB(0, 0, 100, 100); - auto b = Rect::MakeLTRB(-10, -10, 90, 110); - auto u = a.Cutout(b); - auto expected = Rect::MakeLTRB(90, 0, 100, 100); - ASSERT_TRUE(u.has_value()); - ASSERT_RECT_NEAR(u.value(), expected); - } - - // Cutout from right. - { - auto a = Rect::MakeLTRB(0, 0, 100, 100); - auto b = Rect::MakeLTRB(10, -10, 110, 110); - auto u = a.Cutout(b); - auto expected = Rect::MakeLTRB(0, 0, 10, 100); - ASSERT_TRUE(u.has_value()); - ASSERT_RECT_NEAR(u.value(), expected); - } -} - -TEST(GeometryTest, RectContainsPoint) { - { - // Origin is inclusive - Rect r = Rect::MakeXYWH(100, 100, 100, 100); - Point p(100, 100); - ASSERT_TRUE(r.Contains(p)); - } - { - // Size is exclusive - Rect r = Rect::MakeXYWH(100, 100, 100, 100); - Point p(200, 200); - ASSERT_FALSE(r.Contains(p)); - } - { - Rect r = Rect::MakeXYWH(100, 100, 100, 100); - Point p(99, 99); - ASSERT_FALSE(r.Contains(p)); - } - { - Rect r = Rect::MakeXYWH(100, 100, 100, 100); - Point p(199, 199); - ASSERT_TRUE(r.Contains(p)); - } - - { - Rect r = Rect::MakeMaximum(); - Point p(199, 199); - ASSERT_TRUE(r.Contains(p)); - } -} - -TEST(GeometryTest, RectContainsRect) { - { - Rect a = Rect::MakeXYWH(100, 100, 100, 100); - ASSERT_TRUE(a.Contains(a)); - } - { - Rect a = Rect::MakeXYWH(100, 100, 100, 100); - Rect b = Rect::MakeXYWH(0, 0, 0, 0); - ASSERT_FALSE(a.Contains(b)); - } - { - Rect a = Rect::MakeXYWH(100, 100, 100, 100); - Rect b = Rect::MakeXYWH(150, 150, 20, 20); - ASSERT_TRUE(a.Contains(b)); - } - { - Rect a = Rect::MakeXYWH(100, 100, 100, 100); - Rect b = Rect::MakeXYWH(150, 150, 100, 100); - ASSERT_FALSE(a.Contains(b)); - } - { - Rect a = Rect::MakeXYWH(100, 100, 100, 100); - Rect b = Rect::MakeXYWH(50, 50, 100, 100); - ASSERT_FALSE(a.Contains(b)); - } - { - Rect a = Rect::MakeXYWH(100, 100, 100, 100); - Rect b = Rect::MakeXYWH(0, 0, 300, 300); - ASSERT_FALSE(a.Contains(b)); - } - { - Rect a = Rect::MakeMaximum(); - Rect b = Rect::MakeXYWH(0, 0, 300, 300); - ASSERT_TRUE(a.Contains(b)); - } -} - -TEST(GeometryTest, RectGetPoints) { - { - Rect r = Rect::MakeXYWH(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)); - } - - { - Rect r = Rect::MakeMaximum(); - auto points = r.GetPoints(); - ASSERT_EQ(points[0], Point(-std::numeric_limits::infinity(), - -std::numeric_limits::infinity())); - ASSERT_EQ(points[1], Point(std::numeric_limits::infinity(), - -std::numeric_limits::infinity())); - ASSERT_EQ(points[2], Point(-std::numeric_limits::infinity(), - std::numeric_limits::infinity())); - ASSERT_EQ(points[3], Point(std::numeric_limits::infinity(), - std::numeric_limits::infinity())); - } -} - -TEST(GeometryTest, RectShift) { - auto r = Rect::MakeLTRB(0, 0, 100, 100); - - ASSERT_EQ(r.Shift(Point(10, 5)), Rect::MakeLTRB(10, 5, 110, 105)); - ASSERT_EQ(r.Shift(Point(-10, -5)), Rect::MakeLTRB(-10, -5, 90, 95)); -} - -TEST(GeometryTest, RectGetTransformedPoints) { - Rect r = Rect::MakeXYWH(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) { - { - std::vector points{{1, 5}, {4, -1}, {0, 6}}; - Rect r = Rect::MakePointBounds(points.begin(), points.end()).value(); - auto expected = Rect::MakeXYWH(0, -1, 4, 7); - ASSERT_RECT_NEAR(r, expected); - } - { - std::vector points; - std::optional r = Rect::MakePointBounds(points.begin(), points.end()); - ASSERT_FALSE(r.has_value()); - } -} - -TEST(GeometryTest, RectExpand) { - { - auto a = Rect::MakeLTRB(100, 100, 200, 200); - auto b = a.Expand(1); - auto expected = Rect::MakeLTRB(99, 99, 201, 201); - ASSERT_RECT_NEAR(b, expected); - } - { - auto a = Rect::MakeLTRB(100, 100, 200, 200); - auto b = a.Expand(-1); - auto expected = Rect::MakeLTRB(101, 101, 199, 199); - ASSERT_RECT_NEAR(b, expected); - } - - { - auto a = Rect::MakeLTRB(100, 100, 200, 200); - auto b = a.Expand(1, 2, 3, 4); - auto expected = Rect::MakeLTRB(99, 98, 203, 204); - ASSERT_RECT_NEAR(b, expected); - } - { - auto a = Rect::MakeLTRB(100, 100, 200, 200); - auto b = a.Expand(-1, -2, -3, -4); - auto expected = Rect::MakeLTRB(101, 102, 197, 196); - ASSERT_RECT_NEAR(b, expected); - } -} - -TEST(GeometryTest, RectGetPositive) { - { - Rect r = Rect::MakeXYWH(100, 200, 300, 400); - auto actual = r.GetPositive(); - ASSERT_RECT_NEAR(r, actual); - } - { - Rect r = Rect::MakeXYWH(100, 200, -100, -100); - auto actual = r.GetPositive(); - Rect expected = Rect::MakeXYWH(0, 100, 100, 100); - ASSERT_RECT_NEAR(expected, actual); - } -} - -TEST(GeometryTest, RectScale) { - { - auto r = Rect::MakeLTRB(-100, -100, 100, 100); - auto actual = r.Scale(0); - auto expected = Rect::MakeLTRB(0, 0, 0, 0); - ASSERT_RECT_NEAR(expected, actual); - } - { - auto r = Rect::MakeLTRB(-100, -100, 100, 100); - auto actual = r.Scale(-2); - auto expected = Rect::MakeLTRB(200, 200, -200, -200); - ASSERT_RECT_NEAR(expected, actual); - } - { - auto r = Rect::MakeLTRB(-100, -100, 100, 100); - auto actual = r.Scale(Point{0, 0}); - auto expected = Rect::MakeLTRB(0, 0, 0, 0); - ASSERT_RECT_NEAR(expected, actual); - } - { - auto r = Rect::MakeLTRB(-100, -100, 100, 100); - auto actual = r.Scale(Size{-1, -2}); - auto expected = Rect::MakeLTRB(100, 200, -100, -200); - ASSERT_RECT_NEAR(expected, actual); - } -} - -TEST(GeometryTest, RectDirections) { - auto r = Rect::MakeLTRB(1, 2, 3, 4); - - ASSERT_EQ(r.GetLeft(), 1); - ASSERT_EQ(r.GetTop(), 2); - ASSERT_EQ(r.GetRight(), 3); - ASSERT_EQ(r.GetBottom(), 4); - - ASSERT_POINT_NEAR(r.GetLeftTop(), Point(1, 2)); - ASSERT_POINT_NEAR(r.GetRightTop(), Point(3, 2)); - ASSERT_POINT_NEAR(r.GetLeftBottom(), Point(1, 4)); - ASSERT_POINT_NEAR(r.GetRightBottom(), Point(3, 4)); -} - -TEST(GeometryTest, RectProject) { - { - auto r = Rect::MakeLTRB(-100, -100, 100, 100); - auto actual = r.Project(r); - auto expected = Rect::MakeLTRB(0, 0, 1, 1); - ASSERT_RECT_NEAR(expected, actual); - } - { - auto r = Rect::MakeLTRB(-100, -100, 100, 100); - auto actual = r.Project(Rect::MakeLTRB(0, 0, 100, 100)); - auto expected = Rect::MakeLTRB(0.5, 0.5, 1, 1); - ASSERT_RECT_NEAR(expected, actual); - } -} - -TEST(GeometryTest, RectRoundOut) { - { - auto r = Rect::MakeLTRB(-100, -100, 100, 100); - ASSERT_EQ(Rect::RoundOut(r), r); - } - { - auto r = Rect::MakeLTRB(-100.1, -100.1, 100.1, 100.1); - ASSERT_EQ(Rect::RoundOut(r), Rect::MakeLTRB(-101, -101, 101, 101)); - } -} - TEST(GeometryTest, MatrixPrinting) { { std::stringstream stream; diff --git a/engine/src/flutter/impeller/geometry/point.h b/engine/src/flutter/impeller/geometry/point.h index ad978a08ab..ba10fa3fa0 100644 --- a/engine/src/flutter/impeller/geometry/point.h +++ b/engine/src/flutter/impeller/geometry/point.h @@ -18,6 +18,11 @@ namespace impeller { +#define ONLY_ON_FLOAT_M(Modifiers, Return) \ + template \ + Modifiers std::enable_if_t, Return> +#define ONLY_ON_FLOAT(Return) DL_ONLY_ON_FLOAT_M(, Return) + template struct TPoint { using Type = T; @@ -227,6 +232,9 @@ struct TPoint { } constexpr bool IsZero() const { return x == 0 && y == 0; } + + ONLY_ON_FLOAT_M(constexpr, bool) + IsFinite() const { return std::isfinite(x) && std::isfinite(y); } }; // Specializations for mixed (float & integer) algebraic operations. @@ -312,6 +320,9 @@ using UintPoint32 = TPoint; using Vector2 = Point; using Quad = std::array; +#undef ONLY_ON_FLOAT +#undef ONLY_ON_FLOAT_M + } // namespace impeller namespace std { diff --git a/engine/src/flutter/impeller/geometry/rect.h b/engine/src/flutter/impeller/geometry/rect.h index 817326b96c..2ff83a294a 100644 --- a/engine/src/flutter/impeller/geometry/rect.h +++ b/engine/src/flutter/impeller/geometry/rect.h @@ -13,31 +13,133 @@ #include "fml/logging.h" #include "impeller/geometry/matrix.h" #include "impeller/geometry/point.h" +#include "impeller/geometry/saturated_math.h" #include "impeller/geometry/scalar.h" #include "impeller/geometry/size.h" namespace impeller { +#define ONLY_ON_FLOAT_M(Modifiers, Return) \ + template \ + Modifiers std::enable_if_t, Return> +#define ONLY_ON_FLOAT(Return) DL_ONLY_ON_FLOAT_M(, Return) + +/// Templated struct for holding an axis-aligned rectangle. +/// +/// Rectangles are defined as 4 axis-aligned edges that might contain +/// space. They can be viewed as 2 X coordinates that define the +/// left and right edges and 2 Y coordinates that define the top and +/// bottom edges; or they can be viewed as an origin and horizontal +/// and vertical dimensions (width and height). +/// +/// When the left and right edges are equal or reversed (right <= left) +/// or the top and bottom edges are equal or reversed (bottom <= top), +/// the rectangle is considered empty. Considering the rectangle in XYWH +/// form, the width and/or the height would be negative or zero. Such +/// reversed/empty rectangles contain no space and act as such in the +/// methods that operate on them (Intersection, Union, IntersectsWithRect, +/// Contains, Cutout, etc.) +/// +/// Rectangles cannot be modified by any method and a new value can only +/// be stored into an existing rect using assignment. This keeps the API +/// clean compared to implementations that might have similar methods +/// that produce the answer in place, or construct a new object with +/// the answer, or place the result in an indicated result object. +/// +/// Methods that might fail to produce an answer will use |std::optional| +/// to indicate that success or failure (see |Intersection| and |CutOut|). +/// For convenience, |Intersection| and |Union| both have overloaded +/// variants that take |std::optional| arguments and treat them as if +/// the argument was an empty rect to allow chaining multiple such methods +/// and only needing to check the optional condition of the final result. +/// The primary methods also provide |...OrEmpty| overloaded variants that +/// translate an empty optional answer into a simple empty rectangle of the +/// same type. +/// +/// Rounding instance methods are not provided as the return value might +/// be wanted as another floating point rectangle or sometimes as an integer +/// rectangle. Instead a |RoundOut| factory, defined only for floating point +/// input rectangles, is provided to provide control over the result type. +/// +/// NaN and Infinity values +/// +/// Constructing an LTRB rectangle using Infinity values should work as +/// expected with either 0 or +Infinity returned as dimensions depending on +/// which side the Infinity values are on and the sign. +/// +/// Constructing an XYWH rectangle using Infinity values will usually +/// not work if the math requires the object to compute a right or bottom +/// edge from ([xy] -Infinity + [wh] +Infinity). Other combinations might +/// work. +/// +/// The special factory |MakeMaximum| is provided to construct a rectangle +/// of the indicated coordinate type that covers all finite coordinates. +/// It does not use infinity values, but rather the largest finite values +/// to avoid math that might produce a NaN value from various getters. +/// +/// Any rectangle that is constructed with, or computed to have a NaN value +/// will be considered the same as any empty rectangle. +/// +/// Empty Rectangle canonical results summary: +/// +/// Union will ignore any empty rects and return the other rect +/// Intersection will return nullopt if either rect is empty +/// IntersectsWithRect will return false if either rect is empty +/// Cutout will return the source rect if the argument is empty +/// Cutout will return nullopt if the source rectangle is empty +/// Contains(Point) will return false if the source rectangle is empty +/// Contains(Rect) will return false if the source rectangle is empty +/// Contains(Rect) will otherwise return true if the argument is empty +/// Specifically, EmptyRect.Contains(EmptyRect) returns false +/// +/// --------------- +/// Special notes on problems using the XYWH form of specifying rectangles: +/// +/// It is possible to have integer rectangles whose dimensions exceed +/// the maximum number that their coordinates can represent since +/// (MAX_INT - MIN_INT) overflows the representable positive numbers. +/// Floating point rectangles technically have a similar issue in that +/// overflow can occur, but it will be automatically converted into +/// either an infinity, or a finite-overflow value and still be +/// representable, just with little to no precision. +/// +/// Secondly, specifying a rectangle using XYWH leads to cases where the +/// math for (x+w) and/or (y+h) are also beyond the maximum representable +/// coordinates. For N-bit integer rectangles declared as XYWH, the +/// maximum right coordinate will require N+1 signed bits which cannot be +/// stored in storage that uses N-bit integers. +/// +/// Saturated math is used when constructing a rectangle from XYWH values +/// and when returning the dimensions of the rectangle. Constructing an +/// integer rectangle from values such that xy + wh is beyond the range +/// of the integer type will place the right or bottom edges at the maximum +/// value for the integer type. Similarly, constructing an integer rectangle +/// such that the distance from the left to the right (or top to bottom) is +/// greater than the range of the integer type will simply return the +/// maximum integer value as the dimension. Floating point rectangles are +/// naturally saturated by the rules of IEEE arithmetic. template struct TRect { + private: using Type = T; - constexpr TRect() : origin({0, 0}), size({0, 0}) {} + public: + constexpr TRect() : left_(0), top_(0), right_(0), bottom_(0) {} constexpr static TRect MakeLTRB(Type left, Type top, Type right, Type bottom) { - return TRect(left, top, right - left, bottom - top); + return TRect(left, top, right, bottom); } constexpr static TRect MakeXYWH(Type x, Type y, Type width, Type height) { - return TRect(x, y, width, height); + return TRect(x, y, saturated::Add(x, width), saturated::Add(y, height)); } constexpr static TRect MakeOriginSize(const TPoint& origin, const TSize& size) { - return TRect(origin, size); + return MakeXYWH(origin.x, origin.y, size.width, size.height); } template @@ -69,185 +171,224 @@ struct TRect { return TRect::MakeLTRB(left, top, right, bottom); } - constexpr static TRect MakeMaximum() { - return TRect::MakeLTRB(-std::numeric_limits::infinity(), - -std::numeric_limits::infinity(), - std::numeric_limits::infinity(), - std::numeric_limits::infinity()); - } - - template - constexpr explicit TRect(const TRect& other) - : origin(static_cast(other.GetX()), static_cast(other.GetY())), - size(static_cast(other.GetWidth()), - static_cast(other.GetHeight())) {} - - [[nodiscard]] constexpr TRect operator+(const TRect& r) const { - return TRect({origin.x + r.origin.x, origin.y + r.origin.y}, - {size.width + r.size.width, size.height + r.size.height}); - } - - [[nodiscard]] constexpr TRect operator-(const TRect& r) const { - return TRect({origin.x - r.origin.x, origin.y - r.origin.y}, - {size.width - r.size.width, size.height - r.size.height}); - } - - [[nodiscard]] constexpr TRect operator*(Type scale) const { - return Scale(scale); - } - - [[nodiscard]] constexpr TRect operator*(const TRect& r) const { - return TRect({origin.x * r.origin.x, origin.y * r.origin.y}, - {size.width * r.size.width, size.height * r.size.height}); + [[nodiscard]] constexpr static TRect MakeMaximum() { + return TRect::MakeLTRB(std::numeric_limits::lowest(), + std::numeric_limits::lowest(), + std::numeric_limits::max(), + std::numeric_limits::max()); } [[nodiscard]] constexpr bool operator==(const TRect& r) const { - return origin == r.origin && size == r.size; + return left_ == r.left_ && // + top_ == r.top_ && // + right_ == r.right_ && // + bottom_ == r.bottom_; } [[nodiscard]] constexpr TRect Scale(Type scale) const { - return TRect({origin.x * scale, origin.y * scale}, - {size.width * scale, size.height * scale}); + return TRect(left_ * scale, // + top_ * scale, // + right_ * scale, // + bottom_ * scale); } [[nodiscard]] constexpr TRect Scale(Type scale_x, Type scale_y) const { - return TRect({origin.x * scale_x, origin.y * scale_y}, - {size.width * scale_x, size.height * scale_y}); + return TRect(left_ * scale_x, // + top_ * scale_y, // + right_ * scale_x, // + bottom_ * scale_y); } [[nodiscard]] constexpr TRect Scale(TPoint scale) const { - return TRect({origin.x * scale.x, origin.y * scale.y}, - {size.width * scale.x, size.height * scale.y}); + return Scale(scale.x, scale.y); } [[nodiscard]] constexpr TRect Scale(TSize scale) const { - return Scale(TPoint(scale)); + return Scale(scale.width, scale.height); } + /// @brief Returns true iff the provided point |p| is inside the + /// half-open interior of this rectangle. + /// + /// For purposes of containment, a rectangle contains points + /// along the top and left edges but not points along the + /// right and bottom edges so that a point is only ever + /// considered inside one of two abutting rectangles. [[nodiscard]] constexpr bool Contains(const TPoint& p) const { - return p.x >= GetLeft() && p.x < GetRight() && p.y >= GetTop() && - p.y < GetBottom(); + return !this->IsEmpty() && // + p.x >= left_ && // + p.y >= top_ && // + p.x < right_ && // + p.y < bottom_; } + /// @brief Returns true iff this rectangle is not empty and it also + /// contains every point considered inside the provided + /// rectangle |o| (as determined by |Contains(TPoint)|). + /// + /// This is similar to a definition where the result is true iff + /// the union of the two rectangles is equal to this rectangle, + /// ignoring precision issues with performing those operations + /// and assuming that empty rectangles are never equal. + /// + /// An empty rectangle can contain no other rectangle. + /// + /// An empty rectangle is, however, contained within any + /// other non-empy rectangle as the set of points it contains + /// is an empty set and so there are no points to fail the + /// containment criteria. [[nodiscard]] constexpr bool Contains(const TRect& o) const { - return o.GetLeft() >= GetLeft() && o.GetTop() >= GetTop() && - o.GetRight() <= GetRight() && o.GetBottom() <= GetBottom(); + return !this->IsEmpty() && // + (o.IsEmpty() || (o.left_ >= left_ && // + o.top_ >= top_ && // + o.right_ <= right_ && // + o.bottom_ <= bottom_)); } - /// Returns true if either of the width or height are 0, negative, or NaN. - [[nodiscard]] constexpr bool IsEmpty() const { return size.IsEmpty(); } + /// @brief Returns true if all of the fields of this floating point + /// rectangle are finite. + /// + /// Note that the results of |GetWidth()| and |GetHeight()| may + /// still be infinite due to overflow even if the fields themselves + /// are finite. + ONLY_ON_FLOAT_M([[nodiscard]] constexpr, bool) + IsFinite() const { + return std::isfinite(left_) && // + std::isfinite(top_) && // + std::isfinite(right_) && // + std::isfinite(bottom_); + } - /// Returns true if width and height are equal and neither is NaN. - [[nodiscard]] constexpr bool IsSquare() const { return size.IsSquare(); } + /// @brief Returns true if either of the width or height are 0, negative, + /// or NaN. + [[nodiscard]] constexpr bool IsEmpty() const { + // Computing the non-empty condition and negating the result causes any + // NaN value to return true - i.e. is considered empty. + return !(left_ < right_ && top_ < bottom_); + } + + /// @brief Returns true if width and height are equal and neither is NaN. + [[nodiscard]] constexpr bool IsSquare() const { + // empty rectangles can technically be "square", but would be + // misleading to most callers. Using |IsEmpty| also prevents + // "non-empty and non-overflowing" computations from happening + // to be equal to "empty and overflowing" results. + // (Consider LTRB(10, 15, MAX-2, MIN+2) which is empty, but both + // w/h subtractions equal "5"). + return !IsEmpty() && (right_ - left_) == (bottom_ - top_); + } [[nodiscard]] constexpr bool IsMaximum() const { return *this == MakeMaximum(); } /// @brief Returns the upper left corner of the rectangle as specified - /// when it was constructed. - /// - /// Note that unlike the |GetLeft|, |GetTop|, and |GetLeftTop| - /// methods which will return values as if the rectangle had been - /// "unswapped" by calling |GetPositive| on it, this method - /// returns the raw origin values. - [[nodiscard]] constexpr TPoint GetOrigin() const { return origin; } + /// by the left/top or x/y values when it was constructed. + [[nodiscard]] constexpr TPoint GetOrigin() const { + return {left_, top_}; + } - /// @brief Returns the size of the rectangle as specified when it was - /// constructed and which may be negative in either width or - /// height. - [[nodiscard]] constexpr TSize GetSize() const { return size; } + /// @brief Returns the size of the rectangle which may be negative in + /// either width or height and may have been clipped to the + /// maximum integer values for integer rects whose size overflows. + [[nodiscard]] constexpr TSize GetSize() const { + return {GetWidth(), GetHeight()}; + } /// @brief Returns the X coordinate of the upper left corner, equivalent /// to |GetOrigin().x| - [[nodiscard]] constexpr Type GetX() const { return origin.x; } + [[nodiscard]] constexpr Type GetX() const { return left_; } /// @brief Returns the Y coordinate of the upper left corner, equivalent /// to |GetOrigin().y| - [[nodiscard]] constexpr Type GetY() const { return origin.y; } + [[nodiscard]] constexpr Type GetY() const { return top_; } /// @brief Returns the width of the rectangle, equivalent to /// |GetSize().width| - [[nodiscard]] constexpr Type GetWidth() const { return size.width; } + [[nodiscard]] constexpr Type GetWidth() const { + return saturated::Sub(right_, left_); + } /// @brief Returns the height of the rectangle, equivalent to /// |GetSize().height| - [[nodiscard]] constexpr Type GetHeight() const { return size.height; } - - [[nodiscard]] constexpr auto GetLeft() const { - if (IsMaximum()) { - return -std::numeric_limits::infinity(); - } - return std::min(origin.x, origin.x + size.width); + [[nodiscard]] constexpr Type GetHeight() const { + return saturated::Sub(bottom_, top_); } - [[nodiscard]] constexpr auto GetTop() const { - if (IsMaximum()) { - return -std::numeric_limits::infinity(); - } - return std::min(origin.y, origin.y + size.height); - } + [[nodiscard]] constexpr auto GetLeft() const { return left_; } - [[nodiscard]] constexpr auto GetRight() const { - if (IsMaximum()) { - return std::numeric_limits::infinity(); - } - return std::max(origin.x, origin.x + size.width); - } + [[nodiscard]] constexpr auto GetTop() const { return top_; } - [[nodiscard]] constexpr auto GetBottom() const { - if (IsMaximum()) { - return std::numeric_limits::infinity(); - } - return std::max(origin.y, origin.y + size.height); - } + [[nodiscard]] constexpr auto GetRight() const { return right_; } - [[nodiscard]] constexpr TPoint GetLeftTop() const { - return {GetLeft(), GetTop()}; + [[nodiscard]] constexpr auto GetBottom() const { return bottom_; } + + [[nodiscard]] constexpr TPoint GetLeftTop() const { // + return {left_, top_}; } [[nodiscard]] constexpr TPoint GetRightTop() const { - return {GetRight(), GetTop()}; + return {right_, top_}; } [[nodiscard]] constexpr TPoint GetLeftBottom() const { - return {GetLeft(), GetBottom()}; + return {left_, bottom_}; } [[nodiscard]] constexpr TPoint GetRightBottom() const { - return {GetRight(), GetBottom()}; + return {right_, bottom_}; } /// @brief Get the area of the rectangle, equivalent to |GetSize().Area()| - [[nodiscard]] constexpr T Area() const { return size.Area(); } + [[nodiscard]] constexpr T Area() const { + // TODO(flutter/flutter#141710) - Use saturated math to avoid overflow + // https://github.com/flutter/flutter/issues/141710 + return IsEmpty() ? 0 : (right_ - left_) * (bottom_ - top_); + } /// @brief Get the center point as a |Point|. [[nodiscard]] constexpr Point GetCenter() const { - return Point(origin.x + size.width * 0.5f, origin.y + size.height * 0.5f); + return {saturated::AverageScalar(left_, right_), + saturated::AverageScalar(top_, bottom_)}; } [[nodiscard]] constexpr std::array GetLTRB() const { - return {GetLeft(), GetTop(), GetRight(), GetBottom()}; + return {left_, top_, right_, bottom_}; } /// @brief Get the x, y coordinates of the origin and the width and /// height of the rectangle in an array. [[nodiscard]] constexpr std::array GetXYWH() const { - return {origin.x, origin.y, size.width, size.height}; + return {left_, top_, GetWidth(), GetHeight()}; } /// @brief Get a version of this rectangle that has a non-negative size. [[nodiscard]] constexpr TRect GetPositive() const { - auto ltrb = GetLTRB(); - return MakeLTRB(ltrb[0], ltrb[1], ltrb[2], ltrb[3]); + if (!IsEmpty()) { + return *this; + } + return { + std::min(left_, right_), + std::min(top_, bottom_), + std::max(left_, right_), + std::max(top_, bottom_), + }; } - /// @brief Get the points that represent the 4 corners of this rectangle. + /// @brief Get the points that represent the 4 corners of this rectangle + /// in a Z order that is compatible with triangle strips or a set + /// of all zero points if the rectangle is empty. /// The order is: Top left, top right, bottom left, bottom right. [[nodiscard]] constexpr std::array, 4> GetPoints() const { - auto [left, top, right, bottom] = GetLTRB(); - return {TPoint(left, top), TPoint(right, top), TPoint(left, bottom), - TPoint(right, bottom)}; + if (IsEmpty()) { + return {}; + } + return { + TPoint{left_, top_}, + TPoint{right_, top_}, + TPoint{left_, bottom_}, + TPoint{right_, bottom_}, + }; } [[nodiscard]] constexpr std::array, 4> GetTransformedPoints( @@ -262,6 +403,9 @@ struct TRect { /// @brief Creates a new bounding box that contains this transformed /// rectangle. [[nodiscard]] constexpr TRect TransformBounds(const Matrix& transform) const { + if (IsEmpty()) { + return {}; + } auto points = GetTransformedPoints(transform); auto bounds = TRect::MakePointBounds(points.begin(), points.end()); if (bounds.has_value()) { @@ -279,10 +423,10 @@ struct TRect { /// transform that maps all points to (0, 0). [[nodiscard]] constexpr Matrix GetNormalizingTransform() const { if (!IsEmpty()) { - Scalar sx = 1.0 / size.width; - Scalar sy = 1.0 / size.height; - Scalar tx = origin.x * -sx; - Scalar ty = origin.y * -sy; + Scalar sx = 1.0 / GetWidth(); + Scalar sy = 1.0 / GetHeight(); + Scalar tx = left_ * -sx; + Scalar ty = top_ * -sy; // Exclude NaN and infinities and either scale underflowing to zero if (sx != 0.0 && sy != 0.0 && 0.0 * sx * sy * tx * ty == 0.0) { @@ -300,38 +444,55 @@ struct TRect { } [[nodiscard]] constexpr TRect Union(const TRect& o) const { - auto this_ltrb = GetLTRB(); - auto other_ltrb = o.GetLTRB(); - return TRect::MakeLTRB(std::min(this_ltrb[0], other_ltrb[0]), // - std::min(this_ltrb[1], other_ltrb[1]), // - std::max(this_ltrb[2], other_ltrb[2]), // - std::max(this_ltrb[3], other_ltrb[3]) // - ); + if (IsEmpty()) { + return o; + } + if (o.IsEmpty()) { + return *this; + } + return { + std::min(left_, o.left_), + std::min(top_, o.top_), + std::max(right_, o.right_), + std::max(bottom_, o.bottom_), + }; } - [[nodiscard]] constexpr std::optional> Intersection( + [[nodiscard]] constexpr std::optional Intersection( const TRect& o) const { - auto this_ltrb = GetLTRB(); - auto other_ltrb = o.GetLTRB(); - auto intersection = - TRect::MakeLTRB(std::max(this_ltrb[0], other_ltrb[0]), // - std::max(this_ltrb[1], other_ltrb[1]), // - std::min(this_ltrb[2], other_ltrb[2]), // - std::min(this_ltrb[3], other_ltrb[3]) // - ); - if (intersection.size.IsEmpty()) { + if (IntersectsWithRect(o)) { + return TRect{ + std::max(left_, o.left_), + std::max(top_, o.top_), + std::min(right_, o.right_), + std::min(bottom_, o.bottom_), + }; + } else { return std::nullopt; } - return intersection; } [[nodiscard]] constexpr bool IntersectsWithRect(const TRect& o) const { - return Intersection(o).has_value(); + return !IsEmpty() && // + !o.IsEmpty() && // + left_ < o.right_ && // + top_ < o.bottom_ && // + right_ > o.left_ && // + bottom_ > o.top_; } - /// @brief Returns the new boundary rectangle that would result from the - /// rectangle being cutout by a second rectangle. + /// @brief Returns the new boundary rectangle that would result from this + /// rectangle being cut out by the specified rectangle. [[nodiscard]] constexpr std::optional> Cutout(const TRect& o) const { + if (IsEmpty()) { + // This test isn't just a short-circuit, it also prevents the concise + // math below from returning the wrong answer on empty rects. + // Once we know that this rectangle is not empty, the math below can + // only succeed in computing a value if o is also non-empty and non-nan. + // Otherwise, the method returns *this by default. + return std::nullopt; + } + const auto& [a_left, a_top, a_right, a_bottom] = GetLTRB(); // Source rect. const auto& [b_left, b_top, b_right, b_bottom] = o.GetLTRB(); // Cutout. if (b_left <= a_left && b_right >= a_right) { @@ -344,17 +505,17 @@ struct TRect { return TRect::MakeLTRB(a_left, b_bottom, a_right, a_bottom); } if (b_bottom >= a_bottom && b_top < a_bottom) { - // Cuts out the bottom. + // Cuts off the bottom. return TRect::MakeLTRB(a_left, a_top, a_right, b_top); } } if (b_top <= a_top && b_bottom >= a_bottom) { if (b_left <= a_left && b_right > a_left) { - // Cuts out the left. + // Cuts off the left. return TRect::MakeLTRB(b_right, a_top, a_right, a_bottom); } if (b_right >= a_right && b_left < a_right) { - // Cuts out the right. + // Cuts off the right. return TRect::MakeLTRB(a_left, a_top, b_left, a_bottom); } } @@ -362,10 +523,23 @@ struct TRect { return *this; } + [[nodiscard]] constexpr TRect CutoutOrEmpty(const TRect& o) const { + return Cutout(o).value_or(TRect()); + } + + /// @brief Returns a new rectangle translated by the given offset. + [[nodiscard]] constexpr TRect Shift(T dx, T dy) const { + return { + saturated::Add(left_, dx), // + saturated::Add(top_, dy), // + saturated::Add(right_, dx), // + saturated::Add(bottom_, dy), // + }; + } + /// @brief Returns a new rectangle translated by the given offset. [[nodiscard]] constexpr TRect Shift(TPoint offset) const { - return TRect(origin.x + offset.x, origin.y + offset.y, size.width, - size.height); + return Shift(offset.x, offset.y); } /// @brief Returns a rectangle with expanded edges. Negative expansion @@ -374,62 +548,68 @@ struct TRect { T top, T right, T bottom) const { - return TRect(origin.x - left, // - origin.y - top, // - size.width + left + right, // - size.height + top + bottom); + return { + saturated::Sub(left_, left), // + saturated::Sub(top_, top), // + saturated::Add(right_, right), // + saturated::Add(bottom_, bottom), // + }; } /// @brief Returns a rectangle with expanded edges in all directions. /// Negative expansion results in shrinking. [[nodiscard]] constexpr TRect Expand(T amount) const { - return TRect(origin.x - amount, // - origin.y - amount, // - size.width + amount * 2, // - size.height + amount * 2); + return { + saturated::Sub(left_, amount), // + saturated::Sub(top_, amount), // + saturated::Add(right_, amount), // + saturated::Add(bottom_, amount), // + }; } /// @brief Returns a rectangle with expanded edges in all directions. /// Negative expansion results in shrinking. [[nodiscard]] constexpr TRect Expand(T horizontal_amount, T vertical_amount) const { - return TRect(origin.x - horizontal_amount, // - origin.y - vertical_amount, // - size.width + horizontal_amount * 2, // - size.height + vertical_amount * 2); + return { + saturated::Sub(left_, horizontal_amount), // + saturated::Sub(top_, vertical_amount), // + saturated::Add(right_, horizontal_amount), // + saturated::Add(bottom_, vertical_amount), // + }; } /// @brief Returns a rectangle with expanded edges in all directions. /// Negative expansion results in shrinking. [[nodiscard]] constexpr TRect Expand(TPoint amount) const { - return TRect(origin.x - amount.x, // - origin.y - amount.y, // - size.width + amount.x * 2, // - size.height + amount.y * 2); + return Expand(amount.x, amount.y); } /// @brief Returns a rectangle with expanded edges in all directions. /// Negative expansion results in shrinking. [[nodiscard]] constexpr TRect Expand(TSize amount) const { - return TRect(origin.x - amount.width, // - origin.y - amount.height, // - size.width + amount.width * 2, // - size.height + amount.height * 2); + return Expand(amount.width, amount.height); } /// @brief Returns a new rectangle that represents the projection of the /// source rectangle onto this rectangle. In other words, the source - /// rectangle is redefined in terms of the corrdinate space of this + /// rectangle is redefined in terms of the coordinate space of this /// rectangle. [[nodiscard]] constexpr TRect Project(TRect source) const { - return source.Shift(-origin).Scale( - TSize(1.0 / static_cast(size.width), - 1.0 / static_cast(size.height))); + if (IsEmpty()) { + return {}; + } + return source.Shift(-left_, -top_) + .Scale(1.0 / static_cast(GetWidth()), + 1.0 / static_cast(GetHeight())); } - [[nodiscard]] constexpr static TRect RoundOut(const TRect& r) { - return TRect::MakeLTRB(floor(r.GetLeft()), floor(r.GetTop()), - ceil(r.GetRight()), ceil(r.GetBottom())); + ONLY_ON_FLOAT_M([[nodiscard]] constexpr static, TRect) + RoundOut(const TRect& r) { + return TRect::MakeLTRB(saturated::Cast(floor(r.GetLeft())), + saturated::Cast(floor(r.GetTop())), + saturated::Cast(ceil(r.GetRight())), + saturated::Cast(ceil(r.GetBottom()))); } [[nodiscard]] constexpr static std::optional Union( @@ -441,7 +621,7 @@ struct TRect { [[nodiscard]] constexpr static std::optional Union( const std::optional a, const TRect& b) { - return Union(b, a); + return a.has_value() ? a->Union(b) : b; } [[nodiscard]] constexpr static std::optional Union( @@ -459,7 +639,7 @@ struct TRect { [[nodiscard]] constexpr static std::optional Intersection( const std::optional a, const TRect& b) { - return Intersection(b, a); + return a.has_value() ? a->Intersection(b) : b; } [[nodiscard]] constexpr static std::optional Intersection( @@ -469,25 +649,21 @@ struct TRect { } private: - constexpr TRect(Type x, Type y, Type width, Type height) - : origin(x, y), size(width, height) {} + constexpr TRect(Type left, Type top, Type right, Type bottom) + : left_(left), top_(top), right_(right), bottom_(bottom) {} - constexpr TRect(TPoint origin, TSize size) - : origin(origin), size(size) {} - - // NOLINTBEGIN - // These fields should be named origin_ and size_, but will be renamed to - // left_/top_/right_/bottom_ during the next phase of the reworking of the - // TRect class and we will deal with the renaming of all the usages here - // and in path_builder.cc at that time - TPoint origin; - TSize size; - // NOLINTEND + Type left_; + Type top_; + Type right_; + Type bottom_; }; using Rect = TRect; using IRect = TRect; +#undef ONLY_ON_FLOAT +#undef ONLY_ON_FLOAT_M + } // namespace impeller namespace std { diff --git a/engine/src/flutter/impeller/geometry/rect_unittests.cc b/engine/src/flutter/impeller/geometry/rect_unittests.cc index a7611acacf..17ba9efb06 100644 --- a/engine/src/flutter/impeller/geometry/rect_unittests.cc +++ b/engine/src/flutter/impeller/geometry/rect_unittests.cc @@ -11,6 +11,649 @@ namespace impeller { namespace testing { +TEST(RectTest, RectEmptyDeclaration) { + Rect rect; + + EXPECT_EQ(rect.GetLeft(), 0.0f); + EXPECT_EQ(rect.GetTop(), 0.0f); + EXPECT_EQ(rect.GetRight(), 0.0f); + EXPECT_EQ(rect.GetBottom(), 0.0f); + EXPECT_EQ(rect.GetX(), 0.0f); + EXPECT_EQ(rect.GetY(), 0.0f); + EXPECT_EQ(rect.GetWidth(), 0.0f); + EXPECT_EQ(rect.GetHeight(), 0.0f); + EXPECT_TRUE(rect.IsEmpty()); + EXPECT_TRUE(rect.IsFinite()); +} + +TEST(RectTest, IRectEmptyDeclaration) { + IRect rect; + + EXPECT_EQ(rect.GetLeft(), 0); + EXPECT_EQ(rect.GetTop(), 0); + EXPECT_EQ(rect.GetRight(), 0); + EXPECT_EQ(rect.GetBottom(), 0); + EXPECT_EQ(rect.GetX(), 0); + EXPECT_EQ(rect.GetY(), 0); + EXPECT_EQ(rect.GetWidth(), 0); + EXPECT_EQ(rect.GetHeight(), 0); + EXPECT_TRUE(rect.IsEmpty()); + // EXPECT_TRUE(rect.IsFinite()); // should fail to compile +} + +TEST(RectTest, RectDefaultConstructor) { + Rect rect = Rect(); + + EXPECT_EQ(rect.GetLeft(), 0.0f); + EXPECT_EQ(rect.GetTop(), 0.0f); + EXPECT_EQ(rect.GetRight(), 0.0f); + EXPECT_EQ(rect.GetBottom(), 0.0f); + EXPECT_EQ(rect.GetX(), 0.0f); + EXPECT_EQ(rect.GetY(), 0.0f); + EXPECT_EQ(rect.GetWidth(), 0.0f); + EXPECT_EQ(rect.GetHeight(), 0.0f); + EXPECT_TRUE(rect.IsEmpty()); + EXPECT_TRUE(rect.IsFinite()); +} + +TEST(RectTest, IRectDefaultConstructor) { + IRect rect = IRect(); + + EXPECT_EQ(rect.GetLeft(), 0); + EXPECT_EQ(rect.GetTop(), 0); + EXPECT_EQ(rect.GetRight(), 0); + EXPECT_EQ(rect.GetBottom(), 0); + EXPECT_EQ(rect.GetX(), 0); + EXPECT_EQ(rect.GetY(), 0); + EXPECT_EQ(rect.GetWidth(), 0); + EXPECT_EQ(rect.GetHeight(), 0); + EXPECT_TRUE(rect.IsEmpty()); +} + +TEST(RectTest, RectSimpleLTRB) { + // Using fractional-power-of-2 friendly values for equality tests + Rect rect = Rect::MakeLTRB(5.125f, 10.25f, 20.625f, 25.375f); + + EXPECT_EQ(rect.GetLeft(), 5.125f); + EXPECT_EQ(rect.GetTop(), 10.25f); + EXPECT_EQ(rect.GetRight(), 20.625f); + EXPECT_EQ(rect.GetBottom(), 25.375f); + EXPECT_EQ(rect.GetX(), 5.125f); + EXPECT_EQ(rect.GetY(), 10.25f); + EXPECT_EQ(rect.GetWidth(), 15.5f); + EXPECT_EQ(rect.GetHeight(), 15.125f); + EXPECT_FALSE(rect.IsEmpty()); + EXPECT_TRUE(rect.IsFinite()); +} + +TEST(RectTest, IRectSimpleLTRB) { + IRect rect = IRect::MakeLTRB(5, 10, 20, 25); + + EXPECT_EQ(rect.GetLeft(), 5); + EXPECT_EQ(rect.GetTop(), 10); + EXPECT_EQ(rect.GetRight(), 20); + EXPECT_EQ(rect.GetBottom(), 25); + EXPECT_EQ(rect.GetX(), 5); + EXPECT_EQ(rect.GetY(), 10); + EXPECT_EQ(rect.GetWidth(), 15); + EXPECT_EQ(rect.GetHeight(), 15); + EXPECT_FALSE(rect.IsEmpty()); +} + +TEST(RectTest, RectSimpleXYWH) { + // Using fractional-power-of-2 friendly values for equality tests + Rect rect = Rect::MakeXYWH(5.125f, 10.25f, 15.5f, 15.125f); + + EXPECT_EQ(rect.GetLeft(), 5.125f); + EXPECT_EQ(rect.GetTop(), 10.25f); + EXPECT_EQ(rect.GetRight(), 20.625f); + EXPECT_EQ(rect.GetBottom(), 25.375f); + EXPECT_EQ(rect.GetX(), 5.125f); + EXPECT_EQ(rect.GetY(), 10.25f); + EXPECT_EQ(rect.GetWidth(), 15.5f); + EXPECT_EQ(rect.GetHeight(), 15.125f); + EXPECT_FALSE(rect.IsEmpty()); + EXPECT_TRUE(rect.IsFinite()); +} + +TEST(RectTest, IRectSimpleXYWH) { + IRect rect = IRect::MakeXYWH(5, 10, 15, 16); + + EXPECT_EQ(rect.GetLeft(), 5); + EXPECT_EQ(rect.GetTop(), 10); + EXPECT_EQ(rect.GetRight(), 20); + EXPECT_EQ(rect.GetBottom(), 26); + EXPECT_EQ(rect.GetX(), 5); + EXPECT_EQ(rect.GetY(), 10); + EXPECT_EQ(rect.GetWidth(), 15); + EXPECT_EQ(rect.GetHeight(), 16); + EXPECT_FALSE(rect.IsEmpty()); +} + +TEST(RectTest, RectOverflowXYWH) { + auto min = std::numeric_limits::lowest(); + auto max = std::numeric_limits::max(); + auto inf = std::numeric_limits::infinity(); + + // 8 cases: + // finite X, max W + // max X, max W + // finite Y, max H + // max Y, max H + // finite X, min W + // min X, min W + // finite Y, min H + // min Y, min H + + // a small finite value added to a max value will remain max + // a very large finite value (like max) added to max will go to infinity + + { + Rect rect = Rect::MakeXYWH(5.0, 10.0f, max, 15.0f); + + EXPECT_EQ(rect.GetLeft(), 5.0f); + EXPECT_EQ(rect.GetTop(), 10.0f); + EXPECT_EQ(rect.GetRight(), max); + EXPECT_EQ(rect.GetBottom(), 25.0f); + EXPECT_EQ(rect.GetX(), 5.0f); + EXPECT_EQ(rect.GetY(), 10.0f); + EXPECT_EQ(rect.GetWidth(), max); + EXPECT_EQ(rect.GetHeight(), 15.0f); + EXPECT_FALSE(rect.IsEmpty()); + EXPECT_TRUE(rect.IsFinite()); + } + + { + Rect rect = Rect::MakeXYWH(max, 10.0f, max, 15.0f); + + EXPECT_EQ(rect.GetLeft(), max); + EXPECT_EQ(rect.GetTop(), 10.0f); + EXPECT_EQ(rect.GetRight(), inf); + EXPECT_EQ(rect.GetBottom(), 25.0f); + EXPECT_EQ(rect.GetX(), max); + EXPECT_EQ(rect.GetY(), 10.0f); + EXPECT_EQ(rect.GetWidth(), inf); + EXPECT_EQ(rect.GetHeight(), 15.0f); + EXPECT_FALSE(rect.IsEmpty()); + EXPECT_FALSE(rect.IsFinite()); + } + + { + Rect rect = Rect::MakeXYWH(5.0f, 10.0f, 20.0f, max); + + EXPECT_EQ(rect.GetLeft(), 5.0f); + EXPECT_EQ(rect.GetTop(), 10.0f); + EXPECT_EQ(rect.GetRight(), 25.0f); + EXPECT_EQ(rect.GetBottom(), max); + EXPECT_EQ(rect.GetX(), 5.0f); + EXPECT_EQ(rect.GetY(), 10.0f); + EXPECT_EQ(rect.GetWidth(), 20.0f); + EXPECT_EQ(rect.GetHeight(), max); + EXPECT_FALSE(rect.IsEmpty()); + EXPECT_TRUE(rect.IsFinite()); + } + + { + Rect rect = Rect::MakeXYWH(5.0f, max, 20.0f, max); + + EXPECT_EQ(rect.GetLeft(), 5.0f); + EXPECT_EQ(rect.GetTop(), max); + EXPECT_EQ(rect.GetRight(), 25.0f); + EXPECT_EQ(rect.GetBottom(), inf); + EXPECT_EQ(rect.GetX(), 5.0f); + EXPECT_EQ(rect.GetY(), max); + EXPECT_EQ(rect.GetWidth(), 20.0f); + EXPECT_EQ(rect.GetHeight(), inf); + EXPECT_FALSE(rect.IsEmpty()); + EXPECT_FALSE(rect.IsFinite()); + } + + { + Rect rect = Rect::MakeXYWH(5.0, 10.0f, min, 15.0f); + + EXPECT_EQ(rect.GetLeft(), 5.0f); + EXPECT_EQ(rect.GetTop(), 10.0f); + EXPECT_EQ(rect.GetRight(), min); + EXPECT_EQ(rect.GetBottom(), 25.0f); + EXPECT_EQ(rect.GetX(), 5.0f); + EXPECT_EQ(rect.GetY(), 10.0f); + EXPECT_EQ(rect.GetWidth(), min); + EXPECT_EQ(rect.GetHeight(), 15.0f); + EXPECT_TRUE(rect.IsEmpty()); + EXPECT_TRUE(rect.IsFinite()); + } + + { + Rect rect = Rect::MakeXYWH(min, 10.0f, min, 15.0f); + + EXPECT_EQ(rect.GetLeft(), min); + EXPECT_EQ(rect.GetTop(), 10.0f); + EXPECT_EQ(rect.GetRight(), -inf); + EXPECT_EQ(rect.GetBottom(), 25.0f); + EXPECT_EQ(rect.GetX(), min); + EXPECT_EQ(rect.GetY(), 10.0f); + EXPECT_EQ(rect.GetWidth(), -inf); + EXPECT_EQ(rect.GetHeight(), 15.0f); + EXPECT_TRUE(rect.IsEmpty()); + EXPECT_FALSE(rect.IsFinite()); + } + + { + Rect rect = Rect::MakeXYWH(5.0f, 10.0f, 20.0f, min); + + EXPECT_EQ(rect.GetLeft(), 5.0f); + EXPECT_EQ(rect.GetTop(), 10.0f); + EXPECT_EQ(rect.GetRight(), 25.0f); + EXPECT_EQ(rect.GetBottom(), min); + EXPECT_EQ(rect.GetX(), 5.0f); + EXPECT_EQ(rect.GetY(), 10.0f); + EXPECT_EQ(rect.GetWidth(), 20.0f); + EXPECT_EQ(rect.GetHeight(), min); + EXPECT_TRUE(rect.IsEmpty()); + EXPECT_TRUE(rect.IsFinite()); + } + + { + Rect rect = Rect::MakeXYWH(5.0f, min, 20.0f, min); + + EXPECT_EQ(rect.GetLeft(), 5.0f); + EXPECT_EQ(rect.GetTop(), min); + EXPECT_EQ(rect.GetRight(), 25.0f); + EXPECT_EQ(rect.GetBottom(), -inf); + EXPECT_EQ(rect.GetX(), 5.0f); + EXPECT_EQ(rect.GetY(), min); + EXPECT_EQ(rect.GetWidth(), 20.0f); + EXPECT_EQ(rect.GetHeight(), -inf); + EXPECT_TRUE(rect.IsEmpty()); + EXPECT_FALSE(rect.IsFinite()); + } +} + +TEST(RectTest, IRectOverflowXYWH) { + auto min = std::numeric_limits::min(); + auto max = std::numeric_limits::max(); + + // 4 cases + // x near max, positive w takes it past max + // x near min, negative w takes it below min + // y near max, positive h takes it past max + // y near min, negative h takes it below min + + { + IRect rect = IRect::MakeXYWH(max - 5, 10, 10, 16); + + EXPECT_EQ(rect.GetLeft(), max - 5); + EXPECT_EQ(rect.GetTop(), 10); + EXPECT_EQ(rect.GetRight(), max); + EXPECT_EQ(rect.GetBottom(), 26); + EXPECT_EQ(rect.GetX(), max - 5); + EXPECT_EQ(rect.GetY(), 10); + EXPECT_EQ(rect.GetWidth(), 5); + EXPECT_EQ(rect.GetHeight(), 16); + EXPECT_FALSE(rect.IsEmpty()); + } + + { + IRect rect = IRect::MakeXYWH(min + 5, 10, -10, 16); + + EXPECT_EQ(rect.GetLeft(), min + 5); + EXPECT_EQ(rect.GetTop(), 10); + EXPECT_EQ(rect.GetRight(), min); + EXPECT_EQ(rect.GetBottom(), 26); + EXPECT_EQ(rect.GetX(), min + 5); + EXPECT_EQ(rect.GetY(), 10); + EXPECT_EQ(rect.GetWidth(), -5); + EXPECT_EQ(rect.GetHeight(), 16); + EXPECT_TRUE(rect.IsEmpty()); + } + + { + IRect rect = IRect::MakeXYWH(5, max - 10, 10, 16); + + EXPECT_EQ(rect.GetLeft(), 5); + EXPECT_EQ(rect.GetTop(), max - 10); + EXPECT_EQ(rect.GetRight(), 15); + EXPECT_EQ(rect.GetBottom(), max); + EXPECT_EQ(rect.GetX(), 5); + EXPECT_EQ(rect.GetY(), max - 10); + EXPECT_EQ(rect.GetWidth(), 10); + EXPECT_EQ(rect.GetHeight(), 10); + EXPECT_FALSE(rect.IsEmpty()); + } + + { + IRect rect = IRect::MakeXYWH(5, min + 10, 10, -16); + + EXPECT_EQ(rect.GetLeft(), 5); + EXPECT_EQ(rect.GetTop(), min + 10); + EXPECT_EQ(rect.GetRight(), 15); + EXPECT_EQ(rect.GetBottom(), min); + EXPECT_EQ(rect.GetX(), 5); + EXPECT_EQ(rect.GetY(), min + 10); + EXPECT_EQ(rect.GetWidth(), 10); + EXPECT_EQ(rect.GetHeight(), -10); + EXPECT_TRUE(rect.IsEmpty()); + } +} + +TEST(RectTest, RectOverflowLTRB) { + auto min = std::numeric_limits::lowest(); + auto max = std::numeric_limits::max(); + auto inf = std::numeric_limits::infinity(); + + // 8 cases: + // finite negative X, max W + // ~min X, ~max W + // finite negative Y, max H + // ~min Y, ~max H + // finite positive X, min W + // ~min X, ~min W + // finite positive Y, min H + // ~min Y, ~min H + + // a small finite value subtracted from a max value will remain max + // a very large finite value (like min) subtracted from max will go to inf + + { + Rect rect = Rect::MakeLTRB(-5.0f, 10.0f, max, 25.0f); + + EXPECT_EQ(rect.GetLeft(), -5.0f); + EXPECT_EQ(rect.GetTop(), 10.0f); + EXPECT_EQ(rect.GetRight(), max); + EXPECT_EQ(rect.GetBottom(), 25.0f); + EXPECT_EQ(rect.GetX(), -5.0f); + EXPECT_EQ(rect.GetY(), 10.0f); + EXPECT_EQ(rect.GetWidth(), max); + EXPECT_EQ(rect.GetHeight(), 15.0f); + EXPECT_FALSE(rect.IsEmpty()); + EXPECT_TRUE(rect.IsFinite()); + } + + { + Rect rect = Rect::MakeLTRB(min + 5.0f, 10.0f, max - 5.0f, 25.0f); + + EXPECT_EQ(rect.GetLeft(), min + 5.0f); + EXPECT_EQ(rect.GetTop(), 10.0f); + EXPECT_EQ(rect.GetRight(), max - 5.0f); + EXPECT_EQ(rect.GetBottom(), 25.0f); + EXPECT_EQ(rect.GetX(), min + 5.0f); + EXPECT_EQ(rect.GetY(), 10.0f); + EXPECT_EQ(rect.GetWidth(), inf); + EXPECT_EQ(rect.GetHeight(), 15.0f); + EXPECT_FALSE(rect.IsEmpty()); + EXPECT_TRUE(rect.IsFinite()); + } + + { + Rect rect = Rect::MakeLTRB(5.0f, -10.0f, 20.0f, max); + + EXPECT_EQ(rect.GetLeft(), 5.0f); + EXPECT_EQ(rect.GetTop(), -10.0f); + EXPECT_EQ(rect.GetRight(), 20.0f); + EXPECT_EQ(rect.GetBottom(), max); + EXPECT_EQ(rect.GetX(), 5.0f); + EXPECT_EQ(rect.GetY(), -10.0f); + EXPECT_EQ(rect.GetWidth(), 15.0f); + EXPECT_EQ(rect.GetHeight(), max); + EXPECT_FALSE(rect.IsEmpty()); + EXPECT_TRUE(rect.IsFinite()); + } + + { + Rect rect = Rect::MakeLTRB(5.0f, min + 10.0f, 20.0f, max - 15.0f); + + EXPECT_EQ(rect.GetLeft(), 5.0f); + EXPECT_EQ(rect.GetTop(), min + 10.0f); + EXPECT_EQ(rect.GetRight(), 20.0f); + EXPECT_EQ(rect.GetBottom(), max - 15.0f); + EXPECT_EQ(rect.GetX(), 5.0f); + EXPECT_EQ(rect.GetY(), min + 10.0f); + EXPECT_EQ(rect.GetWidth(), 15.0f); + EXPECT_EQ(rect.GetHeight(), inf); + EXPECT_FALSE(rect.IsEmpty()); + EXPECT_TRUE(rect.IsFinite()); + } + + { + Rect rect = Rect::MakeLTRB(5.0f, 10.0f, min, 25.0f); + + EXPECT_EQ(rect.GetLeft(), 5.0f); + EXPECT_EQ(rect.GetTop(), 10.0f); + EXPECT_EQ(rect.GetRight(), min); + EXPECT_EQ(rect.GetBottom(), 25.0f); + EXPECT_EQ(rect.GetX(), 5.0f); + EXPECT_EQ(rect.GetY(), 10.0f); + EXPECT_EQ(rect.GetWidth(), min); + EXPECT_EQ(rect.GetHeight(), 15.0f); + EXPECT_TRUE(rect.IsEmpty()); + EXPECT_TRUE(rect.IsFinite()); + } + + { + Rect rect = Rect::MakeLTRB(max - 5.0f, 10.0f, min + 10.0f, 25.0f); + + EXPECT_EQ(rect.GetLeft(), max - 5.0f); + EXPECT_EQ(rect.GetTop(), 10.0f); + EXPECT_EQ(rect.GetRight(), min + 10.0f); + EXPECT_EQ(rect.GetBottom(), 25.0f); + EXPECT_EQ(rect.GetX(), max - 5.0f); + EXPECT_EQ(rect.GetY(), 10.0f); + EXPECT_EQ(rect.GetWidth(), -inf); + EXPECT_EQ(rect.GetHeight(), 15.0f); + EXPECT_TRUE(rect.IsEmpty()); + EXPECT_TRUE(rect.IsFinite()); + } + + { + Rect rect = Rect::MakeLTRB(5.0f, 10.0f, 20.0f, min); + + EXPECT_EQ(rect.GetLeft(), 5.0f); + EXPECT_EQ(rect.GetTop(), 10.0f); + EXPECT_EQ(rect.GetRight(), 20.0f); + EXPECT_EQ(rect.GetBottom(), min); + EXPECT_EQ(rect.GetX(), 5.0f); + EXPECT_EQ(rect.GetY(), 10.0f); + EXPECT_EQ(rect.GetWidth(), 15.0f); + EXPECT_EQ(rect.GetHeight(), min); + EXPECT_TRUE(rect.IsEmpty()); + EXPECT_TRUE(rect.IsFinite()); + } + + { + Rect rect = Rect::MakeLTRB(5.0f, max - 5.0f, 20.0f, min + 10.0f); + + EXPECT_EQ(rect.GetLeft(), 5.0f); + EXPECT_EQ(rect.GetTop(), max - 5.0f); + EXPECT_EQ(rect.GetRight(), 20.0f); + EXPECT_EQ(rect.GetBottom(), min + 10.0f); + EXPECT_EQ(rect.GetX(), 5.0f); + EXPECT_EQ(rect.GetY(), max - 5.0f); + EXPECT_EQ(rect.GetWidth(), 15.0f); + EXPECT_EQ(rect.GetHeight(), -inf); + EXPECT_TRUE(rect.IsEmpty()); + EXPECT_TRUE(rect.IsFinite()); + } +} + +TEST(RectTest, IRectOverflowLTRB) { + auto min = std::numeric_limits::min(); + auto max = std::numeric_limits::max(); + + // 4 cases + // negative l, r near max takes width past max + // positive l, r near min takes width below min + // negative t, b near max takes width past max + // positive t, b near min takes width below min + + { + IRect rect = IRect::MakeLTRB(-10, 10, max - 5, 26); + + EXPECT_EQ(rect.GetLeft(), -10); + EXPECT_EQ(rect.GetTop(), 10); + EXPECT_EQ(rect.GetRight(), max - 5); + EXPECT_EQ(rect.GetBottom(), 26); + EXPECT_EQ(rect.GetX(), -10); + EXPECT_EQ(rect.GetY(), 10); + EXPECT_EQ(rect.GetWidth(), max); + EXPECT_EQ(rect.GetHeight(), 16); + EXPECT_FALSE(rect.IsEmpty()); + } + + { + IRect rect = IRect::MakeLTRB(10, 10, min + 5, 26); + + EXPECT_EQ(rect.GetLeft(), 10); + EXPECT_EQ(rect.GetTop(), 10); + EXPECT_EQ(rect.GetRight(), min + 5); + EXPECT_EQ(rect.GetBottom(), 26); + EXPECT_EQ(rect.GetX(), 10); + EXPECT_EQ(rect.GetY(), 10); + EXPECT_EQ(rect.GetWidth(), min); + EXPECT_EQ(rect.GetHeight(), 16); + EXPECT_TRUE(rect.IsEmpty()); + } + + { + IRect rect = IRect::MakeLTRB(5, -10, 15, max - 5); + + EXPECT_EQ(rect.GetLeft(), 5); + EXPECT_EQ(rect.GetTop(), -10); + EXPECT_EQ(rect.GetRight(), 15); + EXPECT_EQ(rect.GetBottom(), max - 5); + EXPECT_EQ(rect.GetX(), 5); + EXPECT_EQ(rect.GetY(), -10); + EXPECT_EQ(rect.GetWidth(), 10); + EXPECT_EQ(rect.GetHeight(), max); + EXPECT_FALSE(rect.IsEmpty()); + } + + { + IRect rect = IRect::MakeLTRB(5, 10, 15, min + 5); + + EXPECT_EQ(rect.GetLeft(), 5); + EXPECT_EQ(rect.GetTop(), 10); + EXPECT_EQ(rect.GetRight(), 15); + EXPECT_EQ(rect.GetBottom(), min + 5); + EXPECT_EQ(rect.GetX(), 5); + EXPECT_EQ(rect.GetY(), 10); + EXPECT_EQ(rect.GetWidth(), 10); + EXPECT_EQ(rect.GetHeight(), min); + EXPECT_TRUE(rect.IsEmpty()); + } +} + +TEST(RectTest, RectMakeSize) { + { + Size s(100, 200); + Rect r = Rect::MakeSize(s); + Rect expected = Rect::MakeLTRB(0, 0, 100, 200); + EXPECT_RECT_NEAR(r, expected); + } + + { + ISize s(100, 200); + Rect r = Rect::MakeSize(s); + Rect expected = Rect::MakeLTRB(0, 0, 100, 200); + EXPECT_RECT_NEAR(r, expected); + } + + { + Size s(100, 200); + IRect r = IRect::MakeSize(s); + IRect expected = IRect::MakeLTRB(0, 0, 100, 200); + EXPECT_EQ(r, expected); + } + + { + ISize s(100, 200); + IRect r = IRect::MakeSize(s); + IRect expected = IRect::MakeLTRB(0, 0, 100, 200); + EXPECT_EQ(r, expected); + } +} + +TEST(RectTest, RectMakeMaximum) { + Rect rect = Rect::MakeMaximum(); + auto inf = std::numeric_limits::infinity(); + auto min = std::numeric_limits::lowest(); + auto max = std::numeric_limits::max(); + + EXPECT_EQ(rect.GetLeft(), min); + EXPECT_EQ(rect.GetTop(), min); + EXPECT_EQ(rect.GetRight(), max); + EXPECT_EQ(rect.GetBottom(), max); + EXPECT_EQ(rect.GetX(), min); + EXPECT_EQ(rect.GetY(), min); + EXPECT_EQ(rect.GetWidth(), inf); + EXPECT_EQ(rect.GetHeight(), inf); + EXPECT_FALSE(rect.IsEmpty()); + EXPECT_TRUE(rect.IsFinite()); +} + +TEST(RectTest, IRectMakeMaximum) { + IRect rect = IRect::MakeMaximum(); + auto min = std::numeric_limits::min(); + auto max = std::numeric_limits::max(); + + EXPECT_EQ(rect.GetLeft(), min); + EXPECT_EQ(rect.GetTop(), min); + EXPECT_EQ(rect.GetRight(), max); + EXPECT_EQ(rect.GetBottom(), max); + EXPECT_EQ(rect.GetX(), min); + EXPECT_EQ(rect.GetY(), min); + EXPECT_EQ(rect.GetWidth(), max); + EXPECT_EQ(rect.GetHeight(), max); + EXPECT_FALSE(rect.IsEmpty()); +} + +TEST(RectTest, RectFromRect) { + EXPECT_EQ(Rect(Rect::MakeXYWH(2, 3, 7, 15)), + Rect::MakeXYWH(2.0, 3.0, 7.0, 15.0)); + EXPECT_EQ(Rect(Rect::MakeLTRB(2, 3, 7, 15)), + Rect::MakeLTRB(2.0, 3.0, 7.0, 15.0)); +} + +TEST(RectTest, IRectFromIRect) { + EXPECT_EQ(IRect(IRect::MakeXYWH(2, 3, 7, 15)), // + IRect::MakeXYWH(2, 3, 7, 15)); + EXPECT_EQ(IRect(IRect::MakeLTRB(2, 3, 7, 15)), // + IRect::MakeLTRB(2, 3, 7, 15)); +} + +TEST(RectTest, RectCopy) { + // Using fractional-power-of-2 friendly values for equality tests + Rect rect = Rect::MakeLTRB(5.125f, 10.25f, 20.625f, 25.375f); + Rect copy = rect; + + EXPECT_EQ(rect, copy); + EXPECT_EQ(copy.GetLeft(), 5.125f); + EXPECT_EQ(copy.GetTop(), 10.25f); + EXPECT_EQ(copy.GetRight(), 20.625f); + EXPECT_EQ(copy.GetBottom(), 25.375f); + EXPECT_EQ(copy.GetX(), 5.125f); + EXPECT_EQ(copy.GetY(), 10.25f); + EXPECT_EQ(copy.GetWidth(), 15.5f); + EXPECT_EQ(copy.GetHeight(), 15.125f); + EXPECT_FALSE(copy.IsEmpty()); + EXPECT_TRUE(copy.IsFinite()); +} + +TEST(RectTest, IRectCopy) { + IRect rect = IRect::MakeLTRB(5, 10, 20, 25); + IRect copy = rect; + + EXPECT_EQ(rect, copy); + EXPECT_EQ(copy.GetLeft(), 5); + EXPECT_EQ(copy.GetTop(), 10); + EXPECT_EQ(copy.GetRight(), 20); + EXPECT_EQ(copy.GetBottom(), 25); + EXPECT_EQ(copy.GetX(), 5); + EXPECT_EQ(copy.GetY(), 10); + EXPECT_EQ(copy.GetWidth(), 15); + EXPECT_EQ(copy.GetHeight(), 15); + EXPECT_FALSE(copy.IsEmpty()); +} + TEST(RectTest, RectOriginSizeXYWHGetters) { { Rect r = Rect::MakeOriginSize({10, 20}, {50, 40}); @@ -63,67 +706,179 @@ TEST(RectTest, IRectOriginSizeXYWHGetters) { } } -TEST(RectTest, RectFromRect) { - EXPECT_EQ(Rect(Rect::MakeXYWH(2, 3, 7, 15)), - Rect::MakeXYWH(2.0, 3.0, 7.0, 15.0)); - EXPECT_EQ(Rect(Rect::MakeLTRB(2, 3, 7, 15)), - Rect::MakeLTRB(2.0, 3.0, 7.0, 15.0)); +TEST(RectTest, RectRoundOutEmpty) { + Rect rect; + + EXPECT_EQ(Rect::RoundOut(rect), Rect()); + + EXPECT_EQ(IRect::RoundOut(rect), IRect()); } -TEST(RectTest, RectFromIRect) { - EXPECT_EQ(Rect(IRect::MakeXYWH(2, 3, 7, 15)), - Rect::MakeXYWH(2.0, 3.0, 7.0, 15.0)); - EXPECT_EQ(Rect(IRect::MakeLTRB(2, 3, 7, 15)), - Rect::MakeLTRB(2.0, 3.0, 7.0, 15.0)); +TEST(RectTest, RectRoundOutSimple) { + Rect rect = Rect::MakeLTRB(5.125f, 10.75f, 20.625f, 25.375f); + + EXPECT_EQ(Rect::RoundOut(rect), Rect::MakeLTRB(5.0f, 10.0f, 21.0f, 26.0f)); + + EXPECT_EQ(IRect::RoundOut(rect), IRect::MakeLTRB(5, 10, 21, 26)); } -TEST(RectTest, IRectFromRect) { - EXPECT_EQ(IRect(Rect::MakeXYWH(2, 3, 7, 15)), // - IRect::MakeXYWH(2, 3, 7, 15)); - EXPECT_EQ(IRect(Rect::MakeLTRB(2, 3, 7, 15)), // - IRect::MakeLTRB(2, 3, 7, 15)); +TEST(RectTest, RectRoundOutToIRectHuge) { + auto test = [](int corners) { + EXPECT_TRUE(corners >= 0 && corners <= 0xf); + Scalar l, t, r, b; + int64_t il, it, ir, ib; + l = il = 50; + t = it = 50; + r = ir = 80; + b = ib = 80; + if ((corners & (1 << 0)) != 0) { + l = -1E20; + il = std::numeric_limits::min(); + } + if ((corners & (1 << 1)) != 0) { + t = -1E20; + it = std::numeric_limits::min(); + } + if ((corners & (1 << 2)) != 0) { + r = +1E20; + ir = std::numeric_limits::max(); + } + if ((corners & (1 << 3)) != 0) { + b = +1E20; + ib = std::numeric_limits::max(); + } - EXPECT_EQ(IRect(Rect::MakeXYWH(2.5, 3.5, 7.75, 15.75)), - IRect::MakeXYWH(2, 3, 7, 15)); - EXPECT_EQ(IRect(Rect::MakeLTRB(2.5, 3.5, 7.75, 15.75)), - IRect::MakeLTRB(2, 3, 7, 15)); + Rect rect = Rect::MakeLTRB(l, t, r, b); + IRect irect = IRect::RoundOut(rect); + EXPECT_EQ(irect.GetLeft(), il) << corners; + EXPECT_EQ(irect.GetTop(), it) << corners; + EXPECT_EQ(irect.GetRight(), ir) << corners; + EXPECT_EQ(irect.GetBottom(), ib) << corners; + }; + + for (int corners = 0; corners <= 15; corners++) { + test(corners); + } } -TEST(RectTest, IRectFromIRect) { - EXPECT_EQ(IRect(IRect::MakeXYWH(2, 3, 7, 15)), // - IRect::MakeXYWH(2, 3, 7, 15)); - EXPECT_EQ(IRect(IRect::MakeLTRB(2, 3, 7, 15)), // - IRect::MakeLTRB(2, 3, 7, 15)); +TEST(RectTest, RectDoesNotIntersectEmpty) { + Rect rect = Rect::MakeLTRB(50, 50, 100, 100); + + auto test = [&rect](Scalar l, Scalar t, Scalar r, Scalar b, + const std::string& label) { + EXPECT_FALSE(rect.IntersectsWithRect(Rect::MakeLTRB(l, b, r, t))) + << label << " with Top/Bottom swapped"; + EXPECT_FALSE(rect.IntersectsWithRect(Rect::MakeLTRB(r, b, l, t))) + << label << " with Left/Right swapped"; + EXPECT_FALSE(rect.IntersectsWithRect(Rect::MakeLTRB(r, t, l, b))) + << label << " with all sides swapped"; + }; + + test(20, 20, 30, 30, "Above and Left"); + test(70, 20, 80, 30, "Above"); + test(120, 20, 130, 30, "Above and Right"); + test(120, 70, 130, 80, "Right"); + test(120, 120, 130, 130, "Below and Right"); + test(70, 120, 80, 130, "Below"); + test(20, 120, 30, 130, "Below and Left"); + test(20, 70, 30, 80, "Left"); + + test(70, 70, 80, 80, "Inside"); + + test(40, 70, 60, 80, "Straddling Left"); + test(70, 40, 80, 60, "Straddling Top"); + test(90, 70, 110, 80, "Straddling Right"); + test(70, 90, 80, 110, "Straddling Bottom"); } -TEST(RectTest, RectMakeSize) { - { - Size s(100, 200); - Rect r = Rect::MakeSize(s); - Rect expected = Rect::MakeLTRB(0, 0, 100, 200); - EXPECT_RECT_NEAR(r, expected); - } +TEST(RectTest, IRectDoesNotIntersectEmpty) { + IRect rect = IRect::MakeLTRB(50, 50, 100, 100); - { - ISize s(100, 200); - Rect r = Rect::MakeSize(s); - Rect expected = Rect::MakeLTRB(0, 0, 100, 200); - EXPECT_RECT_NEAR(r, expected); - } + auto test = [&rect](int64_t l, int64_t t, int64_t r, int64_t b, + const std::string& label) { + EXPECT_FALSE(rect.IntersectsWithRect(IRect::MakeLTRB(l, b, r, t))) + << label << " with Top/Bottom swapped"; + EXPECT_FALSE(rect.IntersectsWithRect(IRect::MakeLTRB(r, b, l, t))) + << label << " with Left/Right swapped"; + EXPECT_FALSE(rect.IntersectsWithRect(IRect::MakeLTRB(r, t, l, b))) + << label << " with all sides swapped"; + }; - { - Size s(100, 200); - IRect r = IRect::MakeSize(s); - IRect expected = IRect::MakeLTRB(0, 0, 100, 200); - EXPECT_EQ(r, expected); - } + test(20, 20, 30, 30, "Above and Left"); + test(70, 20, 80, 30, "Above"); + test(120, 20, 130, 30, "Above and Right"); + test(120, 70, 130, 80, "Right"); + test(120, 120, 130, 130, "Below and Right"); + test(70, 120, 80, 130, "Below"); + test(20, 120, 30, 130, "Below and Left"); + test(20, 70, 30, 80, "Left"); - { - ISize s(100, 200); - IRect r = IRect::MakeSize(s); - IRect expected = IRect::MakeLTRB(0, 0, 100, 200); - EXPECT_EQ(r, expected); - } + test(70, 70, 80, 80, "Inside"); + + test(40, 70, 60, 80, "Straddling Left"); + test(70, 40, 80, 60, "Straddling Top"); + test(90, 70, 110, 80, "Straddling Right"); + test(70, 90, 80, 110, "Straddling Bottom"); +} + +TEST(RectTest, EmptyRectDoesNotIntersect) { + Rect rect = Rect::MakeLTRB(50, 50, 100, 100); + + auto test = [&rect](Scalar l, Scalar t, Scalar r, Scalar b, + const std::string& label) { + EXPECT_FALSE(Rect::MakeLTRB(l, b, r, t).IntersectsWithRect(rect)) + << label << " with Top/Bottom swapped"; + EXPECT_FALSE(Rect::MakeLTRB(r, b, l, t).IntersectsWithRect(rect)) + << label << " with Left/Right swapped"; + EXPECT_FALSE(Rect::MakeLTRB(r, t, l, b).IntersectsWithRect(rect)) + << label << " with all sides swapped"; + }; + + test(20, 20, 30, 30, "Above and Left"); + test(70, 20, 80, 30, "Above"); + test(120, 20, 130, 30, "Above and Right"); + test(120, 70, 130, 80, "Right"); + test(120, 120, 130, 130, "Below and Right"); + test(70, 120, 80, 130, "Below"); + test(20, 120, 30, 130, "Below and Left"); + test(20, 70, 30, 80, "Left"); + + test(70, 70, 80, 80, "Inside"); + + test(40, 70, 60, 80, "Straddling Left"); + test(70, 40, 80, 60, "Straddling Top"); + test(90, 70, 110, 80, "Straddling Right"); + test(70, 90, 80, 110, "Straddling Bottom"); +} + +TEST(RectTest, EmptyIRectDoesNotIntersect) { + IRect rect = IRect::MakeLTRB(50, 50, 100, 100); + + auto test = [&rect](int64_t l, int64_t t, int64_t r, int64_t b, + const std::string& label) { + EXPECT_FALSE(IRect::MakeLTRB(l, b, r, t).IntersectsWithRect(rect)) + << label << " with Top/Bottom swapped"; + EXPECT_FALSE(IRect::MakeLTRB(r, b, l, t).IntersectsWithRect(rect)) + << label << " with Left/Right swapped"; + EXPECT_FALSE(IRect::MakeLTRB(r, t, l, b).IntersectsWithRect(rect)) + << label << " with all sides swapped"; + }; + + test(20, 20, 30, 30, "Above and Left"); + test(70, 20, 80, 30, "Above"); + test(120, 20, 130, 30, "Above and Right"); + test(120, 70, 130, 80, "Right"); + test(120, 120, 130, 130, "Below and Right"); + test(70, 120, 80, 130, "Below"); + test(20, 120, 30, 130, "Below and Left"); + test(20, 70, 30, 80, "Left"); + + test(70, 70, 80, 80, "Inside"); + + test(40, 70, 60, 80, "Straddling Left"); + test(70, 40, 80, 60, "Straddling Top"); + test(90, 70, 110, 80, "Straddling Right"); + test(70, 90, 80, 110, "Straddling Bottom"); } TEST(RectTest, RectScale) { @@ -160,6 +915,9 @@ TEST(RectTest, RectScale) { test1(rect, scale_y); }; + test2(Rect::MakeLTRB(10, 15, 100, 150), 1.0, 0.0); + test2(Rect::MakeLTRB(10, 15, 100, 150), 0.0, 1.0); + test2(Rect::MakeLTRB(10, 15, 100, 150), 0.0, 0.0); test2(Rect::MakeLTRB(10, 15, 100, 150), 2.5, 3.5); test2(Rect::MakeLTRB(10, 15, 100, 150), 3.5, 2.5); test2(Rect::MakeLTRB(10, 15, -100, 150), 2.5, 3.5); @@ -366,7 +1124,7 @@ TEST(RectTest, IRectGetNormalizingTransform) { } } -TEST(SizeTest, RectIsEmpty) { +TEST(RectTest, RectXYWHIsEmpty) { auto nan = std::numeric_limits::quiet_NaN(); // Non-empty @@ -390,7 +1148,7 @@ TEST(SizeTest, RectIsEmpty) { EXPECT_TRUE(Rect::MakeXYWH(1.5, 2.3, nan, nan).IsEmpty()); } -TEST(SizeTest, IRectIsEmpty) { +TEST(RectTest, IRectXYWHIsEmpty) { // Non-empty EXPECT_FALSE(IRect::MakeXYWH(1, 2, 10, 7).IsEmpty()); @@ -425,19 +1183,23 @@ TEST(RectTest, IsSquare) { EXPECT_TRUE(Rect::MakeXYWH(10, 30, 20, 20).IsSquare()); EXPECT_FALSE(Rect::MakeXYWH(10, 30, 20, 19).IsSquare()); EXPECT_FALSE(Rect::MakeXYWH(10, 30, 19, 20).IsSquare()); + EXPECT_TRUE(Rect::MakeMaximum().IsSquare()); EXPECT_TRUE(IRect::MakeXYWH(10, 30, 20, 20).IsSquare()); EXPECT_FALSE(IRect::MakeXYWH(10, 30, 20, 19).IsSquare()); EXPECT_FALSE(IRect::MakeXYWH(10, 30, 19, 20).IsSquare()); + EXPECT_TRUE(IRect::MakeMaximum().IsSquare()); } TEST(RectTest, GetCenter) { EXPECT_EQ(Rect::MakeXYWH(10, 30, 20, 20).GetCenter(), Point(20, 40)); EXPECT_EQ(Rect::MakeXYWH(10, 30, 20, 19).GetCenter(), Point(20, 39.5)); + EXPECT_EQ(Rect::MakeMaximum().GetCenter(), Point(0, 0)); // Note that we expect a Point as the answer from an IRect EXPECT_EQ(IRect::MakeXYWH(10, 30, 20, 20).GetCenter(), Point(20, 40)); EXPECT_EQ(IRect::MakeXYWH(10, 30, 20, 19).GetCenter(), Point(20, 39.5)); + EXPECT_EQ(IRect::MakeMaximum().GetCenter(), Point(0, 0)); } TEST(RectTest, RectExpand) { @@ -453,6 +1215,13 @@ TEST(RectTest, RectExpand) { EXPECT_EQ(rect.Expand(-10, 10), Rect::MakeLTRB(110, 90, 190, 210)); EXPECT_EQ(rect.Expand(-10, -10), Rect::MakeLTRB(110, 110, 190, 190)); + // Expand(amount, amount, amount, amount) + EXPECT_EQ(rect.Expand(10, 20, 30, 40), Rect::MakeLTRB(90, 80, 230, 240)); + EXPECT_EQ(rect.Expand(-10, 20, 30, 40), Rect::MakeLTRB(110, 80, 230, 240)); + EXPECT_EQ(rect.Expand(10, -20, 30, 40), Rect::MakeLTRB(90, 120, 230, 240)); + EXPECT_EQ(rect.Expand(10, 20, -30, 40), Rect::MakeLTRB(90, 80, 170, 240)); + EXPECT_EQ(rect.Expand(10, 20, 30, -40), Rect::MakeLTRB(90, 80, 230, 160)); + // Expand(Point amount) EXPECT_EQ(rect.Expand(Point{10, 10}), Rect::MakeLTRB(90, 90, 210, 210)); EXPECT_EQ(rect.Expand(Point{10, -10}), Rect::MakeLTRB(90, 110, 210, 190)); @@ -479,6 +1248,13 @@ TEST(RectTest, IRectExpand) { EXPECT_EQ(rect.Expand(-10, 10), IRect::MakeLTRB(110, 90, 190, 210)); EXPECT_EQ(rect.Expand(-10, -10), IRect::MakeLTRB(110, 110, 190, 190)); + // Expand(amount, amount, amount, amount) + EXPECT_EQ(rect.Expand(10, 20, 30, 40), IRect::MakeLTRB(90, 80, 230, 240)); + EXPECT_EQ(rect.Expand(-10, 20, 30, 40), IRect::MakeLTRB(110, 80, 230, 240)); + EXPECT_EQ(rect.Expand(10, -20, 30, 40), IRect::MakeLTRB(90, 120, 230, 240)); + EXPECT_EQ(rect.Expand(10, 20, -30, 40), IRect::MakeLTRB(90, 80, 170, 240)); + EXPECT_EQ(rect.Expand(10, 20, 30, -40), IRect::MakeLTRB(90, 80, 230, 160)); + // Expand(IPoint amount) EXPECT_EQ(rect.Expand(IPoint{10, 10}), IRect::MakeLTRB(90, 90, 210, 210)); EXPECT_EQ(rect.Expand(IPoint{10, -10}), IRect::MakeLTRB(90, 110, 210, 190)); @@ -499,5 +1275,1592 @@ TEST(RectTest, ContainsFloatingPoint) { EXPECT_TRUE(rect1.Contains(rect2)); } +template +static constexpr inline R flip_lr(R rect) { + return R::MakeLTRB(rect.GetRight(), rect.GetTop(), // + rect.GetLeft(), rect.GetBottom()); +} + +template +static constexpr inline R flip_tb(R rect) { + return R::MakeLTRB(rect.GetLeft(), rect.GetBottom(), // + rect.GetRight(), rect.GetTop()); +} + +template +static constexpr inline R flip_lrtb(R rect) { + return flip_lr(flip_tb(rect)); +} + +static constexpr inline Rect swap_nan(const Rect& rect, int index) { + Scalar nan = std::numeric_limits::quiet_NaN(); + FML_DCHECK(index >= 0 && index <= 15); + Scalar l = ((index & (1 << 0)) != 0) ? nan : rect.GetLeft(); + Scalar t = ((index & (1 << 1)) != 0) ? nan : rect.GetTop(); + Scalar r = ((index & (1 << 2)) != 0) ? nan : rect.GetRight(); + Scalar b = ((index & (1 << 3)) != 0) ? nan : rect.GetBottom(); + return Rect::MakeLTRB(l, t, r, b); +} + +static constexpr inline Point swap_nan(const Point& point, int index) { + Scalar nan = std::numeric_limits::quiet_NaN(); + FML_DCHECK(index >= 0 && index <= 3); + Scalar x = ((index & (1 << 0)) != 0) ? nan : point.x; + Scalar y = ((index & (1 << 1)) != 0) ? nan : point.y; + return Point(x, y); +} + +TEST(RectTest, RectUnion) { + auto check_nans = [](const Rect& a, const Rect& b, const std::string& label) { + ASSERT_TRUE(a.IsFinite()) << label; + ASSERT_TRUE(b.IsFinite()) << label; + ASSERT_FALSE(a.Union(b).IsEmpty()); + + for (int i = 1; i < 16; i++) { + // NaN in a produces b + EXPECT_EQ(swap_nan(a, i).Union(b), b) << label << ", index = " << i; + // NaN in b produces a + EXPECT_EQ(a.Union(swap_nan(b, i)), a) << label << ", index = " << i; + // NaN in both is empty + for (int j = 1; j < 16; j++) { + EXPECT_TRUE(swap_nan(a, i).Union(swap_nan(b, j)).IsEmpty()) + << label << ", indices = " << i << ", " << j; + } + } + }; + + auto check_empty_flips = [](const Rect& a, const Rect& b, + const std::string& label) { + ASSERT_FALSE(a.IsEmpty()); + // b is allowed to be empty + + // unflipped a vs flipped (empty) b yields a + EXPECT_EQ(a.Union(flip_lr(b)), a) << label; + EXPECT_EQ(a.Union(flip_tb(b)), a) << label; + EXPECT_EQ(a.Union(flip_lrtb(b)), a) << label; + + // flipped (empty) a vs unflipped b yields b + EXPECT_EQ(flip_lr(a).Union(b), b) << label; + EXPECT_EQ(flip_tb(a).Union(b), b) << label; + EXPECT_EQ(flip_lrtb(a).Union(b), b) << label; + + // flipped (empty) a vs flipped (empty) b yields empty + EXPECT_TRUE(flip_lr(a).Union(flip_lr(b)).IsEmpty()) << label; + EXPECT_TRUE(flip_tb(a).Union(flip_tb(b)).IsEmpty()) << label; + EXPECT_TRUE(flip_lrtb(a).Union(flip_lrtb(b)).IsEmpty()) << label; + }; + + auto test = [&check_nans, &check_empty_flips](const Rect& a, const Rect& b, + const Rect& result) { + ASSERT_FALSE(a.IsEmpty()) << a; + // b is allowed to be empty + + std::stringstream stream; + stream << a << " union " << b; + auto label = stream.str(); + + EXPECT_EQ(a.Union(b), result) << label; + EXPECT_EQ(b.Union(a), result) << label; + check_empty_flips(a, b, label); + check_nans(a, b, label); + }; + + { + auto a = Rect::MakeXYWH(100, 100, 100, 100); + auto b = Rect::MakeXYWH(0, 0, 0, 0); + auto expected = Rect::MakeXYWH(100, 100, 100, 100); + test(a, b, expected); + } + + { + auto a = Rect::MakeXYWH(100, 100, 100, 100); + auto b = Rect::MakeXYWH(0, 0, 1, 1); + auto expected = Rect::MakeXYWH(0, 0, 200, 200); + test(a, b, expected); + } + + { + auto a = Rect::MakeXYWH(100, 100, 100, 100); + auto b = Rect::MakeXYWH(10, 10, 1, 1); + auto expected = Rect::MakeXYWH(10, 10, 190, 190); + test(a, b, expected); + } + + { + auto a = Rect::MakeXYWH(0, 0, 100, 100); + auto b = Rect::MakeXYWH(10, 10, 100, 100); + auto expected = Rect::MakeXYWH(0, 0, 110, 110); + test(a, b, expected); + } + + { + auto a = Rect::MakeXYWH(0, 0, 100, 100); + auto b = Rect::MakeXYWH(100, 100, 100, 100); + auto expected = Rect::MakeXYWH(0, 0, 200, 200); + test(a, b, expected); + } +} + +TEST(RectTest, OptRectUnion) { + auto a = Rect::MakeLTRB(0, 0, 100, 100); + auto b = Rect::MakeLTRB(100, 100, 200, 200); + auto c = Rect::MakeLTRB(100, 0, 200, 100); + + // NullOpt, NullOpt + EXPECT_FALSE(Rect::Union(std::nullopt, std::nullopt).has_value()); + EXPECT_EQ(Rect::Union(std::nullopt, std::nullopt), std::nullopt); + + auto test1 = [](const Rect& r) { + // Rect, NullOpt + EXPECT_TRUE(Rect::Union(r, std::nullopt).has_value()); + EXPECT_EQ(Rect::Union(r, std::nullopt).value(), r); + + // OptRect, NullOpt + EXPECT_TRUE(Rect::Union(std::optional(r), std::nullopt).has_value()); + EXPECT_EQ(Rect::Union(std::optional(r), std::nullopt).value(), r); + + // NullOpt, Rect + EXPECT_TRUE(Rect::Union(std::nullopt, r).has_value()); + EXPECT_EQ(Rect::Union(std::nullopt, r).value(), r); + + // NullOpt, OptRect + EXPECT_TRUE(Rect::Union(std::nullopt, std::optional(r)).has_value()); + EXPECT_EQ(Rect::Union(std::nullopt, std::optional(r)).value(), r); + }; + + test1(a); + test1(b); + test1(c); + + auto test2 = [](const Rect& a, const Rect& b, const Rect& u) { + ASSERT_EQ(a.Union(b), u); + + // Rect, OptRect + EXPECT_TRUE(Rect::Union(a, std::optional(b)).has_value()); + EXPECT_EQ(Rect::Union(a, std::optional(b)).value(), u); + + // OptRect, Rect + EXPECT_TRUE(Rect::Union(std::optional(a), b).has_value()); + EXPECT_EQ(Rect::Union(std::optional(a), b).value(), u); + + // OptRect, OptRect + EXPECT_TRUE(Rect::Union(std::optional(a), std::optional(b)).has_value()); + EXPECT_EQ(Rect::Union(std::optional(a), std::optional(b)).value(), u); + }; + + test2(a, b, Rect::MakeLTRB(0, 0, 200, 200)); + test2(a, c, Rect::MakeLTRB(0, 0, 200, 100)); + test2(b, c, Rect::MakeLTRB(100, 0, 200, 200)); +} + +TEST(RectTest, IRectUnion) { + auto check_empty_flips = [](const IRect& a, const IRect& b, + const std::string& label) { + ASSERT_FALSE(a.IsEmpty()); + // b is allowed to be empty + + // unflipped a vs flipped (empty) b yields a + EXPECT_EQ(a.Union(flip_lr(b)), a) << label; + EXPECT_EQ(a.Union(flip_tb(b)), a) << label; + EXPECT_EQ(a.Union(flip_lrtb(b)), a) << label; + + // flipped (empty) a vs unflipped b yields b + EXPECT_EQ(flip_lr(a).Union(b), b) << label; + EXPECT_EQ(flip_tb(a).Union(b), b) << label; + EXPECT_EQ(flip_lrtb(a).Union(b), b) << label; + + // flipped (empty) a vs flipped (empty) b yields empty + EXPECT_TRUE(flip_lr(a).Union(flip_lr(b)).IsEmpty()) << label; + EXPECT_TRUE(flip_tb(a).Union(flip_tb(b)).IsEmpty()) << label; + EXPECT_TRUE(flip_lrtb(a).Union(flip_lrtb(b)).IsEmpty()) << label; + }; + + auto test = [&check_empty_flips](const IRect& a, const IRect& b, + const IRect& result) { + ASSERT_FALSE(a.IsEmpty()) << a; + // b is allowed to be empty + + std::stringstream stream; + stream << a << " union " << b; + auto label = stream.str(); + + EXPECT_EQ(a.Union(b), result) << label; + EXPECT_EQ(b.Union(a), result) << label; + check_empty_flips(a, b, label); + }; + + { + auto a = IRect::MakeXYWH(100, 100, 100, 100); + auto b = IRect::MakeXYWH(0, 0, 0, 0); + auto expected = IRect::MakeXYWH(100, 100, 100, 100); + test(a, b, expected); + } + + { + auto a = IRect::MakeXYWH(100, 100, 100, 100); + auto b = IRect::MakeXYWH(0, 0, 1, 1); + auto expected = IRect::MakeXYWH(0, 0, 200, 200); + test(a, b, expected); + } + + { + auto a = IRect::MakeXYWH(100, 100, 100, 100); + auto b = IRect::MakeXYWH(10, 10, 1, 1); + auto expected = IRect::MakeXYWH(10, 10, 190, 190); + test(a, b, expected); + } + + { + auto a = IRect::MakeXYWH(0, 0, 100, 100); + auto b = IRect::MakeXYWH(10, 10, 100, 100); + auto expected = IRect::MakeXYWH(0, 0, 110, 110); + test(a, b, expected); + } + + { + auto a = IRect::MakeXYWH(0, 0, 100, 100); + auto b = IRect::MakeXYWH(100, 100, 100, 100); + auto expected = IRect::MakeXYWH(0, 0, 200, 200); + test(a, b, expected); + } +} + +TEST(RectTest, OptIRectUnion) { + auto a = IRect::MakeLTRB(0, 0, 100, 100); + auto b = IRect::MakeLTRB(100, 100, 200, 200); + auto c = IRect::MakeLTRB(100, 0, 200, 100); + + // NullOpt, NullOpt + EXPECT_FALSE(IRect::Union(std::nullopt, std::nullopt).has_value()); + EXPECT_EQ(IRect::Union(std::nullopt, std::nullopt), std::nullopt); + + auto test1 = [](const IRect& r) { + // Rect, NullOpt + EXPECT_TRUE(IRect::Union(r, std::nullopt).has_value()); + EXPECT_EQ(IRect::Union(r, std::nullopt).value(), r); + + // OptRect, NullOpt + EXPECT_TRUE(IRect::Union(std::optional(r), std::nullopt).has_value()); + EXPECT_EQ(IRect::Union(std::optional(r), std::nullopt).value(), r); + + // NullOpt, Rect + EXPECT_TRUE(IRect::Union(std::nullopt, r).has_value()); + EXPECT_EQ(IRect::Union(std::nullopt, r).value(), r); + + // NullOpt, OptRect + EXPECT_TRUE(IRect::Union(std::nullopt, std::optional(r)).has_value()); + EXPECT_EQ(IRect::Union(std::nullopt, std::optional(r)).value(), r); + }; + + test1(a); + test1(b); + test1(c); + + auto test2 = [](const IRect& a, const IRect& b, const IRect& u) { + ASSERT_EQ(a.Union(b), u); + + // Rect, OptRect + EXPECT_TRUE(IRect::Union(a, std::optional(b)).has_value()); + EXPECT_EQ(IRect::Union(a, std::optional(b)).value(), u); + + // OptRect, Rect + EXPECT_TRUE(IRect::Union(std::optional(a), b).has_value()); + EXPECT_EQ(IRect::Union(std::optional(a), b).value(), u); + + // OptRect, OptRect + EXPECT_TRUE(IRect::Union(std::optional(a), std::optional(b)).has_value()); + EXPECT_EQ(IRect::Union(std::optional(a), std::optional(b)).value(), u); + }; + + test2(a, b, IRect::MakeLTRB(0, 0, 200, 200)); + test2(a, c, IRect::MakeLTRB(0, 0, 200, 100)); + test2(b, c, IRect::MakeLTRB(100, 0, 200, 200)); +} + +TEST(RectTest, RectIntersection) { + auto check_nans = [](const Rect& a, const Rect& b, const std::string& label) { + ASSERT_TRUE(a.IsFinite()) << label; + ASSERT_TRUE(b.IsFinite()) << label; + + for (int i = 1; i < 16; i++) { + // NaN in a produces empty + EXPECT_FALSE(swap_nan(a, i).Intersection(b).has_value()) + << label << ", index = " << i; + // NaN in b produces empty + EXPECT_FALSE(a.Intersection(swap_nan(b, i)).has_value()) + << label << ", index = " << i; + // NaN in both is empty + for (int j = 1; j < 16; j++) { + EXPECT_FALSE(swap_nan(a, i).Intersection(swap_nan(b, j)).has_value()) + << label << ", indices = " << i << ", " << j; + } + } + }; + + auto check_empty_flips = [](const Rect& a, const Rect& b, + const std::string& label) { + ASSERT_FALSE(a.IsEmpty()); + // b is allowed to be empty + + // unflipped a vs flipped (empty) b yields a + EXPECT_FALSE(a.Intersection(flip_lr(b)).has_value()) << label; + EXPECT_FALSE(a.Intersection(flip_tb(b)).has_value()) << label; + EXPECT_FALSE(a.Intersection(flip_lrtb(b)).has_value()) << label; + + // flipped (empty) a vs unflipped b yields b + EXPECT_FALSE(flip_lr(a).Intersection(b).has_value()) << label; + EXPECT_FALSE(flip_tb(a).Intersection(b).has_value()) << label; + EXPECT_FALSE(flip_lrtb(a).Intersection(b).has_value()) << label; + + // flipped (empty) a vs flipped (empty) b yields empty + EXPECT_FALSE(flip_lr(a).Intersection(flip_lr(b)).has_value()) << label; + EXPECT_FALSE(flip_tb(a).Intersection(flip_tb(b)).has_value()) << label; + EXPECT_FALSE(flip_lrtb(a).Intersection(flip_lrtb(b)).has_value()) << label; + }; + + auto test_non_empty = [&check_nans, &check_empty_flips]( + const Rect& a, const Rect& b, const Rect& result) { + ASSERT_FALSE(a.IsEmpty()) << a; + // b is allowed to be empty + + std::stringstream stream; + stream << a << " union " << b; + auto label = stream.str(); + + EXPECT_TRUE(a.Intersection(b).has_value()) << label; + EXPECT_TRUE(b.Intersection(a).has_value()) << label; + EXPECT_EQ(a.Intersection(b), result) << label; + EXPECT_EQ(b.Intersection(a), result) << label; + check_empty_flips(a, b, label); + check_nans(a, b, label); + }; + + auto test_empty = [&check_nans, &check_empty_flips](const Rect& a, + const Rect& b) { + ASSERT_FALSE(a.IsEmpty()) << a; + // b is allowed to be empty + + std::stringstream stream; + stream << a << " union " << b; + auto label = stream.str(); + + EXPECT_FALSE(a.Intersection(b).has_value()) << label; + EXPECT_FALSE(b.Intersection(a).has_value()) << label; + check_empty_flips(a, b, label); + check_nans(a, b, label); + }; + + { + auto a = Rect::MakeXYWH(100, 100, 100, 100); + auto b = Rect::MakeXYWH(0, 0, 0, 0); + + test_empty(a, b); + } + + { + auto a = Rect::MakeXYWH(100, 100, 100, 100); + auto b = Rect::MakeXYWH(10, 10, 0, 0); + + test_empty(a, b); + } + + { + auto a = Rect::MakeXYWH(0, 0, 100, 100); + auto b = Rect::MakeXYWH(10, 10, 100, 100); + auto expected = Rect::MakeXYWH(10, 10, 90, 90); + + test_non_empty(a, b, expected); + } + + { + auto a = Rect::MakeXYWH(0, 0, 100, 100); + auto b = Rect::MakeXYWH(100, 100, 100, 100); + + test_empty(a, b); + } + + { + auto a = Rect::MakeMaximum(); + auto b = Rect::MakeXYWH(10, 10, 300, 300); + + test_non_empty(a, b, b); + } + + { + auto a = Rect::MakeMaximum(); + auto b = Rect::MakeMaximum(); + + test_non_empty(a, b, Rect::MakeMaximum()); + } +} + +TEST(RectTest, OptRectIntersection) { + auto a = Rect::MakeLTRB(0, 0, 110, 110); + auto b = Rect::MakeLTRB(100, 100, 200, 200); + auto c = Rect::MakeLTRB(100, 0, 200, 110); + + // NullOpt, NullOpt + EXPECT_FALSE(Rect::Intersection(std::nullopt, std::nullopt).has_value()); + EXPECT_EQ(Rect::Intersection(std::nullopt, std::nullopt), std::nullopt); + + auto test1 = [](const Rect& r) { + // Rect, NullOpt + EXPECT_TRUE(Rect::Intersection(r, std::nullopt).has_value()); + EXPECT_EQ(Rect::Intersection(r, std::nullopt).value(), r); + + // OptRect, NullOpt + EXPECT_TRUE(Rect::Intersection(std::optional(r), std::nullopt).has_value()); + EXPECT_EQ(Rect::Intersection(std::optional(r), std::nullopt).value(), r); + + // NullOpt, Rect + EXPECT_TRUE(Rect::Intersection(std::nullopt, r).has_value()); + EXPECT_EQ(Rect::Intersection(std::nullopt, r).value(), r); + + // NullOpt, OptRect + EXPECT_TRUE(Rect::Intersection(std::nullopt, std::optional(r)).has_value()); + EXPECT_EQ(Rect::Intersection(std::nullopt, std::optional(r)).value(), r); + }; + + test1(a); + test1(b); + test1(c); + + auto test2 = [](const Rect& a, const Rect& b, const Rect& i) { + ASSERT_EQ(a.Intersection(b), i); + + // Rect, OptRect + EXPECT_TRUE(Rect::Intersection(a, std::optional(b)).has_value()); + EXPECT_EQ(Rect::Intersection(a, std::optional(b)).value(), i); + + // OptRect, Rect + EXPECT_TRUE(Rect::Intersection(std::optional(a), b).has_value()); + EXPECT_EQ(Rect::Intersection(std::optional(a), b).value(), i); + + // OptRect, OptRect + EXPECT_TRUE( + Rect::Intersection(std::optional(a), std::optional(b)).has_value()); + EXPECT_EQ(Rect::Intersection(std::optional(a), std::optional(b)).value(), + i); + }; + + test2(a, b, Rect::MakeLTRB(100, 100, 110, 110)); + test2(a, c, Rect::MakeLTRB(100, 0, 110, 110)); + test2(b, c, Rect::MakeLTRB(100, 100, 200, 110)); +} + +TEST(RectTest, IRectIntersection) { + auto check_empty_flips = [](const IRect& a, const IRect& b, + const std::string& label) { + ASSERT_FALSE(a.IsEmpty()); + // b is allowed to be empty + + // unflipped a vs flipped (empty) b yields a + EXPECT_FALSE(a.Intersection(flip_lr(b)).has_value()) << label; + EXPECT_FALSE(a.Intersection(flip_tb(b)).has_value()) << label; + EXPECT_FALSE(a.Intersection(flip_lrtb(b)).has_value()) << label; + + // flipped (empty) a vs unflipped b yields b + EXPECT_FALSE(flip_lr(a).Intersection(b).has_value()) << label; + EXPECT_FALSE(flip_tb(a).Intersection(b).has_value()) << label; + EXPECT_FALSE(flip_lrtb(a).Intersection(b).has_value()) << label; + + // flipped (empty) a vs flipped (empty) b yields empty + EXPECT_FALSE(flip_lr(a).Intersection(flip_lr(b)).has_value()) << label; + EXPECT_FALSE(flip_tb(a).Intersection(flip_tb(b)).has_value()) << label; + EXPECT_FALSE(flip_lrtb(a).Intersection(flip_lrtb(b)).has_value()) << label; + }; + + auto test_non_empty = [&check_empty_flips](const IRect& a, const IRect& b, + const IRect& result) { + ASSERT_FALSE(a.IsEmpty()) << a; + // b is allowed to be empty + + std::stringstream stream; + stream << a << " union " << b; + auto label = stream.str(); + + EXPECT_TRUE(a.Intersection(b).has_value()) << label; + EXPECT_TRUE(b.Intersection(a).has_value()) << label; + EXPECT_EQ(a.Intersection(b), result) << label; + EXPECT_EQ(b.Intersection(a), result) << label; + check_empty_flips(a, b, label); + }; + + auto test_empty = [&check_empty_flips](const IRect& a, const IRect& b) { + ASSERT_FALSE(a.IsEmpty()) << a; + // b is allowed to be empty + + std::stringstream stream; + stream << a << " union " << b; + auto label = stream.str(); + + EXPECT_FALSE(a.Intersection(b).has_value()) << label; + EXPECT_FALSE(b.Intersection(a).has_value()) << label; + check_empty_flips(a, b, label); + }; + + { + auto a = IRect::MakeXYWH(100, 100, 100, 100); + auto b = IRect::MakeXYWH(0, 0, 0, 0); + + test_empty(a, b); + } + + { + auto a = IRect::MakeXYWH(100, 100, 100, 100); + auto b = IRect::MakeXYWH(10, 10, 0, 0); + + test_empty(a, b); + } + + { + auto a = IRect::MakeXYWH(0, 0, 100, 100); + auto b = IRect::MakeXYWH(10, 10, 100, 100); + auto expected = IRect::MakeXYWH(10, 10, 90, 90); + + test_non_empty(a, b, expected); + } + + { + auto a = IRect::MakeXYWH(0, 0, 100, 100); + auto b = IRect::MakeXYWH(100, 100, 100, 100); + + test_empty(a, b); + } + + { + auto a = IRect::MakeMaximum(); + auto b = IRect::MakeXYWH(10, 10, 300, 300); + + test_non_empty(a, b, b); + } + + { + auto a = IRect::MakeMaximum(); + auto b = IRect::MakeMaximum(); + + test_non_empty(a, b, IRect::MakeMaximum()); + } +} + +TEST(RectTest, OptIRectIntersection) { + auto a = IRect::MakeLTRB(0, 0, 110, 110); + auto b = IRect::MakeLTRB(100, 100, 200, 200); + auto c = IRect::MakeLTRB(100, 0, 200, 110); + + // NullOpt, NullOpt + EXPECT_FALSE(IRect::Intersection(std::nullopt, std::nullopt).has_value()); + EXPECT_EQ(IRect::Intersection(std::nullopt, std::nullopt), std::nullopt); + + auto test1 = [](const IRect& r) { + // Rect, NullOpt + EXPECT_TRUE(IRect::Intersection(r, std::nullopt).has_value()); + EXPECT_EQ(IRect::Intersection(r, std::nullopt).value(), r); + + // OptRect, NullOpt + EXPECT_TRUE( + IRect::Intersection(std::optional(r), std::nullopt).has_value()); + EXPECT_EQ(IRect::Intersection(std::optional(r), std::nullopt).value(), r); + + // NullOpt, Rect + EXPECT_TRUE(IRect::Intersection(std::nullopt, r).has_value()); + EXPECT_EQ(IRect::Intersection(std::nullopt, r).value(), r); + + // NullOpt, OptRect + EXPECT_TRUE( + IRect::Intersection(std::nullopt, std::optional(r)).has_value()); + EXPECT_EQ(IRect::Intersection(std::nullopt, std::optional(r)).value(), r); + }; + + test1(a); + test1(b); + test1(c); + + auto test2 = [](const IRect& a, const IRect& b, const IRect& i) { + ASSERT_EQ(a.Intersection(b), i); + + // Rect, OptRect + EXPECT_TRUE(IRect::Intersection(a, std::optional(b)).has_value()); + EXPECT_EQ(IRect::Intersection(a, std::optional(b)).value(), i); + + // OptRect, Rect + EXPECT_TRUE(IRect::Intersection(std::optional(a), b).has_value()); + EXPECT_EQ(IRect::Intersection(std::optional(a), b).value(), i); + + // OptRect, OptRect + EXPECT_TRUE( + IRect::Intersection(std::optional(a), std::optional(b)).has_value()); + EXPECT_EQ(IRect::Intersection(std::optional(a), std::optional(b)).value(), + i); + }; + + test2(a, b, IRect::MakeLTRB(100, 100, 110, 110)); + test2(a, c, IRect::MakeLTRB(100, 0, 110, 110)); + test2(b, c, IRect::MakeLTRB(100, 100, 200, 110)); +} + +TEST(RectTest, RectIntersectsWithRect) { + auto check_nans = [](const Rect& a, const Rect& b, const std::string& label) { + ASSERT_TRUE(a.IsFinite()) << label; + ASSERT_TRUE(b.IsFinite()) << label; + + for (int i = 1; i < 16; i++) { + // NaN in a produces b + EXPECT_FALSE(swap_nan(a, i).IntersectsWithRect(b)) + << label << ", index = " << i; + // NaN in b produces a + EXPECT_FALSE(a.IntersectsWithRect(swap_nan(b, i))) + << label << ", index = " << i; + // NaN in both is empty + for (int j = 1; j < 16; j++) { + EXPECT_FALSE(swap_nan(a, i).IntersectsWithRect(swap_nan(b, j))) + << label << ", indices = " << i << ", " << j; + } + } + }; + + auto check_empty_flips = [](const Rect& a, const Rect& b, + const std::string& label) { + ASSERT_FALSE(a.IsEmpty()); + // b is allowed to be empty + + // unflipped a vs flipped (empty) b yields a + EXPECT_FALSE(a.IntersectsWithRect(flip_lr(b))) << label; + EXPECT_FALSE(a.IntersectsWithRect(flip_tb(b))) << label; + EXPECT_FALSE(a.IntersectsWithRect(flip_lrtb(b))) << label; + + // flipped (empty) a vs unflipped b yields b + EXPECT_FALSE(flip_lr(a).IntersectsWithRect(b)) << label; + EXPECT_FALSE(flip_tb(a).IntersectsWithRect(b)) << label; + EXPECT_FALSE(flip_lrtb(a).IntersectsWithRect(b)) << label; + + // flipped (empty) a vs flipped (empty) b yields empty + EXPECT_FALSE(flip_lr(a).IntersectsWithRect(flip_lr(b))) << label; + EXPECT_FALSE(flip_tb(a).IntersectsWithRect(flip_tb(b))) << label; + EXPECT_FALSE(flip_lrtb(a).IntersectsWithRect(flip_lrtb(b))) << label; + }; + + auto test_non_empty = [&check_nans, &check_empty_flips](const Rect& a, + const Rect& b) { + ASSERT_FALSE(a.IsEmpty()) << a; + // b is allowed to be empty + + std::stringstream stream; + stream << a << " union " << b; + auto label = stream.str(); + + EXPECT_TRUE(a.IntersectsWithRect(b)) << label; + EXPECT_TRUE(b.IntersectsWithRect(a)) << label; + check_empty_flips(a, b, label); + check_nans(a, b, label); + }; + + auto test_empty = [&check_nans, &check_empty_flips](const Rect& a, + const Rect& b) { + ASSERT_FALSE(a.IsEmpty()) << a; + // b is allowed to be empty + + std::stringstream stream; + stream << a << " union " << b; + auto label = stream.str(); + + EXPECT_FALSE(a.IntersectsWithRect(b)) << label; + EXPECT_FALSE(b.IntersectsWithRect(a)) << label; + check_empty_flips(a, b, label); + check_nans(a, b, label); + }; + + { + auto a = Rect::MakeXYWH(100, 100, 100, 100); + auto b = Rect::MakeXYWH(0, 0, 0, 0); + + test_empty(a, b); + } + + { + auto a = Rect::MakeXYWH(100, 100, 100, 100); + auto b = Rect::MakeXYWH(10, 10, 0, 0); + + test_empty(a, b); + } + + { + auto a = Rect::MakeXYWH(0, 0, 100, 100); + auto b = Rect::MakeXYWH(10, 10, 100, 100); + + test_non_empty(a, b); + } + + { + auto a = Rect::MakeXYWH(0, 0, 100, 100); + auto b = Rect::MakeXYWH(100, 100, 100, 100); + + test_empty(a, b); + } + + { + auto a = Rect::MakeMaximum(); + auto b = Rect::MakeXYWH(10, 10, 100, 100); + + test_non_empty(a, b); + } + + { + auto a = Rect::MakeMaximum(); + auto b = Rect::MakeMaximum(); + + test_non_empty(a, b); + } +} + +TEST(RectTest, IRectIntersectsWithRect) { + auto check_empty_flips = [](const IRect& a, const IRect& b, + const std::string& label) { + ASSERT_FALSE(a.IsEmpty()); + // b is allowed to be empty + + // unflipped a vs flipped (empty) b yields a + EXPECT_FALSE(a.IntersectsWithRect(flip_lr(b))) << label; + EXPECT_FALSE(a.IntersectsWithRect(flip_tb(b))) << label; + EXPECT_FALSE(a.IntersectsWithRect(flip_lrtb(b))) << label; + + // flipped (empty) a vs unflipped b yields b + EXPECT_FALSE(flip_lr(a).IntersectsWithRect(b)) << label; + EXPECT_FALSE(flip_tb(a).IntersectsWithRect(b)) << label; + EXPECT_FALSE(flip_lrtb(a).IntersectsWithRect(b)) << label; + + // flipped (empty) a vs flipped (empty) b yields empty + EXPECT_FALSE(flip_lr(a).IntersectsWithRect(flip_lr(b))) << label; + EXPECT_FALSE(flip_tb(a).IntersectsWithRect(flip_tb(b))) << label; + EXPECT_FALSE(flip_lrtb(a).IntersectsWithRect(flip_lrtb(b))) << label; + }; + + auto test_non_empty = [&check_empty_flips](const IRect& a, const IRect& b) { + ASSERT_FALSE(a.IsEmpty()) << a; + // b is allowed to be empty + + std::stringstream stream; + stream << a << " union " << b; + auto label = stream.str(); + + EXPECT_TRUE(a.IntersectsWithRect(b)) << label; + EXPECT_TRUE(b.IntersectsWithRect(a)) << label; + check_empty_flips(a, b, label); + }; + + auto test_empty = [&check_empty_flips](const IRect& a, const IRect& b) { + ASSERT_FALSE(a.IsEmpty()) << a; + // b is allowed to be empty + + std::stringstream stream; + stream << a << " union " << b; + auto label = stream.str(); + + EXPECT_FALSE(a.IntersectsWithRect(b)) << label; + EXPECT_FALSE(b.IntersectsWithRect(a)) << label; + check_empty_flips(a, b, label); + }; + + { + auto a = IRect::MakeXYWH(100, 100, 100, 100); + auto b = IRect::MakeXYWH(0, 0, 0, 0); + + test_empty(a, b); + } + + { + auto a = IRect::MakeXYWH(100, 100, 100, 100); + auto b = IRect::MakeXYWH(10, 10, 0, 0); + + test_empty(a, b); + } + + { + auto a = IRect::MakeXYWH(0, 0, 100, 100); + auto b = IRect::MakeXYWH(10, 10, 100, 100); + + test_non_empty(a, b); + } + + { + auto a = IRect::MakeXYWH(0, 0, 100, 100); + auto b = IRect::MakeXYWH(100, 100, 100, 100); + + test_empty(a, b); + } + + { + auto a = IRect::MakeMaximum(); + auto b = IRect::MakeXYWH(10, 10, 100, 100); + + test_non_empty(a, b); + } + + { + auto a = IRect::MakeMaximum(); + auto b = IRect::MakeMaximum(); + + test_non_empty(a, b); + } +} + +TEST(RectTest, RectContainsPoint) { + auto check_nans = [](const Rect& rect, const Point& point, + const std::string& label) { + ASSERT_TRUE(rect.IsFinite()) << label; + ASSERT_TRUE(point.IsFinite()) << label; + + for (int i = 1; i < 16; i++) { + EXPECT_FALSE(swap_nan(rect, i).Contains(point)) + << label << ", index = " << i; + for (int j = 1; j < 4; j++) { + EXPECT_FALSE(swap_nan(rect, i).Contains(swap_nan(point, j))) + << label << ", indices = " << i << ", " << j; + } + } + }; + + auto check_empty_flips = [](const Rect& rect, const Point& point, + const std::string& label) { + ASSERT_FALSE(rect.IsEmpty()); + + EXPECT_FALSE(flip_lr(rect).Contains(point)) << label; + EXPECT_FALSE(flip_tb(rect).Contains(point)) << label; + EXPECT_FALSE(flip_lrtb(rect).Contains(point)) << label; + }; + + auto test_inside = [&check_nans, &check_empty_flips](const Rect& rect, + const Point& point) { + ASSERT_FALSE(rect.IsEmpty()) << rect; + + std::stringstream stream; + stream << rect << " contains " << point; + auto label = stream.str(); + + EXPECT_TRUE(rect.Contains(point)) << label; + check_empty_flips(rect, point, label); + check_nans(rect, point, label); + }; + + auto test_outside = [&check_nans, &check_empty_flips](const Rect& rect, + const Point& point) { + ASSERT_FALSE(rect.IsEmpty()) << rect; + + std::stringstream stream; + stream << rect << " contains " << point; + auto label = stream.str(); + + EXPECT_FALSE(rect.Contains(point)) << label; + check_empty_flips(rect, point, label); + check_nans(rect, point, label); + }; + + { + // Origin is inclusive + auto r = Rect::MakeXYWH(100, 100, 100, 100); + auto p = Point(100, 100); + + test_inside(r, p); + } + { + // Size is exclusive + auto r = Rect::MakeXYWH(100, 100, 100, 100); + auto p = Point(200, 200); + + test_outside(r, p); + } + { + auto r = Rect::MakeXYWH(100, 100, 100, 100); + auto p = Point(99, 99); + + test_outside(r, p); + } + { + auto r = Rect::MakeXYWH(100, 100, 100, 100); + auto p = Point(199, 199); + + test_inside(r, p); + } + + { + auto r = Rect::MakeMaximum(); + auto p = Point(199, 199); + + test_inside(r, p); + } +} + +TEST(RectTest, IRectContainsIPoint) { + auto check_empty_flips = [](const IRect& rect, const IPoint& point, + const std::string& label) { + ASSERT_FALSE(rect.IsEmpty()); + + EXPECT_FALSE(flip_lr(rect).Contains(point)) << label; + EXPECT_FALSE(flip_tb(rect).Contains(point)) << label; + EXPECT_FALSE(flip_lrtb(rect).Contains(point)) << label; + }; + + auto test_inside = [&check_empty_flips](const IRect& rect, + const IPoint& point) { + ASSERT_FALSE(rect.IsEmpty()) << rect; + + std::stringstream stream; + stream << rect << " contains " << point; + auto label = stream.str(); + + EXPECT_TRUE(rect.Contains(point)) << label; + check_empty_flips(rect, point, label); + }; + + auto test_outside = [&check_empty_flips](const IRect& rect, + const IPoint& point) { + ASSERT_FALSE(rect.IsEmpty()) << rect; + + std::stringstream stream; + stream << rect << " contains " << point; + auto label = stream.str(); + + EXPECT_FALSE(rect.Contains(point)) << label; + check_empty_flips(rect, point, label); + }; + + { + // Origin is inclusive + auto r = IRect::MakeXYWH(100, 100, 100, 100); + auto p = IPoint(100, 100); + + test_inside(r, p); + } + { + // Size is exclusive + auto r = IRect::MakeXYWH(100, 100, 100, 100); + auto p = IPoint(200, 200); + + test_outside(r, p); + } + { + auto r = IRect::MakeXYWH(100, 100, 100, 100); + auto p = IPoint(99, 99); + + test_outside(r, p); + } + { + auto r = IRect::MakeXYWH(100, 100, 100, 100); + auto p = IPoint(199, 199); + + test_inside(r, p); + } + + { + auto r = IRect::MakeMaximum(); + auto p = IPoint(199, 199); + + test_inside(r, p); + } +} + +TEST(RectTest, RectContainsRect) { + auto check_nans = [](const Rect& a, const Rect& b, const std::string& label) { + ASSERT_TRUE(a.IsFinite()) << label; + ASSERT_TRUE(b.IsFinite()) << label; + ASSERT_FALSE(a.IsEmpty()); + + for (int i = 1; i < 16; i++) { + // NaN in a produces false + EXPECT_FALSE(swap_nan(a, i).Contains(b)) << label << ", index = " << i; + // NaN in b produces false + EXPECT_TRUE(a.Contains(swap_nan(b, i))) << label << ", index = " << i; + // NaN in both is false + for (int j = 1; j < 16; j++) { + EXPECT_FALSE(swap_nan(a, i).Contains(swap_nan(b, j))) + << label << ", indices = " << i << ", " << j; + } + } + }; + + auto check_empty_flips = [](const Rect& a, const Rect& b, + const std::string& label) { + ASSERT_FALSE(a.IsEmpty()); + // test b rects are allowed to have 0 w/h, but not be backwards + ASSERT_FALSE(b.GetLeft() > b.GetRight() || b.GetTop() > b.GetBottom()); + + // unflipped a vs flipped (empty) b yields false + EXPECT_TRUE(a.Contains(flip_lr(b))) << label; + EXPECT_TRUE(a.Contains(flip_tb(b))) << label; + EXPECT_TRUE(a.Contains(flip_lrtb(b))) << label; + + // flipped (empty) a vs unflipped b yields false + EXPECT_FALSE(flip_lr(a).Contains(b)) << label; + EXPECT_FALSE(flip_tb(a).Contains(b)) << label; + EXPECT_FALSE(flip_lrtb(a).Contains(b)) << label; + + // flipped (empty) a vs flipped (empty) b yields empty + EXPECT_FALSE(flip_lr(a).Contains(flip_lr(b))) << label; + EXPECT_FALSE(flip_tb(a).Contains(flip_tb(b))) << label; + EXPECT_FALSE(flip_lrtb(a).Contains(flip_lrtb(b))) << label; + }; + + auto test_inside = [&check_nans, &check_empty_flips](const Rect& a, + const Rect& b) { + ASSERT_FALSE(a.IsEmpty()) << a; + // test b rects are allowed to have 0 w/h, but not be backwards + ASSERT_FALSE(b.GetLeft() > b.GetRight() || b.GetTop() > b.GetBottom()); + + std::stringstream stream; + stream << a << " contains " << b; + auto label = stream.str(); + + EXPECT_TRUE(a.Contains(b)) << label; + check_empty_flips(a, b, label); + check_nans(a, b, label); + }; + + auto test_not_inside = [&check_nans, &check_empty_flips](const Rect& a, + const Rect& b) { + ASSERT_FALSE(a.IsEmpty()) << a; + // If b was empty, it would be contained and should not be tested with + // this function - use |test_inside| instead. + ASSERT_FALSE(b.IsEmpty()) << b; + + std::stringstream stream; + stream << a << " contains " << b; + auto label = stream.str(); + + EXPECT_FALSE(a.Contains(b)) << label; + check_empty_flips(a, b, label); + check_nans(a, b, label); + }; + + { + auto a = Rect::MakeXYWH(100, 100, 100, 100); + + test_inside(a, a); + } + { + auto a = Rect::MakeXYWH(100, 100, 100, 100); + auto b = Rect::MakeXYWH(0, 0, 0, 0); + + test_inside(a, b); + } + { + auto a = Rect::MakeXYWH(100, 100, 100, 100); + auto b = Rect::MakeXYWH(150, 150, 20, 20); + + test_inside(a, b); + } + { + auto a = Rect::MakeXYWH(100, 100, 100, 100); + auto b = Rect::MakeXYWH(150, 150, 100, 100); + + test_not_inside(a, b); + } + { + auto a = Rect::MakeXYWH(100, 100, 100, 100); + auto b = Rect::MakeXYWH(50, 50, 100, 100); + + test_not_inside(a, b); + } + { + auto a = Rect::MakeXYWH(100, 100, 100, 100); + auto b = Rect::MakeXYWH(0, 0, 300, 300); + + test_not_inside(a, b); + } + { + auto a = Rect::MakeMaximum(); + auto b = Rect::MakeXYWH(0, 0, 300, 300); + + test_inside(a, b); + } +} + +TEST(RectTest, IRectContainsIRect) { + auto check_empty_flips = [](const IRect& a, const IRect& b, + const std::string& label) { + ASSERT_FALSE(a.IsEmpty()); + // test b rects are allowed to have 0 w/h, but not be backwards + ASSERT_FALSE(b.GetLeft() > b.GetRight() || b.GetTop() > b.GetBottom()); + + // unflipped a vs flipped (empty) b yields true + EXPECT_TRUE(a.Contains(flip_lr(b))) << label; + EXPECT_TRUE(a.Contains(flip_tb(b))) << label; + EXPECT_TRUE(a.Contains(flip_lrtb(b))) << label; + + // flipped (empty) a vs unflipped b yields false + EXPECT_FALSE(flip_lr(a).Contains(b)) << label; + EXPECT_FALSE(flip_tb(a).Contains(b)) << label; + EXPECT_FALSE(flip_lrtb(a).Contains(b)) << label; + + // flipped (empty) a vs flipped (empty) b yields empty + EXPECT_FALSE(flip_lr(a).Contains(flip_lr(b))) << label; + EXPECT_FALSE(flip_tb(a).Contains(flip_tb(b))) << label; + EXPECT_FALSE(flip_lrtb(a).Contains(flip_lrtb(b))) << label; + }; + + auto test_inside = [&check_empty_flips](const IRect& a, const IRect& b) { + ASSERT_FALSE(a.IsEmpty()) << a; + // test b rects are allowed to have 0 w/h, but not be backwards + ASSERT_FALSE(b.GetLeft() > b.GetRight() || b.GetTop() > b.GetBottom()); + + std::stringstream stream; + stream << a << " contains " << b; + auto label = stream.str(); + + EXPECT_TRUE(a.Contains(b)) << label; + check_empty_flips(a, b, label); + }; + + auto test_not_inside = [&check_empty_flips](const IRect& a, const IRect& b) { + ASSERT_FALSE(a.IsEmpty()) << a; + // If b was empty, it would be contained and should not be tested with + // this function - use |test_inside| instead. + ASSERT_FALSE(b.IsEmpty()) << b; + + std::stringstream stream; + stream << a << " contains " << b; + auto label = stream.str(); + + EXPECT_FALSE(a.Contains(b)) << label; + check_empty_flips(a, b, label); + }; + + { + auto a = IRect::MakeXYWH(100, 100, 100, 100); + + test_inside(a, a); + } + { + auto a = IRect::MakeXYWH(100, 100, 100, 100); + auto b = IRect::MakeXYWH(0, 0, 0, 0); + + test_inside(a, b); + } + { + auto a = IRect::MakeXYWH(100, 100, 100, 100); + auto b = IRect::MakeXYWH(150, 150, 20, 20); + + test_inside(a, b); + } + { + auto a = IRect::MakeXYWH(100, 100, 100, 100); + auto b = IRect::MakeXYWH(150, 150, 100, 100); + + test_not_inside(a, b); + } + { + auto a = IRect::MakeXYWH(100, 100, 100, 100); + auto b = IRect::MakeXYWH(50, 50, 100, 100); + + test_not_inside(a, b); + } + { + auto a = IRect::MakeXYWH(100, 100, 100, 100); + auto b = IRect::MakeXYWH(0, 0, 300, 300); + + test_not_inside(a, b); + } + { + auto a = IRect::MakeMaximum(); + auto b = IRect::MakeXYWH(0, 0, 300, 300); + + test_inside(a, b); + } +} + +TEST(RectTest, RectCutOut) { + Rect cull_rect = Rect::MakeLTRB(20, 20, 40, 40); + + auto check_nans = [&cull_rect](const Rect& diff_rect, + const std::string& label) { + EXPECT_TRUE(cull_rect.IsFinite()) << label; + EXPECT_TRUE(diff_rect.IsFinite()) << label; + + for (int i = 1; i < 16; i++) { + // NaN in cull_rect produces empty + EXPECT_FALSE(swap_nan(cull_rect, i).Cutout(diff_rect).has_value()) + << label << ", index " << i; + EXPECT_EQ(swap_nan(cull_rect, i).CutoutOrEmpty(diff_rect), Rect()) + << label << ", index " << i; + + // NaN in diff_rect is nop + EXPECT_TRUE(cull_rect.Cutout(swap_nan(diff_rect, i)).has_value()) + << label << ", index " << i; + EXPECT_EQ(cull_rect.CutoutOrEmpty(swap_nan(diff_rect, i)), cull_rect) + << label << ", index " << i; + + for (int j = 1; j < 16; j++) { + // NaN in both is also empty + EXPECT_FALSE( + swap_nan(cull_rect, i).Cutout(swap_nan(diff_rect, j)).has_value()) + << label << ", indices " << i << ", " << j; + EXPECT_EQ(swap_nan(cull_rect, i).CutoutOrEmpty(swap_nan(diff_rect, j)), + Rect()) + << label << ", indices " << i << ", " << j; + } + } + }; + + auto check_empty_flips = [&cull_rect](const Rect& diff_rect, + const std::string& label) { + EXPECT_FALSE(cull_rect.IsEmpty()) << label; + EXPECT_FALSE(diff_rect.IsEmpty()) << label; + + // unflipped cull_rect vs flipped(empty) diff_rect + // == cull_rect + EXPECT_TRUE(cull_rect.Cutout(flip_lr(diff_rect)).has_value()) << label; + EXPECT_EQ(cull_rect.Cutout(flip_lr(diff_rect)), cull_rect) << label; + EXPECT_TRUE(cull_rect.Cutout(flip_tb(diff_rect)).has_value()) << label; + EXPECT_EQ(cull_rect.Cutout(flip_tb(diff_rect)), cull_rect) << label; + EXPECT_TRUE(cull_rect.Cutout(flip_lrtb(diff_rect)).has_value()) << label; + EXPECT_EQ(cull_rect.Cutout(flip_lrtb(diff_rect)), cull_rect) << label; + + // flipped(empty) cull_rect vs unflipped diff_rect + // == empty + EXPECT_FALSE(flip_lr(cull_rect).Cutout(diff_rect).has_value()) << label; + EXPECT_EQ(flip_lr(cull_rect).CutoutOrEmpty(diff_rect), Rect()) << label; + EXPECT_FALSE(flip_tb(cull_rect).Cutout(diff_rect).has_value()) << label; + EXPECT_EQ(flip_tb(cull_rect).CutoutOrEmpty(diff_rect), Rect()) << label; + EXPECT_FALSE(flip_lrtb(cull_rect).Cutout(diff_rect).has_value()) << label; + EXPECT_EQ(flip_lrtb(cull_rect).CutoutOrEmpty(diff_rect), Rect()) << label; + + // flipped(empty) cull_rect vs flipped(empty) diff_rect + // == empty + EXPECT_FALSE(flip_lr(cull_rect).Cutout(flip_lr(diff_rect)).has_value()) + << label; + EXPECT_EQ(flip_lr(cull_rect).CutoutOrEmpty(flip_lr(diff_rect)), Rect()) + << label; + EXPECT_FALSE(flip_tb(cull_rect).Cutout(flip_tb(diff_rect)).has_value()) + << label; + EXPECT_EQ(flip_tb(cull_rect).CutoutOrEmpty(flip_tb(diff_rect)), Rect()) + << label; + EXPECT_FALSE(flip_lrtb(cull_rect).Cutout(flip_lrtb(diff_rect)).has_value()) + << label; + EXPECT_EQ(flip_lrtb(cull_rect).CutoutOrEmpty(flip_lrtb(diff_rect)), Rect()) + << label; + }; + + auto non_reducing = [&cull_rect, &check_empty_flips, &check_nans]( + const Rect& diff_rect, const std::string& label) { + EXPECT_EQ(cull_rect.Cutout(diff_rect), cull_rect) << label; + EXPECT_EQ(cull_rect.CutoutOrEmpty(diff_rect), cull_rect) << label; + check_empty_flips(diff_rect, label); + check_nans(diff_rect, label); + }; + + auto reducing = [&cull_rect, &check_empty_flips, &check_nans]( + const Rect& diff_rect, const Rect& result_rect, + const std::string& label) { + EXPECT_TRUE(!result_rect.IsEmpty()); + EXPECT_EQ(cull_rect.Cutout(diff_rect), result_rect) << label; + EXPECT_EQ(cull_rect.CutoutOrEmpty(diff_rect), result_rect) << label; + check_empty_flips(diff_rect, label); + check_nans(diff_rect, label); + }; + + auto emptying = [&cull_rect, &check_empty_flips, &check_nans]( + const Rect& diff_rect, const std::string& label) { + EXPECT_FALSE(cull_rect.Cutout(diff_rect).has_value()) << label; + EXPECT_EQ(cull_rect.CutoutOrEmpty(diff_rect), Rect()) << label; + check_empty_flips(diff_rect, label); + check_nans(diff_rect, label); + }; + + // Skim the corners and edge + non_reducing(Rect::MakeLTRB(10, 10, 20, 20), "outside UL corner"); + non_reducing(Rect::MakeLTRB(20, 10, 40, 20), "Above"); + non_reducing(Rect::MakeLTRB(40, 10, 50, 20), "outside UR corner"); + non_reducing(Rect::MakeLTRB(40, 20, 50, 40), "Right"); + non_reducing(Rect::MakeLTRB(40, 40, 50, 50), "outside LR corner"); + non_reducing(Rect::MakeLTRB(20, 40, 40, 50), "Below"); + non_reducing(Rect::MakeLTRB(10, 40, 20, 50), "outside LR corner"); + non_reducing(Rect::MakeLTRB(10, 20, 20, 40), "Left"); + + // Overlap corners + non_reducing(Rect::MakeLTRB(15, 15, 25, 25), "covering UL corner"); + non_reducing(Rect::MakeLTRB(35, 15, 45, 25), "covering UR corner"); + non_reducing(Rect::MakeLTRB(35, 35, 45, 45), "covering LR corner"); + non_reducing(Rect::MakeLTRB(15, 35, 25, 45), "covering LL corner"); + + // Overlap edges, but not across an entire side + non_reducing(Rect::MakeLTRB(20, 15, 39, 25), "Top edge left-biased"); + non_reducing(Rect::MakeLTRB(21, 15, 40, 25), "Top edge, right biased"); + non_reducing(Rect::MakeLTRB(35, 20, 45, 39), "Right edge, top-biased"); + non_reducing(Rect::MakeLTRB(35, 21, 45, 40), "Right edge, bottom-biased"); + non_reducing(Rect::MakeLTRB(20, 35, 39, 45), "Bottom edge, left-biased"); + non_reducing(Rect::MakeLTRB(21, 35, 40, 45), "Bottom edge, right-biased"); + non_reducing(Rect::MakeLTRB(15, 20, 25, 39), "Left edge, top-biased"); + non_reducing(Rect::MakeLTRB(15, 21, 25, 40), "Left edge, bottom-biased"); + + // Slice all the way through the middle + non_reducing(Rect::MakeLTRB(25, 15, 35, 45), "Vertical interior slice"); + non_reducing(Rect::MakeLTRB(15, 25, 45, 35), "Horizontal interior slice"); + + // Slice off each edge + reducing(Rect::MakeLTRB(20, 15, 40, 25), // + Rect::MakeLTRB(20, 25, 40, 40), // + "Slice off top"); + reducing(Rect::MakeLTRB(35, 20, 45, 40), // + Rect::MakeLTRB(20, 20, 35, 40), // + "Slice off right"); + reducing(Rect::MakeLTRB(20, 35, 40, 45), // + Rect::MakeLTRB(20, 20, 40, 35), // + "Slice off bottom"); + reducing(Rect::MakeLTRB(15, 20, 25, 40), // + Rect::MakeLTRB(25, 20, 40, 40), // + "Slice off left"); + + // cull rect contains diff rect + non_reducing(Rect::MakeLTRB(21, 21, 39, 39), "Contained, non-covering"); + + // cull rect equals diff rect + emptying(cull_rect, "Perfectly covering"); + + // diff rect contains cull rect + emptying(Rect::MakeLTRB(15, 15, 45, 45), "Smothering"); +} + +TEST(RectTest, IRectCutOut) { + IRect cull_rect = IRect::MakeLTRB(20, 20, 40, 40); + + auto check_empty_flips = [&cull_rect](const IRect& diff_rect, + const std::string& label) { + EXPECT_FALSE(diff_rect.IsEmpty()); + EXPECT_FALSE(cull_rect.IsEmpty()); + + // unflipped cull_rect vs flipped(empty) diff_rect + // == cull_rect + EXPECT_TRUE(cull_rect.Cutout(flip_lr(diff_rect)).has_value()) << label; + EXPECT_EQ(cull_rect.Cutout(flip_lr(diff_rect)), cull_rect) << label; + EXPECT_TRUE(cull_rect.Cutout(flip_tb(diff_rect)).has_value()) << label; + EXPECT_EQ(cull_rect.Cutout(flip_tb(diff_rect)), cull_rect) << label; + EXPECT_TRUE(cull_rect.Cutout(flip_lrtb(diff_rect)).has_value()) << label; + EXPECT_EQ(cull_rect.Cutout(flip_lrtb(diff_rect)), cull_rect) << label; + + // flipped(empty) cull_rect vs flipped(empty) diff_rect + // == empty + EXPECT_FALSE(flip_lr(cull_rect).Cutout(diff_rect).has_value()) << label; + EXPECT_EQ(flip_lr(cull_rect).CutoutOrEmpty(diff_rect), IRect()) << label; + EXPECT_FALSE(flip_tb(cull_rect).Cutout(diff_rect).has_value()) << label; + EXPECT_EQ(flip_tb(cull_rect).CutoutOrEmpty(diff_rect), IRect()) << label; + EXPECT_FALSE(flip_lrtb(cull_rect).Cutout(diff_rect).has_value()) << label; + EXPECT_EQ(flip_lrtb(cull_rect).CutoutOrEmpty(diff_rect), IRect()) << label; + + // flipped(empty) cull_rect vs unflipped diff_rect + // == empty + EXPECT_FALSE(flip_lr(cull_rect).Cutout(flip_lr(diff_rect)).has_value()) + << label; + EXPECT_EQ(flip_lr(cull_rect).CutoutOrEmpty(flip_lr(diff_rect)), IRect()) + << label; + EXPECT_FALSE(flip_tb(cull_rect).Cutout(flip_tb(diff_rect)).has_value()) + << label; + EXPECT_EQ(flip_tb(cull_rect).CutoutOrEmpty(flip_tb(diff_rect)), IRect()) + << label; + EXPECT_FALSE(flip_lrtb(cull_rect).Cutout(flip_lrtb(diff_rect)).has_value()) + << label; + EXPECT_EQ(flip_lrtb(cull_rect).CutoutOrEmpty(flip_lrtb(diff_rect)), IRect()) + << label; + }; + + auto non_reducing = [&cull_rect, &check_empty_flips]( + const IRect& diff_rect, const std::string& label) { + EXPECT_EQ(cull_rect.Cutout(diff_rect), cull_rect) << label; + EXPECT_EQ(cull_rect.CutoutOrEmpty(diff_rect), cull_rect) << label; + check_empty_flips(diff_rect, label); + }; + + auto reducing = [&cull_rect, &check_empty_flips](const IRect& diff_rect, + const IRect& result_rect, + const std::string& label) { + EXPECT_TRUE(!result_rect.IsEmpty()); + EXPECT_EQ(cull_rect.Cutout(diff_rect), result_rect) << label; + EXPECT_EQ(cull_rect.CutoutOrEmpty(diff_rect), result_rect) << label; + check_empty_flips(diff_rect, label); + }; + + auto emptying = [&cull_rect, &check_empty_flips](const IRect& diff_rect, + const std::string& label) { + EXPECT_FALSE(cull_rect.Cutout(diff_rect).has_value()) << label; + EXPECT_EQ(cull_rect.CutoutOrEmpty(diff_rect), IRect()) << label; + check_empty_flips(diff_rect, label); + }; + + // Skim the corners and edge + non_reducing(IRect::MakeLTRB(10, 10, 20, 20), "outside UL corner"); + non_reducing(IRect::MakeLTRB(20, 10, 40, 20), "Above"); + non_reducing(IRect::MakeLTRB(40, 10, 50, 20), "outside UR corner"); + non_reducing(IRect::MakeLTRB(40, 20, 50, 40), "Right"); + non_reducing(IRect::MakeLTRB(40, 40, 50, 50), "outside LR corner"); + non_reducing(IRect::MakeLTRB(20, 40, 40, 50), "Below"); + non_reducing(IRect::MakeLTRB(10, 40, 20, 50), "outside LR corner"); + non_reducing(IRect::MakeLTRB(10, 20, 20, 40), "Left"); + + // Overlap corners + non_reducing(IRect::MakeLTRB(15, 15, 25, 25), "covering UL corner"); + non_reducing(IRect::MakeLTRB(35, 15, 45, 25), "covering UR corner"); + non_reducing(IRect::MakeLTRB(35, 35, 45, 45), "covering LR corner"); + non_reducing(IRect::MakeLTRB(15, 35, 25, 45), "covering LL corner"); + + // Overlap edges, but not across an entire side + non_reducing(IRect::MakeLTRB(20, 15, 39, 25), "Top edge left-biased"); + non_reducing(IRect::MakeLTRB(21, 15, 40, 25), "Top edge, right biased"); + non_reducing(IRect::MakeLTRB(35, 20, 45, 39), "Right edge, top-biased"); + non_reducing(IRect::MakeLTRB(35, 21, 45, 40), "Right edge, bottom-biased"); + non_reducing(IRect::MakeLTRB(20, 35, 39, 45), "Bottom edge, left-biased"); + non_reducing(IRect::MakeLTRB(21, 35, 40, 45), "Bottom edge, right-biased"); + non_reducing(IRect::MakeLTRB(15, 20, 25, 39), "Left edge, top-biased"); + non_reducing(IRect::MakeLTRB(15, 21, 25, 40), "Left edge, bottom-biased"); + + // Slice all the way through the middle + non_reducing(IRect::MakeLTRB(25, 15, 35, 45), "Vertical interior slice"); + non_reducing(IRect::MakeLTRB(15, 25, 45, 35), "Horizontal interior slice"); + + // Slice off each edge + reducing(IRect::MakeLTRB(20, 15, 40, 25), // + IRect::MakeLTRB(20, 25, 40, 40), // + "Slice off top"); + reducing(IRect::MakeLTRB(35, 20, 45, 40), // + IRect::MakeLTRB(20, 20, 35, 40), // + "Slice off right"); + reducing(IRect::MakeLTRB(20, 35, 40, 45), // + IRect::MakeLTRB(20, 20, 40, 35), // + "Slice off bottom"); + reducing(IRect::MakeLTRB(15, 20, 25, 40), // + IRect::MakeLTRB(25, 20, 40, 40), // + "Slice off left"); + + // cull rect contains diff rect + non_reducing(IRect::MakeLTRB(21, 21, 39, 39), "Contained, non-covering"); + + // cull rect equals diff rect + emptying(cull_rect, "Perfectly covering"); + + // diff rect contains cull rect + emptying(IRect::MakeLTRB(15, 15, 45, 45), "Smothering"); +} + +TEST(RectTest, RectGetPoints) { + { + Rect r = Rect::MakeXYWH(100, 200, 300, 400); + auto points = r.GetPoints(); + EXPECT_POINT_NEAR(points[0], Point(100, 200)); + EXPECT_POINT_NEAR(points[1], Point(400, 200)); + EXPECT_POINT_NEAR(points[2], Point(100, 600)); + EXPECT_POINT_NEAR(points[3], Point(400, 600)); + } + + { + Rect r = Rect::MakeMaximum(); + auto points = r.GetPoints(); + EXPECT_EQ(points[0], Point(std::numeric_limits::lowest(), + std::numeric_limits::lowest())); + EXPECT_EQ(points[1], Point(std::numeric_limits::max(), + std::numeric_limits::lowest())); + EXPECT_EQ(points[2], Point(std::numeric_limits::lowest(), + std::numeric_limits::max())); + EXPECT_EQ(points[3], Point(std::numeric_limits::max(), + std::numeric_limits::max())); + } +} + +TEST(RectTest, RectShift) { + auto r = Rect::MakeLTRB(0, 0, 100, 100); + + EXPECT_EQ(r.Shift(Point(10, 5)), Rect::MakeLTRB(10, 5, 110, 105)); + EXPECT_EQ(r.Shift(Point(-10, -5)), Rect::MakeLTRB(-10, -5, 90, 95)); +} + +TEST(RectTest, RectGetTransformedPoints) { + Rect r = Rect::MakeXYWH(100, 200, 300, 400); + auto points = r.GetTransformedPoints(Matrix::MakeTranslation({10, 20})); + EXPECT_POINT_NEAR(points[0], Point(110, 220)); + EXPECT_POINT_NEAR(points[1], Point(410, 220)); + EXPECT_POINT_NEAR(points[2], Point(110, 620)); + EXPECT_POINT_NEAR(points[3], Point(410, 620)); +} + +TEST(RectTest, RectMakePointBounds) { + { + std::vector points{{1, 5}, {4, -1}, {0, 6}}; + auto r = Rect::MakePointBounds(points.begin(), points.end()); + auto expected = Rect::MakeXYWH(0, -1, 4, 7); + EXPECT_TRUE(r.has_value()); + if (r.has_value()) { + EXPECT_RECT_NEAR(r.value(), expected); + } + } + { + std::vector points; + std::optional r = Rect::MakePointBounds(points.begin(), points.end()); + EXPECT_FALSE(r.has_value()); + } +} + +TEST(RectTest, RectGetPositive) { + { + Rect r = Rect::MakeXYWH(100, 200, 300, 400); + auto actual = r.GetPositive(); + EXPECT_RECT_NEAR(r, actual); + } + { + Rect r = Rect::MakeXYWH(100, 200, -100, -100); + auto actual = r.GetPositive(); + Rect expected = Rect::MakeXYWH(0, 100, 100, 100); + EXPECT_RECT_NEAR(expected, actual); + } +} + +TEST(RectTest, RectDirections) { + auto r = Rect::MakeLTRB(1, 2, 3, 4); + + EXPECT_EQ(r.GetLeft(), 1); + EXPECT_EQ(r.GetTop(), 2); + EXPECT_EQ(r.GetRight(), 3); + EXPECT_EQ(r.GetBottom(), 4); + + EXPECT_POINT_NEAR(r.GetLeftTop(), Point(1, 2)); + EXPECT_POINT_NEAR(r.GetRightTop(), Point(3, 2)); + EXPECT_POINT_NEAR(r.GetLeftBottom(), Point(1, 4)); + EXPECT_POINT_NEAR(r.GetRightBottom(), Point(3, 4)); +} + +TEST(RectTest, RectProject) { + { + auto r = Rect::MakeLTRB(-100, -100, 100, 100); + auto actual = r.Project(r); + auto expected = Rect::MakeLTRB(0, 0, 1, 1); + EXPECT_RECT_NEAR(expected, actual); + } + { + auto r = Rect::MakeLTRB(-100, -100, 100, 100); + auto actual = r.Project(Rect::MakeLTRB(0, 0, 100, 100)); + auto expected = Rect::MakeLTRB(0.5, 0.5, 1, 1); + EXPECT_RECT_NEAR(expected, actual); + } +} + +TEST(RectTest, RectRoundOut) { + { + auto r = Rect::MakeLTRB(-100, -100, 100, 100); + EXPECT_EQ(Rect::RoundOut(r), r); + } + { + auto r = Rect::MakeLTRB(-100.1, -100.1, 100.1, 100.1); + EXPECT_EQ(Rect::RoundOut(r), Rect::MakeLTRB(-101, -101, 101, 101)); + } +} + +TEST(RectTest, IRectRoundOut) { + { + auto r = Rect::MakeLTRB(-100, -100, 100, 100); + auto ir = IRect::MakeLTRB(-100, -100, 100, 100); + EXPECT_EQ(IRect::RoundOut(r), ir); + } + { + auto r = Rect::MakeLTRB(-100.1, -100.1, 100.1, 100.1); + auto ir = IRect::MakeLTRB(-101, -101, 101, 101); + EXPECT_EQ(IRect::RoundOut(r), ir); + } +} + } // namespace testing } // namespace impeller diff --git a/engine/src/flutter/impeller/geometry/saturated_math.h b/engine/src/flutter/impeller/geometry/saturated_math.h new file mode 100644 index 0000000000..bd92cbf424 --- /dev/null +++ b/engine/src/flutter/impeller/geometry/saturated_math.h @@ -0,0 +1,148 @@ +// 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. + +#ifndef FLUTTER_IMPELLER_GEOMETRY_SATURATED_MATH_H_ +#define FLUTTER_IMPELLER_GEOMETRY_SATURATED_MATH_H_ + +#include +#include +#include + +#include "flutter/fml/logging.h" +#include "impeller/geometry/scalar.h" + +namespace impeller { + +namespace saturated { + +// NOLINTBEGIN(readability-identifier-naming) +template +inline constexpr bool is_signed_integral_v = + std::is_integral_v && std::is_signed_v; +// NOLINTEND(readability-identifier-naming) + +#define ONLY_ON_SIGNED_INT_RET(Type, Ret) \ + template \ + constexpr inline std::enable_if_t, Ret> +#define ONLY_ON_SIGNED_INT(Type) ONLY_ON_SIGNED_INT_RET(Type, Type) + +#define ONLY_ON_FLOAT_RET(Type, Ret) \ + template \ + constexpr inline std::enable_if_t, Ret> +#define ONLY_ON_FLOAT(Type) ONLY_ON_FLOAT_RET(Type, Type) + +#define ONLY_ON_FLOAT_TO_SIGNED_INT_RET(FPType, SIType, Ret) \ + template \ + constexpr inline std::enable_if_t< \ + std::is_floating_point_v && is_signed_integral_v, Ret> +#define ONLY_ON_FLOAT_TO_SIGNED_INT(FPType, SIType) \ + ONLY_ON_FLOAT_TO_SIGNED_INT_RET(FPType, SIType, SIType) + +#define ONLY_ON_DIFFERING_FLOAT_RET(FPType1, FPType2, Ret) \ + template \ + constexpr inline std::enable_if_t && \ + std::is_floating_point_v && \ + !std::is_same_v, \ + Ret> +#define ONLY_ON_DIFFERING_FLOAT(FPType1, FPType2) \ + ONLY_ON_DIFFERING_FLOAT_RET(FPType1, FPType2, FPType2) + +#define ONLY_ON_SAME_TYPES_RET(Type1, Type2, Ret) \ + template \ + constexpr inline std::enable_if_t, Ret> +#define ONLY_ON_SAME_TYPES(Type1, Type2) \ + ONLY_ON_SAME_TYPES_RET(Type1, Type2, Type2) + +ONLY_ON_SIGNED_INT(SI) Add(SI location, SI distance) { + if (location >= 0) { + if (distance > std::numeric_limits::max() - location) { + return std::numeric_limits::max(); + } + } else if (distance < std::numeric_limits::min() - location) { + return std::numeric_limits::min(); + } + return location + distance; +} + +ONLY_ON_FLOAT(FP) Add(FP location, FP distance) { + return location + distance; +} + +ONLY_ON_SIGNED_INT(SI) Sub(SI upper, SI lower) { + if (upper >= 0) { + if (lower < 0 && upper > std::numeric_limits::max() + lower) { + return std::numeric_limits::max(); + } + } else if (lower > 0 && upper < std::numeric_limits::min() + lower) { + return std::numeric_limits::min(); + } + return upper - lower; +} + +ONLY_ON_FLOAT(FP) Sub(FP upper, FP lower) { + return upper - lower; +} + +ONLY_ON_SIGNED_INT_RET(SI, Scalar) AverageScalar(SI a, SI b) { + // scalbn has an implementation for ints that converts to double + // while adjusting the exponent. + return static_cast(std::scalbn(a, -1) + std::scalbn(b, -1)); +} + +ONLY_ON_FLOAT(FP) AverageScalar(FP a, FP b) { + // GetCenter might want this to return 0 for a Maximum Rect, but it + // will currently produce NaN instead. For the Maximum Rect itself + // a 0 would make sense as the center, but for a computed rect that + // incidentally ended up with infinities, NaN may be a better choice. + // return static_cast(std::scalbn(a, -1) + std::scalbn(b, -1)); + + // This equation would save an extra scalbn operation but at the cost + // of having very large (or very neagive) a's and b's overflow to + // +/- infinity. Scaling first allows finite numbers to be more likely + // to have a finite average. + // return std::scalbn(a + b, -1); + + return static_cast(std::scalbn(a, -1) + std::scalbn(b, -1)); +} + +ONLY_ON_SAME_TYPES(T, U) Cast(T v) { + return v; +} + +ONLY_ON_FLOAT_TO_SIGNED_INT(FP, SI) Cast(FP v) { + if (v <= static_cast(std::numeric_limits::min())) { + return std::numeric_limits::min(); + } else if (v >= static_cast(std::numeric_limits::max())) { + return std::numeric_limits::max(); + } + return static_cast(v); +} + +ONLY_ON_DIFFERING_FLOAT(FP1, FP2) Cast(FP1 v) { + if (std::isfinite(v)) { + // Avoid truncation to inf/-inf. + return std::clamp(static_cast(v), // + std::numeric_limits::lowest(), + std::numeric_limits::max()); + } else { + return static_cast(v); + } +} + +#undef ONLY_ON_SAME_TYPES +#undef ONLY_ON_SAME_TYPES_RET +#undef ONLY_ON_DIFFERING_FLOAT +#undef ONLY_ON_DIFFERING_FLOAT_RET +#undef ONLY_ON_FLOAT_TO_SIGNED_INT +#undef ONLY_ON_FLOAT_TO_SIGNED_INT_RET +#undef ONLY_ON_FLOAT +#undef ONLY_ON_FLOAT_RET +#undef ONLY_ON_SIGNED_INT +#undef ONLY_ON_SIGNED_INT_RET + +} // namespace saturated + +} // namespace impeller + +#endif // FLUTTER_IMPELLER_GEOMETRY_SATURATED_MATH_H_ diff --git a/engine/src/flutter/impeller/geometry/saturated_math_unittests.cc b/engine/src/flutter/impeller/geometry/saturated_math_unittests.cc new file mode 100644 index 0000000000..4e845e5a4b --- /dev/null +++ b/engine/src/flutter/impeller/geometry/saturated_math_unittests.cc @@ -0,0 +1,1133 @@ +// 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 "gtest/gtest.h" + +#include "impeller/geometry/saturated_math.h" + +namespace impeller { +namespace testing { + +TEST(SaturatedMath, ExplicitAddOfSignedInts) { + { + EXPECT_EQ(saturated::Add(0x79, 5), int8_t(0x7E)); + EXPECT_EQ(saturated::Add(0x7A, 5), int8_t(0x7F)); + EXPECT_EQ(saturated::Add(0x7B, 5), int8_t(0x7F)); + } + { + EXPECT_EQ(saturated::Add(0x86, -5), int8_t(0x81)); + EXPECT_EQ(saturated::Add(0x85, -5), int8_t(0x80)); + EXPECT_EQ(saturated::Add(0x84, -5), int8_t(0x80)); + } + { + EXPECT_EQ(saturated::Add(0x7FF9, 5), int16_t(0x7FFE)); + EXPECT_EQ(saturated::Add(0x7FFA, 5), int16_t(0x7FFF)); + EXPECT_EQ(saturated::Add(0x7FFB, 5), int16_t(0x7FFF)); + } + { + EXPECT_EQ(saturated::Add(0x8006, -5), int16_t(0x8001)); + EXPECT_EQ(saturated::Add(0x8005, -5), int16_t(0x8000)); + EXPECT_EQ(saturated::Add(0x8004, -5), int16_t(0x8000)); + } + { + EXPECT_EQ(saturated::Add(0x7FFFFFF9, 5), int32_t(0x7FFFFFFE)); + EXPECT_EQ(saturated::Add(0x7FFFFFFA, 5), int32_t(0x7FFFFFFF)); + EXPECT_EQ(saturated::Add(0x7FFFFFFB, 5), int32_t(0x7FFFFFFF)); + } + { + EXPECT_EQ(saturated::Add(0x80000006, -5), int32_t(0x80000001)); + EXPECT_EQ(saturated::Add(0x80000005, -5), int32_t(0x80000000)); + EXPECT_EQ(saturated::Add(0x80000004, -5), int32_t(0x80000000)); + } + { + EXPECT_EQ(saturated::Add(0x7FFFFFFFFFFFFFF9, 5), + int64_t(0x7FFFFFFFFFFFFFFE)); + EXPECT_EQ(saturated::Add(0x7FFFFFFFFFFFFFFA, 5), + int64_t(0x7FFFFFFFFFFFFFFF)); + EXPECT_EQ(saturated::Add(0x7FFFFFFFFFFFFFFB, 5), + int64_t(0x7FFFFFFFFFFFFFFF)); + } + { + EXPECT_EQ(saturated::Add(0x8000000000000006, -5), + int64_t(0x8000000000000001)); + EXPECT_EQ(saturated::Add(0x8000000000000005, -5), + int64_t(0x8000000000000000)); + EXPECT_EQ(saturated::Add(0x8000000000000004, -5), + int64_t(0x8000000000000000)); + } +} + +TEST(SaturatedMath, ImplicitAddOfSignedInts) { + { + int8_t a = 0x79; + int8_t b = 5; + EXPECT_EQ(saturated::Add(a, b), int8_t(0x7E)); + a = 0x7A; + EXPECT_EQ(saturated::Add(a, b), int8_t(0x7F)); + a = 0x7B; + EXPECT_EQ(saturated::Add(a, b), int8_t(0x7F)); + } + { + int8_t a = 0x86; + int8_t b = -5; + EXPECT_EQ(saturated::Add(a, b), int8_t(0x81)); + a = 0x85; + EXPECT_EQ(saturated::Add(a, b), int8_t(0x80)); + a = 0x84; + EXPECT_EQ(saturated::Add(a, b), int8_t(0x80)); + } + { + int16_t a = 0x7FF9; + int16_t b = 5; + EXPECT_EQ(saturated::Add(a, b), int16_t(0x7FFE)); + a = 0x7FFA; + EXPECT_EQ(saturated::Add(a, b), int16_t(0x7FFF)); + a = 0x7FFB; + EXPECT_EQ(saturated::Add(a, b), int16_t(0x7FFF)); + } + { + int16_t a = 0x8006; + int16_t b = -5; + EXPECT_EQ(saturated::Add(a, b), int16_t(0x8001)); + a = 0x8005; + EXPECT_EQ(saturated::Add(a, b), int16_t(0x8000)); + a = 0x8004; + EXPECT_EQ(saturated::Add(a, b), int16_t(0x8000)); + } + { + int32_t a = 0x7FFFFFF9; + int32_t b = 5; + EXPECT_EQ(saturated::Add(a, b), int32_t(0x7FFFFFFE)); + a = 0x7FFFFFFA; + EXPECT_EQ(saturated::Add(a, b), int32_t(0x7FFFFFFF)); + a = 0x7FFFFFFB; + EXPECT_EQ(saturated::Add(a, b), int32_t(0x7FFFFFFF)); + } + { + int32_t a = 0x80000006; + int32_t b = -5; + EXPECT_EQ(saturated::Add(a, b), int32_t(0x80000001)); + a = 0x80000005; + EXPECT_EQ(saturated::Add(a, b), int32_t(0x80000000)); + a = 0x80000004; + EXPECT_EQ(saturated::Add(a, b), int32_t(0x80000000)); + } + { + int64_t a = 0x7FFFFFFFFFFFFFF9; + int64_t b = 5; + EXPECT_EQ(saturated::Add(a, b), int64_t(0x7FFFFFFFFFFFFFFE)); + a = 0x7FFFFFFFFFFFFFFA; + EXPECT_EQ(saturated::Add(a, b), int64_t(0x7FFFFFFFFFFFFFFF)); + a = 0x7FFFFFFFFFFFFFFB; + EXPECT_EQ(saturated::Add(a, b), int64_t(0x7FFFFFFFFFFFFFFF)); + } + { + int64_t a = 0x8000000000000006; + int64_t b = -5; + EXPECT_EQ(saturated::Add(a, b), int64_t(0x8000000000000001)); + a = 0x8000000000000005; + EXPECT_EQ(saturated::Add(a, b), int64_t(0x8000000000000000)); + a = 0x8000000000000004; + EXPECT_EQ(saturated::Add(a, b), int64_t(0x8000000000000000)); + } +} + +TEST(SaturatedMath, ExplicitAddOfFloatingPoint) { + { + const float inf = std::numeric_limits::infinity(); + const float max = std::numeric_limits::max(); + const float big = max * 0.5f; + + EXPECT_EQ(saturated::Add(big, big), max); + EXPECT_EQ(saturated::Add(max, big), inf); + EXPECT_EQ(saturated::Add(big, max), inf); + EXPECT_EQ(saturated::Add(max, max), inf); + EXPECT_EQ(saturated::Add(max, inf), inf); + EXPECT_EQ(saturated::Add(inf, max), inf); + EXPECT_EQ(saturated::Add(inf, inf), inf); + + EXPECT_EQ(saturated::Add(-big, -big), -max); + EXPECT_EQ(saturated::Add(-max, -big), -inf); + EXPECT_EQ(saturated::Add(-big, -max), -inf); + EXPECT_EQ(saturated::Add(-max, -max), -inf); + EXPECT_EQ(saturated::Add(-max, -inf), -inf); + EXPECT_EQ(saturated::Add(-inf, -max), -inf); + EXPECT_EQ(saturated::Add(-inf, -inf), -inf); + + EXPECT_EQ(saturated::Add(big, -big), 0.0f); + EXPECT_EQ(saturated::Add(max, -big), big); + EXPECT_EQ(saturated::Add(big, -max), -big); + EXPECT_EQ(saturated::Add(max, -max), 0.0f); + EXPECT_EQ(saturated::Add(max, -inf), -inf); + EXPECT_EQ(saturated::Add(inf, -max), inf); + EXPECT_TRUE(std::isnan(saturated::Add(inf, -inf))); + + EXPECT_EQ(saturated::Add(-big, big), 0.0f); + EXPECT_EQ(saturated::Add(-max, big), -big); + EXPECT_EQ(saturated::Add(-big, max), big); + EXPECT_EQ(saturated::Add(-max, max), 0.0f); + EXPECT_EQ(saturated::Add(-max, inf), inf); + EXPECT_EQ(saturated::Add(-inf, max), -inf); + EXPECT_TRUE(std::isnan(saturated::Add(-inf, inf))); + } + { + const double inf = std::numeric_limits::infinity(); + const double max = std::numeric_limits::max(); + const double big = max * 0.5f; + + EXPECT_EQ(saturated::Add(big, big), max); + EXPECT_EQ(saturated::Add(max, big), inf); + EXPECT_EQ(saturated::Add(big, max), inf); + EXPECT_EQ(saturated::Add(max, max), inf); + EXPECT_EQ(saturated::Add(max, inf), inf); + EXPECT_EQ(saturated::Add(inf, max), inf); + EXPECT_EQ(saturated::Add(inf, inf), inf); + + EXPECT_EQ(saturated::Add(-big, -big), -max); + EXPECT_EQ(saturated::Add(-max, -big), -inf); + EXPECT_EQ(saturated::Add(-big, -max), -inf); + EXPECT_EQ(saturated::Add(-max, -max), -inf); + EXPECT_EQ(saturated::Add(-max, -inf), -inf); + EXPECT_EQ(saturated::Add(-inf, -max), -inf); + EXPECT_EQ(saturated::Add(-inf, -inf), -inf); + + EXPECT_EQ(saturated::Add(big, -big), 0.0f); + EXPECT_EQ(saturated::Add(max, -big), big); + EXPECT_EQ(saturated::Add(big, -max), -big); + EXPECT_EQ(saturated::Add(max, -max), 0.0f); + EXPECT_EQ(saturated::Add(max, -inf), -inf); + EXPECT_EQ(saturated::Add(inf, -max), inf); + EXPECT_TRUE(std::isnan(saturated::Add(inf, -inf))); + + EXPECT_EQ(saturated::Add(-big, big), 0.0f); + EXPECT_EQ(saturated::Add(-max, big), -big); + EXPECT_EQ(saturated::Add(-big, max), big); + EXPECT_EQ(saturated::Add(-max, max), 0.0f); + EXPECT_EQ(saturated::Add(-max, inf), inf); + EXPECT_EQ(saturated::Add(-inf, max), -inf); + EXPECT_TRUE(std::isnan(saturated::Add(-inf, inf))); + } + { + const Scalar inf = std::numeric_limits::infinity(); + const Scalar max = std::numeric_limits::max(); + const Scalar big = max * 0.5f; + + EXPECT_EQ(saturated::Add(big, big), max); + EXPECT_EQ(saturated::Add(max, big), inf); + EXPECT_EQ(saturated::Add(big, max), inf); + EXPECT_EQ(saturated::Add(max, max), inf); + EXPECT_EQ(saturated::Add(max, inf), inf); + EXPECT_EQ(saturated::Add(inf, max), inf); + EXPECT_EQ(saturated::Add(inf, inf), inf); + + EXPECT_EQ(saturated::Add(-big, -big), -max); + EXPECT_EQ(saturated::Add(-max, -big), -inf); + EXPECT_EQ(saturated::Add(-big, -max), -inf); + EXPECT_EQ(saturated::Add(-max, -max), -inf); + EXPECT_EQ(saturated::Add(-max, -inf), -inf); + EXPECT_EQ(saturated::Add(-inf, -max), -inf); + EXPECT_EQ(saturated::Add(-inf, -inf), -inf); + + EXPECT_EQ(saturated::Add(big, -big), 0.0f); + EXPECT_EQ(saturated::Add(max, -big), big); + EXPECT_EQ(saturated::Add(big, -max), -big); + EXPECT_EQ(saturated::Add(max, -max), 0.0f); + EXPECT_EQ(saturated::Add(max, -inf), -inf); + EXPECT_EQ(saturated::Add(inf, -max), inf); + EXPECT_TRUE(std::isnan(saturated::Add(inf, -inf))); + + EXPECT_EQ(saturated::Add(-big, big), 0.0f); + EXPECT_EQ(saturated::Add(-max, big), -big); + EXPECT_EQ(saturated::Add(-big, max), big); + EXPECT_EQ(saturated::Add(-max, max), 0.0f); + EXPECT_EQ(saturated::Add(-max, inf), inf); + EXPECT_EQ(saturated::Add(-inf, max), -inf); + EXPECT_TRUE(std::isnan(saturated::Add(-inf, inf))); + } +} + +TEST(SaturatedMath, ImplicitAddOfFloatingPoint) { + { + const float inf = std::numeric_limits::infinity(); + const float max = std::numeric_limits::max(); + const float big = max * 0.5f; + + EXPECT_EQ(saturated::Add(big, big), max); + EXPECT_EQ(saturated::Add(max, big), inf); + EXPECT_EQ(saturated::Add(big, max), inf); + EXPECT_EQ(saturated::Add(max, max), inf); + EXPECT_EQ(saturated::Add(max, inf), inf); + EXPECT_EQ(saturated::Add(inf, max), inf); + EXPECT_EQ(saturated::Add(inf, inf), inf); + + EXPECT_EQ(saturated::Add(-big, -big), -max); + EXPECT_EQ(saturated::Add(-max, -big), -inf); + EXPECT_EQ(saturated::Add(-big, -max), -inf); + EXPECT_EQ(saturated::Add(-max, -max), -inf); + EXPECT_EQ(saturated::Add(-max, -inf), -inf); + EXPECT_EQ(saturated::Add(-inf, -max), -inf); + EXPECT_EQ(saturated::Add(-inf, -inf), -inf); + + EXPECT_EQ(saturated::Add(big, -big), 0.0f); + EXPECT_EQ(saturated::Add(max, -big), big); + EXPECT_EQ(saturated::Add(big, -max), -big); + EXPECT_EQ(saturated::Add(max, -max), 0.0f); + EXPECT_EQ(saturated::Add(max, -inf), -inf); + EXPECT_EQ(saturated::Add(inf, -max), inf); + EXPECT_TRUE(std::isnan(saturated::Add(inf, -inf))); + + EXPECT_EQ(saturated::Add(-big, big), 0.0f); + EXPECT_EQ(saturated::Add(-max, big), -big); + EXPECT_EQ(saturated::Add(-big, max), big); + EXPECT_EQ(saturated::Add(-max, max), 0.0f); + EXPECT_EQ(saturated::Add(-max, inf), inf); + EXPECT_EQ(saturated::Add(-inf, max), -inf); + EXPECT_TRUE(std::isnan(saturated::Add(-inf, inf))); + } + { + const double inf = std::numeric_limits::infinity(); + const double max = std::numeric_limits::max(); + const double big = max * 0.5f; + + EXPECT_EQ(saturated::Add(big, big), max); + EXPECT_EQ(saturated::Add(max, big), inf); + EXPECT_EQ(saturated::Add(big, max), inf); + EXPECT_EQ(saturated::Add(max, max), inf); + EXPECT_EQ(saturated::Add(max, inf), inf); + EXPECT_EQ(saturated::Add(inf, max), inf); + EXPECT_EQ(saturated::Add(inf, inf), inf); + + EXPECT_EQ(saturated::Add(-big, -big), -max); + EXPECT_EQ(saturated::Add(-max, -big), -inf); + EXPECT_EQ(saturated::Add(-big, -max), -inf); + EXPECT_EQ(saturated::Add(-max, -max), -inf); + EXPECT_EQ(saturated::Add(-max, -inf), -inf); + EXPECT_EQ(saturated::Add(-inf, -max), -inf); + EXPECT_EQ(saturated::Add(-inf, -inf), -inf); + + EXPECT_EQ(saturated::Add(big, -big), 0.0f); + EXPECT_EQ(saturated::Add(max, -big), big); + EXPECT_EQ(saturated::Add(big, -max), -big); + EXPECT_EQ(saturated::Add(max, -max), 0.0f); + EXPECT_EQ(saturated::Add(max, -inf), -inf); + EXPECT_EQ(saturated::Add(inf, -max), inf); + EXPECT_TRUE(std::isnan(saturated::Add(inf, -inf))); + + EXPECT_EQ(saturated::Add(-big, big), 0.0f); + EXPECT_EQ(saturated::Add(-max, big), -big); + EXPECT_EQ(saturated::Add(-big, max), big); + EXPECT_EQ(saturated::Add(-max, max), 0.0f); + EXPECT_EQ(saturated::Add(-max, inf), inf); + EXPECT_EQ(saturated::Add(-inf, max), -inf); + EXPECT_TRUE(std::isnan(saturated::Add(-inf, inf))); + } + { + const Scalar inf = std::numeric_limits::infinity(); + const Scalar max = std::numeric_limits::max(); + const Scalar big = max * 0.5f; + + EXPECT_EQ(saturated::Add(big, big), max); + EXPECT_EQ(saturated::Add(max, big), inf); + EXPECT_EQ(saturated::Add(big, max), inf); + EXPECT_EQ(saturated::Add(max, max), inf); + EXPECT_EQ(saturated::Add(max, inf), inf); + EXPECT_EQ(saturated::Add(inf, max), inf); + EXPECT_EQ(saturated::Add(inf, inf), inf); + + EXPECT_EQ(saturated::Add(-big, -big), -max); + EXPECT_EQ(saturated::Add(-max, -big), -inf); + EXPECT_EQ(saturated::Add(-big, -max), -inf); + EXPECT_EQ(saturated::Add(-max, -max), -inf); + EXPECT_EQ(saturated::Add(-max, -inf), -inf); + EXPECT_EQ(saturated::Add(-inf, -max), -inf); + EXPECT_EQ(saturated::Add(-inf, -inf), -inf); + + EXPECT_EQ(saturated::Add(big, -big), 0.0f); + EXPECT_EQ(saturated::Add(max, -big), big); + EXPECT_EQ(saturated::Add(big, -max), -big); + EXPECT_EQ(saturated::Add(max, -max), 0.0f); + EXPECT_EQ(saturated::Add(max, -inf), -inf); + EXPECT_EQ(saturated::Add(inf, -max), inf); + EXPECT_TRUE(std::isnan(saturated::Add(inf, -inf))); + + EXPECT_EQ(saturated::Add(-big, big), 0.0f); + EXPECT_EQ(saturated::Add(-max, big), -big); + EXPECT_EQ(saturated::Add(-big, max), big); + EXPECT_EQ(saturated::Add(-max, max), 0.0f); + EXPECT_EQ(saturated::Add(-max, inf), inf); + EXPECT_EQ(saturated::Add(-inf, max), -inf); + EXPECT_TRUE(std::isnan(saturated::Add(-inf, inf))); + } +} + +TEST(SaturatedMath, ExplicitSubOfSignedInts) { + { + EXPECT_EQ(saturated::Sub(0x79, -5), int8_t(0x7E)); + EXPECT_EQ(saturated::Sub(0x7A, -5), int8_t(0x7F)); + EXPECT_EQ(saturated::Sub(0x7B, -5), int8_t(0x7F)); + } + { + EXPECT_EQ(saturated::Sub(0x86, 5), int8_t(0x81)); + EXPECT_EQ(saturated::Sub(0x85, 5), int8_t(0x80)); + EXPECT_EQ(saturated::Sub(0x84, 5), int8_t(0x80)); + } + { + EXPECT_EQ(saturated::Sub(0x7FF9, -5), int16_t(0x7FFE)); + EXPECT_EQ(saturated::Sub(0x7FFA, -5), int16_t(0x7FFF)); + EXPECT_EQ(saturated::Sub(0x7FFB, -5), int16_t(0x7FFF)); + } + { + EXPECT_EQ(saturated::Sub(0x8006, 5), int16_t(0x8001)); + EXPECT_EQ(saturated::Sub(0x8005, 5), int16_t(0x8000)); + EXPECT_EQ(saturated::Sub(0x8004, 5), int16_t(0x8000)); + } + { + EXPECT_EQ(saturated::Sub(0x7FFFFFF9, -5), int32_t(0x7FFFFFFE)); + EXPECT_EQ(saturated::Sub(0x7FFFFFFA, -5), int32_t(0x7FFFFFFF)); + EXPECT_EQ(saturated::Sub(0x7FFFFFFB, -5), int32_t(0x7FFFFFFF)); + } + { + EXPECT_EQ(saturated::Sub(0x80000006, 5), int32_t(0x80000001)); + EXPECT_EQ(saturated::Sub(0x80000005, 5), int32_t(0x80000000)); + EXPECT_EQ(saturated::Sub(0x80000004, 5), int32_t(0x80000000)); + } + { + EXPECT_EQ(saturated::Sub(0x7FFFFFFFFFFFFFF9, -5), + int64_t(0x7FFFFFFFFFFFFFFE)); + EXPECT_EQ(saturated::Sub(0x7FFFFFFFFFFFFFFA, -5), + int64_t(0x7FFFFFFFFFFFFFFF)); + EXPECT_EQ(saturated::Sub(0x7FFFFFFFFFFFFFFB, -5), + int64_t(0x7FFFFFFFFFFFFFFF)); + } + { + EXPECT_EQ(saturated::Sub(0x8000000000000006, 5), + int64_t(0x8000000000000001)); + EXPECT_EQ(saturated::Sub(0x8000000000000005, 5), + int64_t(0x8000000000000000)); + EXPECT_EQ(saturated::Sub(0x8000000000000004, 5), + int64_t(0x8000000000000000)); + } +} + +TEST(SaturatedMath, ImplicitSubOfSignedInts) { + { + int8_t a = 0x79; + int8_t b = -5; + EXPECT_EQ(saturated::Sub(a, b), int8_t(0x7E)); + a = 0x7A; + EXPECT_EQ(saturated::Sub(a, b), int8_t(0x7F)); + a = 0x7B; + EXPECT_EQ(saturated::Sub(a, b), int8_t(0x7F)); + } + { + int8_t a = 0x86; + int8_t b = 5; + EXPECT_EQ(saturated::Sub(a, b), int8_t(0x81)); + a = 0x85; + EXPECT_EQ(saturated::Sub(a, b), int8_t(0x80)); + a = 0x84; + EXPECT_EQ(saturated::Sub(a, b), int8_t(0x80)); + } + { + int16_t a = 0x7FF9; + int16_t b = -5; + EXPECT_EQ(saturated::Sub(a, b), int16_t(0x7FFE)); + a = 0x7FFA; + EXPECT_EQ(saturated::Sub(a, b), int16_t(0x7FFF)); + a = 0x7FFB; + EXPECT_EQ(saturated::Sub(a, b), int16_t(0x7FFF)); + } + { + int16_t a = 0x8006; + int16_t b = 5; + EXPECT_EQ(saturated::Sub(a, b), int16_t(0x8001)); + a = 0x8005; + EXPECT_EQ(saturated::Sub(a, b), int16_t(0x8000)); + a = 0x8004; + EXPECT_EQ(saturated::Sub(a, b), int16_t(0x8000)); + } + { + int32_t a = 0x7FFFFFF9; + int32_t b = -5; + EXPECT_EQ(saturated::Sub(a, b), int32_t(0x7FFFFFFE)); + a = 0x7FFFFFFA; + EXPECT_EQ(saturated::Sub(a, b), int32_t(0x7FFFFFFF)); + a = 0x7FFFFFFB; + EXPECT_EQ(saturated::Sub(a, b), int32_t(0x7FFFFFFF)); + } + { + int32_t a = 0x80000006; + int32_t b = 5; + EXPECT_EQ(saturated::Sub(a, b), int32_t(0x80000001)); + a = 0x80000005; + EXPECT_EQ(saturated::Sub(a, b), int32_t(0x80000000)); + a = 0x80000004; + EXPECT_EQ(saturated::Sub(a, b), int32_t(0x80000000)); + } + { + int64_t a = 0x7FFFFFFFFFFFFFF9; + int64_t b = -5; + EXPECT_EQ(saturated::Sub(a, b), int64_t(0x7FFFFFFFFFFFFFFE)); + a = 0x7FFFFFFFFFFFFFFA; + EXPECT_EQ(saturated::Sub(a, b), int64_t(0x7FFFFFFFFFFFFFFF)); + a = 0x7FFFFFFFFFFFFFFB; + EXPECT_EQ(saturated::Sub(a, b), int64_t(0x7FFFFFFFFFFFFFFF)); + } + { + int64_t a = 0x8000000000000006; + int64_t b = 5; + EXPECT_EQ(saturated::Sub(a, b), int64_t(0x8000000000000001)); + a = 0x8000000000000005; + EXPECT_EQ(saturated::Sub(a, b), int64_t(0x8000000000000000)); + a = 0x8000000000000004; + EXPECT_EQ(saturated::Sub(a, b), int64_t(0x8000000000000000)); + } +} + +TEST(SaturatedMath, ExplicitSubOfFloatingPoint) { + { + const float inf = std::numeric_limits::infinity(); + const float max = std::numeric_limits::max(); + const float big = max * 0.5f; + + EXPECT_EQ(saturated::Sub(big, big), 0.0f); + EXPECT_EQ(saturated::Sub(max, big), big); + EXPECT_EQ(saturated::Sub(big, max), -big); + EXPECT_EQ(saturated::Sub(max, max), 0.0f); + EXPECT_EQ(saturated::Sub(max, inf), -inf); + EXPECT_EQ(saturated::Sub(inf, max), inf); + EXPECT_TRUE(std::isnan(saturated::Sub(inf, inf))); + + EXPECT_EQ(saturated::Sub(-big, -big), 0.0f); + EXPECT_EQ(saturated::Sub(-max, -big), -big); + EXPECT_EQ(saturated::Sub(-big, -max), big); + EXPECT_EQ(saturated::Sub(-max, -max), 0.0f); + EXPECT_EQ(saturated::Sub(-max, -inf), inf); + EXPECT_EQ(saturated::Sub(-inf, -max), -inf); + EXPECT_TRUE(std::isnan(saturated::Sub(-inf, -inf))); + + EXPECT_EQ(saturated::Sub(big, -big), max); + EXPECT_EQ(saturated::Sub(max, -big), inf); + EXPECT_EQ(saturated::Sub(big, -max), inf); + EXPECT_EQ(saturated::Sub(max, -max), inf); + EXPECT_EQ(saturated::Sub(max, -inf), inf); + EXPECT_EQ(saturated::Sub(inf, -max), inf); + EXPECT_EQ(saturated::Sub(inf, -inf), inf); + + EXPECT_EQ(saturated::Sub(-big, big), -max); + EXPECT_EQ(saturated::Sub(-max, big), -inf); + EXPECT_EQ(saturated::Sub(-big, max), -inf); + EXPECT_EQ(saturated::Sub(-max, max), -inf); + EXPECT_EQ(saturated::Sub(-max, inf), -inf); + EXPECT_EQ(saturated::Sub(-inf, max), -inf); + EXPECT_EQ(saturated::Sub(-inf, inf), -inf); + } + { + const double inf = std::numeric_limits::infinity(); + const double max = std::numeric_limits::max(); + const double big = max * 0.5f; + + EXPECT_EQ(saturated::Sub(big, big), 0.0f); + EXPECT_EQ(saturated::Sub(max, big), big); + EXPECT_EQ(saturated::Sub(big, max), -big); + EXPECT_EQ(saturated::Sub(max, max), 0.0f); + EXPECT_EQ(saturated::Sub(max, inf), -inf); + EXPECT_EQ(saturated::Sub(inf, max), inf); + EXPECT_TRUE(std::isnan(saturated::Sub(inf, inf))); + + EXPECT_EQ(saturated::Sub(-big, -big), 0.0f); + EXPECT_EQ(saturated::Sub(-max, -big), -big); + EXPECT_EQ(saturated::Sub(-big, -max), big); + EXPECT_EQ(saturated::Sub(-max, -max), 0.0f); + EXPECT_EQ(saturated::Sub(-max, -inf), inf); + EXPECT_EQ(saturated::Sub(-inf, -max), -inf); + EXPECT_TRUE(std::isnan(saturated::Sub(-inf, -inf))); + + EXPECT_EQ(saturated::Sub(big, -big), max); + EXPECT_EQ(saturated::Sub(max, -big), inf); + EXPECT_EQ(saturated::Sub(big, -max), inf); + EXPECT_EQ(saturated::Sub(max, -max), inf); + EXPECT_EQ(saturated::Sub(max, -inf), inf); + EXPECT_EQ(saturated::Sub(inf, -max), inf); + EXPECT_EQ(saturated::Sub(inf, -inf), inf); + + EXPECT_EQ(saturated::Sub(-big, big), -max); + EXPECT_EQ(saturated::Sub(-max, big), -inf); + EXPECT_EQ(saturated::Sub(-big, max), -inf); + EXPECT_EQ(saturated::Sub(-max, max), -inf); + EXPECT_EQ(saturated::Sub(-max, inf), -inf); + EXPECT_EQ(saturated::Sub(-inf, max), -inf); + EXPECT_EQ(saturated::Sub(-inf, inf), -inf); + } + { + const Scalar inf = std::numeric_limits::infinity(); + const Scalar max = std::numeric_limits::max(); + const Scalar big = max * 0.5f; + + EXPECT_EQ(saturated::Sub(big, big), 0.0f); + EXPECT_EQ(saturated::Sub(max, big), big); + EXPECT_EQ(saturated::Sub(big, max), -big); + EXPECT_EQ(saturated::Sub(max, max), 0.0f); + EXPECT_EQ(saturated::Sub(max, inf), -inf); + EXPECT_EQ(saturated::Sub(inf, max), inf); + EXPECT_TRUE(std::isnan(saturated::Sub(inf, inf))); + + EXPECT_EQ(saturated::Sub(-big, -big), 0.0f); + EXPECT_EQ(saturated::Sub(-max, -big), -big); + EXPECT_EQ(saturated::Sub(-big, -max), big); + EXPECT_EQ(saturated::Sub(-max, -max), 0.0f); + EXPECT_EQ(saturated::Sub(-max, -inf), inf); + EXPECT_EQ(saturated::Sub(-inf, -max), -inf); + EXPECT_TRUE(std::isnan(saturated::Sub(-inf, -inf))); + + EXPECT_EQ(saturated::Sub(big, -big), max); + EXPECT_EQ(saturated::Sub(max, -big), inf); + EXPECT_EQ(saturated::Sub(big, -max), inf); + EXPECT_EQ(saturated::Sub(max, -max), inf); + EXPECT_EQ(saturated::Sub(max, -inf), inf); + EXPECT_EQ(saturated::Sub(inf, -max), inf); + EXPECT_EQ(saturated::Sub(inf, -inf), inf); + + EXPECT_EQ(saturated::Sub(-big, big), -max); + EXPECT_EQ(saturated::Sub(-max, big), -inf); + EXPECT_EQ(saturated::Sub(-big, max), -inf); + EXPECT_EQ(saturated::Sub(-max, max), -inf); + EXPECT_EQ(saturated::Sub(-max, inf), -inf); + EXPECT_EQ(saturated::Sub(-inf, max), -inf); + EXPECT_EQ(saturated::Sub(-inf, inf), -inf); + } +} + +TEST(SaturatedMath, ImplicitSubOfFloatingPoint) { + { + const float inf = std::numeric_limits::infinity(); + const float max = std::numeric_limits::max(); + const float big = max * 0.5f; + + EXPECT_EQ(saturated::Sub(big, big), 0.0f); + EXPECT_EQ(saturated::Sub(max, big), big); + EXPECT_EQ(saturated::Sub(big, max), -big); + EXPECT_EQ(saturated::Sub(max, max), 0.0f); + EXPECT_EQ(saturated::Sub(max, inf), -inf); + EXPECT_EQ(saturated::Sub(inf, max), inf); + EXPECT_TRUE(std::isnan(saturated::Sub(inf, inf))); + + EXPECT_EQ(saturated::Sub(-big, -big), 0.0f); + EXPECT_EQ(saturated::Sub(-max, -big), -big); + EXPECT_EQ(saturated::Sub(-big, -max), big); + EXPECT_EQ(saturated::Sub(-max, -max), 0.0f); + EXPECT_EQ(saturated::Sub(-max, -inf), inf); + EXPECT_EQ(saturated::Sub(-inf, -max), -inf); + EXPECT_TRUE(std::isnan(saturated::Sub(-inf, -inf))); + + EXPECT_EQ(saturated::Sub(big, -big), max); + EXPECT_EQ(saturated::Sub(max, -big), inf); + EXPECT_EQ(saturated::Sub(big, -max), inf); + EXPECT_EQ(saturated::Sub(max, -max), inf); + EXPECT_EQ(saturated::Sub(max, -inf), inf); + EXPECT_EQ(saturated::Sub(inf, -max), inf); + EXPECT_EQ(saturated::Sub(inf, -inf), inf); + + EXPECT_EQ(saturated::Sub(-big, big), -max); + EXPECT_EQ(saturated::Sub(-max, big), -inf); + EXPECT_EQ(saturated::Sub(-big, max), -inf); + EXPECT_EQ(saturated::Sub(-max, max), -inf); + EXPECT_EQ(saturated::Sub(-max, inf), -inf); + EXPECT_EQ(saturated::Sub(-inf, max), -inf); + EXPECT_EQ(saturated::Sub(-inf, inf), -inf); + } + { + const double inf = std::numeric_limits::infinity(); + const double max = std::numeric_limits::max(); + const double big = max * 0.5f; + + EXPECT_EQ(saturated::Sub(big, big), 0.0f); + EXPECT_EQ(saturated::Sub(max, big), big); + EXPECT_EQ(saturated::Sub(big, max), -big); + EXPECT_EQ(saturated::Sub(max, max), 0.0f); + EXPECT_EQ(saturated::Sub(max, inf), -inf); + EXPECT_EQ(saturated::Sub(inf, max), inf); + EXPECT_TRUE(std::isnan(saturated::Sub(inf, inf))); + + EXPECT_EQ(saturated::Sub(-big, -big), 0.0f); + EXPECT_EQ(saturated::Sub(-max, -big), -big); + EXPECT_EQ(saturated::Sub(-big, -max), big); + EXPECT_EQ(saturated::Sub(-max, -max), 0.0f); + EXPECT_EQ(saturated::Sub(-max, -inf), inf); + EXPECT_EQ(saturated::Sub(-inf, -max), -inf); + EXPECT_TRUE(std::isnan(saturated::Sub(-inf, -inf))); + + EXPECT_EQ(saturated::Sub(big, -big), max); + EXPECT_EQ(saturated::Sub(max, -big), inf); + EXPECT_EQ(saturated::Sub(big, -max), inf); + EXPECT_EQ(saturated::Sub(max, -max), inf); + EXPECT_EQ(saturated::Sub(max, -inf), inf); + EXPECT_EQ(saturated::Sub(inf, -max), inf); + EXPECT_EQ(saturated::Sub(inf, -inf), inf); + + EXPECT_EQ(saturated::Sub(-big, big), -max); + EXPECT_EQ(saturated::Sub(-max, big), -inf); + EXPECT_EQ(saturated::Sub(-big, max), -inf); + EXPECT_EQ(saturated::Sub(-max, max), -inf); + EXPECT_EQ(saturated::Sub(-max, inf), -inf); + EXPECT_EQ(saturated::Sub(-inf, max), -inf); + EXPECT_EQ(saturated::Sub(-inf, inf), -inf); + } + { + const Scalar inf = std::numeric_limits::infinity(); + const Scalar max = std::numeric_limits::max(); + const Scalar big = max * 0.5f; + + EXPECT_EQ(saturated::Sub(big, big), 0.0f); + EXPECT_EQ(saturated::Sub(max, big), big); + EXPECT_EQ(saturated::Sub(big, max), -big); + EXPECT_EQ(saturated::Sub(max, max), 0.0f); + EXPECT_EQ(saturated::Sub(max, inf), -inf); + EXPECT_EQ(saturated::Sub(inf, max), inf); + EXPECT_TRUE(std::isnan(saturated::Sub(inf, inf))); + + EXPECT_EQ(saturated::Sub(-big, -big), 0.0f); + EXPECT_EQ(saturated::Sub(-max, -big), -big); + EXPECT_EQ(saturated::Sub(-big, -max), big); + EXPECT_EQ(saturated::Sub(-max, -max), 0.0f); + EXPECT_EQ(saturated::Sub(-max, -inf), inf); + EXPECT_EQ(saturated::Sub(-inf, -max), -inf); + EXPECT_TRUE(std::isnan(saturated::Sub(-inf, -inf))); + + EXPECT_EQ(saturated::Sub(big, -big), max); + EXPECT_EQ(saturated::Sub(max, -big), inf); + EXPECT_EQ(saturated::Sub(big, -max), inf); + EXPECT_EQ(saturated::Sub(max, -max), inf); + EXPECT_EQ(saturated::Sub(max, -inf), inf); + EXPECT_EQ(saturated::Sub(inf, -max), inf); + EXPECT_EQ(saturated::Sub(inf, -inf), inf); + + EXPECT_EQ(saturated::Sub(-big, big), -max); + EXPECT_EQ(saturated::Sub(-max, big), -inf); + EXPECT_EQ(saturated::Sub(-big, max), -inf); + EXPECT_EQ(saturated::Sub(-max, max), -inf); + EXPECT_EQ(saturated::Sub(-max, inf), -inf); + EXPECT_EQ(saturated::Sub(-inf, max), -inf); + EXPECT_EQ(saturated::Sub(-inf, inf), -inf); + } +} + +TEST(SaturatedMath, ExplicitAverageScalarOfSignedInts) { + // For each type try: + // + // - near the limits, averaging to 0 + // - at the limits, averaging to 0 or 0.5 depending on precision + // - both large enough for the sum to overflow + // - both negative enough for the sum to underflow + { + EXPECT_EQ(saturated::AverageScalar(0x81, 0x7F), -0.0f); + EXPECT_EQ(saturated::AverageScalar(0x80, 0x7F), -0.5f); + EXPECT_EQ(saturated::AverageScalar(0x70, 0x75), 114.5f); + EXPECT_EQ(saturated::AverageScalar(0x85, 0x8A), -120.5f); + } + { + EXPECT_EQ(saturated::AverageScalar(0x8001, 0x7FFF), -0.0f); + EXPECT_EQ(saturated::AverageScalar(0x8000, 0x7FFF), -0.5f); + EXPECT_EQ(saturated::AverageScalar(0x7000, 0x7005), 28674.5f); + EXPECT_EQ(saturated::AverageScalar(0x8005, 0x800A), -32760.5f); + } + { + EXPECT_EQ(saturated::AverageScalar(0x80000001, 0x7FFFFFFF), -0.0f); + EXPECT_EQ(saturated::AverageScalar(0x80000000, 0x7FFFFFFF), -0.5f); + EXPECT_EQ(saturated::AverageScalar(0x70000000, 0x70000005), + 1879048195.5f); + EXPECT_EQ(saturated::AverageScalar(0x80000005, 0x8000000A), + -2147483655.5f); + } + { + EXPECT_EQ(saturated::AverageScalar(0x8000000000000001, + 0x7FFFFFFFFFFFFFFF), + 0.0f); + // 64-bit integers overflow the ability of a Scalar (float) to + // represent discrete integers and so the two numbers we are + // averaging here will look like the same number with different + // signs and the answer will be "0" + EXPECT_EQ(saturated::AverageScalar(0x8000000000000000, + 0x7FFFFFFFFFFFFFFF), + 0.0f); + EXPECT_NEAR(saturated::AverageScalar(0x7000000000000000, + 0x7000000000000005), + 8.07045053e+18, 1e18); + EXPECT_NEAR(saturated::AverageScalar(0x8000000000000005, + 0x800000000000000A), + -9.223372e+18, 1e18); + } +} + +TEST(SaturatedMath, ImplicitAverageScalarOfSignedInts) { + // For each type try: + // + // - near the limits, averaging to 0 + // - at the limits, averaging to 0 or 0.5 depending on precision + // - both large enough for the sum to overflow + // - both negative enough for the sum to underflow + { + int8_t a = 0x81; + int8_t b = 0x7f; + EXPECT_EQ(saturated::AverageScalar(a, b), -0.0f); + a = 0x80; + EXPECT_EQ(saturated::AverageScalar(a, b), -0.5f); + a = 0x70; + b = 0x75; + EXPECT_EQ(saturated::AverageScalar(a, b), 114.5f); + a = 0x85; + b = 0x8A; + EXPECT_EQ(saturated::AverageScalar(a, b), -120.5f); + } + { + int16_t a = 0x8001; + int16_t b = 0x7FFF; + EXPECT_EQ(saturated::AverageScalar(a, b), -0.0f); + a = 0x8000; + EXPECT_EQ(saturated::AverageScalar(a, b), -0.5f); + a = 0x7000; + b = 0x7005; + EXPECT_EQ(saturated::AverageScalar(a, b), 28674.5f); + a = 0x8005; + b = 0x800A; + EXPECT_EQ(saturated::AverageScalar(a, b), -32760.5f); + } + { + int32_t a = 0x80000001; + int32_t b = 0x7FFFFFFF; + EXPECT_EQ(saturated::AverageScalar(a, b), -0.0f); + a = 0x80000000; + EXPECT_EQ(saturated::AverageScalar(a, b), -0.5f); + a = 0x70000000; + b = 0x70000005; + EXPECT_EQ(saturated::AverageScalar(a, b), 1879048195.5f); + a = 0x80000005; + b = 0x8000000A; + EXPECT_EQ(saturated::AverageScalar(a, b), -2147483655.5f); + } + { + int64_t a = 0x8000000000000001; + int64_t b = 0x7FFFFFFFFFFFFFFF; + EXPECT_EQ(saturated::AverageScalar(a, b), 0.0f); + // 64-bit integers overflow the ability of a Scalar (float) to + // represent discrete integers and so the two numbers we are + // averaging here will look like the same number with different + // signs and the answer will be "0" + a = 0x8000000000000000; + EXPECT_EQ(saturated::AverageScalar(a, b), 0.0f); + a = 0x7000000000000000; + b = 0x7000000000000005; + EXPECT_NEAR(saturated::AverageScalar(a, b), 8.0704505e+18, 1e18); + a = 0x8000000000000005; + b = 0x800000000000000A; + EXPECT_NEAR(saturated::AverageScalar(a, b), -9.223372e+18, 1e18); + } +} + +TEST(SaturatedMath, ExplicitAverageScalarOfFloatingPoint) { + const Scalar s_inf = std::numeric_limits::infinity(); + const Scalar s_max = std::numeric_limits::max(); + const Scalar s_big = s_max * 0.5f; + + { + const float inf = std::numeric_limits::infinity(); + const float max = std::numeric_limits::max(); + const float big = max * 0.5f; + + EXPECT_EQ(saturated::AverageScalar(big, big), s_big); + EXPECT_EQ(saturated::AverageScalar(max, max), s_max); + EXPECT_EQ(saturated::AverageScalar(big, -big), 0.0f); + EXPECT_EQ(saturated::AverageScalar(max, -max), 0.0f); + EXPECT_EQ(saturated::AverageScalar(-big, big), 0.0f); + EXPECT_EQ(saturated::AverageScalar(-max, max), 0.0f); + EXPECT_EQ(saturated::AverageScalar(-big, -big), -s_big); + EXPECT_EQ(saturated::AverageScalar(-max, -max), -s_max); + + EXPECT_EQ(saturated::AverageScalar(inf, inf), s_inf); + EXPECT_EQ(saturated::AverageScalar(-inf, -inf), -s_inf); + EXPECT_TRUE(std::isnan(saturated::AverageScalar(-inf, inf))); + EXPECT_TRUE(std::isnan(saturated::AverageScalar(inf, -inf))); + } + { + const double inf = std::numeric_limits::infinity(); + const double max = std::numeric_limits::max(); + const double big = max * 0.5; + + // Most of the averages below using the double constants will + // overflow the Scalar return value and result in infinity, + // so we also test with some Scalar constants (promoted to double) + // to verify that they don't overflow in the double template + EXPECT_EQ(saturated::AverageScalar(s_big, s_big), s_big); + EXPECT_EQ(saturated::AverageScalar(s_max, s_max), s_max); + EXPECT_EQ(saturated::AverageScalar(-s_big, -s_big), -s_big); + EXPECT_EQ(saturated::AverageScalar(-s_max, -s_max), -s_max); + + // And now testing continues with the double constants which + // mostly overflow + EXPECT_EQ(saturated::AverageScalar(big, big), s_inf); + EXPECT_EQ(saturated::AverageScalar(max, max), s_inf); + EXPECT_EQ(saturated::AverageScalar(big, -big), 0.0f); + EXPECT_EQ(saturated::AverageScalar(max, -max), 0.0f); + EXPECT_EQ(saturated::AverageScalar(-big, big), 0.0f); + EXPECT_EQ(saturated::AverageScalar(-max, max), 0.0f); + EXPECT_EQ(saturated::AverageScalar(-big, -big), -s_inf); + EXPECT_EQ(saturated::AverageScalar(-max, -max), -s_inf); + + EXPECT_EQ(saturated::AverageScalar(inf, inf), s_inf); + EXPECT_EQ(saturated::AverageScalar(-inf, -inf), -s_inf); + EXPECT_TRUE(std::isnan(saturated::AverageScalar(-inf, inf))); + EXPECT_TRUE(std::isnan(saturated::AverageScalar(inf, -inf))); + } + { + const Scalar inf = std::numeric_limits::infinity(); + const Scalar max = std::numeric_limits::max(); + const Scalar big = max * 0.5f; + + EXPECT_EQ(saturated::AverageScalar(big, big), s_big); + EXPECT_EQ(saturated::AverageScalar(max, max), s_max); + EXPECT_EQ(saturated::AverageScalar(big, -big), 0.0f); + EXPECT_EQ(saturated::AverageScalar(max, -max), 0.0f); + EXPECT_EQ(saturated::AverageScalar(-big, big), 0.0f); + EXPECT_EQ(saturated::AverageScalar(-max, max), 0.0f); + EXPECT_EQ(saturated::AverageScalar(-big, -big), -s_big); + EXPECT_EQ(saturated::AverageScalar(-max, -max), -s_max); + + EXPECT_EQ(saturated::AverageScalar(inf, inf), s_inf); + EXPECT_EQ(saturated::AverageScalar(-inf, -inf), -s_inf); + EXPECT_TRUE(std::isnan(saturated::AverageScalar(-inf, s_inf))); + EXPECT_TRUE(std::isnan(saturated::AverageScalar(inf, -s_inf))); + } +} + +TEST(SaturatedMath, ImplicitAverageScalarOfFloatingPoint) { + // All return values are Scalar regardless of the operand types + // so these constants are used as the expected answers. + const Scalar s_inf = std::numeric_limits::infinity(); + const Scalar s_max = std::numeric_limits::max(); + const Scalar s_big = s_max * 0.5f; + + { + const float inf = std::numeric_limits::infinity(); + const float max = std::numeric_limits::max(); + const float big = max * 0.5f; + + EXPECT_EQ(saturated::AverageScalar(big, big), s_big); + EXPECT_EQ(saturated::AverageScalar(max, max), s_max); + EXPECT_EQ(saturated::AverageScalar(big, -big), 0.0f); + EXPECT_EQ(saturated::AverageScalar(max, -max), 0.0f); + EXPECT_EQ(saturated::AverageScalar(-big, big), 0.0f); + EXPECT_EQ(saturated::AverageScalar(-max, max), 0.0f); + EXPECT_EQ(saturated::AverageScalar(-big, -big), -s_big); + EXPECT_EQ(saturated::AverageScalar(-max, -max), -s_max); + + EXPECT_EQ(saturated::AverageScalar(inf, inf), s_inf); + EXPECT_EQ(saturated::AverageScalar(-inf, -inf), -s_inf); + EXPECT_TRUE(std::isnan(saturated::AverageScalar(-inf, inf))); + EXPECT_TRUE(std::isnan(saturated::AverageScalar(inf, -inf))); + } + { + const double inf = std::numeric_limits::infinity(); + const double max = std::numeric_limits::max(); + const double big = max * 0.5; + + // The s_constants converted to double. We should get finite results + // from finding the averages of these values, but we'll get a lot of + // overflow to infinity when testing the large double constants. + const double d_s_max = s_max; + const double d_s_big = s_big; + EXPECT_EQ(saturated::AverageScalar(d_s_big, d_s_big), s_big); + EXPECT_EQ(saturated::AverageScalar(d_s_max, d_s_max), s_max); + EXPECT_EQ(saturated::AverageScalar(-d_s_big, -d_s_big), -s_big); + EXPECT_EQ(saturated::AverageScalar(-d_s_max, -d_s_max), -s_max); + + // And now testing continues with the double constants which + // mostly overflow + EXPECT_EQ(saturated::AverageScalar(big, big), s_inf); + EXPECT_EQ(saturated::AverageScalar(max, max), s_inf); + EXPECT_EQ(saturated::AverageScalar(big, -big), 0.0f); + EXPECT_EQ(saturated::AverageScalar(max, -max), 0.0f); + EXPECT_EQ(saturated::AverageScalar(-big, big), 0.0f); + EXPECT_EQ(saturated::AverageScalar(-max, max), 0.0f); + EXPECT_EQ(saturated::AverageScalar(-big, -big), -s_inf); + EXPECT_EQ(saturated::AverageScalar(-max, -max), -s_inf); + + EXPECT_EQ(saturated::AverageScalar(inf, inf), s_inf); + EXPECT_EQ(saturated::AverageScalar(-inf, -inf), -s_inf); + EXPECT_TRUE(std::isnan(saturated::AverageScalar(-inf, inf))); + EXPECT_TRUE(std::isnan(saturated::AverageScalar(inf, -inf))); + } + { + const Scalar inf = std::numeric_limits::infinity(); + const Scalar max = std::numeric_limits::max(); + const Scalar big = max * 0.5f; + + EXPECT_EQ(saturated::AverageScalar(big, big), s_big); + EXPECT_EQ(saturated::AverageScalar(max, max), s_max); + EXPECT_EQ(saturated::AverageScalar(big, -big), 0.0f); + EXPECT_EQ(saturated::AverageScalar(max, -max), 0.0f); + EXPECT_EQ(saturated::AverageScalar(-big, big), 0.0f); + EXPECT_EQ(saturated::AverageScalar(-max, max), 0.0f); + EXPECT_EQ(saturated::AverageScalar(-big, -big), -s_big); + EXPECT_EQ(saturated::AverageScalar(-max, -max), -s_max); + + EXPECT_EQ(saturated::AverageScalar(inf, inf), s_inf); + EXPECT_EQ(saturated::AverageScalar(-inf, -inf), -s_inf); + EXPECT_TRUE(std::isnan(saturated::AverageScalar(-inf, s_inf))); + EXPECT_TRUE(std::isnan(saturated::AverageScalar(inf, -s_inf))); + } +} + +TEST(SaturatedMath, CastingFiniteDoubleToFloatStaysFinite) { + const double d_max = std::numeric_limits::max(); + const float f_max = std::numeric_limits::max(); + + { + const float result = saturated::Cast(d_max); + EXPECT_EQ(result, f_max); + } + + { + const float result = saturated::Cast(-d_max); + EXPECT_EQ(result, -f_max); + } +} + +TEST(SaturatedMath, CastingInfiniteDoubleToFloatStaysInfinite) { + const double d_inf = std::numeric_limits::infinity(); + const float f_max = std::numeric_limits::infinity(); + + { + const float result = saturated::Cast(d_inf); + EXPECT_EQ(result, f_max); + } + + { + const float result = saturated::Cast(-d_inf); + EXPECT_EQ(result, -f_max); + } +} + +TEST(SaturatedMath, CastingNaNDoubleToFloatStaysNaN) { + const double d_nan = std::numeric_limits::quiet_NaN(); + + { + const float result = saturated::Cast(d_nan); + EXPECT_TRUE(std::isnan(result)); + } + + { + const float result = saturated::Cast(-d_nan); + EXPECT_TRUE(std::isnan(result)); + } +} + +TEST(SaturatedMath, CastingLargeScalarToSignedIntProducesLimit) { + // larger than even any [u]int64_t; + const Scalar large = 1e20f; + + { + const auto result = saturated::Cast(large); + EXPECT_EQ(result, int8_t(0x7F)); + } + { + const auto result = saturated::Cast(-large); + EXPECT_EQ(result, int8_t(0x80)); + } + + { + const auto result = saturated::Cast(large); + EXPECT_EQ(result, int16_t(0x7FFF)); + } + { + const auto result = saturated::Cast(-large); + EXPECT_EQ(result, int16_t(0x8000)); + } + + { + const auto result = saturated::Cast(large); + EXPECT_EQ(result, int32_t(0x7FFFFFFF)); + } + { + const auto result = saturated::Cast(-large); + EXPECT_EQ(result, int32_t(0x80000000)); + } + + { + const auto result = saturated::Cast(large); + EXPECT_EQ(result, int64_t(0x7FFFFFFFFFFFFFFF)); + } + { + const auto result = saturated::Cast(-large); + EXPECT_EQ(result, int64_t(0x8000000000000000)); + } +} + +TEST(SaturatedMath, CastingInfiniteScalarToSignedIntProducesLimit) { + // larger than even any [u]int64_t; + const Scalar inf = std::numeric_limits::infinity(); + + { + const auto result = saturated::Cast(inf); + EXPECT_EQ(result, int8_t(0x7F)); + } + { + const auto result = saturated::Cast(-inf); + EXPECT_EQ(result, int8_t(0x80)); + } + + { + const auto result = saturated::Cast(inf); + EXPECT_EQ(result, int16_t(0x7FFF)); + } + { + const auto result = saturated::Cast(-inf); + EXPECT_EQ(result, int16_t(0x8000)); + } + + { + const auto result = saturated::Cast(inf); + EXPECT_EQ(result, int32_t(0x7FFFFFFF)); + } + { + const auto result = saturated::Cast(-inf); + EXPECT_EQ(result, int32_t(0x80000000)); + } + + { + const auto result = saturated::Cast(inf); + EXPECT_EQ(result, int64_t(0x7FFFFFFFFFFFFFFF)); + } + { + const auto result = saturated::Cast(-inf); + EXPECT_EQ(result, int64_t(0x8000000000000000)); + } +} + +TEST(SaturatedMath, CastingNaNScalarToSignedIntProducesZero) { + // larger than even any [u]int64_t; + const Scalar nan = std::numeric_limits::quiet_NaN(); + + { + const auto result = saturated::Cast(nan); + EXPECT_EQ(result, int8_t(0)); + } + + { + const auto result = saturated::Cast(nan); + EXPECT_EQ(result, int16_t(0)); + } + + { + const auto result = saturated::Cast(nan); + EXPECT_EQ(result, int32_t(0)); + } + + { + const auto result = saturated::Cast(nan); + EXPECT_EQ(result, int64_t(0)); + } +} + +} // namespace testing +} // namespace impeller diff --git a/engine/src/flutter/impeller/playground/imgui/imgui_impl_impeller.cc b/engine/src/flutter/impeller/playground/imgui/imgui_impl_impeller.cc index ada717b742..1cc48386e4 100644 --- a/engine/src/flutter/impeller/playground/imgui/imgui_impl_impeller.cc +++ b/engine/src/flutter/impeller/playground/imgui/imgui_impl_impeller.cc @@ -239,7 +239,7 @@ void ImGui_ImplImpeller_RenderDrawData(ImDrawData* draw_data, render_pass.SetCommandLabel(impeller::SPrintF( "ImGui draw list %d (command %d)", draw_list_i, cmd_i)); render_pass.SetViewport(viewport); - render_pass.SetScissor(impeller::IRect(clip_rect)); + render_pass.SetScissor(impeller::IRect::RoundOut(clip_rect)); render_pass.SetPipeline(bd->pipeline); VS::BindUniformBuffer(render_pass, vtx_uniforms); FS::BindTex(render_pass, bd->font_texture, bd->sampler);