// 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 // // -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(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