diff --git a/.rive_head b/.rive_head index 036d2883..618f3804 100644 --- a/.rive_head +++ b/.rive_head @@ -1 +1 @@ -3734d9bac071052acbcfdbec576ba9532bc84e3b +405b8ef907d29cf480422b94d4717fdcdfad0824 diff --git a/include/rive/drawable_flag.hpp b/include/rive/drawable_flag.hpp index 9f79a152..c21604db 100644 --- a/include/rive/drawable_flag.hpp +++ b/include/rive/drawable_flag.hpp @@ -20,6 +20,10 @@ enum class DrawableFlag : unsigned short /// Whether this Component lets hit events pass through to components behind it Opaque = 1 << 3, + + /// Whether the computed world bounds for a shape need to be recalculated + /// Using Clean instead of dirty so it doesn't need to be initialized to 1 + WorldBoundsClean = 1 << 4, }; RIVE_MAKE_ENUM_BITSET(DrawableFlag) } // namespace rive diff --git a/include/rive/math/aabb.hpp b/include/rive/math/aabb.hpp index 2eabd8c5..6dda2ab4 100644 --- a/include/rive/math/aabb.hpp +++ b/include/rive/math/aabb.hpp @@ -121,6 +121,8 @@ class AABB return Vec2D(width() == 0.0f ? 0.0f : (point.x - left()) * 2.0f / width() - 1.0f, (height() == 0.0f ? 0.0f : point.y - top()) * 2.0f / height() - 1.0f); } + + bool contains(Vec2D position) const; }; } // namespace rive diff --git a/include/rive/shapes/shape.hpp b/include/rive/shapes/shape.hpp index 3f2c6869..830e8779 100644 --- a/include/rive/shapes/shape.hpp +++ b/include/rive/shapes/shape.hpp @@ -5,6 +5,7 @@ #include "rive/generated/shapes/shape_base.hpp" #include "rive/shapes/path_composer.hpp" #include "rive/shapes/shape_paint_container.hpp" +#include "rive/drawable_flag.hpp" #include namespace rive @@ -17,6 +18,7 @@ class Shape : public ShapeBase, public ShapePaintContainer private: PathComposer m_PathComposer; std::vector m_Paths; + AABB m_WorldBounds; bool m_WantDifferencePath = false; @@ -47,6 +49,18 @@ class Shape : public ShapeBase, public ShapePaintContainer bool isEmpty(); void pathCollapseChanged(); + AABB worldBounds() + { + if ((static_cast(drawableFlags()) & DrawableFlag::WorldBoundsClean) != + DrawableFlag::WorldBoundsClean) + { + drawableFlags(drawableFlags() | + static_cast(DrawableFlag::WorldBoundsClean)); + m_WorldBounds = computeWorldBounds(); + } + return m_WorldBounds; + } + AABB computeWorldBounds(const Mat2D* xform = nullptr) const; AABB computeLocalBounds() const; }; diff --git a/src/animation/state_machine_instance.cpp b/src/animation/state_machine_instance.cpp index 2400ced4..dd3f7858 100644 --- a/src/animation/state_machine_instance.cpp +++ b/src/animation/state_machine_instance.cpp @@ -418,15 +418,28 @@ class HitShape : public HitComponent float hitRadius = 2; Vec2D previousPosition; std::vector listeners; - HitResult processEvent(Vec2D position, ListenerType hitType, bool canHit) override + + bool hitTest(Vec2D position) const { + auto shape = m_component->as(); + auto worldBounds = shape->worldBounds(); + if (!worldBounds.contains(position)) + { + return false; + } auto hitArea = AABB(position.x - hitRadius, position.y - hitRadius, position.x + hitRadius, position.y + hitRadius) .round(); - bool isOver = canHit ? shape->hitTest(hitArea) : false; + return shape->hitTest(hitArea); + } + + HitResult processEvent(Vec2D position, ListenerType hitType, bool canHit) override + { + auto shape = m_component->as(); + bool isOver = canHit ? hitTest(position) : false; bool hoverChange = isHovered != isOver; isHovered = isOver; if (hoverChange && isHovered) diff --git a/src/math/aabb.cpp b/src/math/aabb.cpp index 23688fcc..c6fa11aa 100644 --- a/src/math/aabb.cpp +++ b/src/math/aabb.cpp @@ -80,3 +80,8 @@ void AABB::join(AABB& out, const AABB& a, const AABB& b) out.maxX = std::max(a.maxX, b.maxX); out.maxY = std::max(a.maxY, b.maxY); } + +bool AABB::contains(Vec2D point) const +{ + return point.x >= left() && point.x <= right() && point.y >= top() && point.y <= bottom(); +} diff --git a/src/shapes/shape.cpp b/src/shapes/shape.cpp index e37b4749..157e378f 100644 --- a/src/shapes/shape.cpp +++ b/src/shapes/shape.cpp @@ -70,6 +70,7 @@ bool Shape::collapse(bool value) void Shape::pathChanged() { + drawableFlags(drawableFlags() & ~static_cast(DrawableFlag::WorldBoundsClean)); m_PathComposer.addDirt(ComponentDirt::Path, true); for (auto constraint : constraints()) { diff --git a/test/aabb_test.cpp b/test/aabb_test.cpp index 0f06abe7..2e380b3b 100644 --- a/test/aabb_test.cpp +++ b/test/aabb_test.cpp @@ -49,4 +49,19 @@ TEST_CASE("isEmptyOrNaN", "[AABB]") CHECK(AABB{nan, nan, nan, 10}.isEmptyOrNaN()); CHECK(AABB{nan, nan, nan, nan}.isEmptyOrNaN()); } + +TEST_CASE("AABB contains", "[AABB]") +{ + CHECK(AABB{0, 0, 100, 100}.contains(Vec2D(20, 20))); + CHECK(AABB{0, 0, 100, 100}.contains(Vec2D(0, 0))); + CHECK(AABB{0, 0, 100, 100}.contains(Vec2D(100, 100))); + CHECK(!AABB{0, 0, 100, 100}.contains(Vec2D(200, 200))); + CHECK(!AABB{0, 0, 100, 100}.contains(Vec2D(-200, -200))); + auto leftBoundary = 0.f; + auto rightBoundary = 100.f; + CHECK(!AABB{leftBoundary, 0, rightBoundary, 100.0}.contains( + Vec2D(leftBoundary - std::numeric_limits::epsilon(), 50))); + CHECK(!AABB{leftBoundary, 0, rightBoundary, 100.0}.contains( + Vec2D(rightBoundary + rightBoundary * std::numeric_limits::epsilon(), 50))); +} } // namespace rive diff --git a/test/hittest_test.cpp b/test/hittest_test.cpp index cb4f8f8c..87b14296 100644 --- a/test/hittest_test.cpp +++ b/test/hittest_test.cpp @@ -161,6 +161,11 @@ TEST_CASE("hit test on opaque nested artboard", "[hittest]") // toggle changes value because it is not under an opaque nested artboard REQUIRE(secondGrayToggle->value() == true); + stateMachineInstance->pointerDown(rive::Vec2D(301.0f, 50.0f)); + // toggle does not change because it is beyond the area of the square by 1 pixel + // And the 2px padding is unly used after the coarse grained test passes + REQUIRE(secondGrayToggle->value() == true); + stateMachineInstance->pointerDown(rive::Vec2D(100.0f, 50.0f)); // toggle does not change because it is under an opaque nested artboard REQUIRE(secondGrayToggle->value() == true);