// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License. #include "Libraries/DirectiveUtilMathFunctionLibrary.h" #include "Components/SplineComponent.h" #include "Math/RotationMatrix.h" #include "Misc/AutomationTest.h" #include IMPLEMENT_SIMPLE_AUTOMATION_TEST( FDirectiveUtilTransformArrayTest, "DirectiveUtilities.Math.TransformArrays", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter) bool FDirectiveUtilTransformArrayTest::RunTest(const FString& Parameters) { const TArray Locations = { FVector(1.0, 2.0, 3.0), FVector(4.0, 5.0, 6.0), FVector(7.0, 8.0, 9.0) }; const FRotator SharedRotator(10.0, 20.0, 30.0); const FQuat SharedRotation = SharedRotator.Quaternion(); const FVector SharedScale(2.0, 3.0, 4.0); const TArray SharedTransforms = UDirectiveUtilMathFunctionLibrary::LocationsToTransforms( Locations, SharedRotator, SharedScale); bool bSharedTransformsValid = SharedTransforms.Num() == Locations.Num(); for (int32 Index = 0; Index < SharedTransforms.Num(); ++Index) { bSharedTransformsValid &= SharedTransforms[Index].GetLocation() == Locations[Index]; bSharedTransformsValid &= SharedTransforms[Index].GetRotation().Equals(SharedRotation, 1.e-12); bSharedTransformsValid &= SharedTransforms[Index].GetScale3D() == SharedScale; } TestTrue(TEXT("Locations to transforms preserves order and broadcasts rotation and scale"), bSharedTransformsValid); TestTrue(TEXT("Locations to transforms accepts an empty location array"), UDirectiveUtilMathFunctionLibrary::LocationsToTransforms( {}, SharedRotator, SharedScale).IsEmpty()); TArray Transforms; TestTrue(TEXT("Empty attribute arrays use identity rotation and scale"), UDirectiveUtilMathFunctionLibrary::MakeTransformsFromArrays(Locations, {}, {}, Transforms)); bool bIdentityAttributesValid = Transforms.Num() == Locations.Num(); for (int32 Index = 0; Index < Transforms.Num(); ++Index) { bIdentityAttributesValid &= Transforms[Index].GetLocation() == Locations[Index]; bIdentityAttributesValid &= Transforms[Index].GetRotation().Equals(FQuat::Identity, 1.e-12); bIdentityAttributesValid &= Transforms[Index].GetScale3D() == FVector::OneVector; } TestTrue(TEXT("Identity attributes preserve every location"), bIdentityAttributesValid); TestTrue(TEXT("Single attribute values broadcast across the location array"), UDirectiveUtilMathFunctionLibrary::MakeTransformsFromArrays( Locations, { SharedRotator }, { SharedScale }, Transforms)); bool bBroadcastAttributesValid = Transforms.Num() == Locations.Num(); for (const FTransform& Transform : Transforms) { bBroadcastAttributesValid &= Transform.GetRotation().Equals(SharedRotation, 1.e-12); bBroadcastAttributesValid &= Transform.GetScale3D() == SharedScale; } TestTrue(TEXT("Broadcast attributes are applied to every transform"), bBroadcastAttributesValid); const TArray Rotations = { FRotator::ZeroRotator, FRotator(0.0, 90.0, 0.0), FRotator(45.0, 0.0, 0.0) }; const TArray Scales = { FVector::OneVector, FVector(2.0), FVector(-1.0, 1.0, 0.5) }; TestTrue(TEXT("Full attribute arrays map element by element"), UDirectiveUtilMathFunctionLibrary::MakeTransformsFromArrays( Locations, Rotations, Scales, Transforms)); bool bPerTransformAttributesValid = Transforms.Num() == Locations.Num(); for (int32 Index = 0; Index < Transforms.Num(); ++Index) { bPerTransformAttributesValid &= Transforms[Index].GetLocation() == Locations[Index]; bPerTransformAttributesValid &= Transforms[Index].GetRotation().Equals( Rotations[Index].Quaternion(), 1.e-12); bPerTransformAttributesValid &= Transforms[Index].GetScale3D() == Scales[Index]; } TestTrue(TEXT("Per-transform attributes preserve index alignment"), bPerTransformAttributesValid); Transforms = { FTransform::Identity }; TestFalse(TEXT("Mismatched rotation counts are rejected"), UDirectiveUtilMathFunctionLibrary::MakeTransformsFromArrays( Locations, { FRotator::ZeroRotator, FRotator::ZeroRotator }, {}, Transforms)); TestTrue(TEXT("A rejected attribute count clears the output"), Transforms.IsEmpty()); TestFalse(TEXT("Mismatched scale counts are rejected"), UDirectiveUtilMathFunctionLibrary::MakeTransformsFromArrays( Locations, {}, { FVector::OneVector, FVector::OneVector }, Transforms)); TestTrue(TEXT("A rejected scale count clears the output"), Transforms.IsEmpty()); const double Infinity = std::numeric_limits::infinity(); const FRotator InvalidRotation(Infinity, 0.0, 0.0); TestTrue(TEXT("Locations to transforms rejects non-finite values"), UDirectiveUtilMathFunctionLibrary::LocationsToTransforms( { FVector(Infinity, 0.0, 0.0) }, FRotator::ZeroRotator, FVector::OneVector).IsEmpty() && UDirectiveUtilMathFunctionLibrary::LocationsToTransforms( Locations, InvalidRotation, FVector::OneVector).IsEmpty() && UDirectiveUtilMathFunctionLibrary::LocationsToTransforms( Locations, FRotator::ZeroRotator, FVector(Infinity)).IsEmpty()); TestFalse(TEXT("Transform arrays reject a non-finite location"), UDirectiveUtilMathFunctionLibrary::MakeTransformsFromArrays( { FVector::ZeroVector, FVector(Infinity) }, {}, {}, Transforms)); TestTrue(TEXT("A non-finite location clears partial output"), Transforms.IsEmpty()); TestFalse(TEXT("Transform arrays reject a non-finite rotation"), UDirectiveUtilMathFunctionLibrary::MakeTransformsFromArrays( Locations, { InvalidRotation }, {}, Transforms)); TestTrue(TEXT("A non-finite rotation leaves no output"), Transforms.IsEmpty()); TestFalse(TEXT("Transform arrays reject a non-finite scale"), UDirectiveUtilMathFunctionLibrary::MakeTransformsFromArrays( Locations, {}, { FVector(Infinity) }, Transforms)); TestTrue(TEXT("A non-finite scale leaves no output"), Transforms.IsEmpty()); TestTrue(TEXT("Empty locations produce a valid empty transform array"), UDirectiveUtilMathFunctionLibrary::MakeTransformsFromArrays( {}, { SharedRotator }, { SharedScale }, Transforms)); TestTrue(TEXT("An empty transform result contains no values"), Transforms.IsEmpty()); const FVector LargeLocation(1000000.0, -2000000.0, 3000000.0); const FRotator WrappedRotation(-1080.0, 1440.0, 720.0); const FVector SignedScale(-2.0, 0.0, 4.0); const TArray OddTransforms = UDirectiveUtilMathFunctionLibrary::LocationsToTransforms( {LargeLocation, -LargeLocation}, WrappedRotation, SignedScale); TestTrue(TEXT("Locations to transforms preserves large locations and signed scales"), OddTransforms.Num() == 2 && OddTransforms[0].GetLocation() == LargeLocation && OddTransforms[1].GetLocation() == -LargeLocation && OddTransforms[0].GetScale3D() == SignedScale && OddTransforms[1].GetScale3D() == SignedScale && OddTransforms[0].GetRotation().Equals(WrappedRotation.Quaternion(), 1.e-12)); const FVector SingleLocation(-7.0, 11.0, -13.0); const FRotator SingleRotation(17.0, -29.0, 43.0); const FVector SingleScale(0.0, -1.0, 2.0); TestTrue(TEXT("Single-element attribute arrays map without special-case drift"), UDirectiveUtilMathFunctionLibrary::MakeTransformsFromArrays( {SingleLocation}, {SingleRotation}, {SingleScale}, Transforms) && Transforms.Num() == 1 && Transforms[0].GetLocation() == SingleLocation && Transforms[0].GetRotation().Equals(SingleRotation.Quaternion(), 1.e-12) && Transforms[0].GetScale3D() == SingleScale); Transforms = {FTransform::Identity}; TestFalse(TEXT("Empty locations still reject an impossible rotation count"), UDirectiveUtilMathFunctionLibrary::MakeTransformsFromArrays( {}, {FRotator::ZeroRotator, SharedRotator}, {}, Transforms)); TestTrue(TEXT("Rejected empty-location attributes clear the output"), Transforms.IsEmpty()); Transforms = {FTransform::Identity}; TestFalse(TEXT("Empty locations still reject an impossible scale count"), UDirectiveUtilMathFunctionLibrary::MakeTransformsFromArrays( {}, {}, {FVector::OneVector, SharedScale}, Transforms)); TestTrue(TEXT("Rejected empty-location scales clear the output"), Transforms.IsEmpty()); const FVector FacingTarget(100.0, -200.0, 300.0); const TArray FacingLocations = { FacingTarget + FVector(10.0, 0.0, 0.0), FacingTarget + FVector(0.0, -20.0, 0.0), FacingTarget }; const TArray FacingTransforms = UDirectiveUtilMathFunctionLibrary::LocationsToFacingTransforms( FacingLocations, FacingTarget, FVector::UpVector, FRotator::ZeroRotator, SharedScale, false); TestTrue(TEXT("Facing transforms preserve locations and face their target"), FacingTransforms.Num() == 3 && FacingTransforms[0].GetLocation() == FacingLocations[0] && FacingTransforms[0].GetRotation().GetAxisX().Equals(FVector::BackwardVector, 1.e-8) && FacingTransforms[1].GetRotation().GetAxisX().Equals(FVector::RightVector, 1.e-8) && FacingTransforms[2].GetRotation().Equals(FQuat::Identity, 1.e-12) && FacingTransforms[0].GetScale3D() == SharedScale); const TArray AwayTransforms = UDirectiveUtilMathFunctionLibrary::LocationsToFacingTransforms( FacingLocations, FacingTarget, FVector::UpVector, FRotator::ZeroRotator, FVector::OneVector, true); TestTrue(TEXT("Facing transforms can point away from their target"), AwayTransforms.Num() == 3 && AwayTransforms[0].GetRotation().GetAxisX().Equals(FVector::ForwardVector, 1.e-8) && AwayTransforms[1].GetRotation().GetAxisX().Equals(FVector::LeftVector, 1.e-8)); const FRotator FacingOffset(13.0, 17.0, 19.0); const FQuat ExpectedFacingOffset = FRotationMatrix::MakeFromXZ( FVector::BackwardVector, FVector::UpVector).ToQuat() * FacingOffset.Quaternion(); const TArray OffsetFacingTransforms = UDirectiveUtilMathFunctionLibrary::LocationsToFacingTransforms( {FacingLocations[0]}, FacingTarget, FVector::UpVector, FacingOffset); TestTrue(TEXT("Facing transforms apply their rotation offset in local space"), OffsetFacingTransforms.Num() == 1 && OffsetFacingTransforms[0].GetRotation().Equals(ExpectedFacingOffset, 1.e-12)); TestTrue(TEXT("Facing transforms reject invalid shared inputs"), UDirectiveUtilMathFunctionLibrary::LocationsToFacingTransforms( FacingLocations, FacingTarget, FVector::ZeroVector).IsEmpty() && UDirectiveUtilMathFunctionLibrary::LocationsToFacingTransforms( FacingLocations, FVector(Infinity), FVector::UpVector).IsEmpty() && UDirectiveUtilMathFunctionLibrary::LocationsToFacingTransforms( FacingLocations, FacingTarget, FVector::UpVector, InvalidRotation).IsEmpty() && UDirectiveUtilMathFunctionLibrary::LocationsToFacingTransforms( FacingLocations, FacingTarget, FVector::UpVector, FRotator::ZeroRotator, FVector(Infinity)).IsEmpty()); const FVector RadialCenter(1000.0, -2000.0, 3000.0); const FRotator RadialPlane(17.0, 31.0, 43.0); const FQuat RadialPlaneQuaternion = RadialPlane.Quaternion(); const FVector RadialNormal = RadialPlaneQuaternion.GetAxisZ(); const TArray CircleLocations = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnCircle( RadialCenter, RadialPlane, 25.0, 12, 11.0); const TArray InwardCircleTransforms = UDirectiveUtilMathFunctionLibrary::GenerateTransformsOnCircle( RadialCenter, RadialPlane, 25.0, 12, 11.0, EDirectiveUtilRadialOrientation::FaceCenter, FRotator::ZeroRotator, SharedScale); bool bInwardCircleValid = InwardCircleTransforms.Num() == CircleLocations.Num(); for (int32 Index = 0; Index < InwardCircleTransforms.Num(); ++Index) { const FVector Inward = (RadialCenter - CircleLocations[Index]).GetSafeNormal(); bInwardCircleValid &= InwardCircleTransforms[Index].GetLocation().Equals(CircleLocations[Index], 1.e-8); bInwardCircleValid &= InwardCircleTransforms[Index].GetRotation().GetAxisX().Equals(Inward, 1.e-8); bInwardCircleValid &= InwardCircleTransforms[Index].GetScale3D() == SharedScale; } TestTrue(TEXT("Circle transforms match point locations and face their center"), bInwardCircleValid); const TArray OutwardCircleTransforms = UDirectiveUtilMathFunctionLibrary::GenerateTransformsOnCircle( RadialCenter, RadialPlane, 25.0, 12, 11.0, EDirectiveUtilRadialOrientation::FaceAwayFromCenter); const TArray ForwardCircleTransforms = UDirectiveUtilMathFunctionLibrary::GenerateTransformsOnCircle( RadialCenter, RadialPlane, 25.0, 12, 11.0, EDirectiveUtilRadialOrientation::FollowPath); const TArray ReverseCircleTransforms = UDirectiveUtilMathFunctionLibrary::GenerateTransformsOnCircle( RadialCenter, RadialPlane, 25.0, 12, 11.0, EDirectiveUtilRadialOrientation::FaceAgainstPath); bool bCircleOrientationsValid = OutwardCircleTransforms.Num() == CircleLocations.Num() && ForwardCircleTransforms.Num() == CircleLocations.Num() && ReverseCircleTransforms.Num() == CircleLocations.Num(); for (int32 Index = 0; Index < CircleLocations.Num() && bCircleOrientationsValid; ++Index) { const FVector Radial = (CircleLocations[Index] - RadialCenter).GetSafeNormal(); const FVector Tangent = FVector::CrossProduct(RadialNormal, Radial).GetSafeNormal(); bCircleOrientationsValid &= OutwardCircleTransforms[Index].GetRotation().GetAxisX().Equals(Radial, 1.e-8); bCircleOrientationsValid &= ForwardCircleTransforms[Index].GetRotation().GetAxisX().Equals(Tangent, 1.e-8); bCircleOrientationsValid &= ReverseCircleTransforms[Index].GetRotation().GetAxisX().Equals(-Tangent, 1.e-8); } TestTrue(TEXT("Circle transforms support outward and both path orientations"), bCircleOrientationsValid); const TArray FixedCircleTransforms = UDirectiveUtilMathFunctionLibrary::GenerateTransformsOnCircle( RadialCenter, RadialPlane, 25.0, 4, 0.0, EDirectiveUtilRadialOrientation::Fixed, FacingOffset); FQuat ExpectedFixedRotation = RadialPlaneQuaternion * FacingOffset.Quaternion(); ExpectedFixedRotation.Normalize(); TestTrue(TEXT("Fixed circle transforms preserve the plane rotation and local offset"), FixedCircleTransforms.Num() == 4 && FixedCircleTransforms[0].GetRotation().Equals(ExpectedFixedRotation, 1.e-12) && FixedCircleTransforms[3].GetRotation().Equals(ExpectedFixedRotation, 1.e-12)); const TArray NegativeArcTransforms = UDirectiveUtilMathFunctionLibrary::GenerateTransformsOnArc( RadialCenter, RadialPlane, 25.0, 3, 0.0, -90.0, true, EDirectiveUtilRadialOrientation::FollowPath); TestTrue(TEXT("A negative arc reverses follow-path orientation"), NegativeArcTransforms.Num() == 3 && NegativeArcTransforms[0].GetRotation().GetAxisX().Equals( -RadialPlaneQuaternion.GetAxisY(), 1.e-8)); const TArray NegativeArcLocations = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnArc( RadialCenter, RadialPlane, 25.0, 3, 0.0, -90.0, true); TestTrue(TEXT("Arc transforms match translated rotated point generation"), NegativeArcTransforms.Num() == NegativeArcLocations.Num() && NegativeArcTransforms[0].GetLocation().Equals(NegativeArcLocations[0], 1.e-8) && NegativeArcTransforms[1].GetLocation().Equals(NegativeArcLocations[1], 1.e-8) && NegativeArcTransforms[2].GetLocation().Equals(NegativeArcLocations[2], 1.e-8)); const TArray ZeroRadiusArcTransforms = UDirectiveUtilMathFunctionLibrary::GenerateTransformsOnArc( RadialCenter, RadialPlane, 0.0, 3, 0.0, 90.0, true, EDirectiveUtilRadialOrientation::FaceCenter); TestTrue(TEXT("Zero-radius arc transforms retain a deterministic radial orientation"), ZeroRadiusArcTransforms.Num() == 3 && ZeroRadiusArcTransforms[0].GetLocation() == RadialCenter && ZeroRadiusArcTransforms[0].GetRotation().GetAxisX().Equals( -RadialPlaneQuaternion.GetAxisX(), 1.e-8)); TestTrue(TEXT("Radial transform generators reject invalid input"), UDirectiveUtilMathFunctionLibrary::GenerateTransformsOnCircle( RadialCenter, RadialPlane, Infinity, 3).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GenerateTransformsOnArc( RadialCenter, RadialPlane, 1.0, 3, 0.0, Infinity).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GenerateTransformsOnCircle( RadialCenter, RadialPlane, 1.0, 3, 0.0, static_cast(255)).IsEmpty()); USplineComponent* Spline = NewObject(); Spline->SetSplinePoints({FVector::ZeroVector, FVector(100.0, 0.0, 0.0)}, ESplineCoordinateSpace::Local, false); Spline->SetSplinePointType(0, ESplinePointType::Linear, false); Spline->SetSplinePointType(1, ESplinePointType::Linear, false); Spline->SetScaleAtSplinePoint(0, FVector(1.0, 2.0, 3.0), false); Spline->SetScaleAtSplinePoint(1, FVector(3.0, 4.0, 5.0), true); const FVector SplineScaleMultiplier(2.0, 0.5, -1.0); const TArray SplineTransforms = UDirectiveUtilMathFunctionLibrary::GenerateTransformsAlongSpline( Spline, 30.0, true, EDirectiveUtilSplineSpacingMode::Fixed, ESplineCoordinateSpace::World, true, FacingOffset, SplineScaleMultiplier); const double SplineDistances[] = {0.0, 30.0, 60.0, 90.0, 100.0}; bool bSplineTransformsValid = SplineTransforms.Num() == 5; for (int32 Index = 0; Index < SplineTransforms.Num(); ++Index) { FTransform Expected = Spline->GetTransformAtDistanceAlongSpline( static_cast(SplineDistances[Index]), ESplineCoordinateSpace::World, true); FQuat ExpectedRotation = Expected.GetRotation() * FacingOffset.Quaternion(); ExpectedRotation.Normalize(); bSplineTransformsValid &= SplineTransforms[Index].GetLocation().Equals(Expected.GetLocation(), 1.e-8); bSplineTransformsValid &= SplineTransforms[Index].GetRotation().Equals(ExpectedRotation, 1.e-8); bSplineTransformsValid &= SplineTransforms[Index].GetScale3D().Equals( Expected.GetScale3D() * SplineScaleMultiplier, 1.e-8); } TestTrue(TEXT("Spline transforms preserve sampling, spline rotation, and spline scale"), bSplineTransformsValid); const TArray UnscaledSplineTransforms = UDirectiveUtilMathFunctionLibrary::GenerateTransformsAlongSpline( Spline, 1000.0, true, EDirectiveUtilSplineSpacingMode::Fixed, ESplineCoordinateSpace::World, false, FRotator::ZeroRotator, SharedScale); TestTrue(TEXT("Spline scale can be replaced by a shared multiplier"), UnscaledSplineTransforms.Num() == 2 && UnscaledSplineTransforms[0].GetScale3D() == SharedScale && UnscaledSplineTransforms[1].GetScale3D() == SharedScale); const TArray CountedSplineTransforms = UDirectiveUtilMathFunctionLibrary::GenerateTransformsAlongSplineByCount(Spline, 3, true); TestTrue(TEXT("Spline transforms by count include both exact endpoints"), CountedSplineTransforms.Num() == 3 && CountedSplineTransforms[0].GetLocation().Equals(FVector::ZeroVector, 1.e-4) && CountedSplineTransforms[1].GetLocation().Equals(FVector(50.0, 0.0, 0.0), 1.e-4) && CountedSplineTransforms[2].GetLocation().Equals(FVector(100.0, 0.0, 0.0), 1.e-4)); USplineComponent* SinglePointSpline = NewObject(); SinglePointSpline->SetSplinePoints({FVector(3.0, 4.0, 5.0)}, ESplineCoordinateSpace::Local, true); const TArray SinglePointSplineTransforms = UDirectiveUtilMathFunctionLibrary::GenerateTransformsAlongSpline( SinglePointSpline, 10.0); TestTrue(TEXT("A zero-length spline returns one transform"), SinglePointSplineTransforms.Num() == 1 && SinglePointSplineTransforms[0].GetLocation().Equals(FVector(3.0, 4.0, 5.0), 1.e-8)); TestTrue(TEXT("Spline transform generation rejects invalid input"), UDirectiveUtilMathFunctionLibrary::GenerateTransformsAlongSpline( nullptr, 10.0).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GenerateTransformsAlongSpline( Spline, 0.0).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GenerateTransformsAlongSpline( Spline, 10.0, true, EDirectiveUtilSplineSpacingMode::Fixed, ESplineCoordinateSpace::World, true, InvalidRotation).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GenerateTransformsAlongSpline( Spline, 10.0, true, EDirectiveUtilSplineSpacingMode::Fixed, ESplineCoordinateSpace::World, true, FRotator::ZeroRotator, FVector(Infinity)).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GenerateTransformsAlongSplineByCount( Spline, 0).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GenerateTransformsAlongSplineByCount( Spline, 3, true, ESplineCoordinateSpace::World, true, InvalidRotation).IsEmpty()); const TArray HexGridTransforms = UDirectiveUtilMathFunctionLibrary::GenerateRectangularHexGridTransforms( RadialCenter, FRotator::ZeroRotator, FIntPoint(2, 2), 25.0, EDirectiveUtilHexOrientation::PointyTop, 0.0, false, FacingOffset, SharedScale); const TArray HexGridPoints = UDirectiveUtilMathFunctionLibrary::GenerateRectangularHexGrid( RadialCenter, FRotator::ZeroRotator, FIntPoint(2, 2), 25.0, EDirectiveUtilHexOrientation::PointyTop, 0.0, false); bool bHexTransformsValid = HexGridTransforms.Num() == 4 && HexGridPoints.Num() == 4; for (int32 Index = 0; bHexTransformsValid && Index < HexGridTransforms.Num(); ++Index) { bHexTransformsValid &= HexGridTransforms[Index].GetLocation().Equals(HexGridPoints[Index], 1.e-9) && HexGridTransforms[Index].GetRotation().Equals(FacingOffset.Quaternion(), 1.e-8) && HexGridTransforms[Index].GetScale3D() == SharedScale; } TestTrue(TEXT("Rectangular hex grid transforms share instance rotation and scale over grid cells"), bHexTransformsValid); TestEqual(TEXT("Hexagonal hex grid transforms cover the requested rings"), UDirectiveUtilMathFunctionLibrary::GenerateHexagonalHexGridTransforms( RadialCenter, FRotator::ZeroRotator, 1, 25.0).Num(), 7); TestTrue(TEXT("Hex grid transform generators reject invalid input"), UDirectiveUtilMathFunctionLibrary::GenerateRectangularHexGridTransforms( RadialCenter, FRotator::ZeroRotator, FIntPoint(2, 2), 25.0, EDirectiveUtilHexOrientation::PointyTop, 0.0, true, InvalidRotation).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GenerateHexagonalHexGridTransforms( RadialCenter, FRotator::ZeroRotator, 1, 0.0).IsEmpty()); const TArray NoiseBaseLocations = { FVector::ZeroVector, FVector(37.0, 11.0, 5.0), FVector(250.0, -90.0, 40.0) }; const TArray NoiseBaseTransforms = UDirectiveUtilMathFunctionLibrary::LocationsToTransforms( NoiseBaseLocations, FacingOffset, SharedScale); const TArray NoisedTransforms = UDirectiveUtilMathFunctionLibrary::OffsetTransformsByNoise( NoiseBaseTransforms, 100.0, 25.0); const TArray NoisedLocations = UDirectiveUtilMathFunctionLibrary::OffsetLocationsByNoise( NoiseBaseLocations, 100.0, 25.0); bool bNoiseTransformsValid = NoisedTransforms.Num() == 3 && NoisedLocations.Num() == 3; for (int32 Index = 0; bNoiseTransformsValid && Index < NoisedTransforms.Num(); ++Index) { bNoiseTransformsValid &= NoisedTransforms[Index].GetLocation().Equals(NoisedLocations[Index], 1.e-9) && NoisedTransforms[Index].GetRotation().Equals(NoiseBaseTransforms[Index].GetRotation(), 1.e-9) && NoisedTransforms[Index].GetScale3D() == NoiseBaseTransforms[Index].GetScale3D(); } TestTrue(TEXT("Transform noise offsets match location noise offsets and preserve rotation and scale"), bNoiseTransformsValid); TestTrue(TEXT("Transform noise offsets reject invalid input"), UDirectiveUtilMathFunctionLibrary::OffsetTransformsByNoise(NoiseBaseTransforms, -1.0, 25.0).IsEmpty()); const TArray EaseFromLocations = { FVector::ZeroVector, FVector(10.0, 0.0, 0.0), FVector(20.0, 0.0, 0.0) }; const TArray EaseToLocations = { FVector(0.0, 10.0, 0.0), FVector(10.0, 10.0, 0.0), FVector(20.0, 10.0, 0.0) }; const TArray EasedLocations = UDirectiveUtilMathFunctionLibrary::EaseLocationArrays( EaseFromLocations, EaseToLocations, 0.5f, EDirectiveUtilEaseType::Linear, {}); TestTrue(TEXT("Eased location arrays blend element-wise"), EasedLocations.Num() == 3 && EasedLocations[0].Equals(FVector(0.0, 5.0, 0.0), 1.e-4) && EasedLocations[2].Equals(FVector(20.0, 5.0, 0.0), 1.e-4)); const TArray StaggeredLocations = UDirectiveUtilMathFunctionLibrary::EaseLocationArrays( EaseFromLocations, EaseToLocations, 0.0f, EDirectiveUtilEaseType::Linear, { 0.0f, 0.5f, 1.0f }); TestTrue(TEXT("Per-element alphas stagger the blend"), StaggeredLocations.Num() == 3 && StaggeredLocations[0].Equals(EaseFromLocations[0], 1.e-4) && StaggeredLocations[1].Equals(FVector(10.0, 5.0, 0.0), 1.e-4) && StaggeredLocations[2].Equals(EaseToLocations[2], 1.e-4)); TestTrue(TEXT("Eased arrays reject mismatched lengths"), UDirectiveUtilMathFunctionLibrary::EaseLocationArrays( EaseFromLocations, { FVector::ZeroVector }, 0.5f, EDirectiveUtilEaseType::Linear, {}).IsEmpty() && UDirectiveUtilMathFunctionLibrary::EaseLocationArrays( EaseFromLocations, EaseToLocations, 0.5f, EDirectiveUtilEaseType::Linear, { 0.5f, 0.5f }).IsEmpty()); const TArray EaseFromTransforms = UDirectiveUtilMathFunctionLibrary::LocationsToTransforms( EaseFromLocations, FRotator::ZeroRotator, FVector::OneVector); const TArray EaseToTransforms = UDirectiveUtilMathFunctionLibrary::LocationsToTransforms( EaseToLocations, FRotator(0.0, 90.0, 0.0), FVector(3.0)); const TArray EasedTransforms = UDirectiveUtilMathFunctionLibrary::EaseTransformArrays( EaseFromTransforms, EaseToTransforms, 0.5f, EDirectiveUtilEaseType::Linear, {}); TestTrue(TEXT("Eased transform arrays blend location, rotation, and scale element-wise"), EasedTransforms.Num() == 3 && EasedTransforms[1].GetLocation().Equals(FVector(10.0, 5.0, 0.0), 1.e-4) && EasedTransforms[1].GetRotation().Equals(FRotator(0.0, 45.0, 0.0).Quaternion(), 1.e-4) && EasedTransforms[1].GetScale3D().Equals(FVector(2.0), 1.e-4)); const TArray SamplePath = { FVector::ZeroVector, FVector(10.0, 0.0, 0.0), FVector(10.0, 10.0, 0.0) }; TestTrue(TEXT("Location array sampling is distance-weighted"), UDirectiveUtilMathFunctionLibrary::SampleLocationArray(SamplePath, 0.75f).Equals( FVector(10.0, 5.0, 0.0), 1.e-4) && UDirectiveUtilMathFunctionLibrary::SampleLocationArray(SamplePath, 0.0f).Equals( SamplePath[0], 1.e-4) && UDirectiveUtilMathFunctionLibrary::SampleLocationArray(SamplePath, 1.0f).Equals( SamplePath.Last(), 1.e-4) && UDirectiveUtilMathFunctionLibrary::SampleLocationArray(SamplePath, 1.5f).Equals( SamplePath.Last(), 1.e-4)); TestTrue(TEXT("Closed-loop sampling wraps alpha back to the start"), UDirectiveUtilMathFunctionLibrary::SampleLocationArray(SamplePath, 1.0f, true).Equals( SamplePath[0], 1.e-4)); TestTrue(TEXT("Degenerate location array sampling returns the only point or zero"), UDirectiveUtilMathFunctionLibrary::SampleLocationArray({ FVector(3.0, 4.0, 5.0) }, 0.7f).Equals( FVector(3.0, 4.0, 5.0), 1.e-4) && UDirectiveUtilMathFunctionLibrary::SampleLocationArray({}, 0.5f).IsZero()); const TArray SampleTransformPath = { FTransform(FRotator::ZeroRotator, FVector::ZeroVector, FVector::OneVector), FTransform(FRotator(0.0, 90.0, 0.0), FVector(10.0, 0.0, 0.0), FVector(3.0)) }; const FTransform SampledTransform = UDirectiveUtilMathFunctionLibrary::SampleTransformArray( SampleTransformPath, 0.5f); TestTrue(TEXT("Transform array sampling blends location, rotation, and scale"), SampledTransform.GetLocation().Equals(FVector(5.0, 0.0, 0.0), 1.e-4) && SampledTransform.GetRotation().Equals(FRotator(0.0, 45.0, 0.0).Quaternion(), 1.e-4) && SampledTransform.GetScale3D().Equals(FVector(2.0), 1.e-4)); TestTrue(TEXT("Empty transform array sampling returns the identity"), UDirectiveUtilMathFunctionLibrary::SampleTransformArray({}, 0.5f).Equals(FTransform::Identity)); return !HasAnyErrors(); }