Add translation constraint + test.
diff --git a/include/rive/constraints/transform_constraint.hpp b/include/rive/constraints/transform_constraint.hpp
index 20edc76..026c2f9 100644
--- a/include/rive/constraints/transform_constraint.hpp
+++ b/include/rive/constraints/transform_constraint.hpp
@@ -1,7 +1,6 @@
 #ifndef _RIVE_TRANSFORM_CONSTRAINT_HPP_
 #define _RIVE_TRANSFORM_CONSTRAINT_HPP_
 #include "rive/generated/constraints/transform_constraint_base.hpp"
-#include "rive/transform_space.hpp"
 #include "rive/math/transform_components.hpp"
 
 #include <stdio.h>
@@ -15,14 +14,6 @@
 
 	public:
 		void constrain(TransformComponent* component) override;
-		TransformSpace sourceSpace() const
-		{
-			return (TransformSpace)sourceSpaceValue();
-		}
-		TransformSpace destSpace() const
-		{
-			return (TransformSpace)destSpaceValue();
-		}
 	};
 } // namespace rive
 
diff --git a/include/rive/constraints/transform_space_constraint.hpp b/include/rive/constraints/transform_space_constraint.hpp
index 4897451..e538814 100644
--- a/include/rive/constraints/transform_space_constraint.hpp
+++ b/include/rive/constraints/transform_space_constraint.hpp
@@ -1,12 +1,21 @@
 #ifndef _RIVE_TRANSFORM_SPACE_CONSTRAINT_HPP_
 #define _RIVE_TRANSFORM_SPACE_CONSTRAINT_HPP_
 #include "rive/generated/constraints/transform_space_constraint_base.hpp"
+#include "rive/transform_space.hpp"
 #include <stdio.h>
 namespace rive
 {
 	class TransformSpaceConstraint : public TransformSpaceConstraintBase
 	{
 	public:
+		TransformSpace sourceSpace() const
+		{
+			return (TransformSpace)sourceSpaceValue();
+		}
+		TransformSpace destSpace() const
+		{
+			return (TransformSpace)destSpaceValue();
+		}
 	};
 } // namespace rive
 
diff --git a/src/constraints/translation_constraint.cpp b/src/constraints/translation_constraint.cpp
index 3e7bc3f..18fa59f 100644
--- a/src/constraints/translation_constraint.cpp
+++ b/src/constraints/translation_constraint.cpp
@@ -1,8 +1,114 @@
 #include "rive/constraints/translation_constraint.hpp"
 #include "rive/transform_component.hpp"
 #include "rive/math/mat2d.hpp"
+#include "rive/math/vec2d.hpp"
 #include <cmath>
 
 using namespace rive;
 
-void TranslationConstraint::constrain(TransformComponent* component) {}
+void TranslationConstraint::constrain(TransformComponent* component)
+{
+	Mat2D& transformA = component->mutableWorldTransform();
+	Vec2D translationA(transformA[4], transformA[5]);
+	Vec2D translationB;
+	if (m_Target == nullptr)
+	{
+		Vec2D::copy(translationB, translationA);
+	}
+	else
+	{
+		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);
+		}
+		translationB[0] = transformB[4];
+		translationB[1] = transformB[5];
+
+		if (!doesCopy())
+		{
+			translationB[0] =
+			    destSpace() == TransformSpace::local ? 0.0f : translationA[0];
+		}
+		else
+		{
+			translationB[0] *= copyFactor();
+			if (offset())
+			{
+				translationB[0] += component->x();
+			}
+		}
+
+		if (!doesCopyY())
+		{
+			translationB[1] =
+			    destSpace() == TransformSpace::local ? 0.0f : translationA[1];
+		}
+		else
+		{
+			translationB[1] *= copyFactorY();
+
+			if (offset())
+			{
+				translationB[1] += component->y();
+			}
+		}
+
+		if (destSpace() == TransformSpace::local)
+		{
+			// Destination space is in parent transform coordinates.
+			Vec2D::transform(
+			    translationB, translationB, getParentWorld(*component));
+		}
+	}
+
+	bool clampLocal = minMaxSpace() == TransformSpace::local;
+	if (clampLocal)
+	{
+		// Apply min max in local space, so transform to local coordinates
+		// first.
+		Mat2D invert;
+		if (!Mat2D::invert(invert, getParentWorld(*component)))
+		{
+			return;
+		}
+		// Get our target world coordinates in parent local.
+		Vec2D::transform(translationB, translationB, invert);
+	}
+	if (max() && translationB[0] > maxValue())
+	{
+		translationB[0] = maxValue();
+	}
+	if (min() && translationB[0] < minValue())
+	{
+		translationB[0] = minValue();
+	}
+	if (maxY() && translationB[1] > maxValueY())
+	{
+		translationB[1] = maxValueY();
+	}
+	if (minY() && translationB[1] < minValueY())
+	{
+		translationB[1] = minValueY();
+	}
+	if (clampLocal)
+	{
+		// Transform back to world.
+		Vec2D::transform(
+		    translationB, translationB, getParentWorld(*component));
+	}
+
+	float t = strength();
+	float ti = 1.0f - t;
+
+	// Just interpolate world translation
+	transformA[4] = translationA[0] * ti + translationB[0] * t;
+	transformA[5] = translationA[1] * ti + translationB[1] * t;
+}
diff --git a/test/assets/translation_constraint.riv b/test/assets/translation_constraint.riv
new file mode 100644
index 0000000..0070b0b
--- /dev/null
+++ b/test/assets/translation_constraint.riv
Binary files differ
diff --git a/test/translation_constraint_test.cpp b/test/translation_constraint_test.cpp
new file mode 100644
index 0000000..da4f98b
--- /dev/null
+++ b/test/translation_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("translation constraint updates world transform", "[file]")
+{
+	FILE* fp = fopen("../../test/assets/translation_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");
+
+	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());
+
+	delete file;
+	delete[] bytes;
+}
\ No newline at end of file