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>
diff --git a/.rive_head b/.rive_head
index 6e06eb9..8bafa52 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-085f5bd2dce2d25561b00eebc5f7118a2c8769eb
+8486c344528d5f7f84de1984510064a7d0788e73
diff --git a/include/rive/artboard.hpp b/include/rive/artboard.hpp
index b8fdea3..d93d91d 100644
--- a/include/rive/artboard.hpp
+++ b/include/rive/artboard.hpp
@@ -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 @@
     bool m_HasChangedDrawOrderInLastUpdate = false;
 
     unsigned int m_DirtDepth = 0;
+    RawPath m_backgroundRawPath;
     rcp<RenderPath> m_BackgroundPath;
     rcp<RenderPath> m_ClipPath;
     Factory* m_Factory = nullptr;
diff --git a/include/rive/constraints/follow_path_constraint.hpp b/include/rive/constraints/follow_path_constraint.hpp
index 604e578..a59b0f6 100644
--- a/include/rive/constraints/follow_path_constraint.hpp
+++ b/include/rive/constraints/follow_path_constraint.hpp
@@ -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;
diff --git a/include/rive/shapes/metrics_path.hpp b/include/rive/shapes/metrics_path.hpp
deleted file mode 100644
index 7311b9c..0000000
--- a/include/rive/shapes/metrics_path.hpp
+++ /dev/null
@@ -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
diff --git a/include/rive/shapes/paint/fill.hpp b/include/rive/shapes/paint/fill.hpp
index 9e8df0b..97291e6 100644
--- a/include/rive/shapes/paint/fill.hpp
+++ b/include/rive/shapes/paint/fill.hpp
@@ -9,7 +9,10 @@
 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
diff --git a/include/rive/shapes/paint/shape_paint.hpp b/include/rive/shapes/paint/shape_paint.hpp
index 3673580..388fb13 100644
--- a/include/rive/shapes/paint/shape_paint.hpp
+++ b/include/rive/shapes/paint/shape_paint.hpp
@@ -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 @@
 
     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(); }
 
diff --git a/include/rive/shapes/paint/stroke.hpp b/include/rive/shapes/paint/stroke.hpp
index 592eb8f..7663669 100644
--- a/include/rive/shapes/paint/stroke.hpp
+++ b/include/rive/shapes/paint/stroke.hpp
@@ -13,7 +13,10 @@
 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();
diff --git a/include/rive/shapes/paint/stroke_effect.hpp b/include/rive/shapes/paint/stroke_effect.hpp
index 2276d94..72ed716 100644
--- a/include/rive/shapes/paint/stroke_effect.hpp
+++ b/include/rive/shapes/paint/stroke_effect.hpp
@@ -7,13 +7,13 @@
 {
 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
diff --git a/include/rive/shapes/paint/trim_path.hpp b/include/rive/shapes/paint/trim_path.hpp
index 3c55ffb..d937481 100644
--- a/include/rive/shapes/paint/trim_path.hpp
+++ b/include/rive/shapes/paint/trim_path.hpp
@@ -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
 
diff --git a/include/rive/shapes/path.hpp b/include/rive/shapes/path.hpp
index 5732cfb..d5ba979 100644
--- a/include/rive/shapes/path.hpp
+++ b/include/rive/shapes/path.hpp
@@ -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 @@
 {
 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 @@
     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 @@
     std::vector<PathVertex*>& vertices() { return m_Vertices; }
 #endif
 
-    // pour ourselves into a command-path
-    void buildPath(CommandPath&) const;
+    void buildPath(RawPath&) const;
 };
 } // namespace rive
 
diff --git a/include/rive/shapes/path_composer.hpp b/include/rive/shapes/path_composer.hpp
index aa6ccbc..b9b3c8f 100644
--- a/include/rive/shapes/path_composer.hpp
+++ b/include/rive/shapes/path_composer.hpp
@@ -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 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
diff --git a/include/rive/shapes/shape_paint_container.hpp b/include/rive/shapes/shape_paint_container.hpp
index 690ccca..7ba4d76 100644
--- a/include/rive/shapes/shape_paint_container.hpp
+++ b/include/rive/shapes/shape_paint_container.hpp
@@ -35,8 +35,6 @@
 
     void invalidateStrokeEffects();
 
-    rcp<CommandPath> makeCommandPath(PathSpace space);
-
     void propagateOpacity(float opacity);
 
 #ifdef TESTING
diff --git a/src/artboard.cpp b/src/artboard.cpp
index 4458ecf..6fb08e7 100644
--- a/src/artboard.cpp
+++ b/src/artboard.cpp
@@ -454,7 +454,10 @@
             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 @@
     {
         for (auto shapePaint : m_ShapePaints)
         {
-            shapePaint->draw(renderer, m_BackgroundPath.get());
+            shapePaint->draw(renderer, m_BackgroundPath.get(), &m_backgroundRawPath);
         }
     }
 
diff --git a/src/constraints/follow_path_constraint.cpp b/src/constraints/follow_path_constraint.cpp
index 0b8096f..199a0db 100644
--- a/src/constraints/follow_path_constraint.cpp
+++ b/src/constraints/follow_path_constraint.cpp
@@ -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 @@
         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);
diff --git a/src/shapes/metrics_path.cpp b/src/shapes/metrics_path.cpp
deleted file mode 100644
index f66aa5e..0000000
--- a/src/shapes/metrics_path.cpp
+++ /dev/null
@@ -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); }
diff --git a/src/shapes/paint/fill.cpp b/src/shapes/paint/fill.cpp
index 60c39d8..466b61a 100644
--- a/src/shapes/paint/fill.cpp
+++ b/src/shapes/paint/fill.cpp
@@ -18,7 +18,7 @@
     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())
     {
diff --git a/src/shapes/paint/stroke.cpp b/src/shapes/paint/stroke.cpp
index a32b7b3..b0d7348 100644
--- a/src/shapes/paint/stroke.cpp
+++ b/src/shapes/paint/stroke.cpp
@@ -33,18 +33,17 @@
 
 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);
diff --git a/src/shapes/paint/trim_path.cpp b/src/shapes/paint/trim_path.cpp
index 7fad542..6b0e334 100644
--- a/src/shapes/paint/trim_path.cpp
+++ b/src/shapes/paint/trim_path.cpp
@@ -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 @@
     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 @@
                 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 @@
                     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;
+}
\ No newline at end of file
diff --git a/src/shapes/path.cpp b/src/shapes/path.cpp
index 5354580..cdf45aa 100644
--- a/src/shapes/path.cpp
+++ b/src/shapes/path.cpp
@@ -47,14 +47,7 @@
     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 @@
 
 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 @@
         startIn = cubic->renderIn();
         out = cubic->renderOut();
         start = cubic->renderTranslation();
-        commandPath.move(start);
+        rawPath.move(start);
     }
     else
     {
@@ -125,18 +125,18 @@
             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 @@
             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 @@
                 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 @@
             else
             {
                 out = pos;
-                commandPath.line(out);
+                rawPath.line(out);
             }
         }
     }
@@ -214,13 +214,13 @@
     {
         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 @@
 {
     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 @@
         // 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)
     // {
diff --git a/src/shapes/path_composer.cpp b/src/shapes/path_composer.cpp
index e0bf032..71a4c00 100644
--- a/src/shapes/path_composer.cpp
+++ b/src/shapes/path_composer.cpp
@@ -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 @@
         // 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 @@
 {
     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();
     }
 }
 
diff --git a/src/shapes/shape.cpp b/src/shapes/shape.cpp
index e37b474..01310ed 100644
--- a/src/shapes/shape.cpp
+++ b/src/shapes/shape.cpp
@@ -24,9 +24,8 @@
 
 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 @@
                 return false;
             }
         }
-        for (auto path : m_Paths)
-        {
-            if (!path->canDeferPathUpdate())
-            {
-                return false;
-            }
-        }
     }
     return canDefer;
 }
@@ -113,9 +105,10 @@
             {
                 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 @@
         if (!path->isCollapsed())
         {
             tester.setXform(path->pathTransform());
-            path->buildPath(tester);
+            path->rawPath().addTo(&tester);
         }
     }
     return tester.wasHit();
@@ -184,7 +177,7 @@
             {
                 tester.setXform(mx * path->pathTransform());
             }
-            path->buildPath(tester);
+            path->rawPath().addTo(&tester);
         }
         if (tester.wasHit())
         {
@@ -284,8 +277,7 @@
         {
             continue;
         }
-
-        path->buildPath(boundsCalculator);
+        path->rawPath().addTo(&boundsCalculator);
 
         AABB aabb = boundsCalculator.bounds(xform == nullptr ? path->pathTransform()
                                                              : path->pathTransform() * *xform);
diff --git a/src/shapes/shape_paint_container.cpp b/src/shapes/shape_paint_container.cpp
index a0232db..ab9057a 100644
--- a/src/shapes/shape_paint_container.cpp
+++ b/src/shapes/shape_paint_container.cpp
@@ -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 @@
     }
 }
 
-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)
diff --git a/src/text/text_style.cpp b/src/text/text_style.cpp
index e1b1343..a2a81ea 100644
--- a/src/text/text_style.cpp
+++ b/src/text/text_style.cpp
@@ -191,7 +191,7 @@
         {
             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);
         }
     }
 }
diff --git a/tess/test/triangulation_test.cpp b/tess/test/triangulation_test.cpp
index 5837635..d2a4b01 100644
--- a/tess/test/triangulation_test.cpp
+++ b/tess/test/triangulation_test.cpp
@@ -29,7 +29,7 @@
     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;
diff --git a/test/contour_measure_test.cpp b/test/contour_measure_test.cpp
index c9164d6..3548366 100644
--- a/test/contour_measure_test.cpp
+++ b/test/contour_measure_test.cpp
@@ -177,3 +177,25 @@
         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));
+}
diff --git a/test/metrics_path_test.cpp b/test/metrics_path_test.cpp
deleted file mode 100644
index cde4727..0000000
--- a/test/metrics_path_test.cpp
+++ /dev/null
@@ -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));
-}
diff --git a/test/path_test.cpp b/test/path_test.cpp
index bd58078..a664075 100644
--- a/test/path_test.cpp
+++ b/test/path_test.cpp
@@ -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 @@
 
     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 @@
 
     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 @@
     // 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 @@
 
     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 @@
 
     // 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 @@
     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 @@
     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());