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());