JayAnimator / Source / JayAnimatorTests / Private / JayAnimator_EaseAndMathTests.cpp
JayAnimator_EaseAndMathTests.cpp
Raw
// JayAnimator_EaseAndMathTests.cpp
// Tests for FjTweenUtils utilities and easings.
// Module: JayAnimatorTests  |  Namespace path: JayAnimatorTests.*

#include "Misc/AutomationTest.h"
#include "Math/Quat.h"
#include "Curves/CurveFloat.h"
#include "Curves/RichCurve.h"
#include "JayAnimator/Public/jTweenUtils.h"

#if WITH_DEV_AUTOMATION_TESTS

// <Running unattended>
// <UnrealEditor-Cmd.exe> <Project.uproject> -unattended -AutomationRunTests="JayAnimatorTests.*"

namespace JTTest
{
	static constexpr float Eps     = 1e-4f;
	static constexpr float TightEps= 1e-6f;

	FORCEINLINE bool Near(float A, float B, float Tol = Eps)
	{
		return FMath::IsNearlyEqual(A, B, Tol);
	}

	FORCEINLINE bool NearVec(const FVector& A, const FVector& B, float Tol = Eps)
	{
		return A.Equals(B, Tol);
	}

	FORCEINLINE bool NearRot(const FRotator& A, const FRotator& B, float Tol = 1e-2f)
	{
		// Compare via quaternions to avoid wrap issues
		const FQuat QA = A.Quaternion().GetNormalized();
		const FQuat QB = B.Quaternion().GetNormalized();
		return QA.Equals(QB, Tol);
	}
}


// ---------- LERP (float / vector / rotator) ----------

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FJayAnimator_Lerp_Float_Basic,
	"JayAnimatorTests.Math.Lerp.Float.Basic",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

bool FJayAnimator_Lerp_Float_Basic::RunTest(const FString&)
{
	using namespace JTTest;
	TestTrue(TEXT("t=0 -> A"), Near(FjTweenUtils::Lerp(2.f, 10.f, 0.f), 2.f));
	TestTrue(TEXT("t=1 -> B"), Near(FjTweenUtils::Lerp(2.f, 10.f, 1.f), 10.f));
	TestTrue(TEXT("t=0.3"),    Near(FjTweenUtils::Lerp(0.f, 100.f, 0.3f), 30.f));
	return true;
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FJayAnimator_Lerp_Vector_Basic,
	"JayAnimatorTests.Math.Lerp.Vector.Basic",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

bool FJayAnimator_Lerp_Vector_Basic::RunTest(const FString&)
{
	using namespace JTTest;
	const FVector A(0,0,0), B(10,5,-2);
	TestTrue(TEXT("t=0.3 vec"),
		NearVec(FjTweenUtils::Lerp(A, B, 0.3f), FVector(3.f, 1.5f, -0.6f)));
	return true;
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FJayAnimator_Lerp_Rotator_UsesSlerp,
	"JayAnimatorTests.Math.Lerp.Rotator.SlerpMid",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

bool FJayAnimator_Lerp_Rotator_UsesSlerp::RunTest(const FString&)
{
	using namespace JTTest;
	const FRotator R0(0,   0, 0);
	const FRotator R1(0, 180, 0); // halfway should be around yaw 90 (short path across 180 is still 90)
	const FRotator Mid = FjTweenUtils::Lerp(R0, R1, 0.5f);
	TestTrue(TEXT("mid ~ 90 yaw"), FMath::IsNearlyEqual(Mid.Yaw, 90.f, 1.0f) || NearRot(Mid, FRotator(0,90,0)));
	return true;
}


// ---------- MAP / UNLERP / CLAMP01 ----------

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FJayAnimator_Map_RoundTrip,
	"JayAnimatorTests.Math.Map.RoundTrip",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

bool FJayAnimator_Map_RoundTrip::RunTest(const FString&)
{
	using namespace JTTest;
	const float v = 25.f;
	const float t = FjTweenUtils::Unlerp(10.f, 30.f, v);                 // expect 0.75
	TestTrue(TEXT("Unlerp ok"), Near(t, 0.75f));

	const float v2 = FjTweenUtils::Map(v, 10.f, 30.f, 100.f, 200.f);     // expect 175
	TestTrue(TEXT("Map ok"), Near(v2, 175.f));

	// Round trip: Map back to original range
	const float v3 = FjTweenUtils::Map(v2, 100.f, 200.f, 10.f, 30.f);
	TestTrue(TEXT("Map invertible (linear)"), Near(v3, v));
	return true;
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FJayAnimator_Clamp01_Bounds,
	"JayAnimatorTests.Math.Clamp01.Bounds",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

bool FJayAnimator_Clamp01_Bounds::RunTest(const FString&)
{
	using namespace JTTest;
	TestTrue(TEXT("below 0"), Near(FjTweenUtils::Clamp01(-0.2f), 0.f));
	TestTrue(TEXT("in range"), Near(FjTweenUtils::Clamp01(0.2f), 0.2f));
	TestTrue(TEXT("above 1"), Near(FjTweenUtils::Clamp01(1.5f), 1.f));
	return true;
}


// ---------- INDIVIDUAL EASINGS: spot checks at 0, 0.5, 1 ----------

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FJayAnimator_Ease_Quadratic_Spots,
	"JayAnimatorTests.Ease.Quadratic.Spots",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

bool FJayAnimator_Ease_Quadratic_Spots::RunTest(const FString&)
{
	using namespace JTTest;
	TestTrue(TEXT("QuadIn(0)=0"),  Near(FjTweenUtils::QuadraticIn(0.f), 0.f, TightEps));
	TestTrue(TEXT("QuadIn(1)=1"),  Near(FjTweenUtils::QuadraticIn(1.f), 1.f, TightEps));
	TestTrue(TEXT("QuadIn(0.5)=0.25"), Near(FjTweenUtils::QuadraticIn(0.5f), 0.25f, 1e-5f));

	TestTrue(TEXT("QuadOut(0)=0"), Near(FjTweenUtils::QuadraticOut(0.f), 0.f, TightEps));
	TestTrue(TEXT("QuadOut(1)=1"), Near(FjTweenUtils::QuadraticOut(1.f), 1.f, TightEps));
	TestTrue(TEXT("QuadOut(0.5)=0.75"), Near(FjTweenUtils::QuadraticOut(0.5f), 0.75f, 1e-5f));

	// SinusoidalInOut mid should be 0.5 exactly
	TestTrue(TEXT("SineIO(0.5)=0.5"), Near(FjTweenUtils::SinusoidalInOut(0.5f), 0.5f, 1e-6f));
	return true;
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FJayAnimator_Ease_Expo_Bounds,
	"JayAnimatorTests.Ease.Exponential.BoundsAndEdges",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

bool FJayAnimator_Ease_Expo_Bounds::RunTest(const FString&)
{
	using namespace JTTest;
	// As implemented: hard-clamped edges for numerical stability
	TestTrue(TEXT("ExpIn(0)=0"), Near(FjTweenUtils::ExponentialIn(0.f), 0.f, TightEps));
	TestTrue(TEXT("ExpIn(1)=1? (not guaranteed)"), FjTweenUtils::ExponentialIn(1.f) > 0.99f);

	TestTrue(TEXT("ExpOut(0)=0? (near)"), FjTweenUtils::ExponentialOut(0.f) < 0.01f);
	TestTrue(TEXT("ExpOut(1)=1"), Near(FjTweenUtils::ExponentialOut(1.f), 1.f, TightEps));

	// InOut clamps 0/1 and is continuous
	TestTrue(TEXT("ExpIO(0)=0"), Near(FjTweenUtils::ExponentialInOut(0.f), 0.f, TightEps));
	TestTrue(TEXT("ExpIO(1)=1"), Near(FjTweenUtils::ExponentialInOut(1.f), 1.f, TightEps));
	return true;
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FJayAnimator_Ease_Range_And_Monotonicity,
	"JayAnimatorTests.Ease.General.RangeAndMonotonicity",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

bool FJayAnimator_Ease_Range_And_Monotonicity::RunTest(const FString&)
{
	// Sample a few easing functions to ensure outputs remain within [0,1]
	// and (weakly) increase over t in [0,1].
	auto Check = [this](auto Func, const TCHAR* Name)
	{
		float Prev = -1.f;
		for (int i=0;i<=100;++i)
		{
			const float t = i / 100.f;
			const float y = Func(t);
			if (!(y >= -0.001f && y <= 1.001f))
			{
				AddError(FString::Printf(TEXT("%s out of [0,1]: t=%g y=%g"), Name, t, y));
				return false;
			}
			if (i>0 && y + 1e-3f < Prev)
			{
				AddError(FString::Printf(TEXT("%s not monotonic enough: t=%g y=%g prev=%g"), Name, t, y, Prev));
				return false;
			}
			Prev = y;
		}
		return true;
	};

	bool Ok = true;
	Ok &= Check(&FjTweenUtils::Linear,            TEXT("Linear"));
	Ok &= Check(&FjTweenUtils::QuadraticIn,       TEXT("QuadraticIn"));
	Ok &= Check(&FjTweenUtils::QuadraticOut,      TEXT("QuadraticOut"));
	Ok &= Check(&FjTweenUtils::CubicInOut,        TEXT("CubicInOut"));
	Ok &= Check(&FjTweenUtils::SinusoidalInOut,   TEXT("SinusoidalInOut"));
	Ok &= Check(&FjTweenUtils::CircularInOut,     TEXT("CircularInOut"));
	Ok &= Check(&FjTweenUtils::BackInOut,         TEXT("BackInOut"));
	Ok &= Check(&FjTweenUtils::BounceInOut,       TEXT("BounceInOut"));
	return Ok;
}


// ---------- Ease dispatcher & curve ----------

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FJayAnimator_Ease_Dispatcher_SelectsCorrectFunc,
	"JayAnimatorTests.Ease.Dispatcher.SelectsCorrect",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

bool FJayAnimator_Ease_Dispatcher_SelectsCorrectFunc::RunTest(const FString&)
{
	using namespace JTTest;

	// NOTE: Requires EJTweenEasing values in your jTween.h to map to these functions in FjTweenUtils::Ease.
	// Replace enum names if your values differ.
	const float t = 0.5f;

#if 1
	// Common spot checks (adjust enum names to your actual ones)
	TestTrue(TEXT("Linear"),
		Near(FjTweenUtils::Ease(EJTweenEasing::Linear, t), FjTweenUtils::Linear(t)));
	TestTrue(TEXT("QuadIn"),
		Near(FjTweenUtils::Ease(EJTweenEasing::QuadraticIn, t), FjTweenUtils::QuadraticIn(t)));
	TestTrue(TEXT("QuadOut"),
		Near(FjTweenUtils::Ease(EJTweenEasing::QuadraticOut, t), FjTweenUtils::QuadraticOut(t)));
	TestTrue(TEXT("CubicInOut"),
		Near(FjTweenUtils::Ease(EJTweenEasing::CubicInOut, t), FjTweenUtils::CubicInOut(t)));
	TestTrue(TEXT("SineInOut"),
		Near(FjTweenUtils::Ease(EJTweenEasing::SinusoidalInOut, t), FjTweenUtils::SinusoidalInOut(t)));
#endif

	return true;
}

IMPLEMENT_SIMPLE_AUTOMATION_TEST(
	FJayAnimator_Ease_ByCurve_LinearMatchesAlpha,
	"JayAnimatorTests.Ease.ByCurve.LinearMatchesAlpha",
	EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter
)

bool FJayAnimator_Ease_ByCurve_LinearMatchesAlpha::RunTest(const FString&)
{
	// Create a transient linear curve: (0,0) -> (1,1)
	UCurveFloat* Curve = NewObject<UCurveFloat>(GetTransientPackage(), NAME_None, RF_Transient);
	if (!Curve)
	{
		AddError(TEXT("Failed to create UCurveFloat"));
		return false;
	}

	// In UE5, UCurveFloat exposes FRichCurve FloatCurve directly
	FRichCurve& Rich = Curve->FloatCurve;
	Rich.Reset();
	Rich.AddKey(0.f, 0.f);
	Rich.AddKey(1.f, 1.f);

	for (int i = 0; i <= 10; ++i)
	{
		const float a = i / 10.f;
		const float y = FjTweenUtils::EaseByCurve(Curve, a);
		if (!FMath::IsNearlyEqual(y, a, 1e-4f))
		{
			AddError(FString::Printf(TEXT("Curve mismatch: a=%g y=%g"), a, y));
			return false;
		}
	}
	return true;
}

#endif // WITH_DEV_AUTOMATION_TESTS