blob: 76cfdf9edf78da0b55794f6f463af87690185ba1 [file]
#include "rive/shapes/clipping_shape.hpp"
#include "rive/artboard.hpp"
#include "rive/core_context.hpp"
#include "rive/factory.hpp"
#include "rive/node.hpp"
#include "rive/renderer.hpp"
#include "rive/shapes/paint/fill.hpp"
#include "rive/shapes/path_composer.hpp"
#include "rive/shapes/shape.hpp"
using namespace rive;
void ClippingShapeStart::draw(Renderer* renderer, bool needsSaveOperation)
{
if (!m_clippingShape->isVisible())
{
return;
}
if (needsSaveOperation)
{
renderer->save();
}
if (m_clippingShape)
{
ShapePaintPath* path = m_clippingShape->path();
if (!path)
{
return;
}
RenderPath* renderPath = path->renderPath(m_clippingShape);
renderer->clipPath(renderPath);
}
}
int ClippingShapeStart::emptyClipCount()
{
if (m_clippingShape && m_clippingShape->isVisible())
{
ShapePaintPath* path = m_clippingShape->path();
if (!path)
{
return 1;
}
}
return 0;
}
bool ClippingShapeStart::isVisible()
{
if (m_clippingShape)
{
return m_clippingShape->isVisible();
}
return false;
}
int ClippingShapeEnd::emptyClipCount()
{
if (m_clippingShape && m_clippingShape->isVisible())
{
ShapePaintPath* path = m_clippingShape->path();
if (!path)
{
return -1;
}
}
return 0;
}
void ClippingShapeEnd::draw(Renderer* renderer, bool needsSaveOperation)
{
if (!m_clippingShape->isVisible() || !needsSaveOperation)
{
return;
}
renderer->restore();
}
ClippingShape::~ClippingShape()
{
for (auto& proxy : m_proxyDrawables)
{
delete proxy;
}
for (auto& proxy : m_pooledProxyDrawables)
{
delete proxy;
}
}
StatusCode ClippingShape::onAddedClean(CoreContext* context)
{
// Find drawables parented (directly or transitively) to this clipping
// shape's parent; they need to know they'll be clipped by this shape.
parent()->forAll([this](Component* component) -> bool {
if (component->is<Drawable>())
{
component->as<Drawable>()->addClippingShape(this);
}
return true;
});
// Find shapes parented (directly or transitively) to the source node;
// their paths will need to be RenderPaths in order to be used for
// clipping operations.
if (m_Source)
{
m_Source->forAll([this](Component* component) -> bool {
if (component->is<Shape>())
{
auto shape = component->as<Shape>();
shape->addFlags(PathFlags::world | PathFlags::clipping);
m_Shapes.push_back(shape);
}
return true;
});
}
return StatusCode::Ok;
}
StatusCode ClippingShape::onAddedDirty(CoreContext* context)
{
StatusCode code = Super::onAddedDirty(context);
if (code != StatusCode::Ok)
{
return code;
}
auto coreObject = context->resolve(sourceId());
if (coreObject == nullptr || !coreObject->is<Node>())
{
return StatusCode::MissingObject;
}
#ifdef WITH_RIVE_EDITOR
setSourceForEditor(static_cast<Node*>(coreObject));
#else
m_Source = static_cast<Node*>(coreObject);
#endif
return StatusCode::Ok;
}
void ClippingShape::buildDependencies()
{
#ifdef WITH_RIVE_EDITOR
// Dart parity: every dep-rebuild clears + re-walks the source
// subtree. Runtime builds rely on `onAddedClean`'s one-shot init;
// editor needs to re-walk because `source` can change mid-edit
// (sourceIdChanged → setSourceForEditor → next Pass B re-runs us).
// `editorClearDependents` in Pass B-prep wipes stale dependent
// entries on every Component, so re-adding the pathComposer edges
// below is safe and required even when m_Shapes contents didn't
// change.
m_Shapes.clear();
if (auto* src = source())
{
src->forAll([this](Component* c) -> bool {
if (c->is<Shape>())
{
auto* shape = c->as<Shape>();
shape->addFlags(PathFlags::world | PathFlags::clipping);
m_Shapes.push_back(shape);
}
return true;
});
}
#endif
for (auto shape : m_Shapes)
{
shape->pathComposer()->addDependent(this);
// We read what a fill resolves in its own update, so we sort after it.
for (auto paint : shape->shapePaints())
{
if (paint->is<Fill>() && paint->hasEffects())
{
paint->addDependent(this);
}
}
}
clipStart.clippingShape(this);
clipEnd.clippingShape(this);
#ifdef WITH_RIVE_EDITOR
// Dart parity (clipping_shape.dart:97): force the clip path to
// rebuild next update cycle. Without this, anything that triggers
// a Pass B re-run without an accompanying property dirt (e.g. a
// downstream Fill/Stroke removal that only touches m_ShapePaints)
// leaves the cached m_path stale or empty — drawables clipped by
// this ClippingShape render incorrectly until something else
// dirties the source (moving the source shape rebuilds it).
addDirt(ComponentDirt::Path);
// Dirty each source shape's PathComposer directly so its next
// update rebuilds m_worldPath (consumed by our own update reading
// `shape->pathComposer()->worldPath()`).
//
// We target the PathComposer specifically rather than cascading
// from the Shape via `addDirt(Path, recurse=true)`. The cascade
// would traverse the Shape's m_Dependents → PathComposer →
// PathComposer's m_Dependents — and PathComposer isn't an arena
// Component, so Pass B-prep's m_Dependents wipe sweep doesn't
// reach it. Stale ClippingShape* entries from previous batches
// (where a clip was created then freed via undo/redo) accumulate
// in pathComposer->m_Dependents and the cascade dereferences
// them, crashing in the recurse step.
//
// The direct addDirt(Path) on pathComposer sets its dirt bit +
// schedules onComponentDirty via the dependency root, without
// walking the contaminated dependents list. PathComposer.update
// then rebuilds m_worldPath in the topological walk, before
// ClippingShape.update reads it.
for (auto shape : m_Shapes)
{
shape->pathComposer()->addDirt(ComponentDirt::Path);
}
#endif
}
static Mat2D identity;
// Strokes are skipped: their effect output is a centerline, meaningless filled.
bool ClippingShape::addFillPaths(Shape* shape)
{
bool addedEffected = false;
bool needsRawPath = false;
for (auto paint : shape->shapePaints())
{
if (!paint->is<Fill>())
{
continue;
}
auto effected =
paint->hasEffects() ? paint->lastEffectPath(paint) : nullptr;
if (effected == nullptr)
{
// No effect: this fill draws the whole shape.
needsRawPath = true;
continue;
}
if (!effected->empty())
{
const Mat2D& world = shape->worldTransform();
m_path.addPath(effected, effected->isLocal() ? &world : &identity);
}
addedEffected = true;
}
if (addedEffected && !needsRawPath)
{
return true;
}
auto path = shape->pathComposer()->worldPath();
if (path == nullptr)
{
return addedEffected;
}
m_path.addPath(path, &identity);
return true;
}
void ClippingShape::update(ComponentDirt value)
{
if (hasDirt(value,
ComponentDirt::Path | ComponentDirt::WorldTransform |
ComponentDirt::NSlicer))
{
m_path.rewind(false, (FillRule)fillRule());
m_clipPath = nullptr;
for (auto shape : m_Shapes)
{
if (!shape->isEmpty() && addFillPaths(shape))
{
m_clipPath = &m_path;
}
}
}
}
void ClippingShape::isVisibleChanged()
{
artboard()->addDirt(ComponentDirt::Clipping);
}