blob: d4db4519a4fe6242348bb6b37c03abcb779faf9e [file]
#include "rive/constraints/constraint.hpp"
#include "rive/hittest_command_path.hpp"
#include "rive/shapes/deformer.hpp"
#include "rive/shapes/path.hpp"
#include "rive/shapes/points_path.hpp"
#include "rive/shapes/parametric_path.hpp"
#include "rive/shapes/shape.hpp"
#include "rive/layout/layout_participant.hpp"
#include "rive/shapes/clipping_shape.hpp"
#include "rive/shapes/paint/blend_mode.hpp"
#include "rive/shapes/paint/shape_paint.hpp"
#include "rive/shapes/path_composer.hpp"
#include "rive/artboard.hpp"
#include "rive/clip_result.hpp"
#include "rive/math/contour_measure.hpp"
#include "rive/math/raw_path.hpp"
#include "rive/profiler/profiler_macros.h"
#include <algorithm>
#include <limits>
using namespace rive;
Shape::Shape() : m_PathComposer(this) {}
void Shape::addPath(Path* path)
{
// Make sure the path is not already in the shape.
assert(std::find(m_Paths.begin(), m_Paths.end(), path) == m_Paths.end());
m_Paths.push_back(path);
if (path->is<PointsPath>() && path->as<PointsPath>()->skin() != nullptr)
{
// Bones deform a skinned path whether or not the shape is visible.
addFlags(PathFlags::neverDeferUpdate);
}
invalidateIntrinsicBounds();
}
void Shape::addFlags(PathFlags flags) { m_pathFlags |= flags; }
bool Shape::isFlagged(PathFlags flags) const
{
return (int)(pathFlags() & flags) != 0x00;
}
bool Shape::canDeferPathUpdate()
{
if (renderOpacity() != 0 ||
isFlagged(PathFlags::clipping | PathFlags::neverDeferUpdate))
{
return false;
}
#ifdef WITH_RIVE_EDITOR
// Skins come and go under the editor, after addPath set its flag.
for (auto path : m_Paths)
{
if (path->is<PointsPath>() && path->as<PointsPath>()->skin() != nullptr)
{
return false;
}
}
#endif
return true;
}
void Shape::update(ComponentDirt value)
{
Super::update(value);
if (hasDirt(value, ComponentDirt::RenderOpacity))
{
propagateOpacity(renderOpacity());
}
}
bool Shape::collapse(bool value)
{
if (!Super::collapse(value))
{
return false;
}
m_PathComposer.collapse(value);
// Collapsed paths are skipped when measuring, so the bounds change.
invalidateIntrinsicBounds();
return true;
}
float Shape::length()
{
if (m_WorldLength < 0)
{
float l = 0;
for (auto path : m_Paths)
{
const bool pathDirty = path->hasDirt(ComponentDirt::Path |
ComponentDirt::WorldTransform |
ComponentDirt::NSlicer);
RawPath temp;
const RawPath& base =
pathDirty ? (path->buildPath(temp), temp) : path->rawPath();
RawPath source = base.transform(path->pathTransform());
ContourMeasureIter iter(&source);
while (auto contour = iter.next())
{
l += contour->length();
}
}
m_WorldLength = l;
}
return m_WorldLength;
}
void Shape::pathChanged()
{
m_PathComposer.addDirt(ComponentDirt::Path, true);
m_WorldLength = -1;
// Drop the local bounds here rather than only in markBoundsDirty(): this is
// raised for every input they have (Path geometry and Path transform), and
// it still fires when the composer defers its update (a transparent,
// non-clipping shape), where markBoundsDirty() never runs.
m_LocalBoundsClean = false;
invalidateIntrinsicBounds();
for (auto constraint : constraints())
{
constraint->addDirt(ComponentDirt::Path);
}
invalidateStrokeEffects();
}
void Shape::addToRenderPath(RenderPath* path, const Mat2D& transform)
{
if (isFlagged(PathFlags::local))
{
path->addPath(m_PathComposer.localPath()->renderPath(this),
transform * worldTransform());
}
else
{
path->addPath(m_PathComposer.worldPath()->renderPath(this), transform);
}
}
void Shape::addToRawPath(RawPath& path, const Mat2D* transform)
{
if (isFlagged(PathFlags::local))
{
Mat2D xform = transform == nullptr ? worldTransform()
: (*transform) * worldTransform();
path.addPath(*m_PathComposer.localPath()->rawPath(), &xform);
}
else
{
path.addPath(*m_PathComposer.worldPath()->rawPath(), transform);
}
}
void Shape::draw(Renderer* renderer)
{
RIVE_PROF_SCOPE_L(2)
auto needsSaveOperation =
Drawable::needsSaveOperation() || m_ShapePaints.size() > 1;
for (auto shapePaint : m_ShapePaints)
{
if (!shapePaint->isVisible())
{
continue;
}
auto shapePaintPath = shapePaint->pickPath(this);
if (shapePaintPath == nullptr)
{
continue;
}
shapePaint->draw(renderer,
shapePaintPath,
worldTransform(),
false,
nullptr,
needsSaveOperation);
}
}
bool Shape::hitTestAABB(const Vec2D& position)
{
return worldBounds().contains(position);
}
bool Shape::hitTestHiFi(const Vec2D& position, float hitRadius)
{
auto hitArea = AABB(position.x - hitRadius,
position.y - hitRadius,
position.x + hitRadius,
position.y + hitRadius)
.round();
HitTestCommandPath tester(hitArea);
for (auto path : m_Paths)
{
if (!path->isCollapsed())
{
tester.setXform(path->pathTransform());
path->rawPath().addTo(&tester);
}
}
return tester.wasHit();
}
Core* Shape::hitTest(HitInfo* hinfo, const Mat2D& xform)
{
if (renderOpacity() == 0.0f)
{
return nullptr;
}
// TODO: clip:
const bool shapeIsLocal =
isFlagged(PathFlags::local | PathFlags::localClockwise);
for (auto rit = m_ShapePaints.rbegin(); rit != m_ShapePaints.rend(); ++rit)
{
auto shapePaint = *rit;
if (shapePaint->isTranslucent())
{
continue;
}
if (!shapePaint->isVisible())
{
continue;
}
auto paintIsLocal =
shapePaint->isFlagged(PathFlags::local | PathFlags::localClockwise);
auto mx = xform;
if (paintIsLocal)
{
mx *= worldTransform();
}
HitTestCommandPath tester(hinfo->area);
for (auto path : m_Paths)
{
if (shapeIsLocal)
{
tester.setXform(xform * path->pathTransform());
}
else
{
tester.setXform(mx * path->pathTransform());
}
path->rawPath().addTo(&tester);
}
if (tester.wasHit())
{
return this;
}
}
return nullptr;
}
bool Shape::hitTestPoint(const Vec2D& position,
bool skipOnUnclipped,
bool isPrimaryHit)
{
// If we're NOT the primary hit test, don't perform the AABB hit test
// just keep walking up the tree
if (!isPrimaryHit)
{
return Component::hitTestPoint(position, skipOnUnclipped, isPrimaryHit);
}
// Only perform the AABB hit test if we're the primary hit test
// This prevents walking up the tree and having another shape return a
// false hit test because we're not hitting their AABB
if (hitTestAABB(position) &&
Component::hitTestPoint(position, skipOnUnclipped, isPrimaryHit))
{
return hitTestHiFi(position, 2);
}
return false;
}
void Shape::buildDependencies()
{
// Make sure to propagate the call to PathComposer as it's no longer part of
// Core and owned only by the Shape.
#ifdef WITH_RIVE_EDITOR
// PathComposer is constructed as a Shape member; it's not a Core in the
// arena and not reachable via parentId. Component::onAddedDirty's
// editor branch (parentId chain walk) leaves its m_Artboard null and
// m_DependencyHelper.m_dependecyRoot uninitialized. Any subsequent
// `addDirt` from `m_PathComposer` (e.g. Shape::pathChanged →
// m_PathComposer.addDirt(Path, true)) calls onComponentDirty on a
// garbage pointer and crashes. The runtime path side-steps this
// because Component::onAddedDirty's runtime branch sets
// `m_Artboard = static_cast<Artboard*>(context)` unconditionally
// before any subsequent addDirt path runs. Mirror that here:
// forward Shape's wired artboard to its owned PathComposer.
if (artboard() != nullptr)
{
m_PathComposer.setArtboardForEditor(artboard());
}
// PathComposer isn't an arena Core (lives as a value member of
// Shape), so editor_native's Pass B-prep-dependents `forEachLive`
// sweep doesn't reach it — its `m_Dependents` accumulates entries
// across batches and never gets cleaned. Components that depend
// on PathComposer (e.g. ClippingShape, via
// `shape->pathComposer()->addDependent(this)` in their
// buildDependencies) leave dangling pointers after they're freed,
// and the next dirt cascade through pathComposer crashes on the
// stale entries. Mirror Pass B-prep's effect here so PathComposer
// also starts each batch with a clean dependents list — Pass B's
// buildDependencies loop re-adds the live edges (this calls
// `PathComposer::buildDependencies` below, and per-type editor
// overrides like `ClippingShape`, `Feather`, `FollowPathConstraint`,
// `TextFollowPathModifier` re-register themselves in their own
// buildDependencies calls).
m_PathComposer.editorClearDependents();
#endif
m_PathComposer.buildDependencies();
Super::buildDependencies();
// Set the blend mode on all the shape paints. If we ever animate this
// property, we'll need to update it in the update cycle/mark dirty when the
// blend mode changes.
for (auto paint : m_ShapePaints)
{
paint->blendMode(blendMode());
}
}
StatusCode Shape::onAddedDirty(CoreContext* context)
{
auto code = Super::onAddedDirty(context);
if (code != StatusCode::Ok)
{
return code;
}
#ifdef WITH_RIVE_EDITOR
// PathComposer is a Shape value-member, not in the arena, with no
// CoopId. `Component::onAddedDirty`'s editor branch tries to
// resolve its (empty) `parentId()` and returns MissingObject,
// which propagates here and gets the entire Shape culled in
// Pass 4-cull, leaving the artboard with zero drawables.
//
// Mirrors Dart `Shape` (rive_core/lib/shapes/shape.dart:374-380),
// which only propagates `buildDependencies()` to pathComposer —
// never `onAddedDirty`. The runtime branch's optimization
// (`m_Artboard = static_cast<Artboard*>(context)` via Component's
// runtime branch) makes onAddedDirty work in runtime build only.
// In editor mode, PathComposer's artboard wiring happens via the
// `setArtboardForEditor` workaround in `Shape::buildDependencies`.
//
// TODO(value-member helpers): a more general fix would be to give
// value-member helpers like PathComposer an arena registration so
// `Component::onAddedDirty`'s editor branch can detect "no arena,
// run runtime branch instead." Would auto-handle future helpers
// without per-callsite ifdefs. For now, mirror Dart with a
// targeted skip.
return StatusCode::Ok;
#else
// This ensures context propagates to path composer too.
return m_PathComposer.onAddedDirty(context);
#endif
}
StatusCode Shape::onAddedClean(CoreContext* context)
{
StatusCode code = Super::onAddedClean(context);
if (code != StatusCode::Ok)
{
return code;
}
// Find the deformer, if any.
m_deformer = nullptr;
for (auto currentParent = parent(); currentParent != nullptr;
currentParent = currentParent->parent())
{
RenderPathDeformer* deformer = RenderPathDeformer::from(currentParent);
if (deformer)
{
m_deformer = deformer;
return StatusCode::Ok;
}
}
return StatusCode::Ok;
}
bool Shape::isEmpty()
{
for (auto path : m_Paths)
{
if (!path->isHidden() && !path->isCollapsed())
{
return false;
}
}
return true;
}
bool Shape::willDraw() { return Super::willDraw() && renderOpacity() != 0.0f; }
// Do constraints need to be marked as dirty too? From tests it doesn't seem
// they do.
void Shape::pathCollapseChanged()
{
// Collapsed paths are skipped when measuring, so which paths are in the
// bounds just changed. Drop the cache here for the same reason
// pathChanged() does: the composer's update, and with it markBoundsDirty(),
// is deferred for a transparent, non-clipping shape.
m_LocalBoundsClean = false;
m_PathComposer.pathCollapseChanged();
}
AABB Shape::computeWorldBounds(const Mat2D* xform) const
{
bool first = true;
AABB computedBounds = AABB::forExpansion();
for (auto path : m_Paths)
{
if (path->isCollapsed())
{
continue;
}
// The control point hull, transformed: Mat2D::mapBoundingBox fuses the
// transform and the min/max into one SIMD pass over the points, so we
// never copy the path just to measure it. (A Path's raw path only ever
// holds moves, lines and cubics, so there are no quadratic control
// points needing conversion first.)
const Mat2D transform = xform == nullptr
? path->pathTransform()
: *xform * path->pathTransform();
AABB aabb = transform.mapBoundingBox(path->rawPath().points());
if (first)
{
first = false;
computedBounds = aabb;
}
else
{
computedBounds.expand(aabb);
}
}
return computedBounds;
}
AABB Shape::computeLocalBounds() const
{
const Mat2D& world = worldTransform();
Mat2D inverseWorld = world.invertOrIdentity();
return computeWorldBounds(&inverseWorld);
}
AABB Shape::computeIntrinsicBounds() const
{
// Only a participant caches (and only a participant calls this); without
// one we just compute, so a plain Shape stores nothing.
auto* participant = layoutParticipant();
if (participant != nullptr && participant->hostBoundsValid())
{
return participant->hostBounds();
}
// Like computeWorldBounds but in this shape's local space, using each
// path's local transform directly instead of round-tripping through our
// (non-invertible when the layout fold is 0) world transform.
bool first = true;
AABB computedBounds = AABB::forExpansion();
RawPath pendingPath;
bool usedPendingBuild = false;
for (auto path : m_Paths)
{
if (path->isCollapsed())
{
continue;
}
AABB aabb;
AABB propertyBounds;
if (!path->needsPathBuild())
{
aabb = path->rawPath().preciseBounds(path->transform());
}
else if (path->tryPropertyBounds(propertyBounds))
{
// Layout (and a participant's fit scale) runs before Path::update
// in the update pass. A parametric path only positions its vertices
// there, so building it here would measure an unpositioned path —
// take its declared box instead, which is what it will occupy.
usedPendingBuild = true;
aabb = path->transform().mapBoundingBox(propertyBounds);
}
else
{
// Vertex-driven: the vertices are authored, so building a throwaway
// copy measures real geometry. Without this a fill participant
// computes a scale of 1 and renders at its hugged size on frame 1.
usedPendingBuild = true;
pendingPath.rewind();
path->buildPath(pendingPath);
aabb = pendingPath.preciseBounds(path->transform());
}
// An empty (vertex-less) path leaves preciseBounds at its expansion
// sentinel, which is inverted (+/-FLT_MAX). Folding that in would
// poison the bounds, and a participant's anchor derives from them —
// pushing its world translation to -FLT_MAX. Written as !(>= 0) so a
// NaN is rejected too; a real but degenerate path (zero width or
// height) still counts.
if (!(aabb.width() >= 0.0f && aabb.height() >= 0.0f))
{
continue;
}
if (first)
{
first = false;
computedBounds = aabb;
}
else
{
computedBounds.expand(aabb);
}
}
AABB bounds = first ? AABB() : computedBounds;
if (participant != nullptr)
{
// Anything measured before its build is provisional: a declared box can
// be wider than the precise geometry (a polygon is inscribed in it).
// Return it, but don't cache it, so the precise bounds win once the
// paths build — nothing invalidates on the build itself, only when dirt
// is added.
participant->hostBounds(bounds, /*cache*/ !usedPendingBuild);
}
return bounds;
}
void Shape::invalidateIntrinsicBounds()
{
if (auto* participant = layoutParticipant())
{
participant->invalidateHostBounds();
}
}
static ParametricPath* firstParametricPath(std::vector<Path*>& paths)
{
for (auto path : paths)
{
if (path->is<ParametricPath>())
{
return path->as<ParametricPath>();
}
}
return nullptr;
}
Vec2D Shape::measureLayout(float width,
LayoutMeasureMode widthMode,
float height,
LayoutMeasureMode heightMode)
{
#ifdef WITH_RIVE_LAYOUT
// A participant sizes to its combined bounds (all paths); controlSize then
// scales those bounds to fill the slot. Clamps to the available space like
// the parametric path below, so hug means the same thing in both models.
if (isParticipatingInLayout())
{
AABB bounds = computeIntrinsicBounds();
return Vec2D(std::min(widthMode == LayoutMeasureMode::undefined
? std::numeric_limits<float>::max()
: width,
bounds.width()),
std::min(heightMode == LayoutMeasureMode::undefined
? std::numeric_limits<float>::max()
: height,
bounds.height()));
}
#endif
Vec2D size = Vec2D();
for (auto path : m_Paths)
{
Vec2D measured =
path->measureLayout(width, widthMode, height, heightMode);
size =
Vec2D(std::max(size.x, measured.x), std::max(size.y, measured.y));
}
return size;
}
void Shape::controlSize(Vec2D size,
LayoutScaleType widthScaleType,
LayoutScaleType heightScaleType,
LayoutDirection direction)
{
#ifdef WITH_RIVE_LAYOUT
// A participant scales its combined bounds to fill the slot.
if (isParticipatingInLayout())
{
updateLayoutScale(size);
return;
}
#endif
// Content: a parametric shape's size lives on its ParametricPath child.
if (auto* path = firstParametricPath(m_Paths))
{
path->controlSize(size, widthScaleType, heightScaleType, direction);
}
}
void Shape::updateLayoutScale(Vec2D size)
{
// Only reached from the participant branch of controlSize.
auto* participant = layoutParticipant();
if (participant == nullptr)
{
return;
}
AABB bounds = computeIntrinsicBounds();
float w = bounds.width();
float h = bounds.height();
// intrinsicBounds is from local geometry (not the world round-trip) so it
// stays valid even at scale 0.
float newScaleX = w > 0.0f ? size.x / w : 1.0f;
float newScaleY = h > 0.0f ? size.y / h : 1.0f;
if (newScaleX != participant->hostScaleX() ||
newScaleY != participant->hostScaleY())
{
participant->hostScale(newScaleX, newScaleY);
markWorldTransformDirty();
}
}
void Shape::addChild(Component* component)
{
Super::addChild(component);
if (component->is<LayoutParticipant>())
{
m_hasLayoutParticipant = true;
}
}
// The whole shape scales to fit, so place the scaled combined-bounds top-left
// at the slot (the origin is irrelevant once we scale).
LayoutParticipant* Shape::layoutParticipant() const
{
#ifndef WITH_RIVE_EDITOR
// Children are fixed at runtime and few shapes have one to look for.
if (!m_hasLayoutParticipant)
{
return nullptr;
}
#endif
for (auto* child : children())
{
if (child->is<LayoutParticipant>())
{
return child->as<LayoutParticipant>();
}
}
return nullptr;
}
bool Shape::isParticipatingInLayout() const
{
return layoutParticipant() != nullptr;
}
Vec2D Shape::layoutBaseTranslation(LayoutParticipant* participant) const
{
assert(participant != nullptr);
AABB intrinsic = computeIntrinsicBounds();
return Vec2D(participant->resolvedLeft() -
intrinsic.left() * participant->hostScaleX(),
participant->resolvedTop() -
intrinsic.top() * participant->hostScaleY());
}
void Shape::composeWorldTransform()
{
#ifdef WITH_RIVE_LAYOUT
auto* participant = layoutParticipant();
if (participant != nullptr && m_ParentTransformComponent != nullptr)
{
// Scale innermost so it fits the geometry to the resolved box, with
// our own transform composing on top.
float scaleX = participant->hostScaleX();
float scaleY = participant->hostScaleY();
Mat2D base = Mat2D::fromTranslation(layoutBaseTranslation(participant));
m_WorldTransform = m_ParentTransformComponent->worldTransform() * base *
m_Transform * Mat2D::fromScale(scaleX, scaleY);
return;
}
#endif
Super::composeWorldTransform();
}
void Shape::updateConstraints()
{
#ifdef WITH_RIVE_LAYOUT
auto* participant = layoutParticipant();
if (participant != nullptr)
{
participant->applyLayoutConstraints();
}
#endif
Super::updateConstraints();
}
ShapePaintPath* Shape::worldPath() { return m_PathComposer.worldPath(); }
ShapePaintPath* Shape::localPath() { return m_PathComposer.localPath(); }
ShapePaintPath* Shape::localClockwisePath()
{
return m_PathComposer.localClockwisePath();
}
Component* Shape::pathBuilder() { return &m_PathComposer; }