add clipResult enum and render clips to copy the editor behavior

This PR changes how clipping works in the runtime in order to mimic how the editor works.
A hidden path will now behave as an empty path effectively fully hiding the clipped element instead of being skipped.
It also introduces a performance improvement in case shapes don't have any visible paths. The ClipResult enum is used to differentiate between clipping with at least an empty path or no empty paths at all.
For now, that improvement will only be used for hidden paths but an upcoming PR will also use it for collapsed paths inside solos.

Diffs=
e717ed98a add clipResult enum and render clips to copy the editor behavior (#6218)

Co-authored-by: hernan <hernan@rive.app>
diff --git a/.rive_head b/.rive_head
index 08fea99..46863b2 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-faba3ff515e612f2fbc8e65dff11095f9a0dbbe2
+e717ed98acc5d89c3588d595e5c0735b8fbc07f0
diff --git a/include/rive/clip_result.hpp b/include/rive/clip_result.hpp
new file mode 100644
index 0000000..7b9b7dc
--- /dev/null
+++ b/include/rive/clip_result.hpp
@@ -0,0 +1,12 @@
+#ifndef _RIVE_CLIP_RESULT_HPP_
+#define _RIVE_CLIP_RESULT_HPP_
+namespace rive
+{
+enum class ClipResult : unsigned char
+{
+    noClip = 0,
+    clip = 1,
+    emptyClip = 2
+};
+}
+#endif
\ No newline at end of file
diff --git a/include/rive/drawable.hpp b/include/rive/drawable.hpp
index 08a40c2..0e4c494 100644
--- a/include/rive/drawable.hpp
+++ b/include/rive/drawable.hpp
@@ -3,6 +3,7 @@
 #include "rive/generated/drawable_base.hpp"
 #include "rive/hit_info.hpp"
 #include "rive/renderer.hpp"
+#include "rive/clip_result.hpp"
 #include <vector>
 
 namespace rive
@@ -25,7 +26,7 @@
 
 public:
     BlendMode blendMode() const { return (BlendMode)blendModeValue(); }
-    bool clip(Renderer* renderer) const;
+    ClipResult clip(Renderer* renderer) const;
     virtual void draw(Renderer* renderer) = 0;
     virtual Core* hitTest(HitInfo*, const Mat2D&) = 0;
     void addClippingShape(ClippingShape* shape);
diff --git a/include/rive/shapes/path.hpp b/include/rive/shapes/path.hpp
index 4015147..5293faf 100644
--- a/include/rive/shapes/path.hpp
+++ b/include/rive/shapes/path.hpp
@@ -56,6 +56,7 @@
     virtual void markPathDirty();
     virtual bool isPathClosed() const { return true; }
     void onDirty(ComponentDirt dirt) override;
+    inline bool isHidden() const { return (pathFlags() & 0x1) == 0x1; }
 #ifdef ENABLE_QUERY_FLAT_VERTICES
     FlattenedPath* makeFlat(bool transformToParent);
 #endif
diff --git a/include/rive/shapes/shape.hpp b/include/rive/shapes/shape.hpp
index 75b9a66..2696c39 100644
--- a/include/rive/shapes/shape.hpp
+++ b/include/rive/shapes/shape.hpp
@@ -44,6 +44,7 @@
     void pathChanged();
     void addDefaultPathSpace(PathSpace space);
     StatusCode onAddedDirty(CoreContext* context) override;
+    bool isEmpty();
 };
 } // namespace rive
 
diff --git a/src/drawable.cpp b/src/drawable.cpp
index 32b86ec..5f41d68 100644
--- a/src/drawable.cpp
+++ b/src/drawable.cpp
@@ -3,16 +3,17 @@
 #include "rive/shapes/clipping_shape.hpp"
 #include "rive/shapes/path_composer.hpp"
 #include "rive/shapes/shape.hpp"
+#include "rive/clip_result.hpp"
 
 using namespace rive;
 
 void Drawable::addClippingShape(ClippingShape* shape) { m_ClippingShapes.push_back(shape); }
 
-bool Drawable::clip(Renderer* renderer) const
+ClipResult Drawable::clip(Renderer* renderer) const
 {
     if (m_ClippingShapes.size() == 0)
     {
-        return false;
+        return ClipResult::noClip;
     }
 
     renderer->save();
@@ -30,6 +31,12 @@
         {
             renderer->clipPath(renderPath);
         }
+        else
+        {
+            // If one renderPath is null we exit early because we are treating it
+            // as an empty path and its intersection will always be an empty path
+            return ClipResult::emptyClip;
+        }
     }
-    return true;
+    return ClipResult::clip;
 }
\ No newline at end of file
diff --git a/src/nested_artboard.cpp b/src/nested_artboard.cpp
index 25bd218..8b80df4 100644
--- a/src/nested_artboard.cpp
+++ b/src/nested_artboard.cpp
@@ -5,6 +5,7 @@
 #include "rive/importers/backboard_importer.hpp"
 #include "rive/nested_animation.hpp"
 #include "rive/animation/nested_state_machine.hpp"
+#include "rive/clip_result.hpp"
 #include <cassert>
 
 using namespace rive;
@@ -57,14 +58,18 @@
     {
         return;
     }
-    if (!clip(renderer))
+    ClipResult clipResult = clip(renderer);
+    if (clipResult == ClipResult::noClip)
     {
         // We didn't clip, so make sure to save as we'll be doing some
         // transformations.
         renderer->save();
     }
-    renderer->transform(worldTransform());
-    m_Artboard->draw(renderer);
+    if (clipResult != ClipResult::emptyClip)
+    {
+        renderer->transform(worldTransform());
+        m_Artboard->draw(renderer);
+    }
     renderer->restore();
 }
 
diff --git a/src/shapes/clipping_shape.cpp b/src/shapes/clipping_shape.cpp
index f904194..8d68908 100644
--- a/src/shapes/clipping_shape.cpp
+++ b/src/shapes/clipping_shape.cpp
@@ -97,7 +97,7 @@
         m_ClipRenderPath = nullptr;
         for (auto shape : m_Shapes)
         {
-            if (!shape->isHidden())
+            if (!shape->isEmpty())
             {
                 m_RenderPath->addPath(shape->pathComposer()->worldPath(), identity);
                 m_ClipRenderPath = m_RenderPath.get();
diff --git a/src/shapes/image.cpp b/src/shapes/image.cpp
index df51c66..fac4a59 100644
--- a/src/shapes/image.cpp
+++ b/src/shapes/image.cpp
@@ -6,6 +6,7 @@
 #include "rive/assets/image_asset.hpp"
 #include "rive/shapes/mesh.hpp"
 #include "rive/artboard.hpp"
+#include "rive/clip_result.hpp"
 
 using namespace rive;
 
@@ -24,25 +25,30 @@
         return;
     }
 
-    if (!clip(renderer))
+    ClipResult clipResult = clip(renderer);
+
+    if (clipResult == ClipResult::noClip)
     {
         // We didn't clip, so make sure to save as we'll be doing some
         // transformations.
         renderer->save();
     }
 
-    auto width = renderImage->width();
-    auto height = renderImage->height();
+    if (clipResult != ClipResult::emptyClip)
+    {
+        auto width = renderImage->width();
+        auto height = renderImage->height();
 
-    if (m_Mesh != nullptr)
-    {
-        m_Mesh->draw(renderer, renderImage, blendMode(), renderOpacity());
-    }
-    else
-    {
-        renderer->transform(worldTransform());
-        renderer->translate(-width * originX(), -height * originY());
-        renderer->drawImage(renderImage, blendMode(), renderOpacity());
+        if (m_Mesh != nullptr)
+        {
+            m_Mesh->draw(renderer, renderImage, blendMode(), renderOpacity());
+        }
+        else
+        {
+            renderer->transform(worldTransform());
+            renderer->translate(-width * originX(), -height * originY());
+            renderer->drawImage(renderImage, blendMode(), renderOpacity());
+        }
     }
 
     renderer->restore();
diff --git a/src/shapes/path_composer.cpp b/src/shapes/path_composer.cpp
index 34faf3d..6e4764e 100644
--- a/src/shapes/path_composer.cpp
+++ b/src/shapes/path_composer.cpp
@@ -59,8 +59,11 @@
             // Get all the paths into local shape space.
             for (auto path : m_Shape->paths())
             {
-                const auto localTransform = inverseWorld * path->pathTransform();
-                m_LocalPath->addPath(path->commandPath(), localTransform);
+                if (!path->isHidden())
+                {
+                    const auto localTransform = inverseWorld * path->pathTransform();
+                    m_LocalPath->addPath(path->commandPath(), localTransform);
+                }
             }
         }
         if ((space & PathSpace::World) == PathSpace::World)
@@ -77,8 +80,11 @@
             }
             for (auto path : m_Shape->paths())
             {
-                const Mat2D& transform = path->pathTransform();
-                m_WorldPath->addPath(path->commandPath(), transform);
+                if (!path->isHidden())
+                {
+                    const Mat2D& transform = path->pathTransform();
+                    m_WorldPath->addPath(path->commandPath(), transform);
+                }
             }
         }
     }
diff --git a/src/shapes/shape.cpp b/src/shapes/shape.cpp
index 9abbf75..33dd3f9 100644
--- a/src/shapes/shape.cpp
+++ b/src/shapes/shape.cpp
@@ -6,6 +6,7 @@
 #include "rive/shapes/paint/blend_mode.hpp"
 #include "rive/shapes/paint/shape_paint.hpp"
 #include "rive/shapes/path_composer.hpp"
+#include "rive/clip_result.hpp"
 #include <algorithm>
 
 using namespace rive;
@@ -74,26 +75,30 @@
     {
         return;
     }
-    auto shouldRestore = clip(renderer);
+    ClipResult clipResult = clip(renderer);
 
-    for (auto shapePaint : m_ShapePaints)
+    if (clipResult != ClipResult::emptyClip)
     {
-        if (!shapePaint->isVisible())
+        for (auto shapePaint : m_ShapePaints)
         {
-            continue;
+            if (!shapePaint->isVisible())
+            {
+                continue;
+            }
+            renderer->save();
+            bool paintsInLocal = (shapePaint->pathSpace() & PathSpace::Local) == PathSpace::Local;
+            if (paintsInLocal)
+            {
+                renderer->transform(worldTransform());
+            }
+            shapePaint->draw(renderer,
+                             paintsInLocal ? m_PathComposer.localPath()
+                                           : m_PathComposer.worldPath());
+            renderer->restore();
         }
-        renderer->save();
-        bool paintsInLocal = (shapePaint->pathSpace() & PathSpace::Local) == PathSpace::Local;
-        if (paintsInLocal)
-        {
-            renderer->transform(worldTransform());
-        }
-        shapePaint->draw(renderer,
-                         paintsInLocal ? m_PathComposer.localPath() : m_PathComposer.worldPath());
-        renderer->restore();
     }
 
-    if (shouldRestore)
+    if (clipResult != ClipResult::noClip)
     {
         renderer->restore();
     }
@@ -193,3 +198,15 @@
     // This ensures context propagates to path composer too.
     return m_PathComposer.onAddedDirty(context);
 }
+
+bool Shape::isEmpty()
+{
+    for (auto path : m_Paths)
+    {
+        if (!path->isHidden())
+        {
+            return false;
+        }
+    }
+    return true;
+}
diff --git a/src/text/text.cpp b/src/text/text.cpp
index 14bb666..56bbbfd 100644
--- a/src/text/text.cpp
+++ b/src/text/text.cpp
@@ -10,6 +10,7 @@
 #include "rive/shapes/paint/shape_paint.hpp"
 #include "rive/artboard.hpp"
 #include "rive/factory.hpp"
+#include "rive/clip_result.hpp"
 
 void GlyphItr::tryAdvanceRun()
 {
@@ -484,20 +485,25 @@
 
 void Text::draw(Renderer* renderer)
 {
-    if (!clip(renderer))
+
+    ClipResult clipResult = clip(renderer);
+    if (clipResult == ClipResult::noClip)
     {
         // We didn't clip, so make sure to save as we'll be doing some
         // transformations.
         renderer->save();
     }
-    renderer->transform(m_WorldTransform);
-    if (overflow() == TextOverflow::clipped && m_clipRenderPath)
+    if (clipResult != ClipResult::emptyClip)
     {
-        renderer->clipPath(m_clipRenderPath.get());
-    }
-    for (auto style : m_renderStyles)
-    {
-        style->draw(renderer);
+        renderer->transform(m_WorldTransform);
+        if (overflow() == TextOverflow::clipped && m_clipRenderPath)
+        {
+            renderer->clipPath(m_clipRenderPath.get());
+        }
+        for (auto style : m_renderStyles)
+        {
+            style->draw(renderer);
+        }
     }
     renderer->restore();
 }
diff --git a/test/assets/clip_tests.riv b/test/assets/clip_tests.riv
new file mode 100644
index 0000000..2505f3c
--- /dev/null
+++ b/test/assets/clip_tests.riv
Binary files differ
diff --git a/test/clip_test.cpp b/test/clip_test.cpp
index 79e7fcc..1a3cb99 100644
--- a/test/clip_test.cpp
+++ b/test/clip_test.cpp
@@ -1,3 +1,4 @@
+#include <rive/clip_result.hpp>
 #include <rive/file.hpp>
 #include <rive/node.hpp>
 #include <rive/shapes/clipping_shape.hpp>
@@ -88,3 +89,65 @@
         REQUIRE(points[3] == rive::Vec2D(-250.0f, 250.0f));
     }
 }
+
+TEST_CASE("Shape does not have any clipping paths visible", "[clipping]")
+{
+    ClippingFactory factory;
+    auto file = ReadRiveFile("../../test/assets/clip_tests.riv", &factory);
+
+    auto artboard = file->artboard("Empty-Shape");
+    REQUIRE(artboard != nullptr);
+    artboard->updateComponents();
+    auto node = artboard->find("Ellipse-clipper");
+    REQUIRE(node != nullptr);
+    REQUIRE(node->is<rive::Shape>());
+    rive::Shape* shape = static_cast<rive::Shape*>(node);
+    REQUIRE(shape->isEmpty() == true);
+    auto clippedNode = artboard->find("Rectangle-clipped");
+    REQUIRE(clippedNode != nullptr);
+    REQUIRE(clippedNode->is<rive::Shape>());
+    rive::Shape* clippedShape = static_cast<rive::Shape*>(clippedNode);
+    rive::NoOpRenderer renderer;
+    auto clipResult = clippedShape->clip(&renderer);
+    REQUIRE(clipResult == rive::ClipResult::emptyClip);
+}
+
+TEST_CASE("Shape has at least a clipping path visible", "[clipping]")
+{
+    ClippingFactory factory;
+    auto file = ReadRiveFile("../../test/assets/clip_tests.riv", &factory);
+
+    auto artboard = file->artboard("Hidden-Path-Visible-Path");
+    REQUIRE(artboard != nullptr);
+    artboard->updateComponents();
+    auto node = artboard->find("Ellipse-clipper");
+    REQUIRE(node != nullptr);
+    REQUIRE(node->is<rive::Shape>());
+    rive::Shape* shape = static_cast<rive::Shape*>(node);
+    REQUIRE(shape->isEmpty() == false);
+    auto clippedNode = artboard->find("Rectangle-clipped");
+    REQUIRE(clippedNode != nullptr);
+    REQUIRE(clippedNode->is<rive::Shape>());
+    rive::Shape* clippedShape = static_cast<rive::Shape*>(clippedNode);
+    rive::NoOpRenderer renderer;
+    auto clipResult = clippedShape->clip(&renderer);
+    REQUIRE(clipResult == rive::ClipResult::clip);
+}
+
+TEST_CASE("Shape returns an empty clip when one clipping shape is empty", "[clipping]")
+{
+    ClippingFactory factory;
+    auto file = ReadRiveFile("../../test/assets/clip_tests.riv", &factory);
+
+    auto artboard = file->artboard("One-Clipping-Shape-Visible-One-Hidden");
+    REQUIRE(artboard != nullptr);
+    artboard->updateComponents();
+    auto node = artboard->find("Rectangle-clipped");
+    REQUIRE(node != nullptr);
+    REQUIRE(node->is<rive::Shape>());
+    rive::Shape* shape = static_cast<rive::Shape*>(node);
+
+    rive::NoOpRenderer renderer;
+    auto clipResult = shape->clip(&renderer);
+    REQUIRE(clipResult == rive::ClipResult::emptyClip);
+}
\ No newline at end of file