diff --git a/.rive_head b/.rive_head index 33352633..28044705 100644 --- a/.rive_head +++ b/.rive_head @@ -1 +1 @@ -b765280df32889e6df18690d840fe8f240871c09 +5cb42a9b0033a1b9d2360292b7ef14bf67e3d45e diff --git a/include/rive/shapes/shape.hpp b/include/rive/shapes/shape.hpp index b44bfc3b..3f2c6869 100644 --- a/include/rive/shapes/shape.hpp +++ b/include/rive/shapes/shape.hpp @@ -46,6 +46,9 @@ class Shape : public ShapeBase, public ShapePaintContainer StatusCode onAddedDirty(CoreContext* context) override; bool isEmpty(); void pathCollapseChanged(); + + AABB computeWorldBounds(const Mat2D* xform = nullptr) const; + AABB computeLocalBounds() const; }; } // namespace rive diff --git a/src/shapes/shape.cpp b/src/shapes/shape.cpp index 86941052..820b5b1f 100644 --- a/src/shapes/shape.cpp +++ b/src/shapes/shape.cpp @@ -7,6 +7,7 @@ #include "rive/shapes/paint/shape_paint.hpp" #include "rive/shapes/path_composer.hpp" #include "rive/clip_result.hpp" +#include "rive/math/raw_path.hpp" #include using namespace rive; @@ -216,3 +217,76 @@ bool Shape::isEmpty() // Do constraints need to be marked as dirty too? From tests it doesn't seem they do. void Shape::pathCollapseChanged() { m_PathComposer.pathCollapseChanged(); } + +class ComputeBoundsCommandPath : public CommandPath +{ +public: + ComputeBoundsCommandPath() {} + + AABB bounds(const Mat2D& xform) + { + m_rawPath.transformInPlace(xform); + return m_rawPath.bounds(); + } + + void rewind() override { m_rawPath.rewind(); } + void fillRule(FillRule value) override {} + void addPath(CommandPath* path, const Mat2D& transform) override { assert(false); } + + void moveTo(float x, float y) override { m_rawPath.moveTo(x, y); } + void lineTo(float x, float y) override { m_rawPath.lineTo(x, y); } + void cubicTo(float ox, float oy, float ix, float iy, float x, float y) override + { + m_rawPath.cubicTo(ox, oy, ix, iy, x, y); + } + void close() override { m_rawPath.close(); } + + RenderPath* renderPath() override + { + assert(false); + return nullptr; + } + +private: + RawPath m_rawPath; +}; + +AABB Shape::computeWorldBounds(const Mat2D* xform) const +{ + bool first = true; + AABB computedBounds = AABB::forExpansion(); + + ComputeBoundsCommandPath boundsCalculator; + for (auto path : m_Paths) + { + if (path->isCollapsed()) + { + continue; + } + + path->buildPath(boundsCalculator); + + AABB aabb = boundsCalculator.bounds(xform == nullptr ? path->pathTransform() + : path->pathTransform() * *xform); + + if (first) + { + first = false; + computedBounds = aabb; + } + else + { + computedBounds.expand(aabb); + } + boundsCalculator.rewind(); + } + + return computedBounds; +} + +AABB Shape::computeLocalBounds() const +{ + const Mat2D& world = worldTransform(); + Mat2D inverseWorld = world.invertOrIdentity(); + return computeWorldBounds(&inverseWorld); +} \ No newline at end of file diff --git a/test/assets/background_measure.riv b/test/assets/background_measure.riv new file mode 100644 index 00000000..7fe885f0 Binary files /dev/null and b/test/assets/background_measure.riv differ diff --git a/test/bounds_test.cpp b/test/bounds_test.cpp new file mode 100644 index 00000000..99afa1e0 --- /dev/null +++ b/test/bounds_test.cpp @@ -0,0 +1,48 @@ +#include "rive/file.hpp" +#include "rive/node.hpp" +#include "rive/shapes/shape.hpp" +#include "rive/math/transform_components.hpp" +#include "rive/text/text_value_run.hpp" +#include "utils/no_op_renderer.hpp" +#include "rive_file_reader.hpp" +#include "rive_testing.hpp" +#include + +TEST_CASE("compute bounds of background shape", "[bounds]") +{ + auto file = ReadRiveFile("../../test/assets/background_measure.riv"); + + auto artboard = file->artboard(); + + REQUIRE(artboard->find("background") != nullptr); + auto background = artboard->find("background"); + REQUIRE(artboard->find("nameRun") != nullptr); + auto name = artboard->find("nameRun"); + artboard->advance(0.0f); + + auto bounds = background->computeWorldBounds(); + CHECK(bounds.width() == Approx(42.010925f)); + CHECK(bounds.height() == Approx(29.995453f)); + + // Change the text and verify the bounds extended further. + name->text("much much longer"); + artboard->advance(0.0f); + + bounds = background->computeWorldBounds(); + CHECK(bounds.width() == Approx(138.01093f)); + CHECK(bounds.height() == Approx(29.995453f)); + + // Apply a transform to the whole artboard. + rive::Mat2D& world = artboard->mutableWorldTransform(); + world.scaleByValues(0.5f, 0.5f); + artboard->markWorldTransformDirty(); + artboard->advance(0.0f); + + bounds = background->computeWorldBounds(); + CHECK(bounds.width() == Approx(138.01093f / 2.0f)); + CHECK(bounds.height() == Approx(29.995453f / 2.0f)); + + bounds = background->computeLocalBounds(); + CHECK(bounds.width() == Approx(138.01093f)); + CHECK(bounds.height() == Approx(29.995453f)); +}