Adding rotation and scale constraints.
diff --git a/include/rive/constraints/rotation_constraint.hpp b/include/rive/constraints/rotation_constraint.hpp index b015c26..1c6e140 100644 --- a/include/rive/constraints/rotation_constraint.hpp +++ b/include/rive/constraints/rotation_constraint.hpp
@@ -1,11 +1,16 @@ #ifndef _RIVE_ROTATION_CONSTRAINT_HPP_ #define _RIVE_ROTATION_CONSTRAINT_HPP_ #include "rive/generated/constraints/rotation_constraint_base.hpp" +#include "rive/math/transform_components.hpp" #include <stdio.h> namespace rive { class RotationConstraint : public RotationConstraintBase { + private: + TransformComponents m_ComponentsA; + TransformComponents m_ComponentsB; + public: void constrain(TransformComponent* component) override; };
diff --git a/include/rive/constraints/scale_constraint.hpp b/include/rive/constraints/scale_constraint.hpp index e9953d5..6249fb0 100644 --- a/include/rive/constraints/scale_constraint.hpp +++ b/include/rive/constraints/scale_constraint.hpp
@@ -1,11 +1,16 @@ #ifndef _RIVE_SCALE_CONSTRAINT_HPP_ #define _RIVE_SCALE_CONSTRAINT_HPP_ #include "rive/generated/constraints/scale_constraint_base.hpp" +#include "rive/math/transform_components.hpp" #include <stdio.h> namespace rive { class ScaleConstraint : public ScaleConstraintBase { + private: + TransformComponents m_ComponentsA; + TransformComponents m_ComponentsB; + public: void constrain(TransformComponent* component) override; };
diff --git a/src/constraints/rotation_constraint.cpp b/src/constraints/rotation_constraint.cpp index a9fead0..30b80e4 100644 --- a/src/constraints/rotation_constraint.cpp +++ b/src/constraints/rotation_constraint.cpp
@@ -5,4 +5,108 @@ using namespace rive; -void RotationConstraint::constrain(TransformComponent* component) {} +void RotationConstraint::constrain(TransformComponent* component) +{ + const Mat2D& transformA = component->worldTransform(); + Mat2D transformB; + Mat2D::decompose(m_ComponentsA, transformA); + if (m_Target == nullptr) + { + Mat2D::copy(transformB, transformA); + TransformComponents::copy(m_ComponentsB, m_ComponentsA); + } + else + { + Mat2D::copy(transformB, m_Target->worldTransform()); + if (sourceSpace() == TransformSpace::local) + { + Mat2D inverse; + + if (!Mat2D::invert(inverse, getParentWorld(*m_Target))) + { + return; + } + Mat2D::multiply(transformB, inverse, transformB); + } + + Mat2D::decompose(m_ComponentsB, transformB); + + if (!doesCopy()) + { + m_ComponentsB.rotation(destSpace() == TransformSpace::local + ? 0.0f + : m_ComponentsA.rotation()); + } + else + { + m_ComponentsB.rotation(m_ComponentsB.rotation() * copyFactor()); + if (offset()) + { + m_ComponentsB.rotation(m_ComponentsB.rotation() + + component->rotation()); + } + } + + if (destSpace() == TransformSpace::local) + { + // Destination space is in parent transform coordinates. Recompose + // the parent local transform and get it in world, then decompose + // the world for interpolation. + + Mat2D::compose(transformB, m_ComponentsB); + Mat2D::multiply(transformB, getParentWorld(*component), transformB); + Mat2D::decompose(m_ComponentsB, transformB); + } + } + bool clampLocal = minMaxSpace() == TransformSpace::local; + if (clampLocal) + { + // Apply min max in local space, so transform to local coordinates + // first. + Mat2D::compose(transformB, m_ComponentsB); + Mat2D inverse = Mat2D(); + if (!Mat2D::invert(inverse, getParentWorld(*component))) + { + return; + } + Mat2D::multiply(transformB, inverse, transformB); + Mat2D::decompose(m_ComponentsB, transformB); + } + if (max() && m_ComponentsB.rotation() > maxValue()) + { + m_ComponentsB.rotation(maxValue()); + } + if (min() && m_ComponentsB.rotation() < minValue()) + { + m_ComponentsB.rotation(minValue()); + } + if (clampLocal) + { + // Transform back to world. + Mat2D::compose(transformB, m_ComponentsB); + Mat2D::multiply(transformB, getParentWorld(*component), transformB); + Mat2D::decompose(m_ComponentsB, transformB); + } + + float angleA = std::fmod(m_ComponentsA.rotation(), (float)M_PI_2); + float angleB = std::fmod(m_ComponentsB.rotation(), (float)M_PI_2); + float diff = angleB - angleA; + + if (diff > M_PI) + { + diff -= M_PI_2; + } + else if (diff < -M_PI) + { + diff += M_PI_2; + } + + m_ComponentsB.rotation(m_ComponentsA.rotation() + diff * strength()); + m_ComponentsB.x(m_ComponentsA.x()); + m_ComponentsB.y(m_ComponentsA.y()); + m_ComponentsB.scaleX(m_ComponentsA.scaleX()); + m_ComponentsB.scaleY(m_ComponentsA.scaleY()); + m_ComponentsB.skew(m_ComponentsA.skew()); + + Mat2D::compose(component->mutableWorldTransform(), m_ComponentsB); +}
diff --git a/src/constraints/scale_constraint.cpp b/src/constraints/scale_constraint.cpp index bcbc11f..0fd6b25 100644 --- a/src/constraints/scale_constraint.cpp +++ b/src/constraints/scale_constraint.cpp
@@ -5,4 +5,123 @@ using namespace rive; -void ScaleConstraint::constrain(TransformComponent* component) {} +void ScaleConstraint::constrain(TransformComponent* component) +{ + const Mat2D& transformA = component->worldTransform(); + Mat2D transformB; + Mat2D::decompose(m_ComponentsA, transformA); + if (m_Target == nullptr) + { + Mat2D::copy(transformB, transformA); + TransformComponents::copy(m_ComponentsB, m_ComponentsA); + } + else + { + Mat2D::copy(transformB, m_Target->worldTransform()); + if (sourceSpace() == TransformSpace::local) + { + Mat2D inverse; + if (!Mat2D::invert(inverse, getParentWorld(*m_Target))) + { + return; + } + Mat2D::multiply(transformB, inverse, transformB); + } + Mat2D::decompose(m_ComponentsB, transformB); + + if (!doesCopy()) + { + m_ComponentsB.scaleX(destSpace() == TransformSpace::local + ? 1.0f + : m_ComponentsA.scaleX()); + } + else + { + m_ComponentsB.scaleX(m_ComponentsB.scaleX() * copyFactor()); + if (offset()) + { + m_ComponentsB.scaleX(m_ComponentsB.scaleX() * + component->scaleX()); + } + } + + if (!doesCopyY()) + { + m_ComponentsB.scaleY(destSpace() == TransformSpace::local + ? 1.0f + : m_ComponentsA.scaleY()); + } + else + { + m_ComponentsB.scaleY(m_ComponentsB.scaleY() * copyFactorY()); + if (offset()) + { + m_ComponentsB.scaleY(m_ComponentsB.scaleY() * + component->scaleY()); + } + } + + if (destSpace() == TransformSpace::local) + { + // Destination space is in parent transform coordinates. Recompose + // the parent local transform and get it in world, then decompose + // the world for interpolation. + + Mat2D::compose(transformB, m_ComponentsB); + Mat2D::multiply(transformB, getParentWorld(*component), transformB); + Mat2D::decompose(m_ComponentsB, transformB); + } + } + + bool clamplocal = minMaxSpace() == TransformSpace::local; + if (clamplocal) + { + // Apply min max in local space, so transform to local coordinates + // first. + Mat2D::compose(transformB, m_ComponentsB); + Mat2D inverse; + if (!Mat2D::invert(inverse, getParentWorld(*component))) + { + return; + } + Mat2D::multiply(transformB, inverse, transformB); + Mat2D::decompose(m_ComponentsB, transformB); + } + if (max() && m_ComponentsB.scaleX() > maxValue()) + { + m_ComponentsB.scaleX(maxValue()); + } + if (min() && m_ComponentsB.scaleX() < minValue()) + { + m_ComponentsB.scaleX(minValue()); + } + if (maxY() && m_ComponentsB.scaleY() > maxValueY()) + { + m_ComponentsB.scaleY(maxValueY()); + } + if (minY() && m_ComponentsB.scaleY() < minValueY()) + { + m_ComponentsB.scaleY(minValueY()); + } + if (clamplocal) + { + // Transform back to world. + Mat2D::compose(transformB, m_ComponentsB); + Mat2D::multiply(transformB, getParentWorld(*component), transformB); + Mat2D::decompose(m_ComponentsB, transformB); + } + + float t = strength(); + float ti = 1.0f - t; + + m_ComponentsB.rotation(m_ComponentsA.rotation()); + m_ComponentsB.x(m_ComponentsA.x()); + m_ComponentsB.y(m_ComponentsA.y()); + m_ComponentsB.scaleX(m_ComponentsA.scaleX() * ti + + m_ComponentsB.scaleX() * t); + m_ComponentsB.scaleY(m_ComponentsA.scaleY() * ti + + m_ComponentsB.scaleY() * t); + m_ComponentsB.skew(m_ComponentsA.skew()); + + Mat2D::compose(component->mutableWorldTransform(), m_ComponentsB); +}
diff --git a/test/assets/rotation_constraint.riv b/test/assets/rotation_constraint.riv new file mode 100644 index 0000000..88363ff --- /dev/null +++ b/test/assets/rotation_constraint.riv Binary files differ
diff --git a/test/assets/scale_constraint.riv b/test/assets/scale_constraint.riv new file mode 100644 index 0000000..d4b5f4c --- /dev/null +++ b/test/assets/scale_constraint.riv Binary files differ
diff --git a/test/rotation_constraint_test.cpp b/test/rotation_constraint_test.cpp new file mode 100644 index 0000000..10b0a89 --- /dev/null +++ b/test/rotation_constraint_test.cpp
@@ -0,0 +1,47 @@ +#include <rive/core/binary_reader.hpp> +#include <rive/file.hpp> +#include <rive/node.hpp> +#include <rive/bones/bone.hpp> +#include <rive/shapes/shape.hpp> +#include <rive/math/transform_components.hpp> +#include "no_op_renderer.hpp" +#include "rive_testing.hpp" +#include <cstdio> + +TEST_CASE("rotation constraint updates world transform", "[file]") +{ + FILE* fp = fopen("../../test/assets/rotation_constraint.riv", "r"); + REQUIRE(fp != nullptr); + + fseek(fp, 0, SEEK_END); + auto length = ftell(fp); + fseek(fp, 0, SEEK_SET); + uint8_t* bytes = new uint8_t[length]; + REQUIRE(fread(bytes, 1, length, fp) == length); + auto reader = rive::BinaryReader(bytes, length); + rive::File* file = nullptr; + auto result = rive::File::import(reader, &file); + + REQUIRE(result == rive::ImportResult::success); + REQUIRE(file != nullptr); + REQUIRE(file->artboard() != nullptr); + + auto artboard = file->artboard(); + + REQUIRE(artboard->find<rive::TransformComponent>("target") != nullptr); + auto target = artboard->find<rive::TransformComponent>("target"); + + REQUIRE(artboard->find<rive::TransformComponent>("rect") != nullptr); + auto rectangle = artboard->find<rive::TransformComponent>("rect"); + + artboard->advance(0.0f); + rive::TransformComponents targetComponents; + rive::Mat2D::decompose(targetComponents, target->worldTransform()); + rive::TransformComponents rectComponents; + rive::Mat2D::decompose(rectComponents, rectangle->worldTransform()); + + REQUIRE(targetComponents.rotation() == rectComponents.rotation()); + + delete file; + delete[] bytes; +} \ No newline at end of file
diff --git a/test/scale_constraint_test.cpp b/test/scale_constraint_test.cpp new file mode 100644 index 0000000..19b312c --- /dev/null +++ b/test/scale_constraint_test.cpp
@@ -0,0 +1,49 @@ +#include <rive/core/binary_reader.hpp> +#include <rive/file.hpp> +#include <rive/node.hpp> +#include <rive/bones/bone.hpp> +#include <rive/shapes/shape.hpp> +#include <rive/math/transform_components.hpp> +#include "no_op_renderer.hpp" +#include "rive_testing.hpp" +#include <cstdio> + +TEST_CASE("scale constraint updates world transform", "[file]") +{ + FILE* fp = fopen("../../test/assets/scale_constraint.riv", "r"); + REQUIRE(fp != nullptr); + + fseek(fp, 0, SEEK_END); + auto length = ftell(fp); + fseek(fp, 0, SEEK_SET); + uint8_t* bytes = new uint8_t[length]; + REQUIRE(fread(bytes, 1, length, fp) == length); + auto reader = rive::BinaryReader(bytes, length); + rive::File* file = nullptr; + auto result = rive::File::import(reader, &file); + + REQUIRE(result == rive::ImportResult::success); + REQUIRE(file != nullptr); + REQUIRE(file->artboard() != nullptr); + + auto artboard = file->artboard(); + + REQUIRE(artboard->find<rive::TransformComponent>("target") != nullptr); + auto target = artboard->find<rive::TransformComponent>("target"); + + REQUIRE(artboard->find<rive::TransformComponent>("rect") != nullptr); + auto rectangle = artboard->find<rive::TransformComponent>("rect"); + + artboard->advance(0.0f); + + rive::TransformComponents targetComponents; + rive::Mat2D::decompose(targetComponents, target->worldTransform()); + rive::TransformComponents rectComponents; + rive::Mat2D::decompose(rectComponents, rectangle->worldTransform()); + + REQUIRE(targetComponents.scaleX() == rectComponents.scaleX()); + REQUIRE(targetComponents.scaleY() == rectComponents.scaleY()); + + delete file; + delete[] bytes; +} \ No newline at end of file
diff --git a/test/translation_constraint_test.cpp b/test/translation_constraint_test.cpp index da4f98b..7d579d1 100644 --- a/test/translation_constraint_test.cpp +++ b/test/translation_constraint_test.cpp
@@ -34,13 +34,13 @@ REQUIRE(artboard->find<rive::TransformComponent>("rect") != nullptr); auto rectangle = artboard->find<rive::TransformComponent>("rect"); + artboard->advance(0.0f); + rive::TransformComponents targetComponents; rive::Mat2D::decompose(targetComponents, target->worldTransform()); rive::TransformComponents rectComponents; rive::Mat2D::decompose(rectComponents, rectangle->worldTransform()); - artboard->advance(0.0f); - REQUIRE(targetComponents.x() == rectComponents.x()); REQUIRE(targetComponents.y() == rectComponents.y());