Adding transform constraint.
diff --git a/dev/defs/constraints/transform_constraint.json b/dev/defs/constraints/transform_constraint.json
new file mode 100644
index 0000000..add65a0
--- /dev/null
+++ b/dev/defs/constraints/transform_constraint.json
@@ -0,0 +1,28 @@
+{
+  "name": "TransformConstraint",
+  "key": {
+    "int": 83,
+    "string": "transformconstraint"
+  },
+  "extends": "constraints/targeted_constraint.json",
+  "properties": {
+    "sourceSpaceValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 179,
+        "string": "sourcespacevalue"
+      },
+      "description": "The source transform space."
+    },
+    "destSpaceValue": {
+      "type": "uint",
+      "initialValue": "0",
+      "key": {
+        "int": 180,
+        "string": "destspacevalue"
+      },
+      "description": "The destination transform space."
+    }
+  }
+}
\ No newline at end of file
diff --git a/include/constraints/constraint.hpp b/include/constraints/constraint.hpp
index e8427b1..093e11d 100644
--- a/include/constraints/constraint.hpp
+++ b/include/constraints/constraint.hpp
@@ -5,6 +5,8 @@
 namespace rive
 {
 	class TransformComponent;
+	class Mat2D;
+
 	class Constraint : public ConstraintBase
 	{
 	public:
@@ -15,6 +17,8 @@
 		void buildDependencies() override;
 		void onDirty(ComponentDirt dirt) override;
 	};
+
+	const Mat2D& getParentWorld(const TransformComponent& component);
 } // namespace rive
 
 #endif
\ No newline at end of file
diff --git a/include/constraints/transform_constraint.hpp b/include/constraints/transform_constraint.hpp
new file mode 100644
index 0000000..4100e7c
--- /dev/null
+++ b/include/constraints/transform_constraint.hpp
@@ -0,0 +1,29 @@
+#ifndef _RIVE_TRANSFORM_CONSTRAINT_HPP_
+#define _RIVE_TRANSFORM_CONSTRAINT_HPP_
+#include "generated/constraints/transform_constraint_base.hpp"
+#include "transform_space.hpp"
+#include "math/transform_components.hpp"
+
+#include <stdio.h>
+namespace rive
+{
+	class TransformConstraint : public TransformConstraintBase
+	{
+	private:
+		TransformComponents m_ComponentsA;
+		TransformComponents m_ComponentsB;
+
+	public:
+		void constrain(TransformComponent* component) override;
+		TransformSpace sourceSpace() const
+		{
+			return (TransformSpace)sourceSpaceValue();
+		}
+		TransformSpace destSpace() const
+		{
+			return (TransformSpace)destSpaceValue();
+		}
+	};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/generated/constraints/transform_constraint_base.hpp b/include/generated/constraints/transform_constraint_base.hpp
new file mode 100644
index 0000000..aa85a2a
--- /dev/null
+++ b/include/generated/constraints/transform_constraint_base.hpp
@@ -0,0 +1,90 @@
+#ifndef _RIVE_TRANSFORM_CONSTRAINT_BASE_HPP_
+#define _RIVE_TRANSFORM_CONSTRAINT_BASE_HPP_
+#include "constraints/targeted_constraint.hpp"
+#include "core/field_types/core_uint_type.hpp"
+namespace rive
+{
+	class TransformConstraintBase : public TargetedConstraint
+	{
+	protected:
+		typedef TargetedConstraint Super;
+
+	public:
+		static const uint16_t typeKey = 83;
+
+		/// Helper to quickly determine if a core object extends another without
+		/// RTTI at runtime.
+		bool isTypeOf(uint16_t typeKey) const override
+		{
+			switch (typeKey)
+			{
+				case TransformConstraintBase::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 sourceSpaceValuePropertyKey = 179;
+		static const uint16_t destSpaceValuePropertyKey = 180;
+
+	private:
+		int m_SourceSpaceValue = 0;
+		int m_DestSpaceValue = 0;
+	public:
+		inline int sourceSpaceValue() const { return m_SourceSpaceValue; }
+		void sourceSpaceValue(int value)
+		{
+			if (m_SourceSpaceValue == value)
+			{
+				return;
+			}
+			m_SourceSpaceValue = value;
+			sourceSpaceValueChanged();
+		}
+
+		inline int destSpaceValue() const { return m_DestSpaceValue; }
+		void destSpaceValue(int value)
+		{
+			if (m_DestSpaceValue == value)
+			{
+				return;
+			}
+			m_DestSpaceValue = value;
+			destSpaceValueChanged();
+		}
+
+		Core* clone() const override;
+		void copy(const TransformConstraintBase& object)
+		{
+			m_SourceSpaceValue = object.m_SourceSpaceValue;
+			m_DestSpaceValue = object.m_DestSpaceValue;
+			TargetedConstraint::copy(object);
+		}
+
+		bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+		{
+			switch (propertyKey)
+			{
+				case sourceSpaceValuePropertyKey:
+					m_SourceSpaceValue = CoreUintType::deserialize(reader);
+					return true;
+				case destSpaceValuePropertyKey:
+					m_DestSpaceValue = CoreUintType::deserialize(reader);
+					return true;
+			}
+			return TargetedConstraint::deserialize(propertyKey, reader);
+		}
+
+	protected:
+		virtual void sourceSpaceValueChanged() {}
+		virtual void destSpaceValueChanged() {}
+	};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/generated/core_registry.hpp b/include/generated/core_registry.hpp
index a4128fc..cd04bc5 100644
--- a/include/generated/core_registry.hpp
+++ b/include/generated/core_registry.hpp
@@ -49,6 +49,7 @@
 #include "constraints/distance_constraint.hpp"
 #include "constraints/ik_constraint.hpp"
 #include "constraints/targeted_constraint.hpp"
+#include "constraints/transform_constraint.hpp"
 #include "container_component.hpp"
 #include "draw_rules.hpp"
 #include "draw_target.hpp"
@@ -94,6 +95,8 @@
 					return new DistanceConstraint();
 				case IKConstraintBase::typeKey:
 					return new IKConstraint();
+				case TransformConstraintBase::typeKey:
+					return new TransformConstraint();
 				case AnimationStateBase::typeKey:
 					return new AnimationState();
 				case KeyedObjectBase::typeKey:
@@ -244,6 +247,14 @@
 				case IKConstraintBase::parentBoneCountPropertyKey:
 					object->as<IKConstraintBase>()->parentBoneCount(value);
 					break;
+				case TransformConstraintBase::sourceSpaceValuePropertyKey:
+					object->as<TransformConstraintBase>()->sourceSpaceValue(
+					    value);
+					break;
+				case TransformConstraintBase::destSpaceValuePropertyKey:
+					object->as<TransformConstraintBase>()->destSpaceValue(
+					    value);
+					break;
 				case AnimationStateBase::animationIdPropertyKey:
 					object->as<AnimationStateBase>()->animationId(value);
 					break;
@@ -657,6 +668,12 @@
 					return object->as<DistanceConstraintBase>()->modeValue();
 				case IKConstraintBase::parentBoneCountPropertyKey:
 					return object->as<IKConstraintBase>()->parentBoneCount();
+				case TransformConstraintBase::sourceSpaceValuePropertyKey:
+					return object->as<TransformConstraintBase>()
+					    ->sourceSpaceValue();
+				case TransformConstraintBase::destSpaceValuePropertyKey:
+					return object->as<TransformConstraintBase>()
+					    ->destSpaceValue();
 				case AnimationStateBase::animationIdPropertyKey:
 					return object->as<AnimationStateBase>()->animationId();
 				case KeyedObjectBase::objectIdPropertyKey:
@@ -941,6 +958,8 @@
 				case TargetedConstraintBase::targetIdPropertyKey:
 				case DistanceConstraintBase::modeValuePropertyKey:
 				case IKConstraintBase::parentBoneCountPropertyKey:
+				case TransformConstraintBase::sourceSpaceValuePropertyKey:
+				case TransformConstraintBase::destSpaceValuePropertyKey:
 				case AnimationStateBase::animationIdPropertyKey:
 				case KeyedObjectBase::objectIdPropertyKey:
 				case BlendAnimationBase::animationIdPropertyKey:
diff --git a/include/transform_space.hpp b/include/transform_space.hpp
new file mode 100644
index 0000000..0bfc44d
--- /dev/null
+++ b/include/transform_space.hpp
@@ -0,0 +1,11 @@
+#ifndef _RIVE_TRANSFORM_SPACE_HPP_
+#define _RIVE_TRANSFORM_SPACE_HPP_
+namespace rive
+{
+	enum class TransformSpace : unsigned int
+	{
+		world = 0,
+		local = 1
+	};
+}
+#endif
\ No newline at end of file
diff --git a/src/constraints/constraint.cpp b/src/constraints/constraint.cpp
index cd32ec6..503f024 100644
--- a/src/constraints/constraint.cpp
+++ b/src/constraints/constraint.cpp
@@ -2,6 +2,8 @@
 #include "container_component.hpp"
 #include "transform_component.hpp"
 #include "core_context.hpp"
+#include "artboard.hpp"
+#include "math/mat2d.hpp"
 
 using namespace rive;
 
@@ -35,4 +37,20 @@
 	// Whenever the constraint gets any dirt, make sure to mark the constrained
 	// component dirty.
 	markConstraintDirty();
+}
+
+static Mat2D identity;
+const Mat2D& rive::getParentWorld(const TransformComponent& component)
+{
+	auto parent = component.parent();
+	if (parent->is<Artboard>())
+	{
+		// TODO: when we have symbols working artboards will need to store their
+		// world transform (probably should just become TransformComponent).
+		return identity;
+	}
+	else
+	{
+		return parent->as<TransformComponent>()->worldTransform();
+	}
 }
\ No newline at end of file
diff --git a/src/constraints/ik_constraint.cpp b/src/constraints/ik_constraint.cpp
index 3e4f5a0..2f74b92 100644
--- a/src/constraints/ik_constraint.cpp
+++ b/src/constraints/ik_constraint.cpp
@@ -5,22 +5,6 @@
 
 using namespace rive;
 
-static Mat2D identity;
-static const Mat2D& getParentWorld(const TransformComponent& component)
-{
-	auto parent = component.parent();
-	if (parent->is<Artboard>())
-	{
-		// TODO: when we have symbols working artboards will need to store their
-		// world transform (probably should just become TransformComponents).
-		return identity;
-	}
-	else
-	{
-		return parent->as<TransformComponent>()->worldTransform();
-	}
-}
-
 StatusCode IKConstraint::onAddedClean(CoreContext* context)
 {
 	if (!parent()->is<Bone>())
@@ -221,6 +205,7 @@
 	{
 		Mat2D::fromRotation(transform, rotation);
 	}
+
 	// Translate
 	transform[4] = c.x();
 	transform[5] = c.y();
diff --git a/src/constraints/transform_constraint.cpp b/src/constraints/transform_constraint.cpp
new file mode 100644
index 0000000..36a9ab3
--- /dev/null
+++ b/src/constraints/transform_constraint.cpp
@@ -0,0 +1,62 @@
+#include "constraints/transform_constraint.hpp"
+#include "transform_component.hpp"
+#include "math/mat2d.hpp"
+#include <cmath>
+
+using namespace rive;
+
+void TransformConstraint::constrain(TransformComponent* component)
+{
+	if (m_Target == nullptr)
+	{
+		return;
+	}
+
+	const Mat2D& transformA = component->worldTransform();
+	Mat2D transformB(m_Target->worldTransform());
+	if (sourceSpace() == TransformSpace::local)
+	{
+		const Mat2D& targetParentWorld = getParentWorld(*m_Target);
+
+		Mat2D inverse;
+		if (!Mat2D::invert(inverse, targetParentWorld))
+		{
+			return;
+		}
+		Mat2D::multiply(transformB, inverse, transformB);
+	}
+	if (destSpace() == TransformSpace::local)
+	{
+		const Mat2D& targetParentWorld = getParentWorld(*component);
+		Mat2D::multiply(transformB, targetParentWorld, transformB);
+	}
+
+	Mat2D::decompose(m_ComponentsA, transformA);
+	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;
+	}
+
+	float t = strength();
+	float ti = 1.0f - t;
+
+	m_ComponentsB.rotation(angleA + diff * t);
+	m_ComponentsB.x(m_ComponentsA.x() * ti + m_ComponentsB.x() * t);
+	m_ComponentsB.y(m_ComponentsA.y() * ti + m_ComponentsB.y() * t);
+	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() * ti + m_ComponentsB.skew() * t);
+
+	Mat2D::compose(component->mutableWorldTransform(), m_ComponentsB);
+}
diff --git a/src/generated/constraints/transform_constraint_base.cpp b/src/generated/constraints/transform_constraint_base.cpp
new file mode 100644
index 0000000..8214dae
--- /dev/null
+++ b/src/generated/constraints/transform_constraint_base.cpp
@@ -0,0 +1,11 @@
+#include "generated/constraints/transform_constraint_base.hpp"
+#include "constraints/transform_constraint.hpp"
+
+using namespace rive;
+
+Core* TransformConstraintBase::clone() const
+{
+	auto cloned = new TransformConstraint();
+	cloned->copy(*this);
+	return cloned;
+}
diff --git a/src/math/mat2d.cpp b/src/math/mat2d.cpp
index 617ebfe..f2d96b5 100644
--- a/src/math/mat2d.cpp
+++ b/src/math/mat2d.cpp
@@ -104,7 +104,7 @@
 {
 	float r = components.rotation();
 
-	if (r != 0.0)
+	if (r != 0.0f)
 	{
 		Mat2D::fromRotation(result, r);
 	}
@@ -119,7 +119,7 @@
 	Mat2D::scale(result, result, scale);
 
 	float sk = components.skew();
-	if (sk != 0.0)
+	if (sk != 0.0f)
 	{
 		result[2] = result[0] * sk + result[2];
 		result[3] = result[1] * sk + result[3];
diff --git a/test/assets/transform_constraint.riv b/test/assets/transform_constraint.riv
new file mode 100644
index 0000000..30f765f
--- /dev/null
+++ b/test/assets/transform_constraint.riv
Binary files differ
diff --git a/test/rive_testing.cpp b/test/rive_testing.cpp
new file mode 100644
index 0000000..0191903
--- /dev/null
+++ b/test/rive_testing.cpp
@@ -0,0 +1,14 @@
+#include "rive_testing.hpp"
+
+bool aboutEqual(const rive::Mat2D& a, const rive::Mat2D& b)
+{
+	const float epsilon = 0.0001f;
+	for (int i = 0; i < 6; i++)
+	{
+		if (std::fabs(a[i] - b[i]) > epsilon)
+		{
+			return false;
+		}
+	}
+	return true;
+}
\ No newline at end of file
diff --git a/test/rive_testing.hpp b/test/rive_testing.hpp
index 8518891..c617cd6 100644
--- a/test/rive_testing.hpp
+++ b/test/rive_testing.hpp
@@ -5,18 +5,7 @@
 #include <sstream>
 #include "math/mat2d.hpp"
 
-static bool aboutEqual(const rive::Mat2D& a, const rive::Mat2D& b)
-{
-	const float epsilon = 0.0001f;
-	for (int i = 0; i < 6; i++)
-	{
-		if (std::fabs(a[i] - b[i]) > epsilon)
-		{
-			return false;
-		}
-	}
-	return true;
-}
+bool aboutEqual(const rive::Mat2D& a, const rive::Mat2D& b);
 
 namespace Catch
 {
diff --git a/test/transform_constraint_test.cpp b/test/transform_constraint_test.cpp
new file mode 100644
index 0000000..eb077eb
--- /dev/null
+++ b/test/transform_constraint_test.cpp
@@ -0,0 +1,44 @@
+#include "rive_testing.hpp"
+#include "core/binary_reader.hpp"
+#include "file.hpp"
+#include "no_op_renderer.hpp"
+#include "node.hpp"
+#include "bones/bone.hpp"
+#include "shapes/shape.hpp"
+#include <cstdio>
+
+TEST_CASE("transform constraint updates world transform", "[file]")
+{
+	FILE* fp = fopen("../../test/assets/transform_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>("Rectangle") != nullptr);
+	auto rectangle = artboard->find<rive::TransformComponent>("Rectangle");
+
+	artboard->advance(0.0f);
+
+	// Expect the transform constraint to have placed the shape in the same
+	// exact world transform as the target.
+	REQUIRE(aboutEqual(target->worldTransform(), rectangle->worldTransform()));
+
+	delete file;
+	delete[] bytes;
+}
\ No newline at end of file