From 64e292e6e1897d7749dac079da4ef263f0e7bec5 Mon Sep 17 00:00:00 2001 From: Jim Graham Date: Fri, 19 Jan 2024 10:27:04 -0800 Subject: [PATCH] [Impeller] switch Rect fields to LTRB implementation (flutter/engine#49816) Switches the Rect class implementation to an LTRB (Left, Top, Right, Bottom) set of fields for efficiency in performing rectangle operations. Additionally, the methods have been somewhat hardened to the following issues: - NaN values should always act as if the rect is empty - Helps with https://github.com/flutter/flutter/issues/132770 - Saturated math methods are added to avoid overflow for integer rects - Normalized treatment of empty rectangles in rect/rect operations - empty.Union(anything) == anything - empty.Intersection(anything) == empty - empty.IntersectsWith(anything) == false - empty.Contains(anything) == false - non-empty.Contains(empty) == true - MakeMaximum now returns finite rectangles which should produce non-nan values from all getters - A lot more testing of all of these behaviors within the unit tests In addition, the new rectangle is closer to what DisplayList and the Skia backend have been using all along so we might be able to switch to using Impeller geometry inside DisplayList to reduce overhead for both backends. --- .../flutter/ci/licenses_golden/excluded_files | 1 + .../ci/licenses_golden/licenses_flutter | 2 + .../entity/geometry/stroke_path_geometry.cc | 9 +- engine/src/flutter/impeller/geometry/BUILD.gn | 2 + .../impeller/geometry/geometry_asserts.h | 8 +- .../impeller/geometry/geometry_unittests.cc | 530 ---- engine/src/flutter/impeller/geometry/point.h | 11 + engine/src/flutter/impeller/geometry/rect.h | 528 ++-- .../impeller/geometry/rect_unittests.cc | 2465 ++++++++++++++++- .../impeller/geometry/saturated_math.h | 148 + .../geometry/saturated_math_unittests.cc | 1133 ++++++++ .../playground/imgui/imgui_impl_impeller.cc | 2 +- 12 files changed, 4070 insertions(+), 769 deletions(-) create mode 100644 engine/src/flutter/impeller/geometry/saturated_math.h create mode 100644 engine/src/flutter/impeller/geometry/saturated_math_unittests.cc 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);