Handle NaN in PLS paths and transforms

Remove an assertion from Mat2D::mapBoundingBox() and instead make it explicitly return {0} when the points are empty or all NaN.

Introduce a "clipIsEmpty" boolean to the render stack so we can bail from draws early when the clip has NaN or empty paths. In the future we can take further advantage of this feature by marking the clip stack empty when its elements have an empty intersection.

Diffs=
7d0125c92 Handle NaN in PLS paths and transforms (#7176)

Co-authored-by: Chris Dalton <99840794+csmartdalton@users.noreply.github.com>
diff --git a/.rive_head b/.rive_head
index 6f48914..8e33160 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-e96739328b7eb82870c5a642f6e3213c7b423d51
+7d0125c9256d329a11844184e6cd1244f3496012
diff --git a/include/rive/math/aabb.hpp b/include/rive/math/aabb.hpp
index 4c79aed..2eabd8c 100644
--- a/include/rive/math/aabb.hpp
+++ b/include/rive/math/aabb.hpp
@@ -75,6 +75,12 @@
     Vec2D size() const { return {width(), height()}; }
     Vec2D center() const { return {(minX + maxX) * 0.5f, (minY + maxY) * 0.5f}; }
 
+    bool isEmptyOrNaN() const
+    {
+        // Use "inverse" logic so we return true if either of the comparisons fail due to a NaN.
+        return !(width() > 0 && height() > 0);
+    }
+
     AABB inset(float dx, float dy) const
     {
         AABB r = {minX + dx, minY + dy, maxX - dx, maxY - dy};
diff --git a/include/rive/math/mat2d.hpp b/include/rive/math/mat2d.hpp
index 8576cd1..dd1414b 100644
--- a/include/rive/math/mat2d.hpp
+++ b/include/rive/math/mat2d.hpp
@@ -46,7 +46,8 @@
     void mapPoints(Vec2D dst[], const Vec2D pts[], size_t n) const;
 
     // Computes a bounding box that would tightly contain the given points if they were to all be
-    // transformed by this matrix.
+    // transformed by this matrix, ignoring NaN values.
+    // Returns {0, 0, 0, 0} if the given points are empty or all NaN.
     AABB mapBoundingBox(const Vec2D pts[], size_t n) const;
     AABB mapBoundingBox(const AABB&) const;
 
diff --git a/src/math/mat2d.cpp b/src/math/mat2d.cpp
index 223e1b6..d88edbf 100644
--- a/src/math/mat2d.cpp
+++ b/src/math/mat2d.cpp
@@ -90,11 +90,6 @@
 
 AABB Mat2D::mapBoundingBox(const Vec2D pts[], size_t n) const
 {
-    if (n == 0)
-    {
-        return {0, 0, 0, 0};
-    }
-
     size_t i = 0;
     float4 scale = float2{m_buffer[0], m_buffer[3]}.xyxy;
     float4 skew = simd::load2f(&m_buffer[1]).yxyx;
@@ -140,10 +135,16 @@
     }
 
     float4 bbox = simd::join(simd::min(mins.xy, mins.zw), simd::max(maxes.xy, maxes.zw));
-    assert(simd::all(bbox.xy <= bbox.zw));
-
-    float4 trans = simd::load2f(&m_buffer[4]).xyxy;
-    bbox += trans;
+    if (!simd::all(bbox.xy <= bbox.zw))
+    {
+        // The given points were NaN or empty.
+        bbox = float4(0);
+    }
+    else
+    {
+        float4 trans = simd::load2f(&m_buffer[4]).xyxy;
+        bbox += trans;
+    }
 
     return math::bit_cast<AABB>(bbox);
 }
diff --git a/test/aabb_test.cpp b/test/aabb_test.cpp
index 4adafea..0f06abe 100644
--- a/test/aabb_test.cpp
+++ b/test/aabb_test.cpp
@@ -29,4 +29,24 @@
                 std::numeric_limits<int32_t>::min()}
               .empty());
 }
+
+TEST_CASE("isEmptyOrNaN", "[AABB]")
+{
+    auto inf = std::numeric_limits<float>::infinity();
+    auto nan = std::numeric_limits<float>::quiet_NaN();
+    CHECK(!AABB{0, 0, 1, 1}.isEmptyOrNaN());
+    CHECK(!AABB{-inf, -inf, inf, inf}.isEmptyOrNaN());
+    CHECK(AABB{0, 0, 0, 0}.isEmptyOrNaN());
+    CHECK(AABB{0, 0, -1, -2}.isEmptyOrNaN());
+    CHECK(AABB{inf, inf, -inf, -inf}.isEmptyOrNaN());
+    CHECK(AABB{inf, -inf, -inf, inf}.isEmptyOrNaN());
+    CHECK(AABB{-inf, inf, inf, -inf}.isEmptyOrNaN());
+    CHECK(AABB{nan, 0, 10, 10}.isEmptyOrNaN());
+    CHECK(AABB{0, nan, 10, 10}.isEmptyOrNaN());
+    CHECK(AABB{0, 0, nan, 10}.isEmptyOrNaN());
+    CHECK(AABB{0, 0, 10, nan}.isEmptyOrNaN());
+    CHECK(AABB{nan, nan, 10, 10}.isEmptyOrNaN());
+    CHECK(AABB{nan, nan, nan, 10}.isEmptyOrNaN());
+    CHECK(AABB{nan, nan, nan, nan}.isEmptyOrNaN());
+}
 } // namespace rive
diff --git a/test/mat2d_test.cpp b/test/mat2d_test.cpp
index aa904ea..af1c559 100644
--- a/test/mat2d_test.cpp
+++ b/test/mat2d_test.cpp
@@ -233,5 +233,17 @@
     checkMatrix(Mat2D(-12, -13, -14, -15, -16, -17));
     checkMatrix(Mat2D(18, 19, 20, 21, 22, 23));
     checkMatrix(Mat2D(-25, 26, 27, -28, 29, -30));
+
+    // Mapping empty or NaN points returns 0.
+    CHECK(Mat2D().mapBoundingBox(nullptr, 0) == AABB());
+    auto nan = std::numeric_limits<float>::quiet_NaN();
+    CHECK(Mat2D().mapBoundingBox(AABB{nan, nan, nan, nan}) == AABB());
+
+    // NaN values are otherwise ignored.
+    CHECK(Mat2D().mapBoundingBox(AABB{-1, -1, 1, 1}) == AABB{-1, -1, 1, 1});
+    CHECK(Mat2D().mapBoundingBox(AABB{nan, -1, 1, 1}) == AABB{1, -1, 1, 1});
+    CHECK(Mat2D().mapBoundingBox(AABB{-1, nan, 1, 1}) == AABB{-1, 1, 1, 1});
+    CHECK(Mat2D().mapBoundingBox(AABB{-1, -1, nan, 1}) == AABB{-1, -1, -1, 1});
+    CHECK(Mat2D().mapBoundingBox(AABB{-1, -1, 1, nan}) == AABB{-1, -1, 1, -1});
 }
 } // namespace rive