Follow Path Constraint (Editor & CPP Runtime)

This PR includes a bunch of unrelated core defs that were missing from the CPP runtime to bring us up to date. Requirements and test cases here: https://www.notion.so/rive-app/FollowPathConstraint-8a1de3aa7494461c8ba79b8915acfd9d?pvs=4

Diffs=
dcf320c64 Follow Path Constraint (Editor & CPP Runtime) (#5510)

Co-authored-by: Philip Chung <philterdesign@gmail.com>
diff --git a/.rive_head b/.rive_head
index fe43500..99b7c41 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-9b8dacbacbfb232303773dd7a6008fdffaecc29c
+dcf320c64584505acef32ddf5f316fa5cb6d0813
diff --git a/dev/defs/constraints/follow_path_constraint.json b/dev/defs/constraints/follow_path_constraint.json
new file mode 100644
index 0000000..846cd01
--- /dev/null
+++ b/dev/defs/constraints/follow_path_constraint.json
@@ -0,0 +1,39 @@
+{
+  "name": "FollowPathConstraint",
+  "key": {
+    "int": 165,
+    "string": "followpathconstraint"
+  },
+  "extends": "constraints/transform_constraint.json",
+  "properties": {
+    "distance": {
+      "type": "double",
+      "initialValue": "0",
+      "animates": true,
+      "key": {
+        "int": 363,
+        "string": "distance"
+      },
+      "description": "Distance along the path to follow."
+    },
+    "orient": {
+      "type": "bool",
+      "initialValue": "true",
+      "animates": true,
+      "key": {
+        "int": 364,
+        "string": "orient"
+      },
+      "description": "True when the orientation from the path is copied to the constrained transform."
+    },
+    "offset": {
+      "type": "bool",
+      "initialValue": "true",
+      "key": {
+        "int": 365,
+        "string": "offset"
+      },
+      "description": "True when the local translation is used to offset the transformed one."
+    }
+  }
+}
\ No newline at end of file
diff --git a/include/rive/constraints/follow_path_constraint.hpp b/include/rive/constraints/follow_path_constraint.hpp
new file mode 100644
index 0000000..a6221d9
--- /dev/null
+++ b/include/rive/constraints/follow_path_constraint.hpp
@@ -0,0 +1,23 @@
+#ifndef _RIVE_FOLLOW_PATH_CONSTRAINT_HPP_
+#define _RIVE_FOLLOW_PATH_CONSTRAINT_HPP_
+#include "rive/generated/constraints/follow_path_constraint_base.hpp"
+#include "rive/shapes/metrics_path.hpp"
+#include <stdio.h>
+namespace rive
+{
+class FollowPathConstraint : public FollowPathConstraintBase
+{
+private:
+    std::unique_ptr<MetricsPath> m_WorldPath;
+
+public:
+    void distanceChanged() override;
+    void orientChanged() override;
+    StatusCode onAddedClean(CoreContext* context) override;
+    const Mat2D targetTransform() const override;
+    void update(ComponentDirt value) override;
+    void buildDependencies() override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/constraints/transform_constraint.hpp b/include/rive/constraints/transform_constraint.hpp
index 3368b08..7aae972 100644
--- a/include/rive/constraints/transform_constraint.hpp
+++ b/include/rive/constraints/transform_constraint.hpp
@@ -13,6 +13,7 @@
     TransformComponents m_ComponentsB;
 
 public:
+    virtual const Mat2D targetTransform() const;
     void constrain(TransformComponent* component) override;
 };
 } // namespace rive
diff --git a/include/rive/generated/constraints/follow_path_constraint_base.hpp b/include/rive/generated/constraints/follow_path_constraint_base.hpp
new file mode 100644
index 0000000..8d80886
--- /dev/null
+++ b/include/rive/generated/constraints/follow_path_constraint_base.hpp
@@ -0,0 +1,112 @@
+#ifndef _RIVE_FOLLOW_PATH_CONSTRAINT_BASE_HPP_
+#define _RIVE_FOLLOW_PATH_CONSTRAINT_BASE_HPP_
+#include "rive/constraints/transform_constraint.hpp"
+#include "rive/core/field_types/core_bool_type.hpp"
+#include "rive/core/field_types/core_double_type.hpp"
+namespace rive
+{
+class FollowPathConstraintBase : public TransformConstraint
+{
+protected:
+    typedef TransformConstraint Super;
+
+public:
+    static const uint16_t typeKey = 165;
+
+    /// Helper to quickly determine if a core object extends another without RTTI
+    /// at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case FollowPathConstraintBase::typeKey:
+            case TransformConstraintBase::typeKey:
+            case TransformSpaceConstraintBase::typeKey:
+            case TargetedConstraintBase::typeKey:
+            case ConstraintBase::typeKey:
+            case ComponentBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t distancePropertyKey = 363;
+    static const uint16_t orientPropertyKey = 364;
+    static const uint16_t offsetPropertyKey = 365;
+
+private:
+    float m_Distance = 0.0f;
+    bool m_Orient = true;
+    bool m_Offset = true;
+
+public:
+    inline float distance() const { return m_Distance; }
+    void distance(float value)
+    {
+        if (m_Distance == value)
+        {
+            return;
+        }
+        m_Distance = value;
+        distanceChanged();
+    }
+
+    inline bool orient() const { return m_Orient; }
+    void orient(bool value)
+    {
+        if (m_Orient == value)
+        {
+            return;
+        }
+        m_Orient = value;
+        orientChanged();
+    }
+
+    inline bool offset() const { return m_Offset; }
+    void offset(bool value)
+    {
+        if (m_Offset == value)
+        {
+            return;
+        }
+        m_Offset = value;
+        offsetChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const FollowPathConstraintBase& object)
+    {
+        m_Distance = object.m_Distance;
+        m_Orient = object.m_Orient;
+        m_Offset = object.m_Offset;
+        TransformConstraint::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case distancePropertyKey:
+                m_Distance = CoreDoubleType::deserialize(reader);
+                return true;
+            case orientPropertyKey:
+                m_Orient = CoreBoolType::deserialize(reader);
+                return true;
+            case offsetPropertyKey:
+                m_Offset = CoreBoolType::deserialize(reader);
+                return true;
+        }
+        return TransformConstraint::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void distanceChanged() {}
+    virtual void orientChanged() {}
+    virtual void offsetChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/core_registry.hpp b/include/rive/generated/core_registry.hpp
index dd2a0a3..ed92964 100644
--- a/include/rive/generated/core_registry.hpp
+++ b/include/rive/generated/core_registry.hpp
@@ -74,6 +74,7 @@
 #include "rive/component.hpp"
 #include "rive/constraints/constraint.hpp"
 #include "rive/constraints/distance_constraint.hpp"
+#include "rive/constraints/follow_path_constraint.hpp"
 #include "rive/constraints/ik_constraint.hpp"
 #include "rive/constraints/rotation_constraint.hpp"
 #include "rive/constraints/scale_constraint.hpp"
@@ -142,10 +143,12 @@
                 return new DistanceConstraint();
             case IKConstraintBase::typeKey:
                 return new IKConstraint();
-            case TranslationConstraintBase::typeKey:
-                return new TranslationConstraint();
             case TransformConstraintBase::typeKey:
                 return new TransformConstraint();
+            case FollowPathConstraintBase::typeKey:
+                return new FollowPathConstraint();
+            case TranslationConstraintBase::typeKey:
+                return new TranslationConstraint();
             case ScaleConstraintBase::typeKey:
                 return new ScaleConstraint();
             case RotationConstraintBase::typeKey:
@@ -601,6 +604,9 @@
             case TransformComponentConstraintYBase::maxValueYPropertyKey:
                 object->as<TransformComponentConstraintYBase>()->maxValueY(value);
                 break;
+            case FollowPathConstraintBase::distancePropertyKey:
+                object->as<FollowPathConstraintBase>()->distance(value);
+                break;
             case WorldTransformComponentBase::opacityPropertyKey:
                 object->as<WorldTransformComponentBase>()->opacity(value);
                 break;
@@ -910,6 +916,12 @@
             case IKConstraintBase::invertDirectionPropertyKey:
                 object->as<IKConstraintBase>()->invertDirection(value);
                 break;
+            case FollowPathConstraintBase::orientPropertyKey:
+                object->as<FollowPathConstraintBase>()->orient(value);
+                break;
+            case FollowPathConstraintBase::offsetPropertyKey:
+                object->as<FollowPathConstraintBase>()->offset(value);
+                break;
             case NestedSimpleAnimationBase::isPlayingPropertyKey:
                 object->as<NestedSimpleAnimationBase>()->isPlaying(value);
                 break;
@@ -1154,6 +1166,8 @@
                 return object->as<TransformComponentConstraintYBase>()->minValueY();
             case TransformComponentConstraintYBase::maxValueYPropertyKey:
                 return object->as<TransformComponentConstraintYBase>()->maxValueY();
+            case FollowPathConstraintBase::distancePropertyKey:
+                return object->as<FollowPathConstraintBase>()->distance();
             case WorldTransformComponentBase::opacityPropertyKey:
                 return object->as<WorldTransformComponentBase>()->opacity();
             case TransformComponentBase::rotationPropertyKey:
@@ -1363,6 +1377,10 @@
                 return object->as<TransformComponentConstraintYBase>()->maxY();
             case IKConstraintBase::invertDirectionPropertyKey:
                 return object->as<IKConstraintBase>()->invertDirection();
+            case FollowPathConstraintBase::orientPropertyKey:
+                return object->as<FollowPathConstraintBase>()->orient();
+            case FollowPathConstraintBase::offsetPropertyKey:
+                return object->as<FollowPathConstraintBase>()->offset();
             case NestedSimpleAnimationBase::isPlayingPropertyKey:
                 return object->as<NestedSimpleAnimationBase>()->isPlaying();
             case KeyFrameBoolBase::valuePropertyKey:
@@ -1495,6 +1513,7 @@
             case TransformComponentConstraintYBase::copyFactorYPropertyKey:
             case TransformComponentConstraintYBase::minValueYPropertyKey:
             case TransformComponentConstraintYBase::maxValueYPropertyKey:
+            case FollowPathConstraintBase::distancePropertyKey:
             case WorldTransformComponentBase::opacityPropertyKey:
             case TransformComponentBase::rotationPropertyKey:
             case TransformComponentBase::scaleXPropertyKey:
@@ -1597,6 +1616,8 @@
             case TransformComponentConstraintYBase::minYPropertyKey:
             case TransformComponentConstraintYBase::maxYPropertyKey:
             case IKConstraintBase::invertDirectionPropertyKey:
+            case FollowPathConstraintBase::orientPropertyKey:
+            case FollowPathConstraintBase::offsetPropertyKey:
             case NestedSimpleAnimationBase::isPlayingPropertyKey:
             case KeyFrameBoolBase::valuePropertyKey:
             case NestedBoolBase::nestedValuePropertyKey:
diff --git a/include/rive/math/vec2d.hpp b/include/rive/math/vec2d.hpp
index c173bd1..2d6b723 100644
--- a/include/rive/math/vec2d.hpp
+++ b/include/rive/math/vec2d.hpp
@@ -47,6 +47,7 @@
     static inline Vec2D lerp(Vec2D a, Vec2D b, float f);
 
     static Vec2D transformDir(const Vec2D& a, const Mat2D& m);
+    static Vec2D transformMat2D(const Vec2D& a, const Mat2D& m);
 
     static float dot(Vec2D a, Vec2D b) { return a.x * b.x + a.y * b.y; }
     static float cross(Vec2D a, Vec2D b) { return a.x * b.y - a.y * b.x; }
diff --git a/include/rive/shapes/metrics_path.hpp b/include/rive/shapes/metrics_path.hpp
index b6798e8..2c1e7f7 100644
--- a/include/rive/shapes/metrics_path.hpp
+++ b/include/rive/shapes/metrics_path.hpp
@@ -21,6 +21,7 @@
 
 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;
diff --git a/include/rive/shapes/path_space.hpp b/include/rive/shapes/path_space.hpp
index 5b54483..d44e5c7 100644
--- a/include/rive/shapes/path_space.hpp
+++ b/include/rive/shapes/path_space.hpp
@@ -10,7 +10,8 @@
     Neither = 0,
     Local = 1 << 1,
     World = 1 << 2,
-    Clipping = 1 << 3
+    Clipping = 1 << 3,
+    FollowPath = 1 << 4
 };
 
 inline constexpr PathSpace operator&(PathSpace lhs, PathSpace rhs)
diff --git a/include/rive/transform_component.hpp b/include/rive/transform_component.hpp
index 258949e..af883a0 100644
--- a/include/rive/transform_component.hpp
+++ b/include/rive/transform_component.hpp
@@ -16,9 +16,7 @@
     std::vector<Constraint*> m_Constraints;
 
 public:
-#ifdef TESTING
     const std::vector<Constraint*>& constraints() const { return m_Constraints; }
-#endif
     StatusCode onAddedClean(CoreContext* context) override;
     void buildDependencies() override;
     void update(ComponentDirt value) override;
diff --git a/src/constraints/follow_path_constraint.cpp b/src/constraints/follow_path_constraint.cpp
new file mode 100644
index 0000000..4f967ce
--- /dev/null
+++ b/src/constraints/follow_path_constraint.cpp
@@ -0,0 +1,107 @@
+#include "rive/artboard.hpp"
+#include "rive/command_path.hpp"
+#include "rive/constraints/follow_path_constraint.hpp"
+#include "rive/factory.hpp"
+#include "rive/math/contour_measure.hpp"
+#include "rive/shapes/metrics_path.hpp"
+#include "rive/shapes/path.hpp"
+#include "rive/shapes/shape.hpp"
+#include <algorithm>
+#include <iostream>
+#include <typeinfo>
+
+using namespace rive;
+
+void FollowPathConstraint::distanceChanged() { markConstraintDirty(); }
+void FollowPathConstraint::orientChanged() { markConstraintDirty(); }
+
+const Mat2D FollowPathConstraint::targetTransform() const
+{
+    if (!m_Target->is<Shape>())
+    {
+        return m_Target->worldTransform();
+    }
+    MetricsPath* metricsPath = m_WorldPath.get();
+    if (metricsPath == nullptr)
+    {
+        return m_Target->worldTransform();
+    }
+
+    const std::vector<MetricsPath*>& paths = metricsPath->paths();
+    float totalLength = metricsPath->length();
+    float distanceUnits = totalLength * std::min(1.0f, std::max(0.0f, distance()));
+    float runningLength = 0;
+    ContourMeasure::PosTan posTan;
+    for (auto path : paths)
+    {
+        float pathLength = path->length();
+        if (distanceUnits < pathLength + runningLength)
+        {
+            posTan = path->contourMeasure()->getPosTan(distanceUnits - runningLength);
+            break;
+        }
+        runningLength += pathLength;
+    }
+    Vec2D position = Vec2D(posTan.pos.x, posTan.pos.y);
+
+    Mat2D transformB = Mat2D(m_Target->worldTransform());
+    transformB[4] = position.x;
+    transformB[5] = position.y;
+
+    if (offset())
+    {
+        if (parent()->is<TransformComponent>())
+        {
+            transformB *= parent()->as<TransformComponent>()->transform();
+        }
+    }
+    if (orient())
+    {
+        transformB *= Mat2D::fromRotation(std::atan2(posTan.tan.y, posTan.tan.x));
+    }
+    return transformB;
+}
+
+void FollowPathConstraint::update(ComponentDirt value)
+{
+    if (!m_Target->is<Shape>())
+    {
+        return;
+    }
+
+    Shape* shape = static_cast<Shape*>(m_Target);
+    if (hasDirt(value, ComponentDirt::Path))
+    {
+        if (m_WorldPath == nullptr)
+        {
+            m_WorldPath = std::unique_ptr<MetricsPath>(new OnlyMetricsPath());
+        }
+        else
+        {
+            m_WorldPath->rewind();
+        }
+        for (auto path : shape->paths())
+        {
+            const Mat2D& transform = path->pathTransform();
+            m_WorldPath->addPath(path->commandPath(), transform);
+        }
+    }
+}
+
+StatusCode FollowPathConstraint::onAddedClean(CoreContext* context)
+{
+    Shape* shape = static_cast<Shape*>(m_Target);
+    shape->addDefaultPathSpace(PathSpace::FollowPath);
+    return Super::onAddedClean(context);
+}
+
+void FollowPathConstraint::buildDependencies()
+{
+    assert(m_Target != nullptr);
+    Super::buildDependencies();
+    if (m_Target != nullptr && m_Target->is<Shape>()) // which should never happen
+    {
+        Shape* shape = static_cast<Shape*>(m_Target);
+        shape->pathComposer()->addDependent(this);
+    }
+}
diff --git a/src/constraints/transform_constraint.cpp b/src/constraints/transform_constraint.cpp
index b66b61a..f68be6e 100644
--- a/src/constraints/transform_constraint.cpp
+++ b/src/constraints/transform_constraint.cpp
@@ -5,6 +5,8 @@
 
 using namespace rive;
 
+const Mat2D TransformConstraint::targetTransform() const { return m_Target->worldTransform(); }
+
 void TransformConstraint::constrain(TransformComponent* component)
 {
     if (m_Target == nullptr)
@@ -13,7 +15,7 @@
     }
 
     const Mat2D& transformA = component->worldTransform();
-    Mat2D transformB(m_Target->worldTransform());
+    Mat2D transformB(targetTransform());
     if (sourceSpace() == TransformSpace::local)
     {
         const Mat2D& targetParentWorld = getParentWorld(*m_Target);
diff --git a/src/generated/constraints/follow_path_constraint_base.cpp b/src/generated/constraints/follow_path_constraint_base.cpp
new file mode 100644
index 0000000..c35b2cf
--- /dev/null
+++ b/src/generated/constraints/follow_path_constraint_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/constraints/follow_path_constraint_base.hpp"
+#include "rive/constraints/follow_path_constraint.hpp"
+
+using namespace rive;
+
+Core* FollowPathConstraintBase::clone() const
+{
+    auto cloned = new FollowPathConstraint();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/math/vec2d.cpp b/src/math/vec2d.cpp
index 6433973..e0b8233 100644
--- a/src/math/vec2d.cpp
+++ b/src/math/vec2d.cpp
@@ -11,6 +11,10 @@
         m[1] * a.x + m[3] * a.y,
     };
 }
+Vec2D Vec2D::transformMat2D(const Vec2D& a, const Mat2D& m)
+{
+    return {m[0] * a.x + m[2] * a.y + m[4], m[1] * a.x + m[3] * a.y + m[5]};
+}
 float Vec2D::length() const { return std::sqrt(lengthSquared()); }
 
 Vec2D Vec2D::normalized() const
diff --git a/src/shapes/path_composer.cpp b/src/shapes/path_composer.cpp
index 30905fc..4025606 100644
--- a/src/shapes/path_composer.cpp
+++ b/src/shapes/path_composer.cpp
@@ -40,12 +40,14 @@
         m_deferredPathDirt = false;
 
         auto space = m_Shape->pathSpace();
-
+        bool hasConstraint = (space & PathSpace::FollowPath) == PathSpace::FollowPath;
         if ((space & PathSpace::Local) == PathSpace::Local)
         {
             if (m_LocalPath == nullptr)
             {
-                m_LocalPath = m_Shape->makeCommandPath(PathSpace::Local);
+                PathSpace localSpace =
+                    (hasConstraint) ? PathSpace::Local & PathSpace::FollowPath : PathSpace::Local;
+                m_LocalPath = m_Shape->makeCommandPath(localSpace);
             }
             else
             {
@@ -64,7 +66,9 @@
         {
             if (m_WorldPath == nullptr)
             {
-                m_WorldPath = m_Shape->makeCommandPath(PathSpace::World);
+                PathSpace worldSpace =
+                    (hasConstraint) ? PathSpace::World & PathSpace::FollowPath : PathSpace::World;
+                m_WorldPath = m_Shape->makeCommandPath(worldSpace);
             }
             else
             {
diff --git a/src/shapes/polygon.cpp b/src/shapes/polygon.cpp
index 1bb12f6..6791b4e 100644
--- a/src/shapes/polygon.cpp
+++ b/src/shapes/polygon.cpp
@@ -25,7 +25,7 @@
     auto oy = -originY() * height() + halfHeight;
 
     auto angle = -math::PI / 2;
-    auto inc = 2 * -math::PI / points();
+    auto inc = 2 * math::PI / points();
 
     for (StraightVertex& vertex : m_PolygonVertices)
     {
diff --git a/src/shapes/shape.cpp b/src/shapes/shape.cpp
index 2695410..002061e 100644
--- a/src/shapes/shape.cpp
+++ b/src/shapes/shape.cpp
@@ -1,3 +1,4 @@
+#include "rive/constraints/constraint.hpp"
 #include "rive/hittest_command_path.hpp"
 #include "rive/shapes/path.hpp"
 #include "rive/shapes/shape.hpp"
@@ -46,6 +47,10 @@
 void Shape::pathChanged()
 {
     m_PathComposer.addDirt(ComponentDirt::Path, true);
+    for (auto constraint : constraints())
+    {
+        constraint->addDirt(ComponentDirt::Path);
+    }
     invalidateStrokeEffects();
 }
 
diff --git a/src/shapes/shape_paint_container.cpp b/src/shapes/shape_paint_container.cpp
index 283568a..1848125 100644
--- a/src/shapes/shape_paint_container.cpp
+++ b/src/shapes/shape_paint_container.cpp
@@ -54,6 +54,8 @@
     // 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;
 
@@ -79,6 +81,10 @@
     {
         return std::unique_ptr<CommandPath>(new RenderMetricsPath(factory->makeEmptyRenderPath()));
     }
+    else if (needForConstraint)
+    {
+        return std::unique_ptr<CommandPath>(new RenderMetricsPath(factory->makeEmptyRenderPath()));
+    }
     else if (needForEffects)
     {
         return std::unique_ptr<CommandPath>(new OnlyMetricsPath());