Get rid of MetricsPath.

This lets us trim a raw path directly which means all the trim path logic from the editor can now be pushed down to the C++ runtime, even for the case where the rendering is still happening via Skia/Impeller, all our path operations can be done in C++ (without using RenderPath/MetricsPath/RenderMetricsPath/OnlyMetricsPath/etc). Basically, I needed to do this now to not have as many edge cases in the editor for the difference between using our runtime to render with PLS vs Flutter.

It also quite greatly simplifies the code! More lines removed than added!

Some tests will fail as I think we're using the MetricsPath in some other parts of the code, but that can be refactored following the patterns used here...

It also means that most of our runtime now speaks RawPath which means we can optimize/reserve memory ahead of time in a lot of cases (not doing that yet).

Diffs=
8486c3445 Get rid of MetricsPath. (#7371)

Co-authored-by: Luigi Rosso <luigi-rosso@users.noreply.github.com>
This commit is contained in:
luigi-rosso
2024-06-07 00:44:49 +00:00
parent dd5cc31088
commit fa6e59865c
27 changed files with 322 additions and 539 deletions

View File

@@ -1 +1 @@
085f5bd2dce2d25561b00eebc5f7118a2c8769eb
8486c344528d5f7f84de1984510064a7d0788e73

View File

@@ -12,6 +12,7 @@
#include "rive/text/text_value_run.hpp"
#include "rive/event.hpp"
#include "rive/audio/audio_engine.hpp"
#include "rive/math/raw_path.hpp"
#include <queue>
#include <vector>
@@ -56,6 +57,7 @@ private:
bool m_HasChangedDrawOrderInLastUpdate = false;
unsigned int m_DirtDepth = 0;
RawPath m_backgroundRawPath;
rcp<RenderPath> m_BackgroundPath;
rcp<RenderPath> m_ClipPath;
Factory* m_Factory = nullptr;

View File

@@ -2,13 +2,11 @@
#define _RIVE_FOLLOW_PATH_CONSTRAINT_HPP_
#include "rive/generated/constraints/follow_path_constraint_base.hpp"
#include "rive/math/transform_components.hpp"
#include "rive/shapes/metrics_path.hpp"
#include <stdio.h>
#include "rive/math/contour_measure.hpp"
namespace rive
{
class FollowPathConstraint : public FollowPathConstraintBase
{
public:
void distanceChanged() override;
void orientChanged() override;

View File

@@ -1,79 +0,0 @@
#ifndef _RIVE_METRICS_PATH_HPP_
#define _RIVE_METRICS_PATH_HPP_
#include "rive/command_path.hpp"
#include "rive/math/contour_measure.hpp"
#include "rive/math/vec2d.hpp"
#include <cassert>
#include <vector>
namespace rive
{
class MetricsPath : public CommandPath
{
private:
RawPath m_RawPath; // temporary, until we build m_Contour
rcp<ContourMeasure> m_Contour;
std::vector<MetricsPath*> m_Paths;
Mat2D m_ComputedLengthTransform;
float m_ComputedLength = 0;
public:
const std::vector<MetricsPath*>& paths() const { return m_Paths; }
rcp<ContourMeasure> contourMeasure() const { return m_Contour; }
void addPath(CommandPath* path, const Mat2D& transform) override;
void rewind() override;
void moveTo(float x, float y) override;
void lineTo(float x, float y) override;
void cubicTo(float ox, float oy, float ix, float iy, float x, float y) override;
void close() override;
float length() const { return m_ComputedLength; }
/// Add commands to the result RenderPath that will draw the segment
/// from startLength to endLength of this MetricsPath. Requires
/// computeLength be called prior to trimming.
void trim(float startLength, float endLength, bool moveTo, RawPath* result);
/// Add this MetricsPath to a raw path with a transform.
RawPath::Iter addToRawPath(RawPath& rawPath, const Mat2D& transform) const;
~MetricsPath() override;
private:
float computeLength(const Mat2D& transform);
};
class OnlyMetricsPath : public MetricsPath
{
public:
void fillRule(FillRule value) override {}
RenderPath* renderPath() override
{
// Should never be used for actual rendering.
assert(false);
return nullptr;
}
};
class RenderMetricsPath : public MetricsPath
{
private:
rcp<RenderPath> m_RenderPath;
public:
RenderMetricsPath(rcp<RenderPath>);
RenderPath* renderPath() override { return m_RenderPath.get(); }
void addPath(CommandPath* path, const Mat2D& transform) override;
void fillRule(FillRule value) override;
void rewind() override;
void moveTo(float x, float y) override;
void lineTo(float x, float y) override;
void cubicTo(float ox, float oy, float ix, float iy, float x, float y) override;
void close() override;
};
} // namespace rive
#endif

View File

@@ -9,7 +9,10 @@ class Fill : public FillBase
public:
RenderPaint* initRenderPaint(ShapePaintMutator* mutator) override;
PathSpace pathSpace() const override;
void draw(Renderer* renderer, CommandPath* path, RenderPaint* paint) override;
void draw(Renderer* renderer,
CommandPath* path,
const RawPath* rawPath,
RenderPaint* paint) override;
void applyTo(RenderPaint* renderPaint, float opacityModifier) const override;
};
} // namespace rive

View File

@@ -5,6 +5,8 @@
#include "rive/shapes/paint/blend_mode.hpp"
#include "rive/shapes/paint/shape_paint_mutator.hpp"
#include "rive/shapes/path_space.hpp"
#include "rive/math/raw_path.hpp"
namespace rive
{
class RenderPaint;
@@ -30,9 +32,17 @@ public:
virtual PathSpace pathSpace() const = 0;
void draw(Renderer* renderer, CommandPath* path) { draw(renderer, path, renderPaint()); }
void draw(Renderer* renderer, CommandPath* path, const RawPath* rawPath = nullptr)
{
draw(renderer, path, rawPath, renderPaint());
}
virtual void draw(Renderer* renderer, CommandPath* path, RenderPaint* paint) = 0;
virtual void draw(Renderer* renderer,
CommandPath* path,
// When every CommandPath stores a RawPath we can get rid
// of this argument.
const RawPath* rawPath,
RenderPaint* paint) = 0;
RenderPaint* renderPaint() { return m_RenderPaint.get(); }

View File

@@ -13,7 +13,10 @@ private:
public:
RenderPaint* initRenderPaint(ShapePaintMutator* mutator) override;
PathSpace pathSpace() const override;
void draw(Renderer* renderer, CommandPath* path, RenderPaint* paint) override;
void draw(Renderer* renderer,
CommandPath* path,
const RawPath* rawPath,
RenderPaint* paint) override;
void addStrokeEffect(StrokeEffect* effect);
bool hasStrokeEffect() { return m_Effect != nullptr; }
void invalidateEffects();

View File

@@ -7,13 +7,13 @@ namespace rive
{
class Factory;
class RenderPath;
class MetricsPath;
class RawPath;
class StrokeEffect
{
public:
virtual ~StrokeEffect() {}
virtual RenderPath* effectPath(MetricsPath* source, Factory*) = 0;
virtual RenderPath* effectPath(const RawPath& source, Factory*) = 0;
virtual void invalidateEffect() = 0;
};
} // namespace rive

View File

@@ -3,24 +3,44 @@
#include "rive/generated/shapes/paint/trim_path_base.hpp"
#include "rive/shapes/paint/stroke_effect.hpp"
#include "rive/renderer.hpp"
#include "rive/math/raw_path.hpp"
#include "rive/math/contour_measure.hpp"
namespace rive
{
enum class TrimPathMode : unsigned char
{
sequential = 1,
synchronized = 2
};
class ContourMeasure;
class TrimPath : public TrimPathBase, public StrokeEffect
{
private:
rcp<RenderPath> m_TrimmedPath;
RenderPath* m_RenderPath = nullptr;
public:
StatusCode onAddedClean(CoreContext* context) override;
RenderPath* effectPath(MetricsPath* source, Factory*) override;
RenderPath* effectPath(const RawPath& source, Factory*) override;
void invalidateEffect() override;
void startChanged() override;
void endChanged() override;
void offsetChanged() override;
void modeValueChanged() override;
TrimPathMode mode() const { return (TrimPathMode)modeValue(); }
StatusCode onAddedDirty(CoreContext* context) override;
const RawPath& rawPath() const { return m_rawPath; }
private:
void invalidateTrim();
void trimRawPath(const RawPath& source);
RawPath m_rawPath;
std::vector<rcp<ContourMeasure>> m_contours;
rcp<RenderPath> m_trimmedPath;
RenderPath* m_renderPath = nullptr;
};
} // namespace rive

View File

@@ -3,6 +3,7 @@
#include "rive/command_path.hpp"
#include "rive/generated/shapes/path_base.hpp"
#include "rive/math/mat2d.hpp"
#include "rive/math/raw_path.hpp"
#include "rive/shapes/shape_paint_container.hpp"
#include <vector>
@@ -37,10 +38,10 @@ class Path : public PathBase
{
protected:
Shape* m_Shape = nullptr;
rcp<CommandPath> m_CommandPath;
std::vector<PathVertex*> m_Vertices;
bool m_deferredPathDirt = false;
PathSpace m_DefaultPathSpace = PathSpace::Neither;
RawPath m_rawPath;
public:
Shape* shape() const { return m_Shape; }
@@ -48,7 +49,7 @@ public:
void buildDependencies() override;
virtual const Mat2D& pathTransform() const;
bool collapse(bool value) override;
CommandPath* commandPath() const { return m_CommandPath.get(); }
const RawPath& rawPath() const { return m_rawPath; }
void update(ComponentDirt value) override;
void addDefaultPathSpace(PathSpace space);
@@ -67,8 +68,7 @@ public:
std::vector<PathVertex*>& vertices() { return m_Vertices; }
#endif
// pour ourselves into a command-path
void buildPath(CommandPath&) const;
void buildPath(RawPath&) const;
};
} // namespace rive

View File

@@ -2,6 +2,8 @@
#define _RIVE_PATH_COMPOSER_HPP_
#include "rive/component.hpp"
#include "rive/refcnt.hpp"
#include "rive/math/raw_path.hpp"
namespace rive
{
class Shape;
@@ -9,23 +11,29 @@ class CommandPath;
class RenderPath;
class PathComposer : public Component
{
private:
Shape* m_Shape;
rcp<CommandPath> m_LocalPath;
rcp<CommandPath> m_WorldPath;
bool m_deferredPathDirt;
public:
PathComposer(Shape* shape);
Shape* shape() const { return m_Shape; }
Shape* shape() const { return m_shape; }
void buildDependencies() override;
void onDirty(ComponentDirt dirt) override;
void update(ComponentDirt value) override;
CommandPath* localPath() const { return m_LocalPath.get(); }
CommandPath* worldPath() const { return m_WorldPath.get(); }
CommandPath* localPath() const { return m_localPath.get(); }
CommandPath* worldPath() const { return m_worldPath.get(); }
const RawPath& localRawPath() const { return m_localRawPath; }
const RawPath& worldRawPath() const { return m_worldRawPath; }
void pathCollapseChanged();
private:
Shape* m_shape;
RawPath m_localRawPath;
RawPath m_worldRawPath;
rcp<CommandPath> m_localPath;
rcp<CommandPath> m_worldPath;
bool m_deferredPathDirt;
};
} // namespace rive
#endif

View File

@@ -35,8 +35,6 @@ public:
void invalidateStrokeEffects();
rcp<CommandPath> makeCommandPath(PathSpace space);
void propagateOpacity(float opacity);
#ifdef TESTING

View File

@@ -454,7 +454,10 @@ void Artboard::update(ComponentDirt value)
clip = bg;
}
m_ClipPath = factory()->makeRenderPath(clip);
m_BackgroundPath = factory()->makeRenderPath(bg);
m_backgroundRawPath.addRect(bg);
m_BackgroundPath->rewind();
m_backgroundRawPath.addTo(m_BackgroundPath.get());
}
if (hasDirt(value, ComponentDirt::RenderOpacity))
{
@@ -602,7 +605,7 @@ void Artboard::draw(Renderer* renderer, DrawOption option)
{
for (auto shapePaint : m_ShapePaints)
{
shapePaint->draw(renderer, m_BackgroundPath.get());
shapePaint->draw(renderer, m_BackgroundPath.get(), &m_backgroundRawPath);
}
}

View File

@@ -5,7 +5,6 @@
#include "rive/math/contour_measure.hpp"
#include "rive/math/mat2d.hpp"
#include "rive/math/math_types.hpp"
#include "rive/shapes/metrics_path.hpp"
#include "rive/shapes/path.hpp"
#include "rive/shapes/shape.hpp"
#include "rive/transform_component.hpp"
@@ -150,8 +149,7 @@ void FollowPathConstraint::update(ComponentDirt value)
m_contours.clear();
for (auto path : paths)
{
auto commandPath = static_cast<MetricsPath*>(path->commandPath());
commandPath->addToRawPath(m_rawPath, path->pathTransform());
m_rawPath.addPath(path->rawPath(), &path->pathTransform());
}
auto measure = ContourMeasureIter(&m_rawPath);

View File

@@ -1,131 +0,0 @@
#include "rive/shapes/metrics_path.hpp"
#include "rive/renderer.hpp"
#include "rive/math/raw_path.hpp"
#include "rive/math/contour_measure.hpp"
#include <iostream>
using namespace rive;
void MetricsPath::rewind()
{
for (auto ptr : m_Paths)
{
delete ptr;
}
m_Paths.clear();
m_Contour.reset(nullptr);
m_RawPath.rewind();
m_ComputedLengthTransform = Mat2D();
m_ComputedLength = 0;
}
MetricsPath::~MetricsPath() { rewind(); }
void MetricsPath::addPath(CommandPath* path, const Mat2D& transform)
{
MetricsPath* metricsPath = static_cast<MetricsPath*>(path);
m_ComputedLength += metricsPath->computeLength(transform);
// We need to copy the data to avoid contention between multiple uses of the same path
// for example when the same path is added as localPath and worldPath
auto metricsPathCopy = new OnlyMetricsPath();
metricsPathCopy->m_Contour = metricsPath->m_Contour;
metricsPathCopy->m_RawPath = metricsPath->m_RawPath;
metricsPathCopy->m_ComputedLength = metricsPath->m_ComputedLength;
m_Paths.emplace_back(metricsPathCopy);
}
RawPath::Iter MetricsPath::addToRawPath(RawPath& rawPath, const Mat2D& transform) const
{
return rawPath.addPath(m_RawPath, &transform);
}
void MetricsPath::moveTo(float x, float y)
{
assert(m_RawPath.points().size() == 0);
m_RawPath.move({x, y});
}
void MetricsPath::lineTo(float x, float y) { m_RawPath.line({x, y}); }
void MetricsPath::cubicTo(float ox, float oy, float ix, float iy, float x, float y)
{
m_RawPath.cubic({ox, oy}, {ix, iy}, {x, y});
}
void MetricsPath::close()
{
// Should we pass the close() to our m_RawPath ???
}
float MetricsPath::computeLength(const Mat2D& transform)
{
// Only compute if our pre-computed length is not valid
if (!m_Contour || transform != m_ComputedLengthTransform)
{
m_ComputedLengthTransform = transform;
RawPath transformedPath = m_RawPath.transform(transform);
m_Contour = ContourMeasureIter(&transformedPath).next();
m_ComputedLength = m_Contour ? m_Contour->length() : 0;
}
return m_ComputedLength;
}
void MetricsPath::trim(float startLength, float endLength, bool moveTo, RawPath* result)
{
assert(endLength >= startLength);
if (!m_Paths.empty())
{
m_Paths.front()->trim(startLength, endLength, moveTo, result);
return;
}
if (!m_Contour)
{
// All the contours were 0 length, so there's nothing to segment.
return;
}
// TODO: if we can change the signature of MetricsPath and/or trim() to speak native
// rawpaths, we wouldn't need this temporary copy (since ContourMeasure speaks
// native rawpaths).
RawPath tmp;
m_Contour->getSegment(startLength, endLength, result, moveTo);
}
RenderMetricsPath::RenderMetricsPath(rcp<RenderPath> path) : m_RenderPath(std::move(path)) {}
void RenderMetricsPath::addPath(CommandPath* path, const Mat2D& transform)
{
MetricsPath::addPath(path, transform);
m_RenderPath->addPath(path->renderPath(), transform);
}
void RenderMetricsPath::rewind()
{
MetricsPath::rewind();
m_RenderPath->rewind();
}
void RenderMetricsPath::moveTo(float x, float y)
{
MetricsPath::moveTo(x, y);
m_RenderPath->moveTo(x, y);
}
void RenderMetricsPath::lineTo(float x, float y)
{
MetricsPath::lineTo(x, y);
m_RenderPath->lineTo(x, y);
}
void RenderMetricsPath::cubicTo(float ox, float oy, float ix, float iy, float x, float y)
{
MetricsPath::cubicTo(ox, oy, ix, iy, x, y);
m_RenderPath->cubicTo(ox, oy, ix, iy, x, y);
}
void RenderMetricsPath::close()
{
MetricsPath::close();
m_RenderPath->close();
}
void RenderMetricsPath::fillRule(FillRule value) { m_RenderPath->fillRule(value); }

View File

@@ -18,7 +18,7 @@ void Fill::applyTo(RenderPaint* renderPaint, float opacityModifier) const
m_PaintMutator->applyTo(renderPaint, opacityModifier);
}
void Fill::draw(Renderer* renderer, CommandPath* path, RenderPaint* paint)
void Fill::draw(Renderer* renderer, CommandPath* path, const RawPath* rawPath, RenderPaint* paint)
{
if (!isVisible())
{

View File

@@ -33,18 +33,17 @@ void Stroke::applyTo(RenderPaint* renderPaint, float opacityModifier) const
bool Stroke::isVisible() const { return Super::isVisible() && thickness() > 0.0f; }
void Stroke::draw(Renderer* renderer, CommandPath* path, RenderPaint* paint)
void Stroke::draw(Renderer* renderer, CommandPath* path, const RawPath* rawPath, RenderPaint* paint)
{
if (!isVisible())
{
return;
}
if (m_Effect != nullptr)
if (m_Effect != nullptr && rawPath != nullptr)
{
/// We're guaranteed to get a metrics path here if we have an effect.
auto factory = artboard()->factory();
path = m_Effect->effectPath(reinterpret_cast<MetricsPath*>(path), factory);
path = m_Effect->effectPath(*rawPath, factory);
}
renderer->drawPath(path->renderPath(), paint);

View File

@@ -1,5 +1,4 @@
#include "rive/shapes/paint/trim_path.hpp"
#include "rive/shapes/metrics_path.hpp"
#include "rive/shapes/paint/stroke.hpp"
#include "rive/factory.hpp"
@@ -17,33 +16,29 @@ StatusCode TrimPath::onAddedClean(CoreContext* context)
return StatusCode::Ok;
}
RenderPath* TrimPath::effectPath(MetricsPath* source, Factory* factory)
void TrimPath::trimRawPath(const RawPath& source)
{
if (m_RenderPath != nullptr)
{
return m_RenderPath;
}
// Source is always a containing (shape) path.
const std::vector<MetricsPath*>& subPaths = source->paths();
RawPath rawTrimmed;
if (!m_TrimmedPath)
{
m_TrimmedPath = factory->makeEmptyRenderPath();
}
else
{
m_TrimmedPath->rewind();
}
m_rawPath.rewind();
auto renderOffset = std::fmod(std::fmod(offset(), 1.0f) + 1.0f, 1.0f);
switch (modeValue())
// Build up contours if they're empty.
if (m_contours.empty())
{
case 1:
ContourMeasureIter iter(&source);
while (auto meas = iter.next())
{
float totalLength = source->length();
m_contours.push_back(meas);
}
}
switch (mode())
{
case TrimPathMode::sequential:
{
float totalLength = 0.0f;
for (auto contour : m_contours)
{
totalLength += contour->length();
}
auto startLength = totalLength * (start() + renderOffset);
auto endLength = totalLength * (end() + renderOffset);
@@ -60,35 +55,35 @@ RenderPath* TrimPath::effectPath(MetricsPath* source, Factory* factory)
endLength -= totalLength;
}
int i = 0, subPathCount = (int)subPaths.size();
int i = 0, subPathCount = (int)m_contours.size();
while (endLength > 0)
{
auto path = subPaths[i % subPathCount];
auto pathLength = path->length();
auto contour = m_contours[i % subPathCount];
auto contourLength = contour->length();
if (startLength < pathLength)
if (startLength < contourLength)
{
path->trim(startLength, endLength, true, &rawTrimmed);
endLength -= pathLength;
contour->getSegment(startLength, endLength, &m_rawPath, true);
endLength -= contourLength;
startLength = 0;
}
else
{
startLength -= pathLength;
endLength -= pathLength;
startLength -= contourLength;
endLength -= contourLength;
}
i++;
}
}
break;
case 2:
case TrimPathMode::synchronized:
{
for (auto path : subPaths)
for (auto contour : m_contours)
{
auto pathLength = path->length();
auto startLength = pathLength * (start() + renderOffset);
auto endLength = pathLength * (end() + renderOffset);
auto contourLength = contour->length();
auto startLength = contourLength * (start() + renderOffset);
auto endLength = contourLength * (end() + renderOffset);
if (endLength < startLength)
{
auto length = startLength;
@@ -96,37 +91,83 @@ RenderPath* TrimPath::effectPath(MetricsPath* source, Factory* factory)
endLength = length;
}
if (startLength > pathLength)
if (startLength > contourLength)
{
startLength -= pathLength;
endLength -= pathLength;
startLength -= contourLength;
endLength -= contourLength;
}
path->trim(startLength, endLength, true, &rawTrimmed);
while (endLength > pathLength)
contour->getSegment(startLength, endLength, &m_rawPath, true);
while (endLength > contourLength)
{
startLength = 0;
endLength -= pathLength;
path->trim(startLength, endLength, true, &rawTrimmed);
endLength -= contourLength;
contour->getSegment(startLength, endLength, &m_rawPath, true);
}
}
}
break;
default:
RIVE_UNREACHABLE();
}
}
RenderPath* TrimPath::effectPath(const RawPath& source, Factory* factory)
{
if (m_renderPath != nullptr)
{
// Previous result hasn't been invalidated, it's still good.
return m_renderPath;
}
m_RenderPath = m_TrimmedPath.get();
rawTrimmed.addTo(m_RenderPath);
return m_RenderPath;
trimRawPath(source);
if (!m_trimmedPath)
{
m_trimmedPath = factory->makeEmptyRenderPath();
}
else
{
m_trimmedPath->rewind();
}
m_renderPath = m_trimmedPath.get();
m_rawPath.addTo(m_renderPath);
return m_renderPath;
}
void TrimPath::invalidateEffect()
{
m_RenderPath = nullptr;
invalidateTrim();
// This is usually sent when the path is changed so we need to also
// invalidate the contours, not just the trim effect.
m_contours.clear();
}
void TrimPath::invalidateTrim()
{
m_renderPath = nullptr;
auto stroke = parent()->as<Stroke>();
stroke->parent()->addDirt(ComponentDirt::Paint);
stroke->invalidateRendering();
}
void TrimPath::startChanged() { invalidateEffect(); }
void TrimPath::endChanged() { invalidateEffect(); }
void TrimPath::offsetChanged() { invalidateEffect(); }
void TrimPath::modeValueChanged() { invalidateEffect(); }
void TrimPath::startChanged() { invalidateTrim(); }
void TrimPath::endChanged() { invalidateTrim(); }
void TrimPath::offsetChanged() { invalidateTrim(); }
void TrimPath::modeValueChanged() { invalidateTrim(); }
StatusCode TrimPath::onAddedDirty(CoreContext* context)
{
auto code = Super::onAddedDirty(context);
if (code != StatusCode::Ok)
{
return code;
}
switch (mode())
{
case TrimPathMode::sequential:
case TrimPathMode::synchronized:
return StatusCode::Ok;
}
return StatusCode::InvalidObject;
}

View File

@@ -47,14 +47,7 @@ StatusCode Path::onAddedClean(CoreContext* context)
return StatusCode::MissingObject;
}
void Path::buildDependencies()
{
Super::buildDependencies();
// Make sure this is called once the shape has all of the paints added
// (paints get added during the added cycle so buildDependencies is a good
// time to do this.)
m_CommandPath = m_Shape->makeCommandPath(m_DefaultPathSpace);
}
void Path::buildDependencies() { Super::buildDependencies(); }
void Path::addVertex(PathVertex* vertex) { m_Vertices.push_back(vertex); }
@@ -62,13 +55,20 @@ void Path::addDefaultPathSpace(PathSpace space) { m_DefaultPathSpace |= space; }
bool Path::canDeferPathUpdate()
{
return ((m_DefaultPathSpace & PathSpace::Clipping) != PathSpace::Clipping) &&
// A path cannot defer its update if the shapes requires an update. Note the
// nuance here where we track that the shape may be marked for follow path
// (meaning all child paths need to follow path). This doesn't mean the
// Shape is necessarily forced to update put the paths are, which is why we
// explicitly also check the shape's path space.
return m_Shape->canDeferPathUpdate() &&
(m_Shape->pathSpace() & PathSpace::FollowPath) != PathSpace::FollowPath &&
((m_DefaultPathSpace & PathSpace::Clipping) != PathSpace::Clipping) &&
((m_DefaultPathSpace & PathSpace::FollowPath) != PathSpace::FollowPath);
}
const Mat2D& Path::pathTransform() const { return worldTransform(); }
void Path::buildPath(CommandPath& commandPath) const
void Path::buildPath(RawPath& rawPath) const
{
const bool isClosed = isPathClosed();
const std::vector<PathVertex*>& vertices = m_Vertices;
@@ -94,7 +94,7 @@ void Path::buildPath(CommandPath& commandPath) const
startIn = cubic->renderIn();
out = cubic->renderOut();
start = cubic->renderTranslation();
commandPath.move(start);
rawPath.move(start);
}
else
{
@@ -125,18 +125,18 @@ void Path::buildPath(CommandPath& commandPath) const
float idealDistance = computeIdealControlPointDistance(toPrev, toNext, renderRadius);
startIn = start = Vec2D::scaleAndAdd(pos, toPrev, renderRadius);
commandPath.move(startIn);
rawPath.move(startIn);
Vec2D outPoint = Vec2D::scaleAndAdd(pos, toPrev, renderRadius - idealDistance);
Vec2D inPoint = Vec2D::scaleAndAdd(pos, toNext, renderRadius - idealDistance);
out = Vec2D::scaleAndAdd(pos, toNext, renderRadius);
commandPath.cubic(outPoint, inPoint, out);
rawPath.cubic(outPoint, inPoint, out);
prevIsCubic = false;
}
else
{
startIn = start = out = point.renderTranslation();
commandPath.move(out);
rawPath.move(out);
}
}
@@ -150,7 +150,7 @@ void Path::buildPath(CommandPath& commandPath) const
auto inPoint = cubic->renderIn();
auto translation = cubic->renderTranslation();
commandPath.cubic(out, inPoint, translation);
rawPath.cubic(out, inPoint, translation);
prevIsCubic = true;
out = cubic->renderOut();
@@ -183,22 +183,22 @@ void Path::buildPath(CommandPath& commandPath) const
Vec2D translation = Vec2D::scaleAndAdd(pos, toPrev, renderRadius);
if (prevIsCubic)
{
commandPath.cubic(out, translation, translation);
rawPath.cubic(out, translation, translation);
}
else
{
commandPath.line(translation);
rawPath.line(translation);
}
Vec2D outPoint = Vec2D::scaleAndAdd(pos, toPrev, renderRadius - idealDistance);
Vec2D inPoint = Vec2D::scaleAndAdd(pos, toNext, renderRadius - idealDistance);
out = Vec2D::scaleAndAdd(pos, toNext, renderRadius);
commandPath.cubic(outPoint, inPoint, out);
rawPath.cubic(outPoint, inPoint, out);
prevIsCubic = false;
}
else if (prevIsCubic)
{
commandPath.cubic(out, pos, pos);
rawPath.cubic(out, pos, pos);
prevIsCubic = false;
out = pos;
@@ -206,7 +206,7 @@ void Path::buildPath(CommandPath& commandPath) const
else
{
out = pos;
commandPath.line(out);
rawPath.line(out);
}
}
}
@@ -214,13 +214,13 @@ void Path::buildPath(CommandPath& commandPath) const
{
if (prevIsCubic || startIsCubic)
{
commandPath.cubic(out, startIn, start);
rawPath.cubic(out, startIn, start);
}
else
{
commandPath.line(start);
rawPath.line(start);
}
commandPath.close();
rawPath.close();
}
}
@@ -249,9 +249,9 @@ void Path::update(ComponentDirt value)
{
Super::update(value);
if (m_CommandPath != nullptr && hasDirt(value, ComponentDirt::Path))
if (hasDirt(value, ComponentDirt::Path))
{
if (m_Shape->canDeferPathUpdate())
if (canDeferPathUpdate())
{
m_deferredPathDirt = true;
return;
@@ -260,8 +260,8 @@ void Path::update(ComponentDirt value)
// Build path doesn't explicitly rewind because we use it to concatenate
// multiple built paths into a single command path (like the hit
// tester).
m_CommandPath->rewind();
buildPath(*m_CommandPath);
m_rawPath.rewind();
buildPath(m_rawPath);
}
// if (hasDirt(value, ComponentDirt::WorldTransform) && m_Shape != nullptr)
// {

View File

@@ -3,16 +3,17 @@
#include "rive/renderer.hpp"
#include "rive/shapes/path.hpp"
#include "rive/shapes/shape.hpp"
#include "rive/factory.hpp"
using namespace rive;
PathComposer::PathComposer(Shape* shape) : m_Shape(shape), m_deferredPathDirt(false) {}
PathComposer::PathComposer(Shape* shape) : m_shape(shape), m_deferredPathDirt(false) {}
void PathComposer::buildDependencies()
{
assert(m_Shape != nullptr);
m_Shape->addDependent(this);
for (auto path : m_Shape->paths())
assert(m_shape != nullptr);
m_shape->addDependent(this);
for (auto path : m_shape->paths())
{
path->addDependent(this);
}
@@ -25,7 +26,7 @@ void PathComposer::onDirty(ComponentDirt dirt)
// We'd deferred the update, let's make sure the rest of our
// dependencies update too. Constraints need to update too, stroke
// effects, etc.
m_Shape->pathChanged();
m_shape->pathChanged();
}
}
@@ -33,61 +34,63 @@ void PathComposer::update(ComponentDirt value)
{
if (hasDirt(value, ComponentDirt::Path))
{
if (m_Shape->canDeferPathUpdate())
if (m_shape->canDeferPathUpdate())
{
m_deferredPathDirt = true;
return;
}
m_deferredPathDirt = false;
auto space = m_Shape->pathSpace();
bool hasConstraint = (space & PathSpace::FollowPath) == PathSpace::FollowPath;
auto space = m_shape->pathSpace();
if ((space & PathSpace::Local) == PathSpace::Local)
{
if (m_LocalPath == nullptr)
if (m_localPath == nullptr)
{
PathSpace localSpace =
(hasConstraint) ? PathSpace::Local & PathSpace::FollowPath : PathSpace::Local;
m_LocalPath = m_Shape->makeCommandPath(localSpace);
m_localPath = artboard()->factory()->makeEmptyRenderPath();
}
else
{
m_LocalPath->rewind();
m_localPath->rewind();
m_localRawPath.rewind();
}
auto world = m_Shape->worldTransform();
auto world = m_shape->worldTransform();
Mat2D inverseWorld = world.invertOrIdentity();
// Get all the paths into local shape space.
for (auto path : m_Shape->paths())
for (auto path : m_shape->paths())
{
if (!path->isHidden() && !path->isCollapsed())
{
const auto localTransform = inverseWorld * path->pathTransform();
m_LocalPath->addPath(path->commandPath(), localTransform);
m_localRawPath.addPath(path->rawPath(), &localTransform);
}
}
// TODO: add a CommandPath::copy(RawPath)
m_localRawPath.addTo(m_localPath.get());
}
if ((space & PathSpace::World) == PathSpace::World)
{
if (m_WorldPath == nullptr)
if (m_worldPath == nullptr)
{
PathSpace worldSpace =
(hasConstraint) ? PathSpace::World & PathSpace::FollowPath : PathSpace::World;
m_WorldPath = m_Shape->makeCommandPath(worldSpace);
m_worldPath = artboard()->factory()->makeEmptyRenderPath();
}
else
{
m_WorldPath->rewind();
m_worldPath->rewind();
m_worldRawPath.rewind();
}
for (auto path : m_Shape->paths())
for (auto path : m_shape->paths())
{
if (!path->isHidden() && !path->isCollapsed())
{
const Mat2D& transform = path->pathTransform();
m_WorldPath->addPath(path->commandPath(), transform);
m_worldRawPath.addPath(path->rawPath(), &transform);
}
}
// TODO: add a CommandPath::copy(RawPath)
m_worldRawPath.addTo(m_worldPath.get());
}
m_Shape->markBoundsDirty();
m_shape->markBoundsDirty();
}
}

View File

@@ -24,9 +24,8 @@ void Shape::addPath(Path* path)
bool Shape::canDeferPathUpdate()
{
auto canDefer = renderOpacity() == 0 &&
(pathSpace() & PathSpace::Clipping) != PathSpace::Clipping &&
(pathSpace() & PathSpace::FollowPath) != PathSpace::FollowPath;
auto canDefer =
renderOpacity() == 0 && (pathSpace() & PathSpace::Clipping) != PathSpace::Clipping;
if (canDefer)
{
// If we have a dependent Skin, don't defer the update
@@ -37,13 +36,6 @@ bool Shape::canDeferPathUpdate()
return false;
}
}
for (auto path : m_Paths)
{
if (!path->canDeferPathUpdate())
{
return false;
}
}
}
return canDefer;
}
@@ -113,9 +105,10 @@ void Shape::draw(Renderer* renderer)
{
renderer->transform(worldTransform());
}
shapePaint->draw(renderer,
paintsInLocal ? m_PathComposer.localPath()
: m_PathComposer.worldPath());
shapePaint->draw(
renderer,
paintsInLocal ? m_PathComposer.localPath() : m_PathComposer.worldPath(),
paintsInLocal ? &m_PathComposer.localRawPath() : &m_PathComposer.worldRawPath());
renderer->restore();
}
}
@@ -135,7 +128,7 @@ bool Shape::hitTest(const IAABB& area) const
if (!path->isCollapsed())
{
tester.setXform(path->pathTransform());
path->buildPath(tester);
path->rawPath().addTo(&tester);
}
}
return tester.wasHit();
@@ -184,7 +177,7 @@ Core* Shape::hitTest(HitInfo* hinfo, const Mat2D& xform)
{
tester.setXform(mx * path->pathTransform());
}
path->buildPath(tester);
path->rawPath().addTo(&tester);
}
if (tester.wasHit())
{
@@ -284,8 +277,7 @@ AABB Shape::computeWorldBounds(const Mat2D* xform) const
{
continue;
}
path->buildPath(boundsCalculator);
path->rawPath().addTo(&boundsCalculator);
AABB aabb = boundsCalculator.bounds(xform == nullptr ? path->pathTransform()
: path->pathTransform() * *xform);

View File

@@ -2,7 +2,6 @@
#include "rive/artboard.hpp"
#include "rive/factory.hpp"
#include "rive/component.hpp"
#include "rive/shapes/metrics_path.hpp"
#include "rive/shapes/paint/stroke.hpp"
#include "rive/shapes/shape.hpp"
#include "rive/text/text_style.hpp"
@@ -46,53 +45,6 @@ void ShapePaintContainer::invalidateStrokeEffects()
}
}
rcp<CommandPath> ShapePaintContainer::makeCommandPath(PathSpace space)
{
// Force a render path if we specifically request to use it for clipping or
// this shape is used for clipping.
bool needForRender =
((space | m_DefaultPathSpace) & PathSpace::Clipping) == PathSpace::Clipping;
bool needForConstraint =
((space | m_DefaultPathSpace) & PathSpace::FollowPath) == PathSpace::FollowPath;
bool needForEffects = false;
for (auto paint : m_ShapePaints)
{
if (space != PathSpace::Neither && (space & paint->pathSpace()) != space)
{
continue;
}
if (paint->is<Stroke>() && paint->as<Stroke>()->hasStrokeEffect())
{
needForEffects = true;
}
else
{
needForRender = true;
}
}
auto factory = getArtboard()->factory();
if (needForEffects && needForRender)
{
return make_rcp<RenderMetricsPath>(factory->makeEmptyRenderPath());
}
else if (needForConstraint)
{
return make_rcp<RenderMetricsPath>(factory->makeEmptyRenderPath());
}
else if (needForEffects)
{
return make_rcp<OnlyMetricsPath>();
}
else
{
return factory->makeEmptyRenderPath();
}
}
void ShapePaintContainer::propagateOpacity(float opacity)
{
for (auto shapePaint : m_ShapePaints)

View File

@@ -191,7 +191,7 @@ void TextStyle::draw(Renderer* renderer)
{
RenderPaint* renderPaint = m_paintPool[paintIndex++].get();
shapePaint->applyTo(renderPaint, itr->first);
shapePaint->draw(renderer, itr->second.get(), renderPaint);
shapePaint->draw(renderer, itr->second.get(), nullptr, renderPaint);
}
}
}

View File

@@ -29,7 +29,7 @@ TEST_CASE("simple triangle path triangulates as expected", "[file]")
auto path = artboard->find<rive::Path>("triangle_path");
REQUIRE(path != nullptr);
TestRenderPath renderPath;
path->buildPath(renderPath);
path->rawPath().addTo(&renderPath);
rive::Mat2D identity;
TestRenderPath shapeRenderPath;

View File

@@ -177,3 +177,25 @@ TEST_CASE("nan-path", "[contourmeasure]")
CHECK(iter.next() == nullptr);
}
}
// Regression test for a crash found by fuzzing.
TEST_CASE("fuzz_issue_7295", "[MetricsPath]")
{
NoOpFactory factory;
RawPath innerPath;
innerPath.moveTo(.0f, -20.5f);
innerPath.cubicTo(11.3218384f, -20.5f, 20.5f, -11.3218384f, 20.5f, .0f);
innerPath.cubicTo(20.5f, 11.3218384f, 11.3218384f, 20.5f, .0f, 20.5f);
innerPath.cubicTo(-11.3218384f, 20.5f, -20.5f, 11.3218384f, -20.5f, .0f);
innerPath.cubicTo(-20.5f, -11.3218384f, -11.3218384f, -20.5f, .0f, -20.5f);
RawPath outerPath;
Mat2D transform(1.f, .0f, .0f, 1.f, -134217728.f, -134217728.f);
outerPath.addPath(innerPath, &transform);
auto contour = ContourMeasureIter(&outerPath).next();
RawPath result;
contour->getSegment(.0f, 168.389008f, &result, true);
CHECK(math::nearly_equal(contour->length(), 168.389008f));
}

View File

@@ -1,54 +0,0 @@
#include <catch.hpp>
#include <rive/math/math_types.hpp>
#include <rive/shapes/metrics_path.hpp>
#include <rive/shapes/paint/stroke.hpp>
#include <rive/shapes/paint/trim_path.hpp>
#include <utils/no_op_factory.hpp>
using namespace rive;
TEST_CASE("path metrics compute correctly", "[bezier]")
{
// TODO: fix these based on new logic
// Make a square with sides length 10.
// rive::OnlyMetricsPath path;
// path.moveTo(0, 0);
// path.lineTo(10, 0);
// path.lineTo(10, 10);
// path.lineTo(0, 10);
// path.close();
// // Total length should be 40.
// rive::Mat2D identity;
// float length = path.computeLength(identity);
// REQUIRE(length == 40);
// // Make a path with a single cubic.
// rive::OnlyMetricsPath cubicPath;
// cubicPath.moveTo(102, 22);
// cubicPath.cubicTo(10, 80, 120, 100, 150, 222);
// cubicPath.close();
// float cubicLength = cubicPath.computeLength(identity);
// REQUIRE(cubicLength == 238.38698f);
}
// Regression test for a crash found by fuzzing.
TEST_CASE("fuzz_issue_7295", "[MetricsPath]")
{
NoOpFactory factory;
OnlyMetricsPath innerPath;
innerPath.moveTo(.0f, -20.5f);
innerPath.cubicTo(11.3218384f, -20.5f, 20.5f, -11.3218384f, 20.5f, .0f);
innerPath.cubicTo(20.5f, 11.3218384f, 11.3218384f, 20.5f, .0f, 20.5f);
innerPath.cubicTo(-11.3218384f, 20.5f, -20.5f, 11.3218384f, -20.5f, .0f);
innerPath.cubicTo(-20.5f, -11.3218384f, -11.3218384f, -20.5f, .0f, -20.5f);
OnlyMetricsPath outerPath;
outerPath.addPath(&innerPath, Mat2D(1.f, .0f, .0f, 1.f, -134217728.f, -134217728.f));
RawPath result;
outerPath.paths()[0]->trim(.0f, 168.389008f, true, &result);
CHECK(math::nearly_equal(outerPath.paths()[0]->length(), 168.389008f));
}

View File

@@ -12,6 +12,7 @@
#include <rive/solo.hpp>
#include <rive/animation/linear_animation_instance.hpp>
#include "rive_file_reader.hpp"
#include "rive/math/path_types.hpp"
#include <catch.hpp>
#include <cstdio>
@@ -111,18 +112,16 @@ TEST_CASE("rectangle path builds expected commands", "[path]")
artboard.advance(0.0f);
REQUIRE(rectangle->commandPath() != nullptr);
auto rawPath = rectangle->rawPath();
auto path = static_cast<TestRenderPath*>(rectangle->commandPath());
REQUIRE(path->commands.size() == 7);
REQUIRE(path->commands[0].command == TestPathCommandType::Reset);
REQUIRE(path->commands[1].command == TestPathCommandType::MoveTo);
REQUIRE(path->commands[2].command == TestPathCommandType::LineTo);
REQUIRE(path->commands[3].command == TestPathCommandType::LineTo);
REQUIRE(path->commands[4].command == TestPathCommandType::LineTo);
REQUIRE(path->commands[5].command == TestPathCommandType::LineTo);
REQUIRE(path->commands[6].command == TestPathCommandType::Close);
auto verbs = rawPath.verbs();
REQUIRE(verbs.size() == 6);
REQUIRE(verbs[0] == rive::PathVerb::move);
REQUIRE(verbs[1] == rive::PathVerb::line);
REQUIRE(verbs[2] == rive::PathVerb::line);
REQUIRE(verbs[3] == rive::PathVerb::line);
REQUIRE(verbs[4] == rive::PathVerb::line);
REQUIRE(verbs[5] == rive::PathVerb::close);
}
TEST_CASE("rounded rectangle path builds expected commands", "[path]")
@@ -148,9 +147,7 @@ TEST_CASE("rounded rectangle path builds expected commands", "[path]")
artboard.advance(0.0f);
REQUIRE(rectangle->commandPath() != nullptr);
auto path = static_cast<TestRenderPath*>(rectangle->commandPath());
auto rawPath = rectangle->rawPath();
// rewind
// moveTo
@@ -161,31 +158,30 @@ TEST_CASE("rounded rectangle path builds expected commands", "[path]")
// lineTo, cubicTo for 4th corner
// close
REQUIRE(path->commands.size() == 11);
auto verbs = rawPath.verbs();
REQUIRE(verbs.size() == 10);
// Init
REQUIRE(path->commands[0].command == TestPathCommandType::Reset);
REQUIRE(path->commands[1].command == TestPathCommandType::MoveTo);
REQUIRE(verbs[0] == rive::PathVerb::move);
// 1st
REQUIRE(path->commands[2].command == TestPathCommandType::CubicTo);
REQUIRE(verbs[1] == rive::PathVerb::cubic);
// 2nd
REQUIRE(path->commands[3].command == TestPathCommandType::LineTo);
REQUIRE(path->commands[4].command == TestPathCommandType::CubicTo);
REQUIRE(verbs[2] == rive::PathVerb::line);
REQUIRE(verbs[3] == rive::PathVerb::cubic);
// 3rd
REQUIRE(path->commands[5].command == TestPathCommandType::LineTo);
REQUIRE(path->commands[6].command == TestPathCommandType::CubicTo);
REQUIRE(verbs[4] == rive::PathVerb::line);
REQUIRE(verbs[5] == rive::PathVerb::cubic);
// 4th
REQUIRE(path->commands[7].command == TestPathCommandType::LineTo);
REQUIRE(path->commands[8].command == TestPathCommandType::CubicTo);
REQUIRE(verbs[6] == rive::PathVerb::line);
REQUIRE(verbs[7] == rive::PathVerb::cubic);
REQUIRE(path->commands[9].command == TestPathCommandType::LineTo);
REQUIRE(verbs[8] == rive::PathVerb::line);
REQUIRE(path->commands[10].command == TestPathCommandType::Close);
REQUIRE(verbs[9] == rive::PathVerb::close);
}
TEST_CASE("ellipse path builds expected commands", "[path]")
@@ -209,9 +205,7 @@ TEST_CASE("ellipse path builds expected commands", "[path]")
artboard.advance(0.0f);
REQUIRE(ellipse->commandPath() != nullptr);
auto path = static_cast<TestRenderPath*>(ellipse->commandPath());
auto path = ellipse->rawPath();
// rewind
// moveTo
@@ -223,51 +217,52 @@ TEST_CASE("ellipse path builds expected commands", "[path]")
// close
REQUIRE(path->commands.size() == 7);
auto verbs = path.verbs();
auto points = path.points();
REQUIRE(verbs.size() == 6);
// Init
REQUIRE(path->commands[0].command == TestPathCommandType::Reset);
REQUIRE(path->commands[1].command == TestPathCommandType::MoveTo);
REQUIRE(path->commands[1].x == 0.0f);
REQUIRE(path->commands[1].y == -100.0f);
REQUIRE(verbs[0] == rive::PathVerb::move);
REQUIRE(points[0].x == 0.0f);
REQUIRE(points[0].y == -100.0f);
// 1st
REQUIRE(path->commands[2].command == TestPathCommandType::CubicTo);
REQUIRE(path->commands[2].outX == 50.0f * rive::circleConstant);
REQUIRE(path->commands[2].outY == -100.0f);
REQUIRE(path->commands[2].inX == 50.0f);
REQUIRE(path->commands[2].inY == -100.0f * rive::circleConstant);
REQUIRE(path->commands[2].x == 50.0f);
REQUIRE(path->commands[2].y == 0.0f);
REQUIRE(verbs[1] == rive::PathVerb::cubic);
REQUIRE(points[1].x == 50.0f * rive::circleConstant);
REQUIRE(points[1].y == -100.0f);
REQUIRE(points[2].x == 50.0f);
REQUIRE(points[2].y == -100.0f * rive::circleConstant);
REQUIRE(points[3].x == 50.0f);
REQUIRE(points[3].y == 0.0f);
// 2nd
REQUIRE(path->commands[3].command == TestPathCommandType::CubicTo);
REQUIRE(path->commands[3].outX == 50.0f);
REQUIRE(path->commands[3].outY == 100.0f * rive::circleConstant);
REQUIRE(path->commands[3].inX == 50.0f * rive::circleConstant);
REQUIRE(path->commands[3].inY == 100.0f);
REQUIRE(path->commands[3].x == 0.0f);
REQUIRE(path->commands[3].y == 100.0f);
REQUIRE(verbs[2] == rive::PathVerb::cubic);
REQUIRE(points[4].x == 50.0f);
REQUIRE(points[4].y == 100.0f * rive::circleConstant);
REQUIRE(points[5].x == 50.0f * rive::circleConstant);
REQUIRE(points[5].y == 100.0f);
REQUIRE(points[6].x == 0.0f);
REQUIRE(points[6].y == 100.0f);
// 3rd
REQUIRE(path->commands[4].command == TestPathCommandType::CubicTo);
REQUIRE(path->commands[4].outX == -50.0f * rive::circleConstant);
REQUIRE(path->commands[4].outY == 100.0f);
REQUIRE(path->commands[4].inX == -50.0f);
REQUIRE(path->commands[4].inY == 100.0f * rive::circleConstant);
REQUIRE(path->commands[4].x == -50.0f);
REQUIRE(path->commands[4].y == 0.0f);
REQUIRE(verbs[3] == rive::PathVerb::cubic);
REQUIRE(points[7].x == -50.0f * rive::circleConstant);
REQUIRE(points[7].y == 100.0f);
REQUIRE(points[8].x == -50.0f);
REQUIRE(points[8].y == 100.0f * rive::circleConstant);
REQUIRE(points[9].x == -50.0f);
REQUIRE(points[9].y == 0.0f);
// 4th
REQUIRE(path->commands[5].command == TestPathCommandType::CubicTo);
REQUIRE(path->commands[5].outX == -50.0f);
REQUIRE(path->commands[5].outY == -100.0f * rive::circleConstant);
REQUIRE(path->commands[5].inX == -50.0f * rive::circleConstant);
REQUIRE(path->commands[5].inY == -100.0f);
REQUIRE(path->commands[5].x == 0.0f);
REQUIRE(path->commands[5].y == -100.0f);
REQUIRE(verbs[4] == rive::PathVerb::cubic);
REQUIRE(points[10].x == -50.0f);
REQUIRE(points[10].y == -100.0f * rive::circleConstant);
REQUIRE(points[11].x == -50.0f * rive::circleConstant);
REQUIRE(points[11].y == -100.0f);
REQUIRE(points[12].x == 0.0f);
REQUIRE(points[12].y == -100.0f);
REQUIRE(path->commands[6].command == TestPathCommandType::Close);
REQUIRE(verbs[5] == rive::PathVerb::close);
}
TEST_CASE("nested solo with shape expanded and path collapsed", "[path]")
@@ -295,7 +290,7 @@ TEST_CASE("nested solo with shape expanded and path collapsed", "[path]")
auto path = solo->children()[1]->as<rive::Path>();
REQUIRE(rectangleShape->isCollapsed() == false);
REQUIRE(path->isCollapsed() == true);
REQUIRE(path->commandPath() != nullptr);
auto pathComposer = rootShape->pathComposer();
auto pathComposerPath = static_cast<TestRenderPath*>(pathComposer->localPath());
// Path is skipped and the nested shape forms its own drawable, so size is 0
@@ -327,7 +322,7 @@ TEST_CASE("nested solo clipping with shape collapsed and path expanded", "[path]
auto path = solo->children()[1]->as<rive::Path>();
REQUIRE(rectangleShape->isCollapsed() == true);
REQUIRE(path->isCollapsed() == false);
REQUIRE(path->commandPath() != nullptr);
auto clippingShape = rectangleClip->clippingShapes()[0];
REQUIRE(clippingShape != nullptr);
auto clippingPath = static_cast<TestRenderPath*>(clippingShape->renderPath());