Unity compute bounds

This is something that the Pocketwatch team requested but I found myself needing it when doing a Unity test this weekend.

It adds the ability to dynamically retrieve the dimensions of any Shape from C++.

In Unity, it uses a similar API to GameKit where it exposes elements of the Artboard as "Components" and then you can call different methods that will only function on certain types but it hides needing to expose all different kinds of types to the end user.

I also added the ability to change a run's value so I could check that the shape was resizing properly from Unity. @HayesGordon we really should land your text run api too, but we should modify it to use the same component logic (might require changing from the base TransformComponent to just Component in the C++ plugin code).

The Unity Artboard Components are wrapped and track the owning Artboard. This is to make sure that if the C# code releases all references of Artboard while someone is still holding a Component, Artboard will still be available at the C++ level (so you can't accidentally create a race condition that causes a crash in native). I didn't need to do this for the Text Runs as I just have a set api that doesn't store the TextValueRun reference in Unity yet.

Diffs=
5cb42a9b0 Unity compute bounds (#6649)

Co-authored-by: Luigi Rosso <luigi-rosso@users.noreply.github.com>
diff --git a/.rive_head b/.rive_head
index 3335263..2804470 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 b44bfc3..3f2c686 100644
--- a/include/rive/shapes/shape.hpp
+++ b/include/rive/shapes/shape.hpp
@@ -46,6 +46,9 @@
     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 8694105..820b5b1 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 <algorithm>
 
 using namespace rive;
@@ -216,3 +217,76 @@
 
 // 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 0000000..7fe885f
--- /dev/null
+++ b/test/assets/background_measure.riv
Binary files differ
diff --git a/test/bounds_test.cpp b/test/bounds_test.cpp
new file mode 100644
index 0000000..99afa1e
--- /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 <cstdio>
+
+TEST_CASE("compute bounds of background shape", "[bounds]")
+{
+    auto file = ReadRiveFile("../../test/assets/background_measure.riv");
+
+    auto artboard = file->artboard();
+
+    REQUIRE(artboard->find<rive::Shape>("background") != nullptr);
+    auto background = artboard->find<rive::Shape>("background");
+    REQUIRE(artboard->find<rive::TextValueRun>("nameRun") != nullptr);
+    auto name = artboard->find<rive::TextValueRun>("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));
+}