diff --git a/openVulkanoCpp/Math/CoordinateSystemConverter.hpp b/openVulkanoCpp/Math/CoordinateSystemConverter.hpp new file mode 100644 index 0000000..212194a --- /dev/null +++ b/openVulkanoCpp/Math/CoordinateSystemConverter.hpp @@ -0,0 +1,104 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#pragma once + +#include "Math.hpp" +#include "CoordinateSystem.hpp" + +namespace OpenVulkano::Math +{ + class CoordinateSystemConverter + { + inline static Math::Matrix4i ROT_XP90 = { 1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1 }; + inline static Math::Matrix4i ROT_XN90 = { 1, 0, 0, 0, 0, 0, -1, 0, 0, 1, 0, 0, 0, 0, 0, 1 }; + CoordinateSystem from, to; + + public: + constexpr CoordinateSystemConverter(CoordinateSystem from, CoordinateSystem to) : from(from), to(to) {} + + template + [[nodiscard]] Vector<3, T, Q> Convert(const Vector<3, T, Q>& v) const + { + if (from == to) return v; + return ConvertHandedness(ConvertUp(v)); + } + + template + [[nodiscard]] Vector<4, T, Q> Convert(const Vector<4, T, Q>& v) const + { + if (from == to) return v; + return { ConvertHandedness(ConvertUp(reinterpret_cast&>(v))), v.w }; + } + + template + [[nodiscard]] Matrix<3, T, Q> Convert(const Matrix<3, T, Q>& mat) const + { + if (from == to) return mat; + return ConvertHandedness(ConvertUp(mat)); + } + + template + [[nodiscard]] Matrix<4, T, Q> Convert(const Matrix<4, T, Q>& mat) const + { + if (from == to) return mat; + return ConvertHandedness(ConvertUp(mat)); + } + + private: + // Conversion functions + template + [[nodiscard]] Vector<3, T, Q> ConvertHandedness(const Vector<3, T, Q>& v) const + { + if (from.GetHandedness() == to.GetHandedness()) return v; + if (to == CoordinateSystem::UpAxis::Y) + return { v.x, v.y, -v.z }; + return { v.x, -v.y, v.z }; + } + + template + [[nodiscard]] Vector<3, T, Q> ConvertUp(const Vector<3, T, Q>& v) const + { + if (from.GetUpAxis() == to.GetUpAxis()) return v; + if (from.GetUpAxis() == CoordinateSystem::UpAxis::Y) + return { v.x, -v.z, v.y }; + return { v.x, v.z, -v.y }; + } + + template + [[nodiscard]] Matrix ConvertHandedness(Matrix mat) const + { + constexpr int AXIS_Y = 1; + constexpr int AXIS_Z = 2; + if (from.GetHandedness() == to.GetHandedness()) return mat; + if (to == CoordinateSystem::UpAxis::Y) + for(int i = 0; i < S; i++) mat[i][AXIS_Z] = -mat[i][AXIS_Z]; + else + for(int i = 0; i < S; i++) mat[i][AXIS_Y] = -mat[i][AXIS_Y]; + return mat; + } + + template + [[nodiscard]] Matrix ConvertUp(const Matrix& mat) const + { + if (from.GetUpAxis() == to.GetUpAxis()) return mat; + Matrix4i* conv; + Matrix4i* inverse; + if (from.GetUpAxis() == CoordinateSystem::UpAxis::Y && to.GetUpAxis() == CoordinateSystem::UpAxis::Z) + { + conv = &ROT_XP90; + inverse = &ROT_XN90; + } + else if (from.GetUpAxis() == CoordinateSystem::UpAxis::Z && to.GetUpAxis() == CoordinateSystem::UpAxis::Y) + { + conv = &ROT_XN90; + inverse = &ROT_XP90; + } + + return Matrix(*conv) * mat * Matrix(*inverse); + } + }; +} \ No newline at end of file diff --git a/tests/Math/CoordinateSystemConverterTest.cpp b/tests/Math/CoordinateSystemConverterTest.cpp new file mode 100644 index 0000000..128a450 --- /dev/null +++ b/tests/Math/CoordinateSystemConverterTest.cpp @@ -0,0 +1,885 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#include "Math/CoordinateSystemConverter.hpp" +#include + +using namespace OpenVulkano::Math; +using Catch::Approx; + +#define REQUIRE_VEC_EQ(vec1, vec2, eps) { \ + REQUIRE(vec1.length() == vec2.length()); \ + for (size_t i = 0; i < vec1.length(); i++) \ + REQUIRE(Approx(vec1[i]).epsilon(eps) == vec2[i]); \ +} + +#define REQUIRE_VEC_CLOSE(v1, v2) REQUIRE_VEC_EQ(v1, v2, 1e-5f) + +#define REQUIRE_MAT_EQ(mat1, mat2, eps) { \ + REQUIRE(mat1.length() == mat2.length()); \ + for (size_t c = 0; c < mat1.length(); c++) \ + REQUIRE_VEC_EQ(mat1[c], mat2[c], eps) \ +} + +#define REQUIRE_MAT_CLOSE(mat1, mat2) REQUIRE_MAT_EQ(mat1, mat2, 1e-5f) + +TEST_CASE("CoordinateSystemConverter - Same system (no conversion)", "[CoordinateSystemConverter]") +{ + CoordinateSystem sys(CoordinateSystem::RIGHT_HANDED_Y_UP); + CoordinateSystemConverter converter(sys, sys); + + SECTION("Vec3 unchanged") + { + glm::vec3 v(1.0f, 2.0f, 3.0f); + auto result = converter.Convert(v); + REQUIRE(result.x == Approx(1.0f)); + REQUIRE(result.y == Approx(2.0f)); + REQUIRE(result.z == Approx(3.0f)); + } + + SECTION("Vec4 unchanged") + { + glm::vec4 v(1.0f, 2.0f, 3.0f, 4.0f); + auto result = converter.Convert(v); + REQUIRE(result.x == Approx(1.0f)); + REQUIRE(result.y == Approx(2.0f)); + REQUIRE(result.z == Approx(3.0f)); + REQUIRE(result.w == Approx(4.0f)); + } +} + +TEST_CASE("CoordinateSystemConverter - Handedness conversion", "[CoordinateSystemConverter]") +{ + SECTION("Right-handed Y-up to Left-handed Y-up") + { + CoordinateSystemConverter converter( + CoordinateSystem::RIGHT_HANDED_Y_UP, + CoordinateSystem::LEFT_HANDED_Y_UP + ); + + glm::vec3 v(1.0f, 2.0f, 3.0f); + auto result = converter.Convert(v); + REQUIRE(result.x == Approx(1.0f)); + REQUIRE(result.y == Approx(2.0f)); + REQUIRE(result.z == Approx(-3.0f)); + } + + SECTION("Right-handed Z-up to Left-handed Z-up") + { + CoordinateSystemConverter converter( + CoordinateSystem::RIGHT_HANDED_Z_UP, + CoordinateSystem::LEFT_HANDED_Z_UP + ); + + glm::vec3 v(5.0f, 6.0f, 7.0f); + auto result = converter.Convert(v); + REQUIRE(result.x == Approx(5.0f)); + REQUIRE(result.y == Approx(-6.0f)); + REQUIRE(result.z == Approx(7.0f)); + } +} + +TEST_CASE("CoordinateSystemConverter - Up axis conversion", "[CoordinateSystemConverter]") +{ + SECTION("Y-up to Z-up (right-handed)") + { + CoordinateSystemConverter converter( + CoordinateSystem::RIGHT_HANDED_Y_UP, + CoordinateSystem::RIGHT_HANDED_Z_UP + ); + + glm::vec3 v(1.0f, 2.0f, 3.0f); + auto result = converter.Convert(v); + REQUIRE(result.x == Approx(1.0f)); + REQUIRE(result.y == Approx(-3.0f)); + REQUIRE(result.z == Approx(2.0f)); + } + + SECTION("Z-up to Y-up (right-handed)") + { + CoordinateSystemConverter converter( + CoordinateSystem::RIGHT_HANDED_Z_UP, + CoordinateSystem::RIGHT_HANDED_Y_UP + ); + + glm::vec3 v(1.0f, 2.0f, 3.0f); + auto result = converter.Convert(v); + REQUIRE(result.x == Approx(1.0f)); + REQUIRE(result.y == Approx(3.0f)); + REQUIRE(result.z == Approx(-2.0f)); + } +} + +TEST_CASE("CoordinateSystemConverter - Combined conversion", "[CoordinateSystemConverter]") +{ + SECTION("Right-handed Y-up to Left-handed Z-up") + { + CoordinateSystemConverter converter( + CoordinateSystem::RIGHT_HANDED_Y_UP, + CoordinateSystem::LEFT_HANDED_Z_UP + ); + + glm::vec3 v(1.0f, 2.0f, 3.0f); + auto result = converter.Convert(v); + // First convert up axis: Y->Z transforms (x,y,z) to (x,z,-y) = (1,3,-2) + // Then convert handedness: negate z = (1,3,2) + REQUIRE(result.x == Approx(1.0f)); + REQUIRE(result.y == Approx(3.0f)); + REQUIRE(result.z == Approx(2.0f)); + } + + SECTION("Left-handed Z-up to Right-handed Y-up") + { + CoordinateSystemConverter converter( + CoordinateSystem::LEFT_HANDED_Z_UP, + CoordinateSystem::RIGHT_HANDED_Y_UP + ); + + glm::vec3 v(4.0f, 5.0f, 6.0f); + auto result = converter.Convert(v); + // First convert up axis: Z->Y transforms (x,y,z) to (x,-z,y) = (4,-6,5) + // Then convert handedness: negate z = (4,-6,-5) + REQUIRE(result.x == Approx(4.0f)); + REQUIRE(result.y == Approx(6.0f)); + REQUIRE(result.z == Approx(5.0f)); + } +} + +TEST_CASE("CoordinateSystemConverter - Vec4 conversion", "[CoordinateSystemConverter]") +{ + SECTION("Vec4 preserves w component") + { + CoordinateSystemConverter converter( + CoordinateSystem::RIGHT_HANDED_Y_UP, + CoordinateSystem::LEFT_HANDED_Z_UP + ); + + glm::vec4 v(1.0f, 2.0f, 3.0f, 9.5f); + auto result = converter.Convert(v); + REQUIRE(result.x == Approx(1.0f)); + REQUIRE(result.y == Approx(3.0f)); + REQUIRE(result.z == Approx(2.0f)); + REQUIRE(result.w == Approx(9.5f)); + } +} + +TEST_CASE("CoordinateSystemConverter - Edge cases", "[CoordinateSystemConverter]") +{ + SECTION("Zero vector") + { + CoordinateSystemConverter converter( + CoordinateSystem::RIGHT_HANDED_Y_UP, + CoordinateSystem::LEFT_HANDED_Z_UP + ); + + glm::vec3 v(0.0f, 0.0f, 0.0f); + auto result = converter.Convert(v); + REQUIRE(result.x == Approx(0.0f)); + REQUIRE(result.y == Approx(0.0f)); + REQUIRE(result.z == Approx(0.0f)); + } + + SECTION("Negative values") + { + CoordinateSystemConverter converter( + CoordinateSystem::RIGHT_HANDED_Y_UP, + CoordinateSystem::LEFT_HANDED_Y_UP + ); + + glm::vec3 v(-1.0f, -2.0f, -3.0f); + auto result = converter.Convert(v); + REQUIRE(result.x == Approx(-1.0f)); + REQUIRE(result.y == Approx(-2.0f)); + REQUIRE(result.z == Approx(3.0f)); + } +} + +TEST_CASE("CoordinateSystemConverter - Identity Conversion", "[CoordinateSystemConverter]") +{ + SECTION("Same coordinate system should return identity for Vec3") + { + auto systems = GENERATE( + CoordinateSystem::RIGHT_HANDED_Y_UP, + CoordinateSystem::RIGHT_HANDED_Z_UP, + CoordinateSystem::LEFT_HANDED_Y_UP, + CoordinateSystem::LEFT_HANDED_Z_UP + ); + + CoordinateSystemConverter converter(systems, systems); + Vector<3, float, glm::defaultp> v(1.0f, 2.0f, 3.0f); + auto result = converter.Convert(v); + + REQUIRE_VEC_CLOSE(result, v); + } + + SECTION("Same coordinate system should return identity for Vec4") + { + auto systems = GENERATE( + CoordinateSystem::RIGHT_HANDED_Y_UP, + CoordinateSystem::RIGHT_HANDED_Z_UP, + CoordinateSystem::LEFT_HANDED_Y_UP, + CoordinateSystem::LEFT_HANDED_Z_UP + ); + + CoordinateSystemConverter converter(systems, systems); + Vector<4, float, glm::defaultp> v(1.0f, 2.0f, 3.0f, 1.0f); + auto result = converter.Convert(v); + + REQUIRE_VEC_CLOSE(result, v); + } +} + +TEST_CASE("CoordinateSystemConverter - Vec3 Y-up to Z-up Conversions", "[CoordinateSystemConverter]") +{ + SECTION("Right-handed Y-up to Right-handed Z-up - Unit vectors") + { + CoordinateSystemConverter converter( + CoordinateSystem::RIGHT_HANDED_Y_UP, + CoordinateSystem::RIGHT_HANDED_Z_UP + ); + + // RH Y-up: X=right, Y=up, Z=backward (toward camera) + // RH Z-up: X=right, Y=forward (away from camera), Z=up + // Expected transformation: X->X, Y->Z, Z->-Y + + Vector<3, float, glm::defaultp> xAxis(1.0f, 0.0f, 0.0f); + auto resultX = converter.Convert(xAxis); + REQUIRE(resultX.x == Approx(1.0f)); + REQUIRE(resultX.y == Approx(0.0f)); + REQUIRE(resultX.z == Approx(0.0f)); + + Vector<3, float, glm::defaultp> yAxis(0.0f, 1.0f, 0.0f); + auto resultY = converter.Convert(yAxis); + REQUIRE(resultY.x == Approx(0.0f)); + REQUIRE(resultY.z == Approx(1.0f)); + + Vector<3, float, glm::defaultp> zAxis(0.0f, 0.0f, 1.0f); + auto resultZ = converter.Convert(zAxis); + REQUIRE(resultZ.y == Approx(-1.0f).margin(0.0001)); + } + + SECTION("Right-handed Z-up to Right-handed Y-up - Unit vectors") + { + CoordinateSystemConverter converter( + CoordinateSystem::RIGHT_HANDED_Z_UP, + CoordinateSystem::RIGHT_HANDED_Y_UP + ); + + // RH Z-up: X=right, Y=forward, Z=up + // RH Y-up: X=right, Y=up, Z=backward + // Expected transformation: X->X, Y->-Z, Z->Y + + Vector<3, float, glm::defaultp> xAxis(1.0f, 0.0f, 0.0f); + auto resultX = converter.Convert(xAxis); + REQUIRE(resultX.x == Approx(1.0f)); + REQUIRE(resultX.y == Approx(0.0f)); + REQUIRE(resultX.z == Approx(0.0f)); + + Vector<3, float, glm::defaultp> yAxis(0.0f, 1.0f, 0.0f); + auto resultY = converter.Convert(yAxis); + REQUIRE(abs(resultY.z) == Approx(1.0f).margin(0.0001)); + + Vector<3, float, glm::defaultp> zAxis(0.0f, 0.0f, 1.0f); + auto resultZ = converter.Convert(zAxis); + REQUIRE(resultZ.y == Approx(1.0f)); + } + + SECTION("Left-handed Y-up to Left-handed Z-up - Unit vectors") + { + CoordinateSystemConverter converter( + CoordinateSystem::LEFT_HANDED_Y_UP, + CoordinateSystem::LEFT_HANDED_Z_UP + ); + + // LH Y-up: X=right, Y=up, Z=forward (away from camera) + // LH Z-up: X=right, Y=backward, Z=up (or similar) + + Vector<3, float, glm::defaultp> xAxis(1.0f, 0.0f, 0.0f); + auto resultX = converter.Convert(xAxis); + REQUIRE(resultX.x == Approx(1.0f)); + + Vector<3, float, glm::defaultp> yAxis(0.0f, 1.0f, 0.0f); + auto resultY = converter.Convert(yAxis); + REQUIRE(abs(resultY.z) == Approx(1.0f).margin(0.0001)); + } +} + +TEST_CASE("CoordinateSystemConverter - Handedness Changes", "[CoordinateSystemConverter]") +{ + SECTION("Right-handed Y-up to Left-handed Y-up") + { + CoordinateSystemConverter converter( + CoordinateSystem::RIGHT_HANDED_Y_UP, + CoordinateSystem::LEFT_HANDED_Y_UP + ); + + // Handedness change typically negates one axis (usually Z) + // RH Y-up: Z points backward, LH Y-up: Z points forward + + Vector<3, float, glm::defaultp> v(1.0f, 2.0f, 3.0f); + auto result = converter.Convert(v); + + // X and Y should remain the same or similar + REQUIRE(result.x == Approx(1.0f)); + REQUIRE(result.y == Approx(2.0f)); + // Z should be negated + REQUIRE(result.z == Approx(-3.0f)); + } + + SECTION("Right-handed Z-up to Left-handed Z-up") + { + CoordinateSystemConverter converter( + CoordinateSystem::RIGHT_HANDED_Z_UP, + CoordinateSystem::LEFT_HANDED_Z_UP + ); + + Vector<3, float, glm::defaultp> v(1.0f, 2.0f, 3.0f); + auto result = converter.Convert(v); + + // One axis should be negated for handedness change + REQUIRE(result.x == Approx(1.0f)); + REQUIRE(result.y == Approx(-2.0f)); + REQUIRE(result.z == Approx(3.0f)); + } + + SECTION("Left-handed Y-up to Right-handed Y-up") + { + CoordinateSystemConverter converter( + CoordinateSystem::LEFT_HANDED_Y_UP, + CoordinateSystem::RIGHT_HANDED_Y_UP + ); + + Vector<3, float, glm::defaultp> v(1.0f, 2.0f, 3.0f); + auto result = converter.Convert(v); + + REQUIRE(result.x == Approx(1.0f)); + REQUIRE(result.y == Approx(2.0f)); + REQUIRE(result.z == Approx(-3.0f)); + } +} + +TEST_CASE("CoordinateSystemConverter - Vec3 Length Preservation", "[CoordinateSystemConverter]") +{ + SECTION("Vector length should be preserved across all conversions") + { + auto fromSys = GENERATE( + CoordinateSystem::RIGHT_HANDED_Y_UP, + CoordinateSystem::RIGHT_HANDED_Z_UP, + CoordinateSystem::LEFT_HANDED_Y_UP, + CoordinateSystem::LEFT_HANDED_Z_UP + ); + + auto toSys = GENERATE( + CoordinateSystem::RIGHT_HANDED_Y_UP, + CoordinateSystem::RIGHT_HANDED_Z_UP, + CoordinateSystem::LEFT_HANDED_Y_UP, + CoordinateSystem::LEFT_HANDED_Z_UP + ); + + CoordinateSystemConverter converter(fromSys, toSys); + Vector<3, float, glm::defaultp> v(3.0f, 4.0f, 5.0f); + + float originalLength = glm::length(v); + auto result = converter.Convert(v); + float resultLength = glm::length(result); + + REQUIRE(resultLength == Approx(originalLength)); + } +} + +TEST_CASE("CoordinateSystemConverter - Vec4 Conversions", "[CoordinateSystemConverter]") +{ + SECTION("Vec4 w-component should be preserved") + { + CoordinateSystemConverter converter( + CoordinateSystem::RIGHT_HANDED_Y_UP, + CoordinateSystem::RIGHT_HANDED_Z_UP + ); + + Vector<4, float, glm::defaultp> point(1.0f, 2.0f, 3.0f, 1.0f); + auto result = converter.Convert(point); + + REQUIRE(result.w == Approx(1.0f)); + } + + SECTION("Vec4 direction vector (w=0)") + { + CoordinateSystemConverter converter( + CoordinateSystem::RIGHT_HANDED_Y_UP, + CoordinateSystem::LEFT_HANDED_Y_UP + ); + + Vector<4, float, glm::defaultp> direction(1.0f, 2.0f, 3.0f, 0.0f); + auto result = converter.Convert(direction); + + REQUIRE(result.w == Approx(0.0f)); + REQUIRE(result.x == Approx(1.0f)); + REQUIRE(result.y == Approx(2.0f)); + REQUIRE(result.z == Approx(-3.0f)); + } +} + +TEST_CASE("CoordinateSystemConverter - Matrix3 Conversions", "[CoordinateSystemConverter]") +{ + SECTION("Identity matrix through different coordinate systems") + { + CoordinateSystemConverter converter( + CoordinateSystem::RIGHT_HANDED_Y_UP, + CoordinateSystem::RIGHT_HANDED_Z_UP + ); + + Matrix<3, float, glm::defaultp> identity(1.0f); + auto result = converter.Convert(identity); + + // Each column represents a basis vector transformation + // Column 0 is X axis, Column 1 is Y axis, Column 2 is Z axis + REQUIRE(result[0][0] == Approx(1.0f)); // X.x + REQUIRE(result[1][0] == Approx(0.0f)); // X.y + REQUIRE(result[2][0] == Approx(0.0f)); // X.z + } + + SECTION("Matrix3 basis vector conversion") + { + CoordinateSystemConverter converter( + CoordinateSystem::RIGHT_HANDED_Y_UP, + CoordinateSystem::LEFT_HANDED_Y_UP + ); + + Matrix<3, float, glm::defaultp> mat(1.0f); + auto result = converter.Convert(mat); + + // When converting handedness, determinant sign should flip + float detOriginal = glm::determinant(mat); + float detResult = glm::determinant(result); + + REQUIRE(detOriginal * detResult == Approx(-1.0f)); + } + + SECTION("Matrix3 with rotation around Y axis") + { + CoordinateSystemConverter converter( + CoordinateSystem::RIGHT_HANDED_Y_UP, + CoordinateSystem::RIGHT_HANDED_Z_UP + ); + + // Simple 90-degree rotation around Y axis in RH Y-up + Matrix<3, float, glm::defaultp> rotY( + 0.0f, 0.0f, 1.0f, + 0.0f, 1.0f, 0.0f, + -1.0f, 0.0f, 0.0f + ); + + auto result = converter.Convert(rotY); + + // The transformation should maintain the rotation semantics + INFO("Rotation matrix converted between coordinate systems"); + } +} + +TEST_CASE("CoordinateSystemConverter - Matrix4 Conversions", "[CoordinateSystemConverter]") +{ + SECTION("Matrix4 translation component conversion") + { + CoordinateSystemConverter converter( + CoordinateSystem::RIGHT_HANDED_Y_UP, + CoordinateSystem::RIGHT_HANDED_Z_UP + ); + + Matrix<4, float, glm::defaultp> transform(1.0f); + transform[3][0] = 10.0f; + transform[3][1] = 20.0f; + transform[3][2] = 30.0f; + + auto result = converter.Convert(transform); + + // Translation should convert like a position vector + Vector<3, float, glm::defaultp> translation(10.0f, 20.0f, 30.0f); + Vector<3, float, glm::defaultp> expectedTranslation = converter.Convert(translation); + + REQUIRE(result[3][0] == Approx(expectedTranslation.x)); + REQUIRE(result[3][1] == Approx(expectedTranslation.y)); + REQUIRE(result[3][2] == Approx(expectedTranslation.z)); + } + + SECTION("Matrix4 homogeneous row preservation") + { + CoordinateSystemConverter converter( + CoordinateSystem::LEFT_HANDED_Y_UP, + CoordinateSystem::RIGHT_HANDED_Z_UP + ); + + Matrix<4, float, glm::defaultp> mat(1.0f); + auto result = converter.Convert(mat); + + // Bottom row should remain [0,0,0,1] + REQUIRE(result[0][3] == Approx(0.0f)); + REQUIRE(result[1][3] == Approx(0.0f)); + REQUIRE(result[2][3] == Approx(0.0f)); + REQUIRE(result[3][3] == Approx(1.0f)); + } +} + +TEST_CASE("CoordinateSystemConverter - Round-trip Conversions", "[CoordinateSystemConverter]") +{ + SECTION("Vec3 round-trip: RH Y-up -> RH Z-up -> RH Y-up") + { + CoordinateSystemConverter toZUp( + CoordinateSystem::RIGHT_HANDED_Y_UP, + CoordinateSystem::RIGHT_HANDED_Z_UP + ); + CoordinateSystemConverter toYUp( + CoordinateSystem::RIGHT_HANDED_Z_UP, + CoordinateSystem::RIGHT_HANDED_Y_UP + ); + + Vector<3, float, glm::defaultp> original(1.5f, 2.5f, 3.5f); + auto intermediate = toZUp.Convert(original); + auto result = toYUp.Convert(intermediate); + + REQUIRE_VEC_CLOSE(result, original); + } + + SECTION("Vec3 round-trip: RH Y-up -> LH Y-up -> RH Y-up") + { + CoordinateSystemConverter toLH( + CoordinateSystem::RIGHT_HANDED_Y_UP, + CoordinateSystem::LEFT_HANDED_Y_UP + ); + CoordinateSystemConverter toRH( + CoordinateSystem::LEFT_HANDED_Y_UP, + CoordinateSystem::RIGHT_HANDED_Y_UP + ); + + Vector<3, float, glm::defaultp> original(1.5f, 2.5f, 3.5f); + auto intermediate = toLH.Convert(original); + auto result = toRH.Convert(intermediate); + + REQUIRE_VEC_CLOSE(result, original); + } + + SECTION("Vec4 round-trip: LH Z-up -> RH Y-up -> LH Z-up") + { + CoordinateSystemConverter conv1( + CoordinateSystem::LEFT_HANDED_Z_UP, + CoordinateSystem::RIGHT_HANDED_Y_UP + ); + CoordinateSystemConverter conv2( + CoordinateSystem::RIGHT_HANDED_Y_UP, + CoordinateSystem::LEFT_HANDED_Z_UP + ); + + Vector<4, float, glm::defaultp> original(1.5f, 2.5f, 3.5f, 0.75f); + auto intermediate = conv1.Convert(original); + auto result = conv2.Convert(intermediate); + + REQUIRE_VEC_CLOSE(result, original); + } + + SECTION("Mat3 round-trip: RH Z-up -> LH Y-up -> RH Z-up") + { + CoordinateSystemConverter conv1( + CoordinateSystem::RIGHT_HANDED_Z_UP, + CoordinateSystem::LEFT_HANDED_Y_UP + ); + CoordinateSystemConverter conv2( + CoordinateSystem::LEFT_HANDED_Y_UP, + CoordinateSystem::RIGHT_HANDED_Z_UP + ); + + Matrix<3, float, glm::defaultp> original( + 1.0f, 2.0f, 3.0f, + 4.0f, 5.0f, 6.0f, + 7.0f, 8.0f, 9.0f + ); + + auto intermediate = conv1.Convert(original); + auto result = conv2.Convert(intermediate); + + REQUIRE_MAT_CLOSE(result, original); + } + + SECTION("Mat4 round-trip: LH Y-up -> RH Z-up -> LH Y-up") + { + CoordinateSystemConverter conv1( + CoordinateSystem::LEFT_HANDED_Y_UP, + CoordinateSystem::RIGHT_HANDED_Z_UP + ); + CoordinateSystemConverter conv2( + CoordinateSystem::RIGHT_HANDED_Z_UP, + CoordinateSystem::LEFT_HANDED_Y_UP + ); + + Matrix<4, float, glm::defaultp> original(1.0f); + original[3][0] = 5.0f; + original[3][1] = 10.0f; + original[3][2] = 15.0f; + + auto intermediate = conv1.Convert(original); + auto result = conv2.Convert(intermediate); + + REQUIRE_MAT_CLOSE(result, original); + } +} + +TEST_CASE("CoordinateSystemConverter - Chained Conversions", "[CoordinateSystemConverter]") +{ + SECTION("Three-way conversion chain should be consistent") + { + CoordinateSystemConverter conv1( + CoordinateSystem::RIGHT_HANDED_Y_UP, + CoordinateSystem::RIGHT_HANDED_Z_UP + ); + CoordinateSystemConverter conv2( + CoordinateSystem::RIGHT_HANDED_Z_UP, + CoordinateSystem::LEFT_HANDED_Z_UP + ); + CoordinateSystemConverter conv3( + CoordinateSystem::LEFT_HANDED_Z_UP, + CoordinateSystem::LEFT_HANDED_Y_UP + ); + + Vector<3, float, glm::defaultp> v(2.0f, 3.0f, 4.0f); + + auto step1 = conv1.Convert(v); + auto step2 = conv2.Convert(step1); + auto step3 = conv3.Convert(step2); + + // Direct conversion for comparison + CoordinateSystemConverter direct( + CoordinateSystem::RIGHT_HANDED_Y_UP, + CoordinateSystem::LEFT_HANDED_Y_UP + ); + auto directResult = direct.Convert(v); + + // Multi-step and direct conversion should yield same result + REQUIRE_VEC_CLOSE(step3, directResult); + } +} + +TEST_CASE("CoordinateSystemConverter - Edge Cases", "[CoordinateSystemConverter]") +{ + SECTION("Zero vector conversion") + { + CoordinateSystemConverter converter( + CoordinateSystem::RIGHT_HANDED_Y_UP, + CoordinateSystem::LEFT_HANDED_Z_UP + ); + + Vector<3, float, glm::defaultp> zero(0.0f, 0.0f, 0.0f); + auto result = converter.Convert(zero); + + REQUIRE_VEC_CLOSE(result, zero); + } + + SECTION("Negative components") + { + CoordinateSystemConverter converter( + CoordinateSystem::RIGHT_HANDED_Y_UP, + CoordinateSystem::LEFT_HANDED_Y_UP + ); + + Vector<3, float, glm::defaultp> negative(-1.0f, -2.0f, -3.0f); + auto result = converter.Convert(negative); + + REQUIRE(result.x == Approx(-1.0f)); + REQUIRE(result.y == Approx(-2.0f)); + REQUIRE(result.z == Approx(3.0f)); // Z negated for handedness change + } + + SECTION("Very small values") + { + CoordinateSystemConverter converter( + CoordinateSystem::RIGHT_HANDED_Z_UP, + CoordinateSystem::LEFT_HANDED_Y_UP + ); + + Vector<3, float, glm::defaultp> small(0.0001f, 0.0002f, 0.0003f); + auto result = converter.Convert(small); + + float originalLength = glm::length(small); + float resultLength = glm::length(result); + REQUIRE(resultLength == Approx(originalLength)); + } + + SECTION("Double precision support") + { + CoordinateSystemConverter converter( + CoordinateSystem::RIGHT_HANDED_Y_UP, + CoordinateSystem::RIGHT_HANDED_Z_UP + ); + + Vector<3, double, glm::defaultp> v(1.0, 2.0, 3.0); + auto result = converter.Convert(v); + + double originalLength = glm::length(v); + double resultLength = glm::length(result); + REQUIRE(resultLength == Approx(originalLength)); + } +} + +TEST_CASE("CoordinateSystemConverter - Matrix Column Interpretation", "[CoordinateSystemConverter]") +{ + SECTION("Matrix columns should convert as vectors") + { + CoordinateSystemConverter converter( + CoordinateSystem::RIGHT_HANDED_Y_UP, + CoordinateSystem::RIGHT_HANDED_Z_UP + ); + + Matrix<3, float, glm::defaultp> mat( + 1.0f, 0.0f, 0.0f, + 0.0f, 2.0f, 0.0f, + 0.0f, 0.0f, 3.0f + ); + + auto result = converter.Convert(mat); + + // Each column should convert like a vector + Vector<3, float, glm::defaultp> col0(mat[0][0], mat[0][1], mat[0][2]); + Vector<3, float, glm::defaultp> expectedCol0 = converter.Convert(col0); + + REQUIRE(result[0][0] == Approx(expectedCol0.x)); + REQUIRE(result[0][1] == Approx(expectedCol0.y)); + REQUIRE(result[0][2] == Approx(expectedCol0.z)); + } +} + +TEST_CASE("CoordinateSystemConverter - Full 8-system conversions", "[CoordinateSystemConverter][Exhaustive]") +{ + auto fromSys = GENERATE( + CoordinateSystem::RIGHT_HANDED_Y_UP, + CoordinateSystem::RIGHT_HANDED_Z_UP, + CoordinateSystem::LEFT_HANDED_Y_UP, + CoordinateSystem::LEFT_HANDED_Z_UP + ); + + auto toSys = GENERATE( + CoordinateSystem::RIGHT_HANDED_Y_UP, + CoordinateSystem::RIGHT_HANDED_Z_UP, + CoordinateSystem::LEFT_HANDED_Y_UP, + CoordinateSystem::LEFT_HANDED_Z_UP + ); + + CoordinateSystemConverter converter(fromSys, toSys); + + // Random non-zero vector + Vector<3, float, glm::defaultp> v(1.23f, -4.56f, 7.89f); + auto result = converter.Convert(v); + + // Length should be preserved + REQUIRE(glm::length(result) == Approx(glm::length(v))); + + // Check components are finite + REQUIRE(std::isfinite(result.x)); + REQUIRE(std::isfinite(result.y)); + REQUIRE(std::isfinite(result.z)); +} + +TEST_CASE("CoordinateSystemConverter - Vec4 arbitrary w-component", "[CoordinateSystemConverter][Vec4]") +{ + CoordinateSystemConverter converter( + CoordinateSystem::RIGHT_HANDED_Y_UP, + CoordinateSystem::LEFT_HANDED_Z_UP + ); + + Vector<4, float, glm::defaultp> v(2.0f, -3.0f, 4.0f, 0.5f); + auto result = converter.Convert(v); + + // w should remain unchanged + REQUIRE(result.w == Approx(0.5f)); + + // Check vector part length preservation + REQUIRE(glm::length(glm::vec3(result.x, result.y, result.z)) == Approx(glm::length(glm::vec3(2.0f, -3.0f, 4.0f)))); +} + +TEST_CASE("CoordinateSystemConverter - Very large / small / NaN / Inf values", "[CoordinateSystemConverter][Edge]") +{ + CoordinateSystemConverter converter( + CoordinateSystem::RIGHT_HANDED_Y_UP, + CoordinateSystem::LEFT_HANDED_Y_UP + ); + + Vector<3, float, glm::defaultp> largeVec(1e30f, -1e30f, 1e30f); + Vector<3, float, glm::defaultp> smallVec(1e-30f, -1e-30f, 1e-30f); + Vector<3, float, glm::defaultp> nanVec(NAN, 0.0f, 0.0f); + Vector<3, float, glm::defaultp> infVec(INFINITY, -INFINITY, 1.0f); + + auto rLarge = converter.Convert(largeVec); + auto rSmall = converter.Convert(smallVec); + auto rNan = converter.Convert(nanVec); + auto rInf = converter.Convert(infVec); + + // Length preservation for large/small + REQUIRE(glm::length(rLarge) == Approx(glm::length(largeVec))); + REQUIRE(glm::length(rSmall) == Approx(glm::length(smallVec))); + + // NaN and Inf should propagate + REQUIRE(std::isnan(rNan.x)); + REQUIRE(std::isinf(rInf.x)); +} + +TEST_CASE("CoordinateSystemConverter - Mat4 rotation and scale", "[CoordinateSystemConverter][Mat4]") +{ + CoordinateSystemConverter converter( + CoordinateSystem::RIGHT_HANDED_Y_UP, + CoordinateSystem::LEFT_HANDED_Z_UP + ); + + // Non-uniform scale + rotation + glm::mat4 mat(1.0f); + mat[0][0] = 2.0f; mat[1][1] = 3.0f; mat[2][2] = 4.0f; // scaling + mat = glm::rotate(mat, glm::radians(90.0f), glm::vec3(0,1,0)); // rotation around Y axis + + auto result = converter.Convert(mat); + + // Check that determinant magnitude is preserved (handedness may flip sign) + float detOrig = glm::determinant(mat); + float detResult = glm::determinant(result); + REQUIRE(std::abs(detResult) == Approx(std::abs(detOrig))); + + // Check translation row unchanged if identity + REQUIRE(result[3][3] == Approx(1.0f)); +} + +TEST_CASE("CoordinateSystemConverter - Matrix4 round-trip consistency", "[CoordinateSystemConverter][Mat4]") +{ + CoordinateSystemConverter conv1( + CoordinateSystem::RIGHT_HANDED_Y_UP, + CoordinateSystem::LEFT_HANDED_Z_UP + ); + CoordinateSystemConverter conv2( + CoordinateSystem::LEFT_HANDED_Z_UP, + CoordinateSystem::RIGHT_HANDED_Y_UP + ); + + glm::mat4 original(1.0f); + original[3] = glm::vec4(5.0f, -10.0f, 15.0f, 1.0f); + original[0][0] = 2.0f; original[1][1] = 3.0f; original[2][2] = 4.0f; // scale + + auto intermediate = conv1.Convert(original); + auto roundTrip = conv2.Convert(intermediate); + + // All elements should match original + for (int c = 0; c < 4; ++c) + for (int r = 0; r < 4; ++r) + REQUIRE(roundTrip[c][r] == Approx(original[c][r])); +} + +TEST_CASE("CoordinateSystemConverter - Vec3 all-zero / negative / mixed", "[CoordinateSystemConverter][Edge]") +{ + CoordinateSystemConverter converter( + CoordinateSystem::LEFT_HANDED_Y_UP, + CoordinateSystem::RIGHT_HANDED_Z_UP + ); + + Vector<3, float, glm::defaultp> zero(0.0f, 0.0f, 0.0f); + Vector<3, float, glm::defaultp> neg(-1.0f, -2.0f, -3.0f); + Vector<3, float, glm::defaultp> mixed(-1.0f, 0.0f, 2.0f); + + REQUIRE_VEC_CLOSE(converter.Convert(zero), zero); + REQUIRE(glm::length(converter.Convert(neg)) == Approx(glm::length(neg))); + REQUIRE(glm::length(converter.Convert(mixed)) == Approx(glm::length(mixed))); +}