Files
ProjectEleri/Plugins/DirectiveUtilities/Source/DirectiveUtilitiesTests/Private/Tests/DirectiveUtilPointGenerationTest.cpp

838 lines
48 KiB
C++

// 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 <limits>
namespace
{
bool PointsEqual(const TArray<FVector>& A, const TArray<FVector>& 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<FVector>& BasePoints, const TArray<FVector>& 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<FVector>& 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<FVector> Grid2D = UDirectiveUtilMathFunctionLibrary::GenerateGridPoints2D(
FVector::ZeroVector, FRotator::ZeroRotator, FIntPoint(3, 2), FVector2D(10.0, 20.0), true);
const TArray<FVector> 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<FVector> 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<FVector> 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<FTransform> 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<FTransform> 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<double>::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<FIntPoint> HexNeighbors = UDirectiveUtilMathFunctionLibrary::GetHexNeighbors(FIntPoint(3, -2));
const TArray<FIntPoint> 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<FVector> 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<FVector> 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<FVector> HexagonalGrid = UDirectiveUtilMathFunctionLibrary::GenerateHexagonalHexGrid(
FVector::ZeroVector, FRotator::ZeroRotator, 2, HexRadius,
EDirectiveUtilHexOrientation::PointyTop);
TSet<FIntPoint> 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<FIntPoint> HexesInRange = UDirectiveUtilMathFunctionLibrary::GetHexesInRange(FIntPoint(2, -1), 1);
const TArray<FIntPoint> 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<FIntPoint> 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<FIntPoint> 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<FIntPoint> 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<FIntPoint> 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<FIntPoint> HexLine = UDirectiveUtilMathFunctionLibrary::GetHexLine(
FIntPoint(0, 0), FIntPoint(3, -3));
const TArray<FIntPoint> 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<FIntPoint> 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<FIntPoint> 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<FVector> 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<FVector> 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<FVector> 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<FVector> NoiseBase = {
FVector::ZeroVector, FVector(37.0, 11.0, 5.0), FVector(250.0, -90.0, 40.0)
};
const TArray<FVector> 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<FVector> DirectionPoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongDirection(
FVector::ZeroVector, FVector(10.0, 0.0, 0.0), 4, 2.0, true);
const TArray<FVector> 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<FVector> 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<FVector> 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<FVector> 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<FVector> 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<USplineComponent>();
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<FVector> 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<FVector> 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<FVector> 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<FVector> 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<FVector> 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<FVector> 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<FVector> 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<FVector> 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<USplineComponent>();
SinglePointSpline->SetSplinePoints({ FVector(3.0, 4.0, 5.0) }, ESplineCoordinateSpace::Local, true);
const TArray<FVector> 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<EDirectiveUtilSplineSpacingMode>(255)).IsEmpty()
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(
Spline, 10.0, true, EDirectiveUtilSplineSpacingMode::Fixed,
static_cast<ESplineCoordinateSpace::Type>(255)).IsEmpty()
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSpline(
Spline, 10.0, true, EDirectiveUtilSplineSpacingMode::Fixed,
ESplineCoordinateSpace::World, 80.0, 20.0).IsEmpty()
&& UDirectiveUtilMathFunctionLibrary::GeneratePointsAlongSplineByCount(Spline, 0).IsEmpty());
const TArray<FVector> CirclePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnCircle(
FVector::ZeroVector, FRotator::ZeroRotator, 10.0, 4, 0.0);
const TArray<FVector> 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<FVector> 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<FVector> 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<FVector> 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<FVector> DiscPoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnDisc(
PlaneCenter, PlaneRotation, 25.0, 1024, 27.0);
const TArray<FVector> 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<FVector> 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<FVector> SpherePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere(
FVector::ZeroVector, PlaneRotation, 25.0, 1024, 27.0);
const TArray<FVector> 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<FVector> LocalSpherePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere(
FVector::ZeroVector, FRotator::ZeroRotator, 17.0, 257, 23.5);
const TArray<FVector> 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<FVector> UnoffsetSpherePoints = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnSphere(
FVector::ZeroVector, FRotator::ZeroRotator, 17.0, 257, 0.0);
const TArray<FVector> 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<FVector> 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<FVector> ZeroRadiusCircle = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnCircle(
LargeTranslation, AuditRotation, 0.0, 9, 123.0);
const TArray<FVector> ZeroRadiusArc = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnArc(
LargeTranslation, AuditRotation, 0.0, 9, -30.0, -720.0, false);
const TArray<FVector> ZeroRadiusDisc = UDirectiveUtilMathFunctionLibrary::GeneratePointsOnDisc(
LargeTranslation, AuditRotation, 0.0, 9, 123.0);
const TArray<FVector> 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<USplineComponent>();
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<FVector> 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<FVector> 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<double>::infinity();
const double NaN = std::numeric_limits<double>::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();
}