// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License. #include "Libraries/DirectiveUtilMathFunctionLibrary.h" #include "Async/ParallelFor.h" #include "Misc/AutomationTest.h" #include IMPLEMENT_SIMPLE_AUTOMATION_TEST( FDirectiveUtilMathExtendedFunctionLibraryTest, "DirectiveUtilities.Math.ExtendedFunctionLibrary", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter) bool FDirectiveUtilMathExtendedFunctionLibraryTest::RunTest(const FString& Parameters) { float FloatResult = 0.0f; float Strength = 0.0f; TestTrue("Angle array average accepts values across the degree seam", UDirectiveUtilMathFunctionLibrary::GetAngleArrayAverage({350.0f, 10.0f}, FloatResult, Strength)); TestTrue("Angle array average crosses the degree seam", FMath::IsNearlyZero(FloatResult, 1.e-4f)); TestTrue("Angle array average reports concentration", FMath::IsNearlyEqual(Strength, FMath::Cos(FMath::DegreesToRadians(10.0f)), 1.e-4f)); TestFalse("Angle array average rejects an undefined antipodal mean", UDirectiveUtilMathFunctionLibrary::GetAngleArrayAverage({0.0f, 180.0f}, FloatResult, Strength)); TestFalse("Angle array average rejects non-finite values", UDirectiveUtilMathFunctionLibrary::GetAngleArrayAverage( {0.0f, std::numeric_limits::quiet_NaN()}, FloatResult, Strength)); TestTrue("Weighted float average accepts aligned arrays", UDirectiveUtilMathFunctionLibrary::GetWeightedFloatArrayAverage( {10.0f, 20.0f}, {1.0f, 3.0f}, FloatResult)); TestTrue("Weighted float average applies weights", FMath::IsNearlyEqual(FloatResult, 17.5f, 1.e-4f)); TestTrue("Weighted float average ignores negative weights", UDirectiveUtilMathFunctionLibrary::GetWeightedFloatArrayAverage( {10.0f, 20.0f}, {-1.0f, 2.0f}, FloatResult) && FMath::IsNearlyEqual(FloatResult, 20.0f, 1.e-4f)); TestFalse("Weighted float average rejects mismatched arrays", UDirectiveUtilMathFunctionLibrary::GetWeightedFloatArrayAverage( {10.0f}, {1.0f, 2.0f}, FloatResult)); FVector VectorResult = FVector::ZeroVector; TestTrue("Weighted vector average accepts aligned arrays", UDirectiveUtilMathFunctionLibrary::GetWeightedVectorArrayAverage( {FVector::ForwardVector, FVector::RightVector}, {1.0f, 1.0f}, VectorResult)); TestTrue("Weighted vector average applies weights", VectorResult.Equals(FVector(0.5, 0.5, 0.0), 1.e-6)); TestTrue("Weighted vector average preserves large finite values", UDirectiveUtilMathFunctionLibrary::GetWeightedVectorArrayAverage( {FVector(1.e300, 0.0, 0.0)}, {1.e20f}, VectorResult) && FMath::IsNearlyEqual(VectorResult.X / 1.e300, 1.0, 1.e-12)); TestFalse("Weighted vector average rejects non-finite values", UDirectiveUtilMathFunctionLibrary::GetWeightedVectorArrayAverage( {FVector::ForwardVector, FVector(std::numeric_limits::infinity(), 0.0, 0.0)}, {1.0f, 1.0f}, VectorResult)); TArray FloatArrayResult; TestTrue("Float array normalization accepts finite values", UDirectiveUtilMathFunctionLibrary::NormalizeFloatArrayToRange( {2.0f, 4.0f, 6.0f}, -1.0f, 1.0f, FloatArrayResult)); TestTrue("Float array normalization maps the full range", FloatArrayResult.Num() == 3 && FMath::IsNearlyEqual(FloatArrayResult[0], -1.0f) && FMath::IsNearlyZero(FloatArrayResult[1]) && FMath::IsNearlyEqual(FloatArrayResult[2], 1.0f)); TestTrue("Float array normalization accepts reversed output bounds", UDirectiveUtilMathFunctionLibrary::NormalizeFloatArrayToRange( {2.0f, 4.0f, 6.0f}, 1.0f, -1.0f, FloatArrayResult) && FMath::IsNearlyEqual(FloatArrayResult[0], 1.0f) && FMath::IsNearlyEqual(FloatArrayResult[2], -1.0f)); TestTrue("Float array normalization maps a constant array to the output minimum", UDirectiveUtilMathFunctionLibrary::NormalizeFloatArrayToRange( {4.0f, 4.0f}, 5.0f, 10.0f, FloatArrayResult) && FloatArrayResult == TArray({5.0f, 5.0f})); TestFalse("Float array normalization rejects an empty array", UDirectiveUtilMathFunctionLibrary::NormalizeFloatArrayToRange( {}, 0.0f, 1.0f, FloatArrayResult)); TArray InPlaceValues = {2.0f, 4.0f, 6.0f}; TestTrue("Float array normalization supports the same input and output array", UDirectiveUtilMathFunctionLibrary::NormalizeFloatArrayToRange( InPlaceValues, -1.0f, 1.0f, InPlaceValues) && InPlaceValues == TArray({-1.0f, 0.0f, 1.0f})); TestTrue("Weight normalization accepts positive weights", UDirectiveUtilMathFunctionLibrary::NormalizeWeights({1.0f, 3.0f}, FloatArrayResult)); TestTrue("Weight normalization sums to one", FloatArrayResult.Num() == 2 && FMath::IsNearlyEqual(FloatArrayResult[0], 0.25f) && FMath::IsNearlyEqual(FloatArrayResult[1], 0.75f)); TestTrue("Weight normalization clears negative weights", UDirectiveUtilMathFunctionLibrary::NormalizeWeights({-2.0f, 2.0f}, FloatArrayResult) && FMath::IsNearlyZero(FloatArrayResult[0]) && FMath::IsNearlyEqual(FloatArrayResult[1], 1.0f)); TestFalse("Weight normalization rejects all-zero weights", UDirectiveUtilMathFunctionLibrary::NormalizeWeights({0.0f, -1.0f}, FloatArrayResult)); TArray InPlaceWeights = {1.0f, 3.0f}; TestTrue("Weight normalization supports the same input and output array", UDirectiveUtilMathFunctionLibrary::NormalizeWeights(InPlaceWeights, InPlaceWeights) && InPlaceWeights == TArray({0.25f, 0.75f})); TestTrue("Float array percentile accepts finite values", UDirectiveUtilMathFunctionLibrary::GetFloatArrayPercentile( {4.0f, 1.0f, 3.0f, 2.0f}, 25.0f, FloatResult)); TestTrue("Float array percentile interpolates adjacent values", FMath::IsNearlyEqual(FloatResult, 1.75f, 1.e-4f)); TestTrue("Float array percentile clamps above one hundred", UDirectiveUtilMathFunctionLibrary::GetFloatArrayPercentile( {1.0f, 4.0f}, 125.0f, FloatResult) && FMath::IsNearlyEqual(FloatResult, 4.0f)); TestTrue("Float array percentile uses the Type 7 sample position", UDirectiveUtilMathFunctionLibrary::GetFloatArrayPercentile( {10.0f, 1.0f, 8.0f, 2.0f, 7.0f, 3.0f, 6.0f, 4.0f, 9.0f, 5.0f}, 40.0f, FloatResult) && FMath::IsNearlyEqual(FloatResult, 4.6f, 1.e-4f)); TestFalse("Float array percentile rejects non-finite values", UDirectiveUtilMathFunctionLibrary::GetFloatArrayPercentile( {1.0f, std::numeric_limits::infinity()}, 50.0f, FloatResult)); TestTrue("Root mean square accepts finite values", UDirectiveUtilMathFunctionLibrary::GetFloatArrayRootMeanSquare({3.0f, 4.0f}, FloatResult)); TestTrue("Root mean square uses the arithmetic mean of squares", FMath::IsNearlyEqual(FloatResult, FMath::Sqrt(12.5f), 1.e-4f)); TestFalse("Root mean square rejects an empty array", UDirectiveUtilMathFunctionLibrary::GetFloatArrayRootMeanSquare({}, FloatResult)); TestTrue("Smooth Step clamps below its range", FMath::IsNearlyZero(UDirectiveUtilMathFunctionLibrary::SmoothStep(-1.0f, 0.0f, 1.0f))); TestTrue("Smooth Step reaches its midpoint", FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SmoothStep(0.5f, 0.0f, 1.0f), 0.5f)); TestTrue("Smooth Step accepts reversed bounds", FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SmoothStep(0.25f, 1.0f, 0.0f), 0.15625f)); TestTrue("Smooth Step treats equal bounds as a step", FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SmoothStep(2.0f, 2.0f, 2.0f), 1.0f)); TestTrue("Smoother Step reaches its midpoint", FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SmootherStep(0.5f, 0.0f, 1.0f), 0.5f)); TestTrue("Smoother Step has quintic shaping", FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SmootherStep(0.25f, 0.0f, 1.0f), 0.103515625f)); TestTrue("Range Falloff applies linear attenuation", FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RangeFalloff(5.0f, 0.0f, 10.0f), 0.5f)); TestTrue("Range Falloff applies its exponent", FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RangeFalloff(5.0f, 0.0f, 10.0f, 2.0f), 0.25f)); TestTrue("Range Falloff remains one inside the inner radius", FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RangeFalloff(2.0f, 3.0f, 10.0f), 1.0f)); TestTrue("Range Falloff reaches zero at the outer radius", FMath::IsNearlyZero(UDirectiveUtilMathFunctionLibrary::RangeFalloff(10.0f, 3.0f, 10.0f))); double Distance = 0.0; TestTrue("Direction and distance accepts distinct points", UDirectiveUtilMathFunctionLibrary::GetDirectionAndDistance( FVector::ZeroVector, FVector(3.0, 4.0, 0.0), VectorResult, Distance)); TestTrue("Direction and distance returns a unit direction", VectorResult.Equals(FVector(0.6, 0.8, 0.0), 1.e-6)); TestTrue("Direction and distance returns the length", FMath::IsNearlyEqual(Distance, 5.0)); TestFalse("Direction and distance rejects equal points", UDirectiveUtilMathFunctionLibrary::GetDirectionAndDistance( FVector::ZeroVector, FVector::ZeroVector, VectorResult, Distance)); TestTrue("Direction and distance handles large finite coordinates", UDirectiveUtilMathFunctionLibrary::GetDirectionAndDistance( FVector::ZeroVector, FVector(1.e200, 0.0, 0.0), VectorResult, Distance) && VectorResult.Equals(FVector::ForwardVector, 1.e-12) && FMath::IsNearlyEqual(Distance / 1.e200, 1.0, 1.e-12)); TestTrue("Signed angle handles large finite vectors", FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SignedAngleBetweenVectors( FVector(1.e200, 0.0, 0.0), FVector(0.0, 1.e200, 0.0), FVector::UpVector), 90.0f, 1.e-4f)); TestTrue("Direction Within Cone handles large finite vectors", UDirectiveUtilMathFunctionLibrary::IsDirectionWithinCone( FVector(1.e200, 0.0, 0.0), FVector(1.e200, 0.0, 0.0), 0.0f)); TestTrue("Direction Within Cone includes an identical non-axis direction at zero width", UDirectiveUtilMathFunctionLibrary::IsDirectionWithinCone( FVector(3.e200, 2.e200, 1.e200), FVector(3.e200, 2.e200, 1.e200), 0.0f)); TestFalse("Direction Within Cone excludes a measurable angle from a zero-width cone", UDirectiveUtilMathFunctionLibrary::IsDirectionWithinCone( FVector(FMath::Cos(FMath::DegreesToRadians(0.005)), FMath::Sin(FMath::DegreesToRadians(0.005)), 0.0), FVector::ForwardVector, 0.0f)); const FVector2D RotatedPoint = UDirectiveUtilMathFunctionLibrary::RotatePointAroundPivot2D( FVector2D(2.0, 1.0), FVector2D(1.0, 1.0), 90.0f); TestTrue("Rotate Point Around Pivot 2D preserves the pivot offset", RotatedPoint.Equals(FVector2D(1.0, 2.0), 1.e-6)); TestTrue("Rotate Point Around Pivot 2D rejects non-finite input", UDirectiveUtilMathFunctionLibrary::RotatePointAroundPivot2D( FVector2D(std::numeric_limits::infinity(), 0.0), FVector2D::ZeroVector, 90.0f).IsZero()); TestTrue("Signed Distance To Plane is positive in front of the plane", FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SignedDistanceToPlane( FVector(0.0, 0.0, 5.0), FVector(0.0, 0.0, 2.0), FVector::UpVector), 3.0)); TestTrue("Signed Distance To Plane follows the normal direction", FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SignedDistanceToPlane( FVector(0.0, 0.0, 5.0), FVector(0.0, 0.0, 2.0), -FVector::UpVector), -3.0)); TestTrue("Signed Distance To Plane rejects a zero normal", FMath::IsNearlyZero(UDirectiveUtilMathFunctionLibrary::SignedDistanceToPlane( FVector::UpVector, FVector::ZeroVector, FVector::ZeroVector))); TestTrue("Signed Distance To Plane handles a large finite normal", FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SignedDistanceToPlane( FVector::ForwardVector, FVector::ZeroVector, FVector(1.e200, 0.0, 0.0)), 1.0, 1.e-12)); TestTrue("Point Within Cone includes a point inside the cone", UDirectiveUtilMathFunctionLibrary::IsPointWithinCone( FVector(10.0, 0.0, 0.0), FVector::ZeroVector, FVector::ForwardVector, 10.0f, 20.0)); TestFalse("Point Within Cone excludes a point outside the cone", UDirectiveUtilMathFunctionLibrary::IsPointWithinCone( FVector(0.0, 10.0, 0.0), FVector::ZeroVector, FVector::ForwardVector, 10.0f, 20.0)); TestFalse("Point Within Cone applies the maximum distance", UDirectiveUtilMathFunctionLibrary::IsPointWithinCone( FVector(10.0, 0.0, 0.0), FVector::ZeroVector, FVector::ForwardVector, 10.0f, 5.0)); TestTrue("Point Within Cone treats zero maximum distance as unlimited", UDirectiveUtilMathFunctionLibrary::IsPointWithinCone( FVector(10.0, 0.0, 0.0), FVector::ZeroVector, FVector::ForwardVector, 10.0f)); TestFalse("Point Within Cone applies maximum distance to large finite coordinates", UDirectiveUtilMathFunctionLibrary::IsPointWithinCone( FVector(1.5e200, 0.0, 0.0), FVector::ZeroVector, FVector::ForwardVector, 180.0f, 1.e200)); bool bCircleSamplesValid = true; bool bAnnulusSamplesValid = true; bool bSphereSamplesValid = true; for (int32 Index = 0; Index < 100; ++Index) { const FVector2D CirclePoint = UDirectiveUtilMathFunctionLibrary::RandomPointInCircle(5.0f); const FVector2D AnnulusPoint = UDirectiveUtilMathFunctionLibrary::RandomPointInAnnulus(2.0f, 5.0f); const FVector SpherePoint = UDirectiveUtilMathFunctionLibrary::RandomPointInSphere(5.0f); bCircleSamplesValid &= CirclePoint.Size() <= 5.0 + 1.e-6; bAnnulusSamplesValid &= AnnulusPoint.Size() >= 2.0 - 1.e-6 && AnnulusPoint.Size() <= 5.0 + 1.e-6; bSphereSamplesValid &= SpherePoint.Size() <= 5.0 + 1.e-6; } TestTrue("Random Point In Circle stays within its radius", bCircleSamplesValid); TestTrue("Random Point In Annulus stays between its radii", bAnnulusSamplesValid); TestTrue("Random Point In Sphere stays within its radius", bSphereSamplesValid); FRandomStream FirstStream(12345); FRandomStream SecondStream(12345); TestTrue("Random Point In Circle stream variant is deterministic", UDirectiveUtilMathFunctionLibrary::RandomPointInCircleFromStream(FirstStream, 5.0f).Equals( UDirectiveUtilMathFunctionLibrary::RandomPointInCircleFromStream(SecondStream, 5.0f), 1.e-9)); TestTrue("Random Point In Annulus stream variant is deterministic", UDirectiveUtilMathFunctionLibrary::RandomPointInAnnulusFromStream(FirstStream, 2.0f, 5.0f).Equals( UDirectiveUtilMathFunctionLibrary::RandomPointInAnnulusFromStream(SecondStream, 2.0f, 5.0f), 1.e-9)); TestTrue("Random Point In Sphere stream variant is deterministic", UDirectiveUtilMathFunctionLibrary::RandomPointInSphereFromStream(FirstStream, 5.0f).Equals( UDirectiveUtilMathFunctionLibrary::RandomPointInSphereFromStream(SecondStream, 5.0f), 1.e-9)); #if WITH_EDITOR const TArray StreamRandomFunctions = { GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GetRandomIndexFromWeightsFromStream), GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, RandomPointInCircleFromStream), GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, RandomPointInAnnulusFromStream), GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, RandomPointInSphereFromStream) }; for (const FName FunctionName : StreamRandomFunctions) { const UFunction* Function = UDirectiveUtilMathFunctionLibrary::StaticClass()->FindFunctionByName(FunctionName); TestTrue(*FString::Printf(TEXT("%s should be exposed to Blueprint"), *FunctionName.ToString()), Function != nullptr); if (Function) { TestFalse( *FString::Printf(TEXT("%s should not advertise inert Blueprint thread safety"), *FunctionName.ToString()), Function->HasMetaData(TEXT("BlueprintThreadSafe"))); } } #endif TArray ParallelRandomResults; ParallelRandomResults.SetNumUninitialized(64); ParallelFor(ParallelRandomResults.Num(), [&ParallelRandomResults](const int32 TaskIndex) { FRandomStream Stream(99173); const FVector2D Circle = UDirectiveUtilMathFunctionLibrary::RandomPointInCircleFromStream(Stream, 5.0f); const FVector2D Annulus = UDirectiveUtilMathFunctionLibrary::RandomPointInAnnulusFromStream(Stream, 2.0f, 5.0f); ParallelRandomResults[TaskIndex] = UDirectiveUtilMathFunctionLibrary::RandomPointInSphereFromStream(Stream, 5.0f) + FVector(Circle.X, Circle.Y, Annulus.X + Annulus.Y); }); bool bParallelRandomResultsMatch = true; for (int32 Index = 1; Index < ParallelRandomResults.Num(); ++Index) { bParallelRandomResultsMatch &= ParallelRandomResults[Index].Equals(ParallelRandomResults[0], 1.e-12); } TestTrue("Seeded random nodes should remain deterministic across worker tasks", bParallelRandomResultsMatch); FRandomStream ExpectedCircleStream(24680); const double ExpectedCircleAngle = static_cast(ExpectedCircleStream.FRand()) * UE_TWO_PI; const double ExpectedCircleRadius = FMath::Sqrt(static_cast(ExpectedCircleStream.FRand())) * 5.0; const FVector2D ExpectedCirclePoint( FMath::Cos(ExpectedCircleAngle) * ExpectedCircleRadius, FMath::Sin(ExpectedCircleAngle) * ExpectedCircleRadius); FRandomStream ActualCircleStream(24680); const FVector2D ActualCirclePoint = UDirectiveUtilMathFunctionLibrary::RandomPointInCircleFromStream(ActualCircleStream, 5.0f); TestTrue("Random Point In Circle consumes angle before radius", ActualCirclePoint.Equals(ExpectedCirclePoint, 1.e-6)); TestEqual("Random Point In Circle consumes two stream samples", ActualCircleStream.GetCurrentSeed(), ExpectedCircleStream.GetCurrentSeed()); FRandomStream ExpectedAnnulusStream(13579); const double ExpectedAnnulusAngle = static_cast(ExpectedAnnulusStream.FRand()) * UE_TWO_PI; const double ExpectedAnnulusRadius = FMath::Sqrt(FMath::Lerp( 4.0, 25.0, static_cast(ExpectedAnnulusStream.FRand()))); const FVector2D ExpectedAnnulusPoint( FMath::Cos(ExpectedAnnulusAngle) * ExpectedAnnulusRadius, FMath::Sin(ExpectedAnnulusAngle) * ExpectedAnnulusRadius); FRandomStream ActualAnnulusStream(13579); const FVector2D ActualAnnulusPoint = UDirectiveUtilMathFunctionLibrary::RandomPointInAnnulusFromStream(ActualAnnulusStream, 2.0f, 5.0f); TestTrue("Random Point In Annulus consumes angle before radius", ActualAnnulusPoint.Equals(ExpectedAnnulusPoint, 1.e-6)); TestEqual("Random Point In Annulus consumes two stream samples", ActualAnnulusStream.GetCurrentSeed(), ExpectedAnnulusStream.GetCurrentSeed()); FRandomStream ExpectedSphereStream(97531); FVector ExpectedSpherePoint; double ExpectedSphereSizeSquared; do { const double X = static_cast(ExpectedSphereStream.FRand()) * 2.0 - 1.0; const double Y = static_cast(ExpectedSphereStream.FRand()) * 2.0 - 1.0; const double Z = static_cast(ExpectedSphereStream.FRand()) * 2.0 - 1.0; ExpectedSpherePoint = FVector(X, Y, Z); ExpectedSphereSizeSquared = ExpectedSpherePoint.SizeSquared(); } while (ExpectedSphereSizeSquared > 1.0); ExpectedSpherePoint *= 5.0; FRandomStream ActualSphereStream(97531); TestTrue("Random Point In Sphere consumes coordinates in XYZ order", UDirectiveUtilMathFunctionLibrary::RandomPointInSphereFromStream(ActualSphereStream, 5.0f).Equals( ExpectedSpherePoint, 1.e-12) && ActualSphereStream.GetCurrentSeed() == ExpectedSphereStream.GetCurrentSeed()); FRandomStream UnchangedStream(86420); const int32 UnchangedSeed = UnchangedStream.GetCurrentSeed(); UDirectiveUtilMathFunctionLibrary::RandomPointInCircleFromStream(UnchangedStream, 0.0f); UDirectiveUtilMathFunctionLibrary::RandomPointInAnnulusFromStream(UnchangedStream, 0.0f, 0.0f); UDirectiveUtilMathFunctionLibrary::RandomPointInSphereFromStream( UnchangedStream, std::numeric_limits::infinity()); TestEqual("Invalid and zero radii do not advance random streams", UnchangedStream.GetCurrentSeed(), UnchangedSeed); FRandomStream DistributionStream(112358); double CircleDistributionMean = 0.0; double AnnulusDistributionMean = 0.0; double SphereDistributionMean = 0.0; constexpr int32 DistributionSampleCount = 10000; for (int32 Index = 0; Index < DistributionSampleCount; ++Index) { const FVector2D CirclePoint = UDirectiveUtilMathFunctionLibrary::RandomPointInCircleFromStream( DistributionStream, 5.0f); const FVector2D AnnulusPoint = UDirectiveUtilMathFunctionLibrary::RandomPointInAnnulusFromStream( DistributionStream, 2.0f, 5.0f); const FVector SpherePoint = UDirectiveUtilMathFunctionLibrary::RandomPointInSphereFromStream( DistributionStream, 5.0f); CircleDistributionMean += CirclePoint.SizeSquared() / 25.0; AnnulusDistributionMean += (AnnulusPoint.SizeSquared() - 4.0) / 21.0; SphereDistributionMean += FMath::Pow(SpherePoint.Size() / 5.0, 3.0); } CircleDistributionMean /= DistributionSampleCount; AnnulusDistributionMean /= DistributionSampleCount; SphereDistributionMean /= DistributionSampleCount; TestTrue("Random Point In Circle is uniform by area", FMath::IsNearlyEqual(CircleDistributionMean, 0.5, 0.02)); TestTrue("Random Point In Annulus is uniform by area", FMath::IsNearlyEqual(AnnulusDistributionMean, 0.5, 0.02)); TestTrue("Random Point In Sphere is uniform by volume", FMath::IsNearlyEqual(SphereDistributionMean, 0.5, 0.02)); TestTrue("Random point functions reject non-finite radii", UDirectiveUtilMathFunctionLibrary::RandomPointInCircle( std::numeric_limits::quiet_NaN()).IsZero() && UDirectiveUtilMathFunctionLibrary::RandomPointInAnnulus( 0.0f, std::numeric_limits::infinity()).IsZero() && UDirectiveUtilMathFunctionLibrary::RandomPointInSphere( std::numeric_limits::infinity()).IsZero()); TestTrue("Angle array average accepts equivalent angles across multiple turns", UDirectiveUtilMathFunctionLibrary::GetAngleArrayAverage({730.0f, -710.0f}, FloatResult, Strength) && FMath::IsNearlyEqual(FloatResult, 10.0f, 1.e-4f) && FMath::IsNearlyEqual(Strength, 1.0f, 1.e-4f)); FloatResult = 123.0f; Strength = 123.0f; TestFalse("Angle array average rejects an empty array", UDirectiveUtilMathFunctionLibrary::GetAngleArrayAverage({}, FloatResult, Strength)); TestTrue("A rejected angle average resets both outputs", FMath::IsNearlyZero(FloatResult) && FMath::IsNearlyZero(Strength)); FloatResult = 123.0f; TestTrue("Weighted float average ignores unusable weights", UDirectiveUtilMathFunctionLibrary::GetWeightedFloatArrayAverage( {10.0f, 20.0f, 30.0f, 40.0f}, {std::numeric_limits::quiet_NaN(), -1.0f, std::numeric_limits::infinity(), 2.0f}, FloatResult) && FMath::IsNearlyEqual(FloatResult, 40.0f)); FloatResult = 123.0f; TestFalse("Weighted float average rejects arrays without a usable weight", UDirectiveUtilMathFunctionLibrary::GetWeightedFloatArrayAverage( {10.0f, 20.0f}, {-1.0f, std::numeric_limits::quiet_NaN()}, FloatResult)); TestTrue("A rejected weighted float average resets its output", FMath::IsNearlyZero(FloatResult)); VectorResult = FVector(123.0); TestTrue("Weighted vector average ignores unusable weights", UDirectiveUtilMathFunctionLibrary::GetWeightedVectorArrayAverage( {FVector(10.0, 20.0, 30.0), FVector(-4.0, 5.0, -6.0)}, {std::numeric_limits::infinity(), 3.0f}, VectorResult) && VectorResult.Equals(FVector(-4.0, 5.0, -6.0), 1.e-9)); VectorResult = FVector(123.0); TestFalse("Weighted vector average rejects arrays without a usable weight", UDirectiveUtilMathFunctionLibrary::GetWeightedVectorArrayAverage( {FVector::ForwardVector}, {-1.0f}, VectorResult)); TestTrue("A rejected weighted vector average resets its output", VectorResult.IsZero()); FloatArrayResult = {123.0f}; TestFalse("Float array normalization rejects a non-finite source value", UDirectiveUtilMathFunctionLibrary::NormalizeFloatArrayToRange( {1.0f, std::numeric_limits::quiet_NaN()}, 0.0f, 1.0f, FloatArrayResult)); TestTrue("Rejected float array normalization clears its output", FloatArrayResult.IsEmpty()); TestFalse("Float array normalization rejects a non-finite output bound", UDirectiveUtilMathFunctionLibrary::NormalizeFloatArrayToRange( {1.0f, 2.0f}, 0.0f, std::numeric_limits::infinity(), FloatArrayResult)); TestTrue("Weight normalization ignores non-finite and negative weights", UDirectiveUtilMathFunctionLibrary::NormalizeWeights( {std::numeric_limits::quiet_NaN(), std::numeric_limits::infinity(), -2.0f, 4.0f}, FloatArrayResult) && FloatArrayResult == TArray({0.0f, 0.0f, 0.0f, 1.0f})); FloatArrayResult = {123.0f}; TestFalse("Weight normalization rejects an empty array", UDirectiveUtilMathFunctionLibrary::NormalizeWeights({}, FloatArrayResult)); TestTrue("Rejected weight normalization clears its output", FloatArrayResult.IsEmpty()); TestTrue("Percentiles clamp below zero", UDirectiveUtilMathFunctionLibrary::GetFloatArrayPercentile({7.0f, 3.0f, 11.0f}, -50.0f, FloatResult) && FMath::IsNearlyEqual(FloatResult, 3.0f)); TestTrue("Percentiles return the maximum at one hundred", UDirectiveUtilMathFunctionLibrary::GetFloatArrayPercentile({7.0f, 3.0f, 11.0f}, 100.0f, FloatResult) && FMath::IsNearlyEqual(FloatResult, 11.0f)); TestTrue("A single-value percentile is stable at every finite percentile", UDirectiveUtilMathFunctionLibrary::GetFloatArrayPercentile({-7.5f}, 37.25f, FloatResult) && FMath::IsNearlyEqual(FloatResult, -7.5f)); FloatResult = 123.0f; TestFalse("Percentiles reject a non-finite percentile", UDirectiveUtilMathFunctionLibrary::GetFloatArrayPercentile( {1.0f, 2.0f}, std::numeric_limits::quiet_NaN(), FloatResult)); TestTrue("A rejected percentile resets its output", FMath::IsNearlyZero(FloatResult)); const float LargeFiniteValue = std::numeric_limits::max() * 0.25f; TestTrue("Root mean square remains finite near the float limit", UDirectiveUtilMathFunctionLibrary::GetFloatArrayRootMeanSquare( {LargeFiniteValue, -LargeFiniteValue}, FloatResult) && FMath::IsFinite(FloatResult) && FMath::IsNearlyEqual(FloatResult / LargeFiniteValue, 1.0f, 1.e-5f)); FloatResult = 123.0f; TestFalse("Root mean square rejects non-finite values", UDirectiveUtilMathFunctionLibrary::GetFloatArrayRootMeanSquare( {1.0f, std::numeric_limits::infinity()}, FloatResult)); TestTrue("A rejected root mean square resets its output", FMath::IsNearlyZero(FloatResult)); TestTrue("Signed angle is invariant under positive vector scaling", FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SignedAngleBetweenVectors( FVector(20.0, 0.0, 5.0), FVector(0.0, 30.0, -7.0), FVector(0.0, 0.0, 9.0)), 90.0f, 1.e-4f)); TestTrue("A half-turn signed angle has the expected magnitude", FMath::IsNearlyEqual(FMath::Abs(UDirectiveUtilMathFunctionLibrary::SignedAngleBetweenVectors( FVector::ForwardVector, -FVector::ForwardVector, FVector::UpVector)), 180.0f, 1.e-4f)); TestEqual("Signed angle rejects non-finite vectors", UDirectiveUtilMathFunctionLibrary::SignedAngleBetweenVectors( FVector(std::numeric_limits::infinity(), 0.0, 0.0), FVector::RightVector, FVector::UpVector), 0.0f); TestTrue("Delta angle ignores complete turns", FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::DeltaAngle(-1080.0f + 15.0f, 1440.0f - 25.0f), -40.0f)); TestTrue("Angle interpolation permits negative extrapolation", FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::LerpAngle(10.0f, 350.0f, -1.0f), 30.0f)); TestTrue("Angle interpolation ignores complete turns in the delta", FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::LerpAngle( -1080.0f + 15.0f, 1440.0f - 25.0f, 0.5f), -1085.0f)); TestTrue("Ping Pong repeats across multiple positive periods", FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::PingPong(123.0f, -2.0f, 3.0f), 3.0f)); TestTrue("Ping Pong repeats across multiple negative periods", FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::PingPong(-122.0f, -2.0f, 3.0f), -2.0f)); TestTrue("Smooth Step clamps above its range", FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SmoothStep(100.0f, -2.0f, 3.0f), 1.0f)); TestTrue("Smoother Step accepts reversed bounds", FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SmootherStep(0.25f, 1.0f, 0.0f), 0.103515625f)); TestTrue("Step functions reject non-finite values", FMath::IsNearlyZero(UDirectiveUtilMathFunctionLibrary::SmoothStep( std::numeric_limits::quiet_NaN(), 0.0f, 1.0f)) && FMath::IsNearlyZero(UDirectiveUtilMathFunctionLibrary::SmootherStep( 0.5f, 0.0f, std::numeric_limits::infinity()))); TestTrue("Range Falloff accepts reversed radii", FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RangeFalloff(5.0f, 10.0f, 0.0f), 0.5f)); TestTrue("Range Falloff clamps negative distances to zero", FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RangeFalloff(-5.0f, 2.0f, 10.0f), 1.0f)); TestTrue("Range Falloff treats non-positive exponents as a hard inner range", FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RangeFalloff(9.0f, 2.0f, 10.0f, -3.0f), 1.0f)); TestTrue("Range Falloff is full strength at a collapsed shared radius", FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RangeFalloff(5.0f, 5.0f, 5.0f), 1.0f)); TestTrue("Range Falloff is full strength at the origin when both radii are zero", FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::RangeFalloff(0.0f, 0.0f, 0.0f), 1.0f)); TestTrue("Range Falloff is zero outside a collapsed shared radius", FMath::IsNearlyZero(UDirectiveUtilMathFunctionLibrary::RangeFalloff(6.0f, 5.0f, 5.0f))); TestTrue("A full-width cone includes the opposite direction", UDirectiveUtilMathFunctionLibrary::IsDirectionWithinCone( -FVector::ForwardVector, FVector::ForwardVector, 180.0f)); TestFalse("A zero-width cone excludes the opposite direction", UDirectiveUtilMathFunctionLibrary::IsDirectionWithinCone( -FVector::ForwardVector, FVector::ForwardVector, 0.0f)); TestTrue("A point on both cone boundaries is included", UDirectiveUtilMathFunctionLibrary::IsPointWithinCone( FVector(5.0, 5.0, 0.0), FVector::ZeroVector, FVector::ForwardVector, 45.0f, FMath::Sqrt(50.0))); TestTrue("Negative cone distance is treated as unlimited", UDirectiveUtilMathFunctionLibrary::IsPointWithinCone( FVector(100.0, 0.0, 0.0), FVector::ZeroVector, FVector::ForwardVector, 0.0f, -1.0)); TestTrue("Rotating by a complete turn preserves a translated point", UDirectiveUtilMathFunctionLibrary::RotatePointAroundPivot2D( FVector2D(1000003.0, -1999995.0), FVector2D(1000000.0, -2000000.0), 1080.0f) .Equals(FVector2D(1000003.0, -1999995.0), 1.e-8)); TestTrue("Signed plane distance is translation invariant", FMath::IsNearlyEqual(UDirectiveUtilMathFunctionLibrary::SignedDistanceToPlane( FVector(1000000.0, -2000000.0, 3000007.0), FVector(1000000.0, -2000000.0, 3000000.0), FVector(0.0, 0.0, 123.0)), 7.0, 1.e-9)); FRandomStream PositiveRadiusStream(424242); FRandomStream NegativeRadiusStream(424242); TestTrue("Random circle stream treats negative radius as magnitude", UDirectiveUtilMathFunctionLibrary::RandomPointInCircleFromStream(PositiveRadiusStream, 5.0f).Equals( UDirectiveUtilMathFunctionLibrary::RandomPointInCircleFromStream(NegativeRadiusStream, -5.0f), 1.e-12)); FRandomStream OrderedAnnulusStream(31337); FRandomStream ReversedAnnulusStream(31337); TestTrue("Random annulus stream accepts negative reversed radii", UDirectiveUtilMathFunctionLibrary::RandomPointInAnnulusFromStream(OrderedAnnulusStream, 2.0f, 5.0f).Equals( UDirectiveUtilMathFunctionLibrary::RandomPointInAnnulusFromStream(ReversedAnnulusStream, -5.0f, -2.0f), 1.e-12)); FRandomStream PositiveSphereStream(8675309); FRandomStream NegativeSphereStream(8675309); TestTrue("Random sphere stream treats negative radius as magnitude", UDirectiveUtilMathFunctionLibrary::RandomPointInSphereFromStream(PositiveSphereStream, 5.0f).Equals( UDirectiveUtilMathFunctionLibrary::RandomPointInSphereFromStream(NegativeSphereStream, -5.0f), 1.e-12)); return !HasAnyErrors(); }