diff --git a/include/fesa/math/vector3.h b/include/fesa/math/vector3.h new file mode 100644 index 0000000..c27fa6c --- /dev/null +++ b/include/fesa/math/vector3.h @@ -0,0 +1,89 @@ +#ifndef FESA_MATH_VECTOR3_H_ +#define FESA_MATH_VECTOR3_H_ // NOLINT(readability-identifier-naming) + +#include +#include +#include +#include + +namespace fesa { + +/// @brief Represents an owning fixed-size three-dimensional value vector. +class Vector3 { + public: + /// @brief Constructs the zero vector. + constexpr Vector3() noexcept = default; + + /// @brief Constructs a vector from three Cartesian components. + /// @param x First component in the caller-defined coordinate system. + /// @param y Second component in the caller-defined coordinate system. + /// @param z Third component in the caller-defined coordinate system. + constexpr Vector3(double x, double y, double z) noexcept + : components_{{x, y, z}} {} + + /// @brief Returns the first component. + constexpr double X() const noexcept { return components_[0]; } + + /// @brief Returns the second component. + constexpr double Y() const noexcept { return components_[1]; } + + /// @brief Returns the third component. + constexpr double Z() const noexcept { return components_[2]; } + + /// @brief Returns a component by zero-based index. + /// @pre index is less than three. + constexpr double operator[](std::size_t index) const noexcept { + return components_[index]; + } + + /// @brief Adds corresponding vector components. + constexpr Vector3 operator+(const Vector3& rhs) const noexcept { + return Vector3{X() + rhs.X(), Y() + rhs.Y(), Z() + rhs.Z()}; + } + + /// @brief Subtracts corresponding vector components. + constexpr Vector3 operator-(const Vector3& rhs) const noexcept { + return Vector3{X() - rhs.X(), Y() - rhs.Y(), Z() - rhs.Z()}; + } + + /// @brief Multiplies every component by a scalar. + constexpr Vector3 operator*(double scalar) const noexcept { + return Vector3{X() * scalar, Y() * scalar, Z() * scalar}; + } + + /// @brief Computes the Euclidean dot product with rhs. + double Dot(const Vector3& rhs) const noexcept { + return X() * rhs.X() + Y() * rhs.Y() + Z() * rhs.Z(); + } + + /// @brief Computes the right-handed cross product with rhs. + Vector3 Cross(const Vector3& rhs) const noexcept { + return Vector3{Y() * rhs.Z() - Z() * rhs.Y(), Z() * rhs.X() - X() * rhs.Z(), + X() * rhs.Y() - Y() * rhs.X()}; + } + + /// @brief Computes the Euclidean norm. + double Norm() const noexcept { return std::sqrt(Dot(*this)); } + + /// @brief Returns a unit vector when the norm is usable. + /// @return Empty when the norm is exactly zero or nonfinite. + std::optional Normalized() const noexcept { + const double norm = Norm(); // NOLINT(readability-identifier-naming) + if (norm == 0.0 || !std::isfinite(norm)) { + return std::nullopt; + } + return Vector3{X() / norm, Y() / norm, Z() / norm}; + } + + /// @brief Reports whether all components are finite. + bool IsFinite() const noexcept { + return std::isfinite(X()) && std::isfinite(Y()) && std::isfinite(Z()); + } + + private: + std::array components_{}; +}; + +} // namespace fesa + +#endif // FESA_MATH_VECTOR3_H_ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b404eda..dfe32a4 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -17,6 +17,7 @@ add_executable( unit/fem/dof_manager_test.cpp unit/math/matrix_test.cpp unit/math/sparse_matrix_test.cpp + unit/math/vector3_test.cpp unit/math/vector_test.cpp unit/io/abaqus/domain_mapper_test.cpp unit/io/abaqus/input_reader_test.cpp diff --git a/tests/unit/math/vector3_test.cpp b/tests/unit/math/vector3_test.cpp new file mode 100644 index 0000000..17593ed --- /dev/null +++ b/tests/unit/math/vector3_test.cpp @@ -0,0 +1,94 @@ +#include "fesa/math/vector3.h" + +#include + +#include +#include + +namespace fesa { +namespace { + +TEST(Vector3, StoresComponentsWithExactArithmetic) { + constexpr Vector3 zero; + static_assert(zero.X() == 0.0); + static_assert(zero.Y() == 0.0); + static_assert(zero.Z() == 0.0); + + constexpr Vector3 lhs{1.0, -2.0, 3.0}; + constexpr Vector3 rhs{-4.0, 5.0, 6.0}; + constexpr Vector3 sum = lhs + rhs; + constexpr Vector3 difference = lhs - rhs; + constexpr Vector3 scaled = lhs * -2.0; + + EXPECT_DOUBLE_EQ(lhs.X(), 1.0); + EXPECT_DOUBLE_EQ(lhs.Y(), -2.0); + EXPECT_DOUBLE_EQ(lhs.Z(), 3.0); + EXPECT_DOUBLE_EQ(lhs[0], 1.0); + EXPECT_DOUBLE_EQ(lhs[1], -2.0); + EXPECT_DOUBLE_EQ(lhs[2], 3.0); + EXPECT_DOUBLE_EQ(sum[0], -3.0); + EXPECT_DOUBLE_EQ(sum[1], 3.0); + EXPECT_DOUBLE_EQ(sum[2], 9.0); + EXPECT_DOUBLE_EQ(difference[0], 5.0); + EXPECT_DOUBLE_EQ(difference[1], -7.0); + EXPECT_DOUBLE_EQ(difference[2], -3.0); + EXPECT_DOUBLE_EQ(scaled[0], -2.0); + EXPECT_DOUBLE_EQ(scaled[1], 4.0); + EXPECT_DOUBLE_EQ(scaled[2], -6.0); +} + +TEST(Vector3, ComputesDotProduct) { + const Vector3 lhs{1.0, 2.0, 3.0}; + const Vector3 rhs{4.0, -5.0, 6.0}; + + EXPECT_DOUBLE_EQ(lhs.Dot(rhs), 12.0); +} + +TEST(Vector3, UsesRightHandedCrossProductOrientation) { + const Vector3 x_axis{1.0, 0.0, 0.0}; + const Vector3 y_axis{0.0, 1.0, 0.0}; + + const Vector3 positive_z = x_axis.Cross(y_axis); + const Vector3 negative_z = y_axis.Cross(x_axis); + + EXPECT_DOUBLE_EQ(positive_z.X(), 0.0); + EXPECT_DOUBLE_EQ(positive_z.Y(), 0.0); + EXPECT_DOUBLE_EQ(positive_z.Z(), 1.0); + EXPECT_DOUBLE_EQ(negative_z.X(), 0.0); + EXPECT_DOUBLE_EQ(negative_z.Y(), 0.0); + EXPECT_DOUBLE_EQ(negative_z.Z(), -1.0); +} + +TEST(Vector3, ComputesEuclideanNorm) { + EXPECT_DOUBLE_EQ(Vector3(2.0, -3.0, 6.0).Norm(), 7.0); +} + +TEST(Vector3, NormalizesNonzeroVectors) { + const std::optional normalized = Vector3(3.0, 0.0, 4.0).Normalized(); + + ASSERT_TRUE(normalized.has_value()); + EXPECT_DOUBLE_EQ(normalized->X(), 0.6); + EXPECT_DOUBLE_EQ(normalized->Y(), 0.0); + EXPECT_DOUBLE_EQ(normalized->Z(), 0.8); +} + +TEST(Vector3, ReportsFiniteComponents) { + const double infinity = std::numeric_limits::infinity(); + const double not_a_number = std::numeric_limits::quiet_NaN(); + + EXPECT_TRUE(Vector3(1.0, -2.0, 3.0).IsFinite()); + EXPECT_FALSE(Vector3(infinity, 0.0, 0.0).IsFinite()); + EXPECT_FALSE(Vector3(0.0, not_a_number, 0.0).IsFinite()); +} + +TEST(Vector3, RejectsZeroAndNonfiniteNormalization) { + const double infinity = std::numeric_limits::infinity(); + const double not_a_number = std::numeric_limits::quiet_NaN(); + + EXPECT_FALSE(Vector3().Normalized().has_value()); + EXPECT_FALSE(Vector3(infinity, 0.0, 0.0).Normalized().has_value()); + EXPECT_FALSE(Vector3(0.0, not_a_number, 0.0).Normalized().has_value()); +} + +} // namespace +} // namespace fesa