// Copyright (c) 2026 Unreal Directive. Licensed under the MIT License. #include "Libraries/DirectiveUtilMathFunctionLibrary.h" #include "Components/SplineComponent.h" #include "Misc/AutomationTest.h" #include "UObject/Class.h" #include namespace { bool PointsEqual(const TArray& A, const TArray& B, const double Tolerance = 1.e-9) { if (A.Num() != B.Num()) { return false; } for (int32 Index = 0; Index < A.Num(); ++Index) { if (!A[Index].Equals(B[Index], Tolerance)) { return false; } } return true; } bool PointsEqualAfterTranslation(const TArray& BasePoints, const TArray& TranslatedPoints, const FVector& Translation, const double Tolerance = 1.e-6) { if (BasePoints.Num() != TranslatedPoints.Num()) { return false; } for (int32 Index = 0; Index < BasePoints.Num(); ++Index) { if (!(TranslatedPoints[Index] - Translation).Equals(BasePoints[Index], Tolerance)) { return false; } } return true; } bool PointsMatchLocation(const TArray& Points, const FVector& Location, const double Tolerance = 1.e-9) { for (const FVector& Point : Points) { if (!Point.Equals(Location, Tolerance)) { return false; } } return true; } } IMPLEMENT_SIMPLE_AUTOMATION_TEST( FDirectiveUtilPointGenerationTest, "DirectiveUtilities.Math.PointGeneration", EAutomationTestFlags::EditorContext | EAutomationTestFlags::ClientContext | EAutomationTestFlags::EngineFilter) bool FDirectiveUtilPointGenerationTest::RunTest(const FString& Parameters) { const FName CallableGeneratorNames[] = { GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GenerateGridPoints2D), GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GenerateGridPoints3D), GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GenerateGridTransforms2D), GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GenerateGridTransforms3D), GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GenerateRectangularHexGrid), GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GenerateRectangularHexGridTransforms), GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GetRectangularHexGridCoordinates), GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GenerateHexagonalHexGrid), GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GenerateHexagonalHexGridTransforms), GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GetHexesInRange), GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GetHexRing), GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GetHexLine), GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GeneratePointsAlongDirection), GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GeneratePointsBetweenLocations), GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GeneratePointsOnCircle), GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GenerateTransformsOnCircle), GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GeneratePointsOnArc), GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GenerateTransformsOnArc), GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GeneratePointsOnDisc), GET_FUNCTION_NAME_CHECKED(UDirectiveUtilMathFunctionLibrary, GeneratePointsOnSphere) }; for (const FName FunctionName : CallableGeneratorNames) { const UFunction* Function = UDirectiveUtilMathFunctionLibrary::StaticClass()->FindFunctionByName(FunctionName); TestNotNull(*FString::Printf(TEXT("%s should be exposed to Blueprint"), *FunctionName.ToString()), Function); if (Function) { TestTrue(*FString::Printf(TEXT("%s should be callable"), *FunctionName.ToString()), Function->HasAnyFunctionFlags(FUNC_BlueprintCallable)); TestFalse(*FString::Printf(TEXT("%s should not execute as a pure node"), *FunctionName.ToString()), Function->HasAnyFunctionFlags(FUNC_BlueprintPure)); #if WITH_EDITOR TestFalse(*FString::Printf(TEXT("%s should not advertise inert Blueprint thread safety"), *FunctionName.ToString()), Function->HasMetaData(TEXT("BlueprintThreadSafe"))); #endif } } const TArray Grid2D = UDirectiveUtilMathFunctionLibrary::GenerateGridPoints2D( FVector::ZeroVector, FRotator::ZeroRotator, FIntPoint(3, 2), FVector2D(10.0, 20.0), true); const TArray ExpectedGrid2D = { FVector(-10.0, -10.0, 0.0), FVector(0.0, -10.0, 0.0), FVector(10.0, -10.0, 0.0), FVector(-10.0, 10.0, 0.0), FVector(0.0, 10.0, 0.0), FVector(10.0, 10.0, 0.0) }; TestTrue(TEXT("2D grid is centered and ordered by X then Y"), PointsEqual(Grid2D, ExpectedGrid2D)); const TArray RotatedGrid2D = UDirectiveUtilMathFunctionLibrary::GenerateGridPoints2D( FVector(5.0, 7.0, 9.0), FRotator(0.0, 90.0, 0.0), FIntPoint(2, 1), FVector2D(3.0, 0.0), false); TestTrue(TEXT("2D grid rotation places its local X axis in world space"), RotatedGrid2D.Num() == 2 && RotatedGrid2D[0].Equals(FVector(5.0, 7.0, 9.0), 1.e-9) && RotatedGrid2D[1].Equals(FVector(5.0, 10.0, 9.0), 1.e-9)); const TArray Grid3D = UDirectiveUtilMathFunctionLibrary::GenerateGridPoints3D( FVector::ZeroVector, FRotator::ZeroRotator, FIntVector(2, 2, 2), FVector(2.0, 4.0, 6.0), true); TestTrue(TEXT("3D grid is centered and ordered by X then Y then Z"), Grid3D.Num() == 8 && Grid3D[0].Equals(FVector(-1.0, -2.0, -3.0), 1.e-9) && Grid3D[1].Equals(FVector(1.0, -2.0, -3.0), 1.e-9) && Grid3D[2].Equals(FVector(-1.0, 2.0, -3.0), 1.e-9) && Grid3D.Last().Equals(FVector(1.0, 2.0, 3.0), 1.e-9)); TestTrue(TEXT("Grid generation rejects non-positive dimensions"), UDirectiveUtilMathFunctionLibrary::GenerateGridPoints2D( FVector::ZeroVector, FRotator::ZeroRotator, FIntPoint(2, 0), FVector2D(1.0), true).IsEmpty()); TestTrue(TEXT("Grid generation rejects point-count overflow"), UDirectiveUtilMathFunctionLibrary::GenerateGridPoints3D( FVector::ZeroVector, FRotator::ZeroRotator, FIntVector(MAX_int32, 2, 2), FVector::OneVector, true).IsEmpty()); const FRotator GridInstanceRotation(10.0, 20.0, 30.0); const FQuat GridInstanceQuaternion = GridInstanceRotation.Quaternion(); const FVector GridInstanceScale(0.25, 0.5, 0.75); const TArray GridTransforms2D = UDirectiveUtilMathFunctionLibrary::GenerateGridTransforms2D( FVector::ZeroVector, FRotator::ZeroRotator, FIntPoint(3, 2), FVector2D(10.0, 20.0), true, GridInstanceRotation, GridInstanceScale); bool bGridTransforms2DValid = GridTransforms2D.Num() == Grid2D.Num(); for (int32 Index = 0; Index < GridTransforms2D.Num(); ++Index) { bGridTransforms2DValid &= GridTransforms2D[Index].GetLocation().Equals(Grid2D[Index], 1.e-9); bGridTransforms2DValid &= GridTransforms2D[Index].GetRotation().Equals(GridInstanceQuaternion, 1.e-12); bGridTransforms2DValid &= GridTransforms2D[Index].GetScale3D() == GridInstanceScale; } TestTrue(TEXT("2D grid transforms match point locations and broadcast rotation and scale"), bGridTransforms2DValid); const TArray GridTransforms3D = UDirectiveUtilMathFunctionLibrary::GenerateGridTransforms3D( FVector::ZeroVector, FRotator::ZeroRotator, FIntVector(2, 2, 2), FVector(2.0, 4.0, 6.0), true, GridInstanceRotation, GridInstanceScale); bool bGridTransforms3DValid = GridTransforms3D.Num() == Grid3D.Num(); for (int32 Index = 0; Index < GridTransforms3D.Num(); ++Index) { bGridTransforms3DValid &= GridTransforms3D[Index].GetLocation().Equals(Grid3D[Index], 1.e-9); bGridTransforms3DValid &= GridTransforms3D[Index].GetRotation().Equals(GridInstanceQuaternion, 1.e-12); bGridTransforms3DValid &= GridTransforms3D[Index].GetScale3D() == GridInstanceScale; } TestTrue(TEXT("3D grid transforms match point locations and broadcast rotation and scale"), bGridTransforms3DValid); TestTrue(TEXT("Grid transform generation rejects invalid dimensions and non-finite scale"), UDirectiveUtilMathFunctionLibrary::GenerateGridTransforms2D( FVector::ZeroVector, FRotator::ZeroRotator, FIntPoint(2, 0), FVector2D(1.0), true).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GenerateGridTransforms3D( FVector::ZeroVector, FRotator::ZeroRotator, FIntVector(2, 2, 2), FVector::OneVector, true, FRotator::ZeroRotator, FVector(std::numeric_limits::infinity())).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GenerateGridTransforms3D( FVector::ZeroVector, FRotator::ZeroRotator, FIntVector(MAX_int32, 2, 2), FVector::OneVector, true) .IsEmpty()); const double HexRadius = 10.0; const FVector PointyQ = UDirectiveUtilMathFunctionLibrary::HexCoordinateToLocation( FIntPoint(1, 0), FVector::ZeroVector, FRotator::ZeroRotator, HexRadius, EDirectiveUtilHexOrientation::PointyTop); const FVector PointyR = UDirectiveUtilMathFunctionLibrary::HexCoordinateToLocation( FIntPoint(0, 1), FVector::ZeroVector, FRotator::ZeroRotator, HexRadius, EDirectiveUtilHexOrientation::PointyTop); TestTrue(TEXT("Pointy-top axial coordinates use the expected basis"), PointyQ.Equals(FVector(UE_DOUBLE_SQRT_3 * HexRadius, 0.0, 0.0), 1.e-9) && PointyR.Equals(FVector(UE_DOUBLE_SQRT_3 * 0.5 * HexRadius, 15.0, 0.0), 1.e-9)); const FVector FlatQ = UDirectiveUtilMathFunctionLibrary::HexCoordinateToLocation( FIntPoint(1, 0), FVector::ZeroVector, FRotator::ZeroRotator, HexRadius, EDirectiveUtilHexOrientation::FlatTop); const FVector FlatR = UDirectiveUtilMathFunctionLibrary::HexCoordinateToLocation( FIntPoint(0, 1), FVector::ZeroVector, FRotator::ZeroRotator, HexRadius, EDirectiveUtilHexOrientation::FlatTop); TestTrue(TEXT("Flat-top axial coordinates use the expected basis"), FlatQ.Equals(FVector(15.0, UE_DOUBLE_SQRT_3 * 0.5 * HexRadius, 0.0), 1.e-9) && FlatR.Equals(FVector(0.0, UE_DOUBLE_SQRT_3 * HexRadius, 0.0), 1.e-9)); const FVector HexOrigin(11.0, 13.0, 17.0); const FRotator HexRotation(23.0, 37.0, 11.0); const FVector HexPlaneNormal = HexRotation.Quaternion().GetAxisZ(); for (const EDirectiveUtilHexOrientation HexOrientation : { EDirectiveUtilHexOrientation::PointyTop, EDirectiveUtilHexOrientation::FlatTop }) { const FIntPoint Coordinate(-7, 4); const FVector Location = UDirectiveUtilMathFunctionLibrary::HexCoordinateToLocation( Coordinate, HexOrigin, HexRotation, 25.0, HexOrientation, 3.0); TestEqual(TEXT("Hex coordinate conversion round trips through a rotated layout"), UDirectiveUtilMathFunctionLibrary::LocationToHexCoordinate( Location, HexOrigin, HexRotation, 25.0, HexOrientation, 3.0), Coordinate); TestEqual(TEXT("Location conversion projects onto the hex plane"), UDirectiveUtilMathFunctionLibrary::LocationToHexCoordinate( Location + HexPlaneNormal * 500.0, HexOrigin, HexRotation, 25.0, HexOrientation, 3.0), Coordinate); } const FVector GappedHex = UDirectiveUtilMathFunctionLibrary::HexCoordinateToLocation( FIntPoint(1, 0), FVector::ZeroVector, FRotator::ZeroRotator, HexRadius, EDirectiveUtilHexOrientation::PointyTop, 2.0); TestTrue(TEXT("Hex gap adds to the adjacent edge distance"), FMath::IsNearlyEqual(GappedHex.Size(), UE_DOUBLE_SQRT_3 * HexRadius + 2.0, 1.e-9)); const TArray HexNeighbors = UDirectiveUtilMathFunctionLibrary::GetHexNeighbors(FIntPoint(3, -2)); const TArray ExpectedHexNeighbors = { FIntPoint(4, -2), FIntPoint(4, -3), FIntPoint(3, -3), FIntPoint(2, -2), FIntPoint(2, -1), FIntPoint(3, -1) }; TestTrue(TEXT("Hex neighbors use stable axial direction order"), HexNeighbors == ExpectedHexNeighbors); TestEqual(TEXT("Hex distance counts axial grid steps"), UDirectiveUtilMathFunctionLibrary::GetHexDistance(FIntPoint(0, 0), FIntPoint(3, -5)), 5LL); TestEqual(TEXT("Hex distance uses 64-bit intermediates"), UDirectiveUtilMathFunctionLibrary::GetHexDistance( FIntPoint(MAX_int32, MAX_int32), FIntPoint(MIN_int32, MIN_int32)), 8589934590LL); const TArray RectangularHexGrid = UDirectiveUtilMathFunctionLibrary::GenerateRectangularHexGrid( FVector::ZeroVector, FRotator::ZeroRotator, FIntPoint(3, 2), HexRadius, EDirectiveUtilHexOrientation::PointyTop, 0.0, true); TestTrue(TEXT("Rectangular hex grid is centered and ordered by row then column"), RectangularHexGrid.Num() == 6 && RectangularHexGrid[0].Equals(-RectangularHexGrid.Last(), 1.e-9) && RectangularHexGrid[1].X < RectangularHexGrid[2].X && RectangularHexGrid[2].Y < RectangularHexGrid[3].Y); const TArray UncenteredHexGrid = UDirectiveUtilMathFunctionLibrary::GenerateRectangularHexGrid( HexOrigin, FRotator::ZeroRotator, FIntPoint(2, 2), HexRadius, EDirectiveUtilHexOrientation::FlatTop, 0.0, false); TestTrue(TEXT("Uncentered rectangular hex grid starts at its origin"), UncenteredHexGrid.Num() == 4 && UncenteredHexGrid[0] == HexOrigin); const TArray HexagonalGrid = UDirectiveUtilMathFunctionLibrary::GenerateHexagonalHexGrid( FVector::ZeroVector, FRotator::ZeroRotator, 2, HexRadius, EDirectiveUtilHexOrientation::PointyTop); TSet HexagonalCoordinates; bool bHexagonalGridValid = HexagonalGrid.Num() == 19; for (const FVector& Point : HexagonalGrid) { const FIntPoint Coordinate = UDirectiveUtilMathFunctionLibrary::LocationToHexCoordinate( Point, FVector::ZeroVector, FRotator::ZeroRotator, HexRadius, EDirectiveUtilHexOrientation::PointyTop); bHexagonalGridValid &= UDirectiveUtilMathFunctionLibrary::GetHexDistance(FIntPoint::ZeroValue, Coordinate) <= 2; HexagonalCoordinates.Add(Coordinate); } TestTrue(TEXT("Hexagonal grid contains every coordinate through its requested radius"), bHexagonalGridValid && HexagonalCoordinates.Num() == 19); TestEqual(TEXT("A zero-radius hexagonal grid contains its center"), UDirectiveUtilMathFunctionLibrary::GenerateHexagonalHexGrid( HexOrigin, FRotator::ZeroRotator, 0, HexRadius).Num(), 1); TestTrue(TEXT("Hex generators reject invalid layouts and counts"), UDirectiveUtilMathFunctionLibrary::GenerateRectangularHexGrid( FVector::ZeroVector, FRotator::ZeroRotator, FIntPoint(0, 2), HexRadius).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GenerateRectangularHexGrid( FVector::ZeroVector, FRotator::ZeroRotator, FIntPoint(2, 2), 0.0).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GenerateHexagonalHexGrid( FVector::ZeroVector, FRotator::ZeroRotator, -1, HexRadius).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GenerateHexagonalHexGrid( FVector::ZeroVector, FRotator::ZeroRotator, 30000, HexRadius).IsEmpty() && UDirectiveUtilMathFunctionLibrary::HexCoordinateToLocation( FIntPoint(1, 0), FVector::ZeroVector, FRotator::ZeroRotator, HexRadius, EDirectiveUtilHexOrientation::PointyTop, -UE_DOUBLE_SQRT_3 * HexRadius).IsZero() && UDirectiveUtilMathFunctionLibrary::GetHexNeighbors(FIntPoint(MAX_int32, 0)).IsEmpty()); const TArray HexesInRange = UDirectiveUtilMathFunctionLibrary::GetHexesInRange(FIntPoint(2, -1), 1); const TArray ExpectedHexesInRange = { FIntPoint(2, -2), FIntPoint(3, -2), FIntPoint(1, -1), FIntPoint(2, -1), FIntPoint(3, -1), FIntPoint(1, 0), FIntPoint(2, 0) }; TestTrue(TEXT("Hexes in range cover the center and its neighbors ordered by R then Q"), HexesInRange == ExpectedHexesInRange); const TArray HexagonalGridCoordinates = UDirectiveUtilMathFunctionLibrary::GetHexesInRange(FIntPoint::ZeroValue, 2); bool bHexagonalOrderValid = HexagonalGridCoordinates.Num() == HexagonalGrid.Num(); for (int32 Index = 0; bHexagonalOrderValid && Index < HexagonalGrid.Num(); ++Index) { bHexagonalOrderValid &= HexagonalGrid[Index].Equals( UDirectiveUtilMathFunctionLibrary::HexCoordinateToLocation( HexagonalGridCoordinates[Index], FVector::ZeroVector, FRotator::ZeroRotator, HexRadius, EDirectiveUtilHexOrientation::PointyTop), 1.e-9); } TestTrue(TEXT("Hexes in range around zero pair with hexagonal grid cells by index"), bHexagonalOrderValid); const TArray RectangularCoordinates = UDirectiveUtilMathFunctionLibrary::GetRectangularHexGridCoordinates( FIntPoint(2, 2), EDirectiveUtilHexOrientation::FlatTop); bool bRectangularOrderValid = RectangularCoordinates.Num() == UncenteredHexGrid.Num(); for (int32 Index = 0; bRectangularOrderValid && Index < UncenteredHexGrid.Num(); ++Index) { bRectangularOrderValid &= UncenteredHexGrid[Index].Equals( UDirectiveUtilMathFunctionLibrary::HexCoordinateToLocation( RectangularCoordinates[Index], HexOrigin, FRotator::ZeroRotator, HexRadius, EDirectiveUtilHexOrientation::FlatTop), 1.e-9); } TestTrue(TEXT("Rectangular hex grid coordinates pair with grid cells by index"), bRectangularOrderValid); const TArray HexRing = UDirectiveUtilMathFunctionLibrary::GetHexRing(FIntPoint(1, 1), 2); bool bRingValid = HexRing.Num() == 12; for (int32 Index = 0; bRingValid && Index < HexRing.Num(); ++Index) { bRingValid &= UDirectiveUtilMathFunctionLibrary::GetHexDistance(FIntPoint(1, 1), HexRing[Index]) == 2; bRingValid &= UDirectiveUtilMathFunctionLibrary::GetHexDistance( HexRing[Index], HexRing[(Index + 1) % HexRing.Num()]) == 1; } TestTrue(TEXT("A hex ring traces adjacent cells at the requested radius"), bRingValid); const TArray ZeroHexRing = UDirectiveUtilMathFunctionLibrary::GetHexRing(FIntPoint(4, 5), 0); TestTrue(TEXT("A zero-radius hex ring returns the center"), ZeroHexRing.Num() == 1 && ZeroHexRing[0] == FIntPoint(4, 5)); const TArray HexLine = UDirectiveUtilMathFunctionLibrary::GetHexLine( FIntPoint(0, 0), FIntPoint(3, -3)); const TArray ExpectedHexLine = { FIntPoint(0, 0), FIntPoint(1, -1), FIntPoint(2, -2), FIntPoint(3, -3) }; TestTrue(TEXT("A hex line follows a straight axial direction"), HexLine == ExpectedHexLine); const TArray DiagonalHexLine = UDirectiveUtilMathFunctionLibrary::GetHexLine( FIntPoint(-1, 2), FIntPoint(1, 3)); bool bDiagonalLineValid = DiagonalHexLine.Num() == 4 && DiagonalHexLine[0] == FIntPoint(-1, 2) && DiagonalHexLine.Last() == FIntPoint(1, 3); for (int32 Index = 0; bDiagonalLineValid && Index < DiagonalHexLine.Num() - 1; ++Index) { bDiagonalLineValid &= UDirectiveUtilMathFunctionLibrary::GetHexDistance( DiagonalHexLine[Index], DiagonalHexLine[Index + 1]) == 1; } TestTrue(TEXT("A hex line steps through adjacent cells between its endpoints"), bDiagonalLineValid); const TArray SingleHexLine = UDirectiveUtilMathFunctionLibrary::GetHexLine( FIntPoint(7, -2), FIntPoint(7, -2)); TestTrue(TEXT("A zero-length hex line returns its cell"), SingleHexLine.Num() == 1 && SingleHexLine[0] == FIntPoint(7, -2)); const TArray HexCorners = UDirectiveUtilMathFunctionLibrary::GetHexCellCorners( FIntPoint::ZeroValue, FVector::ZeroVector, FRotator::ZeroRotator, HexRadius, EDirectiveUtilHexOrientation::PointyTop); bool bCornersValid = HexCorners.Num() == 6 && HexCorners[0].Equals(FVector(UE_DOUBLE_SQRT_3 * 0.5 * HexRadius, 0.5 * HexRadius, 0.0), 1.e-9); for (int32 Index = 0; bCornersValid && Index < 6; ++Index) { bCornersValid &= FMath::IsNearlyEqual(HexCorners[Index].Size(), HexRadius, 1.e-9); bCornersValid &= FMath::IsNearlyEqual( FVector::Distance(HexCorners[Index], HexCorners[(Index + 1) % 6]), HexRadius, 1.e-9); } TestTrue(TEXT("Pointy-top cell corners lie at the cell radius with matching side length"), bCornersValid); const TArray FlatHexCorners = UDirectiveUtilMathFunctionLibrary::GetHexCellCorners( FIntPoint::ZeroValue, FVector::ZeroVector, FRotator::ZeroRotator, HexRadius, EDirectiveUtilHexOrientation::FlatTop); TestTrue(TEXT("Flat-top cell corners start on the local X axis"), FlatHexCorners.Num() == 6 && FlatHexCorners[0].Equals(FVector(HexRadius, 0.0, 0.0), 1.e-9)); const TArray GappedHexCorners = UDirectiveUtilMathFunctionLibrary::GetHexCellCorners( FIntPoint(1, 0), FVector::ZeroVector, FRotator::ZeroRotator, HexRadius, EDirectiveUtilHexOrientation::PointyTop, 2.0); TestTrue(TEXT("Hex gap moves the cell center but not the corner distance"), GappedHexCorners.Num() == 6 && FMath::IsNearlyEqual(FVector::Distance(GappedHexCorners[0], GappedHex), HexRadius, 1.e-9)); TestTrue(TEXT("Hex queries reject invalid input"), UDirectiveUtilMathFunctionLibrary::GetHexesInRange(FIntPoint::ZeroValue, -1).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GetHexesInRange(FIntPoint(MAX_int32, 0), 1).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GetHexRing(FIntPoint::ZeroValue, -1).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GetHexRing(FIntPoint(MAX_int32, 0), 1).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GetHexLine( FIntPoint(MIN_int32, MIN_int32), FIntPoint(MAX_int32, MAX_int32)).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GetHexCellCorners(FIntPoint(1, 1), FVector::ZeroVector, FRotator::ZeroRotator, 0.0).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GetRectangularHexGridCoordinates(FIntPoint(0, 3)).IsEmpty()); const int32 MaximumGeneratedElementCount = UDirectiveUtilMathFunctionLibrary::MaximumGeneratedElementCount; TestEqual( TEXT("The maximum supported rectangular grid size remains available"), UDirectiveUtilMathFunctionLibrary::GetRectangularHexGridCoordinates(FIntPoint(1000, 1000)).Num(), MaximumGeneratedElementCount); TestTrue(TEXT("Generated collections reject the first unsupported count"), UDirectiveUtilMathFunctionLibrary::GenerateGridPoints2D( FVector::ZeroVector, FRotator::ZeroRotator, FIntPoint(MaximumGeneratedElementCount + 1, 1), FVector2D(1.0, 1.0)).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GetHexesInRange(FIntPoint::ZeroValue, 577).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GetHexRing( FIntPoint::ZeroValue, MaximumGeneratedElementCount / 6 + 1).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GetHexLine( FIntPoint::ZeroValue, FIntPoint(MaximumGeneratedElementCount, 0)).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongDirection( FVector::ZeroVector, FVector::ForwardVector, MaximumGeneratedElementCount + 1, 1.0).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere( FVector::ZeroVector, FRotator::ZeroRotator, 1.0, MaximumGeneratedElementCount + 1).IsEmpty()); const TArray NoiseBase = { FVector::ZeroVector, FVector(37.0, 11.0, 5.0), FVector(250.0, -90.0, 40.0) }; const TArray NoisePoints = UDirectiveUtilMathFunctionLibrary::OffsetLocationsByNoise( NoiseBase, 100.0, 25.0); bool bNoiseValid = NoisePoints.Num() == 3; bool bAnyNoiseOffset = false; for (int32 Index = 0; bNoiseValid && Index < NoisePoints.Num(); ++Index) { const FVector NoiseDelta = NoisePoints[Index] - NoiseBase[Index]; bNoiseValid &= FMath::IsNearlyZero(NoiseDelta.X) && FMath::IsNearlyZero(NoiseDelta.Y) && FMath::Abs(NoiseDelta.Z) <= 25.0 + 1.e-6; bAnyNoiseOffset |= !FMath::IsNearlyZero(NoiseDelta.Z); } TestTrue(TEXT("Noise offsets displace along the requested direction within the amplitude"), bNoiseValid && bAnyNoiseOffset); TestTrue(TEXT("Noise offsets are deterministic"), UDirectiveUtilMathFunctionLibrary::OffsetLocationsByNoise(NoiseBase, 100.0, 25.0) == NoisePoints); TestTrue(TEXT("A zero noise amplitude leaves locations unchanged"), UDirectiveUtilMathFunctionLibrary::OffsetLocationsByNoise(NoiseBase, 100.0, 0.0) == NoiseBase); TestTrue(TEXT("Noise offsets reject invalid input"), UDirectiveUtilMathFunctionLibrary::OffsetLocationsByNoise(NoiseBase, 0.0, 25.0).IsEmpty() && UDirectiveUtilMathFunctionLibrary::OffsetLocationsByNoise( NoiseBase, 100.0, 25.0, FVector::ZeroVector).IsEmpty()); const TArray DirectionPoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongDirection( FVector::ZeroVector, FVector(10.0, 0.0, 0.0), 4, 2.0, true); const TArray ExpectedDirectionPoints = { FVector(-3.0, 0.0, 0.0), FVector(-1.0, 0.0, 0.0), FVector(1.0, 0.0, 0.0), FVector(3.0, 0.0, 0.0) }; TestTrue(TEXT("Direction points normalize once and center around the origin"), PointsEqual(DirectionPoints, ExpectedDirectionPoints)); const TArray ReversedDirectionPoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongDirection( FVector::ZeroVector, FVector::ForwardVector, 3, -2.0, false); TestTrue(TEXT("Direction points preserve signed spacing"), ReversedDirectionPoints.Num() == 3 && ReversedDirectionPoints[0].Equals(FVector::ZeroVector) && ReversedDirectionPoints[2].Equals(FVector(-4.0, 0.0, 0.0))); TestTrue(TEXT("Direction points reject a zero direction"), UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongDirection( FVector::ZeroVector, FVector::ZeroVector, 3, 1.0, false).IsEmpty()); const TArray SegmentPoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsBetweenLocations( FVector::ZeroVector, FVector(10.0, 0.0, 0.0), 3, true); TestTrue(TEXT("Segment points include exact endpoints"), SegmentPoints.Num() == 3 && SegmentPoints[0] == FVector::ZeroVector && SegmentPoints[1].Equals(FVector(5.0, 0.0, 0.0)) && SegmentPoints[2] == FVector(10.0, 0.0, 0.0)); const TArray InteriorSegmentPoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsBetweenLocations( FVector::ZeroVector, FVector(9.0, 0.0, 0.0), 2, false); TestTrue(TEXT("Segment points can exclude both endpoints"), InteriorSegmentPoints.Num() == 2 && InteriorSegmentPoints[0].Equals(FVector(3.0, 0.0, 0.0)) && InteriorSegmentPoints[1].Equals(FVector(6.0, 0.0, 0.0))); const TArray SingleSegmentPoint = UDirectiveUtilMathFunctionLibrary::GeneratePointsBetweenLocations( FVector(2.0, 4.0, 6.0), FVector(6.0, 8.0, 10.0), 1, true); TestTrue(TEXT("A single segment point is the midpoint"), SingleSegmentPoint.Num() == 1 && SingleSegmentPoint[0].Equals(FVector(4.0, 6.0, 8.0))); 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, true); TestTrue(TEXT("Spline generators reject unsupported sample counts"), UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSplineByCount( Spline, MaximumGeneratedElementCount + 1).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GenerateTransformsAlongSplineByCount( Spline, MaximumGeneratedElementCount + 1).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline( Spline, 0.00005).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GenerateTransformsAlongSpline( Spline, 0.00005).IsEmpty()); const TArray SplinePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline( Spline, 30.0, true, EDirectiveUtilSplineSpacingMode::Fixed); TestTrue(TEXT("Spline points use fixed spacing and append the exact open endpoint"), SplinePoints.Num() == 5 && SplinePoints[0].Equals(FVector::ZeroVector) && SplinePoints[1].Equals(FVector(30.0, 0.0, 0.0), 1.e-4) && SplinePoints[3].Equals(FVector(90.0, 0.0, 0.0), 1.e-4) && SplinePoints[4].Equals(FVector(100.0, 0.0, 0.0), 1.e-4)); TestEqual(TEXT("Spline endpoint can be excluded"), UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline( Spline, 30.0, false, EDirectiveUtilSplineSpacingMode::Fixed).Num(), 4); const TArray EvenSplinePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline( Spline, 30.0, true, EDirectiveUtilSplineSpacingMode::Even); TestTrue(TEXT("Even spline spacing divides the range without a short final interval"), EvenSplinePoints.Num() == 5 && EvenSplinePoints[1].Equals(FVector(25.0, 0.0, 0.0), 1.e-4) && EvenSplinePoints[3].Equals(FVector(75.0, 0.0, 0.0), 1.e-4) && EvenSplinePoints[4].Equals(FVector(100.0, 0.0, 0.0), 1.e-4)); const TArray RangedSplinePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline( Spline, 25.0, true, EDirectiveUtilSplineSpacingMode::Fixed, ESplineCoordinateSpace::World, 20.0, 80.0); TestTrue(TEXT("Spline sampling honors a start and end distance"), RangedSplinePoints.Num() == 4 && RangedSplinePoints[0].Equals(FVector(20.0, 0.0, 0.0), 1.e-4) && RangedSplinePoints[1].Equals(FVector(45.0, 0.0, 0.0), 1.e-4) && RangedSplinePoints[3].Equals(FVector(80.0, 0.0, 0.0), 1.e-4)); TestEqual(TEXT("Spline sampling drops a regular sample that lands on the endpoint"), UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline( Spline, 25.0, true, EDirectiveUtilSplineSpacingMode::Fixed, ESplineCoordinateSpace::World, 0.0, 50.0 + 1.e-10).Num(), 3); const TArray CountedSplinePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSplineByCount( Spline, 5, true); TestTrue(TEXT("Spline sampling by count includes both exact endpoints"), CountedSplinePoints.Num() == 5 && CountedSplinePoints[0].Equals(FVector::ZeroVector, 1.e-4) && CountedSplinePoints[2].Equals(FVector(50.0, 0.0, 0.0), 1.e-4) && CountedSplinePoints[4].Equals(FVector(100.0, 0.0, 0.0), 1.e-4)); const TArray InteriorSplinePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSplineByCount( Spline, 4, false); TestTrue(TEXT("Spline sampling by count can exclude both endpoints"), InteriorSplinePoints.Num() == 4 && InteriorSplinePoints[0].Equals(FVector(20.0, 0.0, 0.0), 1.e-4) && InteriorSplinePoints[3].Equals(FVector(80.0, 0.0, 0.0), 1.e-4)); const TArray SingleSplinePoint = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSplineByCount( Spline, 1, true); TestTrue(TEXT("A single spline point by count is the range midpoint"), SingleSplinePoint.Num() == 1 && SingleSplinePoint[0].Equals(FVector(50.0, 0.0, 0.0), 1.e-4)); Spline->SetClosedLoop(true, true); const TArray ClosedSplinePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline( Spline, 30.0, true, EDirectiveUtilSplineSpacingMode::Fixed); TestTrue(TEXT("Closed spline sampling does not repeat its first point"), ClosedSplinePoints.Num() > 1 && !ClosedSplinePoints[0].Equals(ClosedSplinePoints.Last(), 1.e-4)); const TArray ClosedCountedPoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSplineByCount( Spline, 4, true); TestTrue(TEXT("Closed spline sampling by count spreads points around the loop"), ClosedCountedPoints.Num() == 4 && !ClosedCountedPoints[0].Equals(ClosedCountedPoints.Last(), 1.e-4)); USplineComponent* SinglePointSpline = NewObject(); SinglePointSpline->SetSplinePoints({ FVector(3.0, 4.0, 5.0) }, ESplineCoordinateSpace::Local, true); const TArray ZeroLengthSplinePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline( SinglePointSpline, 10.0); TestTrue(TEXT("A zero-length spline returns its only point"), ZeroLengthSplinePoints.Num() == 1 && ZeroLengthSplinePoints[0].Equals(FVector(3.0, 4.0, 5.0))); TestTrue(TEXT("Spline sampling rejects invalid input"), UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(nullptr, 10.0).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(Spline, 0.0).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline( Spline, 10.0, true, static_cast(255)).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline( Spline, 10.0, true, EDirectiveUtilSplineSpacingMode::Fixed, static_cast(255)).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline( Spline, 10.0, true, EDirectiveUtilSplineSpacingMode::Fixed, ESplineCoordinateSpace::World, 80.0, 20.0).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSplineByCount(Spline, 0).IsEmpty()); const TArray CirclePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnCircle( FVector::ZeroVector, FRotator::ZeroRotator, 10.0, 4, 0.0); const TArray ExpectedCirclePoints = { FVector(10.0, 0.0, 0.0), FVector(0.0, 10.0, 0.0), FVector(-10.0, 0.0, 0.0), FVector(0.0, -10.0, 0.0) }; TestTrue(TEXT("Circle points are evenly spaced without repeating the first point"), PointsEqual(CirclePoints, ExpectedCirclePoints, 1.e-8)); const FRotator PlaneRotation(17.0, 31.0, 43.0); const FVector PlaneNormal = PlaneRotation.Quaternion().GetAxisZ(); const FVector PlaneCenter(11.0, 13.0, 17.0); const TArray RotatedCircle = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnCircle( PlaneCenter, PlaneRotation, -7.0, 64, 15.0); bool bCirclePlaneValid = RotatedCircle.Num() == 64; for (const FVector& Point : RotatedCircle) { const FVector Offset = Point - PlaneCenter; bCirclePlaneValid &= FMath::IsNearlyEqual(Offset.Size(), 7.0, 1.e-8); bCirclePlaneValid &= FMath::IsNearlyZero(FVector::DotProduct(Offset, PlaneNormal), 1.e-8); } TestTrue(TEXT("Circle points honor rotation and negative radius"), bCirclePlaneValid); const TArray ArcPoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnArc( FVector::ZeroVector, FRotator::ZeroRotator, 10.0, 3, 0.0, 90.0, true); TestTrue(TEXT("Arc points include the requested endpoint"), ArcPoints.Num() == 3 && ArcPoints[0].Equals(FVector(10.0, 0.0, 0.0), 1.e-8) && ArcPoints[1].Equals(FVector(UE_DOUBLE_INV_SQRT_2 * 10.0, UE_DOUBLE_INV_SQRT_2 * 10.0, 0.0), 1.e-8) && ArcPoints[2].Equals(FVector(0.0, 10.0, 0.0), 1.e-8)); const TArray OpenArcPoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnArc( FVector::ZeroVector, FRotator::ZeroRotator, 10.0, 3, 0.0, 90.0, false); TestTrue(TEXT("Open arc points exclude the requested endpoint"), OpenArcPoints.Num() == 3 && OpenArcPoints.Last().Equals( FVector(FMath::Cos(UE_DOUBLE_PI / 3.0) * 10.0, FMath::Sin(UE_DOUBLE_PI / 3.0) * 10.0, 0.0), 1.e-8)); const TArray DiscPoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnDisc( PlaneCenter, PlaneRotation, 25.0, 1024, 27.0); const TArray RepeatedDiscPoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnDisc( PlaneCenter, PlaneRotation, 25.0, 1024, 27.0); bool bDiscValid = DiscPoints.Num() == 1024; for (const FVector& Point : DiscPoints) { const FVector Offset = Point - PlaneCenter; bDiscValid &= Offset.Size() < 25.0; bDiscValid &= FMath::IsNearlyZero(FVector::DotProduct(Offset, PlaneNormal), 1.e-8); } TestTrue(TEXT("Disc points are deterministic and remain inside the rotated disc"), bDiscValid && PointsEqual(DiscPoints, RepeatedDiscPoints)); const TArray SingleDiscPoint = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnDisc( PlaneCenter, PlaneRotation, 25.0, 1, 27.0); TestTrue(TEXT("A single disc point is its center"), SingleDiscPoint.Num() == 1 && SingleDiscPoint[0] == PlaneCenter); const TArray SpherePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere( FVector::ZeroVector, PlaneRotation, 25.0, 1024, 27.0); const TArray RepeatedSpherePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere( FVector::ZeroVector, PlaneRotation, 25.0, 1024, 27.0); FVector SphereMean = FVector::ZeroVector; bool bSphereValid = SpherePoints.Num() == 1024; for (const FVector& Point : SpherePoints) { bSphereValid &= FMath::IsNearlyEqual(Point.Size(), 25.0, 1.e-8); SphereMean += Point; } if (!SpherePoints.IsEmpty()) { SphereMean /= SpherePoints.Num(); } TestTrue(TEXT("Sphere points are deterministic and remain on the surface"), bSphereValid && SphereMean.Size() < 0.01 && PointsEqual(SpherePoints, RepeatedSpherePoints)); const FVector SphereCenter(-1200.0, 3400.0, -5600.0); const FRotator SphereRotation(-37.0, 123.0, 71.0); const FQuat SphereRotationQuaternion = SphereRotation.Quaternion(); const TArray LocalSpherePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere( FVector::ZeroVector, FRotator::ZeroRotator, 17.0, 257, 23.5); const TArray RotatedSpherePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere( SphereCenter, SphereRotation, 17.0, 257, 23.5); bool bSphereRotationValid = LocalSpherePoints.Num() == RotatedSpherePoints.Num(); for (int32 Index = 0; Index < LocalSpherePoints.Num() && bSphereRotationValid; ++Index) { const FVector ExpectedPoint = SphereCenter + SphereRotationQuaternion.RotateVector(LocalSpherePoints[Index]); bSphereRotationValid &= RotatedSpherePoints[Index].Equals(ExpectedPoint, 1.e-8); } TestTrue(TEXT("Sphere rotation transforms every local distribution point"), bSphereRotationValid); constexpr double SphereAngleOffset = 1153.25; const TArray UnoffsetSpherePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere( FVector::ZeroVector, FRotator::ZeroRotator, 17.0, 257, 0.0); const TArray OffsetSpherePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere( FVector::ZeroVector, FRotator::ZeroRotator, 17.0, 257, SphereAngleOffset); const FQuat SphereOffsetRotation(FVector::UpVector, FMath::DegreesToRadians(FMath::Fmod(SphereAngleOffset, 360.0))); bool bSphereAngleOffsetValid = UnoffsetSpherePoints.Num() == OffsetSpherePoints.Num(); for (int32 Index = 0; Index < UnoffsetSpherePoints.Num() && bSphereAngleOffsetValid; ++Index) { bSphereAngleOffsetValid &= OffsetSpherePoints[Index].Equals( SphereOffsetRotation.RotateVector(UnoffsetSpherePoints[Index]), 1.e-8); } TestTrue(TEXT("Sphere angle offset rotates the distribution around local Z"), bSphereAngleOffsetValid); const TArray SingleSpherePoint = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere( PlaneCenter, PlaneRotation, 25.0, 1, 27.0); TestTrue(TEXT("A single sphere point follows the rotated local Z axis"), SingleSpherePoint.Num() == 1 && SingleSpherePoint[0].Equals(PlaneCenter + PlaneNormal * 25.0, 1.e-8)); const FVector LargeTranslation(1000000.0, -2000000.0, 3000000.0); const FRotator AuditRotation(-37.0, 123.0, 71.0); TestTrue(TEXT("A translated center offsets every generated arc point exactly once"), PointsEqualAfterTranslation( UDirectiveUtilMathFunctionLibrary::GeneratePointsOnArc( FVector::ZeroVector, AuditRotation, -13.5, 11, 1080.25, -450.5, true), UDirectiveUtilMathFunctionLibrary::GeneratePointsOnArc( LargeTranslation, AuditRotation, -13.5, 11, 1080.25, -450.5, true), LargeTranslation)); TestTrue(TEXT("Translated origins preserve every spatial generator's local offsets"), PointsEqualAfterTranslation( UDirectiveUtilMathFunctionLibrary::GenerateGridPoints2D( FVector::ZeroVector, AuditRotation, FIntPoint(4, 3), FVector2D(-7.0, 11.0), true), UDirectiveUtilMathFunctionLibrary::GenerateGridPoints2D( LargeTranslation, AuditRotation, FIntPoint(4, 3), FVector2D(-7.0, 11.0), true), LargeTranslation) && PointsEqualAfterTranslation( UDirectiveUtilMathFunctionLibrary::GenerateGridPoints3D( FVector::ZeroVector, AuditRotation, FIntVector(3, 2, 2), FVector(5.0, -7.0, 0.0), false), UDirectiveUtilMathFunctionLibrary::GenerateGridPoints3D( LargeTranslation, AuditRotation, FIntVector(3, 2, 2), FVector(5.0, -7.0, 0.0), false), LargeTranslation) && PointsEqualAfterTranslation( UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongDirection( FVector::ZeroVector, FVector(-2.0, 3.0, -5.0), 7, -3.25, true), UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongDirection( LargeTranslation, FVector(-2.0, 3.0, -5.0), 7, -3.25, true), LargeTranslation) && PointsEqualAfterTranslation( UDirectiveUtilMathFunctionLibrary::GeneratePointsBetweenLocations( FVector(-11.0, 5.0, 8.0), FVector(17.0, -9.0, 3.0), 8, false), UDirectiveUtilMathFunctionLibrary::GeneratePointsBetweenLocations( LargeTranslation + FVector(-11.0, 5.0, 8.0), LargeTranslation + FVector(17.0, -9.0, 3.0), 8, false), LargeTranslation) && PointsEqualAfterTranslation( UDirectiveUtilMathFunctionLibrary::GeneratePointsOnCircle( FVector::ZeroVector, AuditRotation, -13.5, 17, -725.25), UDirectiveUtilMathFunctionLibrary::GeneratePointsOnCircle( LargeTranslation, AuditRotation, -13.5, 17, -725.25), LargeTranslation) && PointsEqualAfterTranslation( UDirectiveUtilMathFunctionLibrary::GeneratePointsOnDisc( FVector::ZeroVector, AuditRotation, -13.5, 257, 1080.25), UDirectiveUtilMathFunctionLibrary::GeneratePointsOnDisc( LargeTranslation, AuditRotation, -13.5, 257, 1080.25), LargeTranslation) && PointsEqualAfterTranslation( UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere( FVector::ZeroVector, AuditRotation, -13.5, 257, -1080.25), UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere( LargeTranslation, AuditRotation, -13.5, 257, -1080.25), LargeTranslation)); for (const EDirectiveUtilHexOrientation Orientation : { EDirectiveUtilHexOrientation::PointyTop, EDirectiveUtilHexOrientation::FlatTop }) { TestTrue(TEXT("Translated origins preserve rectangular and hexagonal grid offsets"), PointsEqualAfterTranslation( UDirectiveUtilMathFunctionLibrary::GenerateRectangularHexGrid( FVector::ZeroVector, AuditRotation, FIntPoint(4, 3), 9.5, Orientation, -1.25, true), UDirectiveUtilMathFunctionLibrary::GenerateRectangularHexGrid( LargeTranslation, AuditRotation, FIntPoint(4, 3), 9.5, Orientation, -1.25, true), LargeTranslation) && PointsEqualAfterTranslation( UDirectiveUtilMathFunctionLibrary::GenerateHexagonalHexGrid( FVector::ZeroVector, AuditRotation, 4, 9.5, Orientation, -1.25), UDirectiveUtilMathFunctionLibrary::GenerateHexagonalHexGrid( LargeTranslation, AuditRotation, 4, 9.5, Orientation, -1.25), LargeTranslation)); for (const FIntPoint Coordinate : { FIntPoint::ZeroValue, FIntPoint(-17, 29), FIntPoint(1234, -987), FIntPoint(-4096, -2048) }) { const FVector Location = UDirectiveUtilMathFunctionLibrary::HexCoordinateToLocation( Coordinate, LargeTranslation, AuditRotation, 9.5, Orientation, -1.25); TestEqual(TEXT("Odd signed hex coordinates round trip through translated rotated layouts"), UDirectiveUtilMathFunctionLibrary::LocationToHexCoordinate( Location, LargeTranslation, AuditRotation, 9.5, Orientation, -1.25), Coordinate); } } const TArray ZeroRadiusCircle = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnCircle( LargeTranslation, AuditRotation, 0.0, 9, 123.0); const TArray ZeroRadiusArc = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnArc( LargeTranslation, AuditRotation, 0.0, 9, -30.0, -720.0, false); const TArray ZeroRadiusDisc = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnDisc( LargeTranslation, AuditRotation, 0.0, 9, 123.0); const TArray ZeroRadiusSphere = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere( LargeTranslation, AuditRotation, 0.0, 9, 123.0); TestTrue(TEXT("Zero-radius generators preserve their requested count at the center"), ZeroRadiusCircle.Num() == 9 && PointsMatchLocation(ZeroRadiusCircle, LargeTranslation) && ZeroRadiusArc.Num() == 9 && PointsMatchLocation(ZeroRadiusArc, LargeTranslation) && ZeroRadiusDisc.Num() == 9 && PointsMatchLocation(ZeroRadiusDisc, LargeTranslation) && ZeroRadiusSphere.Num() == 9 && PointsMatchLocation(ZeroRadiusSphere, LargeTranslation)); TestTrue(TEXT("Degenerate linear generators preserve their requested count and location"), PointsMatchLocation(UDirectiveUtilMathFunctionLibrary::GenerateGridPoints3D( LargeTranslation, AuditRotation, FIntVector(2, 3, 4), FVector::ZeroVector, true), LargeTranslation) && PointsMatchLocation(UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongDirection( LargeTranslation, FVector(2.0, -3.0, 4.0), 7, 0.0, true), LargeTranslation) && PointsMatchLocation(UDirectiveUtilMathFunctionLibrary::GeneratePointsBetweenLocations( LargeTranslation, LargeTranslation, 7, false), LargeTranslation)); USplineComponent* TransformedSpline = NewObject(); TransformedSpline->SetWorldLocation(LargeTranslation); TransformedSpline->SetWorldRotation(AuditRotation); TransformedSpline->SetWorldScale3D(FVector(2.0, 3.0, 0.5)); TransformedSpline->SetSplinePoints({ FVector::ZeroVector, FVector(100.0, 0.0, 0.0) }, ESplineCoordinateSpace::Local, false); TransformedSpline->SetSplinePointType(0, ESplinePointType::Linear, false); TransformedSpline->SetSplinePointType(1, ESplinePointType::Linear, true); const TArray TransformedSplinePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline( TransformedSpline, 60.0); TestEqual(TEXT("Scaled spline sampling produces the expected point count"), TransformedSplinePoints.Num(), 5); if (TransformedSplinePoints.Num() == 5) { TestTrue(TEXT("Spline sampling returns its world-space start"), TransformedSplinePoints[0].Equals(LargeTranslation, 1.e-4)); TestTrue(TEXT("Spline sampling preserves regular world-space intervals"), FMath::IsNearlyEqual(FVector::Distance(TransformedSplinePoints[0], TransformedSplinePoints[1]), 60.0, 1.e-4) && FMath::IsNearlyEqual(FVector::Distance(TransformedSplinePoints[1], TransformedSplinePoints[2]), 60.0, 1.e-4) && FMath::IsNearlyEqual(FVector::Distance(TransformedSplinePoints[2], TransformedSplinePoints[3]), 60.0, 1.e-4)); TestTrue(TEXT("Spline sampling appends its exact world-space endpoint"), TransformedSplinePoints.Last().Equals(TransformedSpline->GetLocationAtDistanceAlongSpline( TransformedSpline->GetSplineLength(), ESplineCoordinateSpace::World), 1.e-4)); } const TArray LocalSplinePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline( TransformedSpline, 60.0, true, EDirectiveUtilSplineSpacingMode::Fixed, ESplineCoordinateSpace::Local); TestTrue(TEXT("Spline sampling can return local-space points"), LocalSplinePoints.Num() == TransformedSplinePoints.Num() && LocalSplinePoints[0].Equals(FVector::ZeroVector, 1.e-4) && LocalSplinePoints.Last().Equals(FVector(100.0, 0.0, 0.0), 1.e-4)); const double Infinity = std::numeric_limits::infinity(); const double NaN = std::numeric_limits::quiet_NaN(); const FRotator InvalidRotation(Infinity, 0.0, 0.0); TestTrue(TEXT("Point generators reject non-finite values"), UDirectiveUtilMathFunctionLibrary::GenerateGridPoints2D( FVector::ZeroVector, FRotator::ZeroRotator, FIntPoint(2, 2), FVector2D(Infinity, 1.0), true).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongDirection( FVector::ZeroVector, FVector::ForwardVector, 2, Infinity, false).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GeneratePointsOnCircle( FVector::ZeroVector, InvalidRotation, 1.0, 4, 0.0).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GeneratePointsOnArc( FVector::ZeroVector, FRotator::ZeroRotator, 1.0, 4, 0.0, Infinity, true).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GeneratePointsOnDisc( FVector(Infinity, 0.0, 0.0), FRotator::ZeroRotator, 1.0, 4, 0.0).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere( FVector::ZeroVector, FRotator::ZeroRotator, Infinity, 4, 0.0).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GenerateRectangularHexGrid( FVector::ZeroVector, FRotator::ZeroRotator, FIntPoint(2, 2), Infinity).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(Spline, Infinity).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline( Spline, 10.0, true, EDirectiveUtilSplineSpacingMode::Fixed, ESplineCoordinateSpace::World, Infinity, -1.0).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSplineByCount( Spline, 2, true, ESplineCoordinateSpace::World, 0.0, NaN).IsEmpty()); TestTrue(TEXT("Every spatial generator rejects a non-finite origin or center"), UDirectiveUtilMathFunctionLibrary::GenerateGridPoints3D( FVector(NaN, 0.0, 0.0), FRotator::ZeroRotator, FIntVector(1), FVector::OneVector, true).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GeneratePointsBetweenLocations( FVector::ZeroVector, FVector(Infinity, 0.0, 0.0), 2, true).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GeneratePointsOnArc( FVector(NaN, 0.0, 0.0), FRotator::ZeroRotator, 1.0, 2, 0.0, 90.0, true).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GenerateHexagonalHexGrid( FVector(Infinity, 0.0, 0.0), FRotator::ZeroRotator, 1, 1.0).IsEmpty()); TestTrue(TEXT("Spline sampling rejects negative spacing"), UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(Spline, -1.0).IsEmpty()); TestTrue(TEXT("Point generators return empty arrays for non-positive counts"), UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongDirection( FVector::ZeroVector, FVector::ForwardVector, 0, 1.0, false).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GeneratePointsBetweenLocations( FVector::ZeroVector, FVector::OneVector, -1, true).IsEmpty() && UDirectiveUtilMathFunctionLibrary::GeneratePointsOnCircle( FVector::ZeroVector, FRotator::ZeroRotator, 1.0, 0, 0.0).IsEmpty()); return !HasAnyErrors(); }