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