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