diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 70733b4..d3fdc09 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: [ master, main ] + branches: [ master, main, work ] pull_request: jobs: diff --git a/CMakeLists.txt b/CMakeLists.txt index 6689614..92cc82a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,6 +12,7 @@ add_library(vectormatrix VectorMatrixLib/Matrix3f.cpp VectorMatrixLib/Matrix4f.cpp VectorMatrixLib/Quaternionf.cpp + VectorMatrixLib/GeometryTools.cpp ) target_include_directories(vectormatrix PUBLIC VectorMatrixLib) @@ -21,12 +22,27 @@ else() target_compile_options(vectormatrix PRIVATE -Wall -Wextra -Wpedantic) endif() -# Demo program (same as the Visual Studio project) -add_executable(vectormatrix_demo VectorMatrixLib/TestMatrix.cpp) +# Console tour of the API (same program as the Visual Studio project) +add_executable(vectormatrix_demo VectorMatrixLib/VectorMatrixDemo.cpp) target_link_libraries(vectormatrix_demo PRIVATE vectormatrix) # Unit tests enable_testing() -add_executable(vectormatrix_tests tests/TestVectorMatrix.cpp) +add_executable(vectormatrix_tests + tests/TestMain.cpp + tests/TestVectorMatrix.cpp + tests/TestVector2f.cpp + tests/TestVector3f.cpp + tests/TestVector4f.cpp + tests/TestMatrix2f.cpp + tests/TestMatrix3f.cpp + tests/TestMatrix4f.cpp + tests/TestQuaternionf.cpp + tests/TestGaussJordanSolver.cpp + tests/TestVector3fMatrix3f.cpp + tests/TestTools.cpp + tests/TestGeometryTools.cpp +) +target_include_directories(vectormatrix_tests PRIVATE tests) target_link_libraries(vectormatrix_tests PRIVATE vectormatrix) add_test(NAME vectormatrix_tests COMMAND vectormatrix_tests) diff --git a/README.md b/README.md new file mode 100644 index 0000000..da402c2 --- /dev/null +++ b/README.md @@ -0,0 +1,104 @@ +# VectorMatrixLib + +**A small C++17 vector, matrix and quaternion library for 3D graphics, ported from the math package of the [Aventura](https://github.com/obarry/Aventura) Java rendering engine.** + +![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg) +![C++17](https://img.shields.io/badge/C%2B%2B-17-blue.svg) +![Build: CMake | Visual Studio](https://img.shields.io/badge/build-CMake%20%7C%20Visual%20Studio-informational.svg) + +VectorMatrixLib gives you the building blocks of a 3D pipeline: vectors and points in homogeneous coordinates, 2x2, 3x3 and 4x4 matrices with inversion, and quaternions for rotations. It has no dependency other than the C++ standard library, and it follows the same API and semantics as Aventura's `com.aventura.math.vector`, so code and tests move easily from one to the other. + +## Highlights + +- **Vectors:** `Vector2f`, `Vector3f`, `Vector4f` (points with `w = 1`, vectors with `w = 0`), with dot and cross products, length, distance, normalization and interpolation. +- **Matrices:** `Matrix2f`, `Matrix3f`, `Matrix4f`, with products, transpose, determinant, trace, rows and columns, and inversion by Gauss-Jordan elimination with partial pivoting. +- **Quaternions:** `Quaternionf` from an axis and an angle or from a rotation matrix, Hamilton product, conversion to matrices and to axis-angle, and `slerp`. +- **Helpers:** `Tools::interpolate` and `GeometryTools::center`. +- **Robust comparisons:** `==` and `!=` compare within `EPSILON` (`1e-4`), as in Aventura. +- **Clear errors:** out of bound indices, wrong array sizes and singular matrices throw exceptions derived from `Vector3DException`. +- **Tested:** about 230 unit tests, most of them ported from Aventura's JUnit tests, run by CI on Linux, Windows and macOS. + +## Quick start + +```cpp +#include +#include "Matrix4f.h" +#include "Quaternionf.h" +#include "Vector4f.h" + +using namespace vectormatrix; + +int main() +{ + // A point (w = 1) and a rotation of 90 degrees around the z axis + Vector4f p(1, 0, 0, 1); + Quaternionf q(Vector3f::zAxis(), 3.14159265f / 2); + Matrix4f r = q.toMatrix4(); + + // Translation of 10 along x, in the last column + Matrix4f t = Matrix4f::identity(); + t.setColumn(3, Vector4f(10, 0, 0, 1)); + + Vector4f moved = t * r * p; // rotate, then translate: (10, 1, 0, 1) + Vector4f back = (t * r).inverse() * moved; // p again + std::cout << (back == p ? "round trip OK" : "KO") << std::endl; +} +``` + +The demo program prints a guided tour of the whole API, section by section (vectors, homogeneous coordinates, matrices and linear systems, transformations, quaternions and slerp, 2D, geometry helpers, errors). It is the best place to see the library in action. + +## Build + +### CMake (any platform) + +```bash +cmake -S . -B build +cmake --build build --config Release +ctest --test-dir build -C Release --output-on-failure # unit tests +./build/vectormatrix_demo # console tour (build/Release/ on Windows) +``` + +Targets: + +| Target | What it is | +|---|---| +| `vectormatrix` | the static library | +| `vectormatrix_demo` | the console tour, `VectorMatrixLib/VectorMatrixDemo.cpp` | +| `vectormatrix_tests` | the unit tests, `tests/*.cpp` (an argument filters the suites, e.g. `vectormatrix_tests Matrix`) | + +To use the library from your own CMake project, add this repository with `add_subdirectory` and link to `vectormatrix`; its include directory comes with it. + +### Visual Studio + +Open `VectorMatrixLib.sln` (VS 2019 or later, toolset v142, C++17): it builds the library and the demo. Visual Studio can also open the folder directly and use `CMakeLists.txt`, which includes the tests (Test Explorer runs them through CTest). + +## Conventions + +A few rules to know before using the library; [docs/API.md](docs/API.md) details all of them. + +- **Matrices are row-major and act on column vectors:** `M * v` computes A·V. For compatibility with Aventura's `v.times(M)`, `v * M` computes the same A·V. +- **`*` between two vectors is the cross product**; the dot product is `dot()`. +- **Homogeneous coordinates:** `Vector4f` points have `w = 1` and vectors `w = 0`, so a translation matrix moves points and leaves vectors unchanged. +- **Comparisons use `EPSILON`:** `Vector3f(1, 2, 3) == Vector3f(1, 2, 3.00001f)` is true. +- **Index checks:** vector `get(i)`/`set(i, v)` and matrix rows and columns throw `IndexOutOfBoundException`; matrix `get(i, j)`/`set(i, j, v)` are not checked, for speed. +- **No null:** where Aventura returns `null`, the C++ API returns an empty `std::optional` (`Vector4f::get3DPoint`, `GeometryTools::center`). + +## Project layout + +``` +VectorMatrixLib/ library sources (namespace vectormatrix), demo, Visual Studio project +tests/ unit tests: TestFramework.h, TestMain.cpp, one file per class +docs/ documentation +CMakeLists.txt CMake build (library, demo, tests) +.github/workflows/ CI on Linux, Windows and macOS +``` + +## Documentation + +- [docs/API.md](docs/API.md): the classes, their semantics, and the mapping from Aventura's Java API. +- [docs/HISTORY.md](docs/HISTORY.md): the genesis of this library from the Aventura Java project, and how it was brought up to date. +- [docs/TESTING.md](docs/TESTING.md): the test framework and how the JUnit tests were ported. + +## License + +MIT, Copyright (c) 2021 - 2026 Olivier BARRY. diff --git a/VectorMatrixLib/GaussJordanSolver.h b/VectorMatrixLib/GaussJordanSolver.h index ad4b518..72f4e67 100644 --- a/VectorMatrixLib/GaussJordanSolver.h +++ b/VectorMatrixLib/GaussJordanSolver.h @@ -71,19 +71,8 @@ namespace vectormatrix } } - private: - template - static void swapRows(float (&a)[N][N], int r1, int r2) - { - for (int col = 0; col < N; col++) - { - float tmp = a[r1][col]; - a[r1][col] = a[r2][col]; - a[r2][col] = tmp; - } - } - - // Row index (from pivot row down) of the greatest absolute value in column col + // Row index (from pivot row down) of the greatest absolute value in column col. + // Public so that the partial pivoting can be tested directly (package-private in Aventura). template static int indiceOfMaxRowInColumn(const float (&m)[N][N], int col, int pivot) { @@ -100,6 +89,18 @@ namespace vectormatrix } return indiceMax; } + + private: + template + static void swapRows(float (&a)[N][N], int r1, int r2) + { + for (int col = 0; col < N; col++) + { + float tmp = a[r1][col]; + a[r1][col] = a[r2][col]; + a[r2][col] = tmp; + } + } }; } diff --git a/VectorMatrixLib/GeometryTools.cpp b/VectorMatrixLib/GeometryTools.cpp new file mode 100644 index 0000000..c74cb00 --- /dev/null +++ b/VectorMatrixLib/GeometryTools.cpp @@ -0,0 +1,48 @@ +// +// MIT License +// Copyright(c) 2021 - 2026 Olivier BARRY +// +// This file is part of the C++ Aventura Project +// +// VectorMatrix Math Library +// + +#include "GeometryTools.h" + +namespace vectormatrix +{ + std::optional GeometryTools::center(const std::vector& points) + { + if (points.empty()) return std::nullopt; + + float x = 0, y = 0, z = 0; + for (const Vector4f& p : points) + { + x += p.getX(); + y += p.getY(); + z += p.getZ(); + } + float n = static_cast(points.size()); + return Vector4f(x / n, y / n, z / n, 1); + } + + std::optional GeometryTools::center(const std::vector>& points) + { + float x = 0, y = 0, z = 0; + size_t count = 0; + for (const std::vector& row : points) + { + for (const Vector4f& p : row) + { + x += p.getX(); + y += p.getY(); + z += p.getZ(); + } + count += row.size(); + } + if (count == 0) return std::nullopt; + + float n = static_cast(count); + return Vector4f(x / n, y / n, z / n, 1); + } +} diff --git a/VectorMatrixLib/GeometryTools.h b/VectorMatrixLib/GeometryTools.h new file mode 100644 index 0000000..1284459 --- /dev/null +++ b/VectorMatrixLib/GeometryTools.h @@ -0,0 +1,31 @@ +// +// MIT License +// Copyright(c) 2021 - 2026 Olivier BARRY +// +// This file is part of the C++ Aventura Project +// +// VectorMatrix Math Library +// + +#ifndef GEOMETRYTOOLS_H +#define GEOMETRYTOOLS_H + +#include +#include +#include "Vector4f.h" + +namespace vectormatrix +{ + // Geometry helpers (same as Aventura GeometryTools) + class GeometryTools + { + public: + // Geometrical center of the points (average of x, y, z, with w = 1), + // or nothing if there is no point (Java returns null) + static std::optional center(const std::vector& points); + // Same for a grid of points (rows may have different lengths) + static std::optional center(const std::vector>& points); + }; +} + +#endif // GEOMETRYTOOLS_H diff --git a/VectorMatrixLib/TestMatrix.cpp b/VectorMatrixLib/TestMatrix.cpp deleted file mode 100644 index 80afca5..0000000 --- a/VectorMatrixLib/TestMatrix.cpp +++ /dev/null @@ -1,53 +0,0 @@ -// -// MIT License -// Copyright(c) 2021 - 2026 Olivier BARRY -// -// This file is part of the C++ Aventura Project -// -// VectorMatrix Math Library -// - -#include -#include "Matrix3f.h" -#include "Vector3f.h" -#include "Vector4f.h" - -using namespace vectormatrix; - -int main() -{ - std::cout << "Test Matrix3f" << std::endl; - Matrix3f m(7); - m.set(1, 1, 9); - std::cout << m << std::endl; - std::cout << "Multiply with matrice:" << std::endl; - Matrix3f n(4); - std::cout << n << std::endl; - std::cout << "Resulting in:" << std::endl; - Matrix3f p = m * n; - std::cout << p << std::endl; - float f = 0.5f; - std::cout << "Multiplying by float : " << f << std::endl; - p *= f; - std::cout << p << std::endl; - - std::cout << "Test Vector3f" << std::endl; - Vector3f v1(5); - Vector3f v2(2, 3, 4); - std::cout << "Vector v1: " << v1 << std::endl; - std::cout << "Vector v2: " << v2 << std::endl; - std::cout << "dot product: v1.v2 = " << v1.dot(v2) << std::endl; - std::cout << "cross product: v1 x v2 = " << v1*v2 << std::endl; - v2*=2; - std::cout << "v2.timesEquals(2): " << v2 << std::endl; - Vector3f v3 = v1*11; - std::cout << "v3 = v1.times(11): " << v3 << std::endl; - std::cout << "v1 length: " << v1.length() << std::endl; - std::cout << "v2 length: " << v2.length() << std::endl; - std::cout << "v3 length: " << v3.length() << std::endl; - v2.normalize(); - std::cout << "Normalized v2: " << v2 << std::endl; - std::cout << "Normalized v2 length: " << v2.length() << std::endl; - Vector4f v1_4 = v1.V4(); - std::cout << "v1.V4(): " << v1_4 << std::endl; -} \ No newline at end of file diff --git a/VectorMatrixLib/Tools.h b/VectorMatrixLib/Tools.h new file mode 100644 index 0000000..8c38c0b --- /dev/null +++ b/VectorMatrixLib/Tools.h @@ -0,0 +1,52 @@ +// +// MIT License +// Copyright(c) 2021 - 2026 Olivier BARRY +// +// This file is part of the C++ Aventura Project +// +// VectorMatrix Math Library +// + +#ifndef TOOLS_H +#define TOOLS_H + +#include "Vector2f.h" +#include "Vector3f.h" +#include "Vector4f.h" + +namespace vectormatrix +{ + // Linear interpolation helpers (same as Aventura Tools). + // interpolate(a, b, t) = a*(1-t) + b*t, so t=0 gives a, t=1 gives b, + // and t outside [0,1] extrapolates on the (ab) line. + class Tools + { + public: + static Vector4f interpolate(const Vector4f& a, const Vector4f& b, float t) + { + return Vector4f::interpolate(a, b, t); + } + + static Vector3f interpolate(const Vector3f& a, const Vector3f& b, float t) + { + return Vector3f::interpolate(a, b, t); + } + + static Vector2f interpolate(const Vector2f& a, const Vector2f& b, float t) + { + return Vector2f::interpolate(a, b, t); + } + + static double interpolate(double a, double b, double t) + { + return a * (1 - t) + b * t; + } + + static float interpolate(float a, float b, float t) + { + return a * (1 - t) + b * t; + } + }; +} + +#endif // TOOLS_H diff --git a/VectorMatrixLib/VectorMatrixDemo.cpp b/VectorMatrixLib/VectorMatrixDemo.cpp new file mode 100644 index 0000000..8af0262 --- /dev/null +++ b/VectorMatrixLib/VectorMatrixDemo.cpp @@ -0,0 +1,345 @@ +// +// MIT License +// Copyright(c) 2021 - 2026 Olivier BARRY +// +// This file is part of the C++ Aventura Project +// +// VectorMatrix Math Library +// +// Console tour of the library: each section shows a typical use of the API +// on a small geometric example, and prints what happens step by step. +// + +#include +#include +#include +#include +#include +#include +#include "Constants.h" +#include "Exceptions.h" +#include "GeometryTools.h" +#include "Matrix2f.h" +#include "Matrix3f.h" +#include "Matrix4f.h" +#include "Quaternionf.h" +#include "Tools.h" +#include "Vector2f.h" +#include "Vector3f.h" +#include "Vector4f.h" + +using namespace vectormatrix; + +namespace +{ + const float PI = 3.14159265358979f; + + float degrees(float radians) { return radians * 180 / PI; } + float radians(float degrees) { return degrees * PI / 180; } + + // Fixed width number, without the "-0.000" of tiny negative values + std::string num(float f) + { + if (std::fabs(f) < 0.0005f) f = 0; + std::ostringstream s; + s << std::fixed << std::setprecision(3) << std::setw(7) << f; + return s.str(); + } + + // Compact one line representations (the library operator<< prints on several lines) + std::string str(const Vector2f& v) { return "(" + num(v.getX()) + "," + num(v.getY()) + " )"; } + std::string str(const Vector3f& v) { return "(" + num(v.getX()) + "," + num(v.getY()) + "," + num(v.getZ()) + " )"; } + std::string str(const Vector4f& v) { return "(" + num(v.getX()) + "," + num(v.getY()) + "," + num(v.getZ()) + "," + num(v.getW()) + " )"; } + std::string str(const Quaternionf& q) { return "[" + num(q.getX()) + "," + num(q.getY()) + "," + num(q.getZ()) + "," + num(q.getW()) + " ]"; } + + // Print a matrix of any size, aligned under a label + void print(const std::string& label, const std::vector>& a) + { + for (size_t i = 0; i < a.size(); i++) + { + std::cout << " " << (i == 0 ? label : std::string(label.size(), ' ')) << (i == 0 ? " = | " : " | "); + for (float f : a[i]) std::cout << num(f) << " "; + std::cout << "|" << std::endl; + } + } + void print(const std::string& label, const Matrix2f& m) { print(label, m.getArray()); } + void print(const std::string& label, const Matrix3f& m) { print(label, m.getArray()); } + void print(const std::string& label, const Matrix4f& m) { print(label, m.getArray()); } + + void title(const std::string& t) + { + std::cout << std::endl << std::string(70, '=') << std::endl; + std::cout << " " << t << std::endl; + std::cout << std::string(70, '=') << std::endl; + } + + void line(const std::string& label, const std::string& value) + { + std::cout << " " << std::left << std::setw(34) << label << std::right << value << std::endl; + } + + std::string yes(bool b) { return b ? "yes" : "no"; } + + // Rotation of angle around the z axis, as a 4x4 matrix built from a quaternion + Matrix4f rotationZ(float angle) + { + return Quaternionf(Vector3f::zAxis(), angle).toMatrix4(); + } + + // Translation by t, as a 4x4 matrix (acts on points, not on vectors) + Matrix4f translation(const Vector3f& t) + { + Matrix4f m = Matrix4f::identity(); + m.setColumn(3, Vector4f(t.getX(), t.getY(), t.getZ(), 1)); + return m; + } + + // Uniform scaling by s, as a 4x4 matrix + Matrix4f scaling(float s) + { + Matrix4f m = Matrix4f::identity(); + m.set(0, 0, s); + m.set(1, 1, s); + m.set(2, 2, s); + return m; + } +} + +static void demoVectors() +{ + title("1. Vectors in 3D: Vector3f"); + + Vector3f a(1, 2, 3); + Vector3f b(4, 5, 6); + line("a", str(a)); + line("b", str(b)); + line("a + b", str(a + b)); + line("b - a", str(b - a)); + line("a * 2", str(a * 2)); + line("a . b (dot product)", num(a.dot(b))); + line("a ^ b (cross product)", str(a.cross(b))); + line("(a ^ b) . a (orthogonal to a)", num(a.cross(b).dot(a))); + line("|a| (length)", num(a.length())); + line("distance(a, b)", num(a.distance(b))); + + Vector3f n = a; + n.normalize(); + line("a normalized", str(n)); + line("length after normalize", num(n.length())); + + std::cout << std::endl << " Right-handed axes: x ^ y = z, y ^ z = x, z ^ x = y" << std::endl; + line("x ^ y", str(Vector3f::xAxis() * Vector3f::yAxis())); + line("y ^ z", str(Vector3f::yAxis() * Vector3f::zAxis())); + line("z ^ x", str(Vector3f::zAxis() * Vector3f::xAxis())); + + std::cout << std::endl << " Linear interpolation from a to b (Tools::interpolate):" << std::endl; + for (float t = 0; t <= 1.001f; t += 0.25f) + line(" t = " + num(t), str(Tools::interpolate(a, b, t))); +} + +static void demoHomogeneous() +{ + title("2. Points and vectors in homogeneous coordinates: Vector4f"); + + Vector4f p(1, 2, 3, 1); + Vector4f q(4, 6, 3, 1); + line("point p (w = 1)", str(p) + " isPoint: " + yes(p.isPoint())); + line("point q (w = 1)", str(q) + " isPoint: " + yes(q.isPoint())); + Vector4f pq(p, q); + line("vector pq = q - p (w = 0)", str(pq) + " isVector: " + yes(pq.isVector())); + line("|pq|", num(pq.length())); + line("p + pq lands on q", str(p + pq)); + + Vector4f h(2, 4, 6, 2); + line("point h with w = 2", str(h)); + line("h.get3DPoint() = (x/w, y/w, z/w)", str(h.get3DPoint().value())); + std::optional none = pq.get3DPoint(); + line("pq.get3DPoint() on a vector", none ? str(*none) : std::string("empty (w = 0: point at infinity)")); +} + +static void demoMatrices() +{ + title("3. Matrices: Matrix3f"); + + const float values[3][3] = { { 2, 0, 1 }, { 1, 3, 2 }, { 1, 1, 2 } }; + Matrix3f m(values); + print("M", m); + std::cout << std::endl; + print("transpose(M)", m.transpose()); + std::cout << std::endl; + line("det(M)", num(m.determinant())); + line("trace(M)", num(m.trace())); + line("row 1", str(m.getRow(1))); + line("column 2", str(m.getColumn(2))); + std::cout << std::endl; + + Matrix3f inv = m.inverse(); + print("inverse(M)", inv); + std::cout << std::endl; + print("M * inverse(M)", m * inv); + line("is identity", yes((m * inv).isIdentity())); + + std::cout << std::endl << " Solving the system M.x = b with x = inverse(M).b:" << std::endl; + Vector3f b(5, 13, 9); + Vector3f x = inv * b; + line("b", str(b)); + line("x", str(x)); + line("check M.x", str(m * x)); +} + +static void demoTransforms() +{ + title("4. Transformations: Matrix4f on points and vectors"); + + Vector4f point(1, 0, 0, 1); + Vector4f vector(1, 0, 0, 0); + Matrix4f t = translation(Vector3f(10, 0, 0)); + Matrix4f r = rotationZ(radians(90)); + Matrix4f s = scaling(2); + + print("T (translation x+10)", t); + std::cout << std::endl; + print("R (rotation 90 around z)", r); + std::cout << std::endl; + + line("point P", str(point)); + line("vector V", str(vector)); + line("T * P (point is moved)", str(t * point)); + line("T * V (vector is not)", str(t * vector)); + line("R * P", str(r * point)); + line("S * P (scale x2)", str(s * point)); + + std::cout << std::endl << " Matrix products are not commutative: the order matters" << std::endl; + line("(T * R) * P: rotate, then move", str((t * r) * point)); + line("(R * T) * P: move, then rotate", str((r * t) * point)); + + Matrix4f m = t * r * s; + line("M = T * R * S, M * P", str(m * point)); + line("inverse(M) * (M * P) is P again", str(m.inverse() * (m * point))); +} + +static void demoQuaternions() +{ + title("5. Rotations with quaternions: Quaternionf"); + + Quaternionf qz(Vector3f::zAxis(), radians(90)); + Quaternionf qx(Vector3f::xAxis(), radians(90)); + line("qz = 90 around z", str(qz)); + line("qx = 90 around x", str(qx)); + line("|qz|", num(qz.length())); + line("qz * conjugate(qz)", str(qz * qz.conjugate()) + " (identity)"); + + Vector3f v(1, 0, 0); + line("v", str(v)); + line("qz rotates v", str(qz.toMatrix3() * v)); + + Quaternionf both = qx * qz; + Vector3f axis; + float angle = both.toAxisAngle(axis); + line("qx * qz (qz first, then qx)", str(both)); + line(" as axis", str(axis)); + line(" and angle (degrees)", num(degrees(angle))); + line(" rotates v", str(both.toMatrix3() * v)); + + std::cout << std::endl << " A rotation matrix converts back to the same quaternion:" << std::endl; + line("Quaternionf(qz.toMatrix4())", str(Quaternionf(qz.toMatrix4()))); + + std::cout << std::endl << " Smooth rotation from identity to 120 around z (slerp):" << std::endl; + Quaternionf start; + Quaternionf end(Vector3f::zAxis(), radians(120)); + for (int i = 0; i <= 4; i++) + { + float t = i / 4.0f; + Quaternionf qt = Quaternionf::slerp(start, end, t); + Vector3f a; + float deg = degrees(qt.toAxisAngle(a)); + line(" t = " + num(t) + " angle " + num(deg), "v -> " + str(qt.toMatrix3() * v)); + } +} + +static void demo2D() +{ + title("6. 2D: Vector2f and Matrix2f"); + + float c = std::cos(radians(45)); + float s = std::sin(radians(45)); + const float values[2][2] = { { c, -s }, { s, c } }; + Matrix2f rotation(values); + print("R (rotation 45)", rotation); + line("det(R) (a rotation keeps areas)", num(rotation.determinant())); + line("inverse(R) == transpose(R)", yes(rotation.inverse() == rotation.transpose())); + + std::cout << std::endl << " Corners of a unit square, rotated by 45 degrees:" << std::endl; + std::vector square = { Vector2f(0, 0), Vector2f(1, 0), Vector2f(1, 1), Vector2f(0, 1) }; + for (const Vector2f& corner : square) + line(" " + str(corner), "-> " + str(rotation * corner)); +} + +static void demoGeometry() +{ + title("7. Geometry helpers: GeometryTools"); + + std::vector cube; + for (int i = 0; i < 8; i++) + cube.push_back(Vector4f(1.0f + 2 * (i & 1), 1.0f + 2 * ((i >> 1) & 1), 1.0f + 2 * ((i >> 2) & 1), 1)); + std::cout << " The 8 corners of a cube of side 2 starting at (1, 1, 1):" << std::endl; + for (const Vector4f& corner : cube) + std::cout << " " << str(corner) << std::endl; + line("center of the cube", str(GeometryTools::center(cube).value())); + std::optional empty = GeometryTools::center(std::vector()); + line("center of no point", empty ? str(*empty) : std::string("empty")); +} + +static void demoErrors() +{ + title("8. Errors are reported with exceptions (all derive from Vector3DException)"); + + const float singularValues[3][3] = { { 1, 2, 3 }, { 2, 4, 6 }, { 0, 1, 1 } }; + Matrix3f singular(singularValues); + print("S (row 2 = 2 x row 1)", singular); + line("det(S)", num(singular.determinant())); + try + { + singular.inverse(); + } + catch (const NotInvertibleMatrixException& e) + { + line("inverse(S)", std::string("NotInvertibleMatrixException: ") + e.what()); + } + + try + { + Vector3f(1, 2, 3).get(3); + } + catch (const IndexOutOfBoundException& e) + { + line("Vector3f.get(3)", std::string("IndexOutOfBoundException: ") + e.what()); + } + + try + { + Matrix3f m; + m.setArray({ { 1, 2 }, { 3, 4 } }); + } + catch (const Vector3DException& e) + { + line("Matrix3f.setArray(2x2 array)", std::string("caught as Vector3DException: ") + e.what()); + } +} + +int main() +{ + std::cout << "VectorMatrixLib: a tour of the API (EPSILON = " << EPSILON << ")" << std::endl; + + demoVectors(); + demoHomogeneous(); + demoMatrices(); + demoTransforms(); + demoQuaternions(); + demo2D(); + demoGeometry(); + demoErrors(); + + std::cout << std::endl; + return 0; +} diff --git a/VectorMatrixLib/VectorMatrixLib.vcxproj b/VectorMatrixLib/VectorMatrixLib.vcxproj index 4155f9f..c7812f5 100644 --- a/VectorMatrixLib/VectorMatrixLib.vcxproj +++ b/VectorMatrixLib/VectorMatrixLib.vcxproj @@ -154,16 +154,19 @@ + + - + + diff --git a/VectorMatrixLib/VectorMatrixLib.vcxproj.filters b/VectorMatrixLib/VectorMatrixLib.vcxproj.filters index 0d2f74d..9498384 100644 --- a/VectorMatrixLib/VectorMatrixLib.vcxproj.filters +++ b/VectorMatrixLib/VectorMatrixLib.vcxproj.filters @@ -48,6 +48,12 @@ Header Files + + Header Files + + + Header Files + @@ -59,7 +65,7 @@ Source Files - + Source Files @@ -74,5 +80,8 @@ Source Files + + Source Files + \ No newline at end of file diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..b9d9bba --- /dev/null +++ b/docs/API.md @@ -0,0 +1,128 @@ +# API guide + +Everything lives in the `vectormatrix` namespace. Include the header of each class you use (`#include "Vector3f.h"`, ...). The headers are short and commented: they are the reference, this page is the map. + +## Classes + +| Header | Content | +|---|---| +| `Vector2f.h` | 2D vector | +| `Vector3f.h` | 3D vector | +| `Vector4f.h` | 3D point or vector in homogeneous coordinates (x, y, z, w) | +| `Matrix2f.h`, `Matrix3f.h`, `Matrix4f.h` | 2x2, 3x3 and 4x4 matrices of `float` | +| `Quaternionf.h` | quaternion `w + xi + yj + zk`, for rotations | +| `GaussJordanSolver.h` | matrix inversion used by `Matrix3f` and `Matrix4f` (header only) | +| `Tools.h` | `Tools::interpolate` for vectors and scalars (header only) | +| `GeometryTools.h` | `GeometryTools::center` of a set of points | +| `Constants.h` | `EPSILON` (1e-4), `SIZE_2/3/4`, `X_AXIS`, `Y_AXIS`, `Z_AXIS` | +| `MathTools.h` | `MathTools::equals(a, b)`: floats equal within `EPSILON` | +| `Exceptions.h` | `Vector3DException` and its subclasses | + +## Vectors + +All vector classes share the same shape: + +```cpp +Vector3f a(1, 2, 3), b(4, 5, 6); +Vector3f c = a + b - a * 2; // + - and * / by a float, and their += -= *= /= forms +float d = a.dot(b); // dot product +Vector3f n = a.cross(b); // cross product, also a * b (and a.crossEquals(b)) +float l = a.length(); // also lengthSquared(), distance(b), distanceSquared(b) +a.normalize(); // modifies a and returns it +float x = a.get(0); // or getX(); get(3) throws IndexOutOfBoundException +std::array arr = a.toArray(); +Vector3f m = Vector3f::interpolate(a, b, 0.5f); // a*(1-t) + b*t +``` + +- Constant vectors are functions returning a fresh copy: `Vector3f::xAxis()`, `yAxis()`, `zAxis()`, `xOppAxis()`, ..., `zeroVector()`, and `Vector4f::zeroPoint()`. +- Constructors from a `std::vector` throw `VectorArrayWrongSizeException` when it is too short. +- `Vector3f` and `Vector4f` convert into each other: `v3.V4()` (w = 0), `v4.V3()`, `Vector3f(v4)`, `Vector4f(v3)`. +- A row or a column of a matrix: `Vector3f(r, M)` is row `r`, `Vector3f(M, c)` is column `c` (same for `Vector4f`). + +### Points and vectors: `Vector4f` + +`w = 1` marks a point, `w = 0` a vector: a translation matrix moves points and leaves vectors unchanged, and the difference of two points is a vector. + +```cpp +Vector4f p(1, 2, 3, 1), q(4, 6, 3, 1); +Vector4f pq(p, q); // q - p, w = 0 +bool b = pq.isVector(); // also isPoint(), point(), vector() +std::optional r = Vector4f(2, 4, 6, 2).get3DPoint(); // (1, 2, 3): x/w, y/w, z/w +// get3DPoint() of a vector (w = 0) is empty +``` + +`Vector4f` cross product and `*` between two `Vector4f` treat both as vectors (w = 0). + +## Matrices + +Matrices are stored row-major, `get(row, column)`, and act on column vectors. + +```cpp +const float values[3][3] = { { 2, 0, 1 }, { 1, 3, 2 }, { 1, 1, 2 } }; +Matrix3f m(values); // also Matrix3f(a): all elements equal to a, Matrix3f::identity() +Matrix3f p = m * m.transpose(); // + - * with matrices, * with a float, and += -= *= +Vector3f v = m * Vector3f(1, 2, 3); // A.V +float det = m.determinant(); // also trace(), isIdentity() +Matrix3f inv = m.inverse(); // throws NotInvertibleMatrixException if singular +Vector3f row = m.getRow(1); // also getColumn, setRow, setColumn (checked indices) +std::vector> a = m.getArray(); // copy; setArray(a) checks the size +``` + +- `get(i, j)` and `set(i, j, v)` do not check their indices, for speed. +- `setDiagonal(v)` sets the diagonal; `transposeEquals()` transposes in place. +- `Matrix4f(Matrix3f)` copies the 3x3 part and leaves the rest at 0 (including [3][3]); `Matrix4f::getMatrix3()` and `Matrix3f(Matrix4f)` extract the upper-left 3x3 part. +- `v * M` computes the same A·V as `M * v`, to keep the meaning of Java's `v.times(M)`. +- Write float literals for the "all elements" constructor: `Matrix4f(0)` is ambiguous with the array constructor, `Matrix4f(0.0f)` is not. + +## Quaternions + +```cpp +Quaternionf q(Vector3f::zAxis(), angle); // rotation of angle (radians) around an axis +Quaternionf r(rotationMatrix); // from a Matrix3f or Matrix4f +Quaternionf qr = q * r; // Hamilton product: r is applied first, then q +Matrix3f m3 = q.toMatrix3(); // toMatrix4() sets [3][3] to 1 +Vector3f axis; +float a = q.toAxisAngle(axis); +Quaternionf s = Quaternionf::slerp(q, r, 0.5f); // takes the shorter path +``` + +Also `conjugate()`, `inverse()`, `normalize()`, `length()`, `dot()`, and `get`/`set` of the components. `Quaternionf()` is the identity (0, 0, 0, 1). Remember that `q` and `-q` are the same rotation. + +## Helpers + +- `Tools::interpolate(a, b, t)` for `Vector2f`, `Vector3f`, `Vector4f`, `float` and `double`: `a*(1-t) + b*t`; `t` outside [0, 1] extrapolates. +- `GeometryTools::center(points)` for a `std::vector` or a `std::vector>`: the average of x, y, z with w = 1, or an empty `std::optional` when there is no point. +- `GaussJordanSolver::invert(a, result, epsilon)` inverts any N x N `float` array; `indiceOfMaxRowInColumn` is public so that the pivoting can be tested. + +## Exceptions + +All derive from `Vector3DException`, itself a `std::runtime_error`, so one `catch (const Vector3DException&)` handles them all. + +| Exception | Thrown by | +|---|---| +| `IndexOutOfBoundException` | vector and quaternion `get(i)`/`set(i, v)`, matrix rows and columns | +| `VectorArrayWrongSizeException` | vector constructors from a too short `std::vector` | +| `MatrixArrayWrongSizeException` | `setArray` with a wrong size | +| `NotInvertibleMatrixException` | `inverse()` of a singular matrix | + +## Mapping from Aventura (Java) + +| Java (`com.aventura.math.vector`) | C++ (`vectormatrix`) | +|---|---| +| `Vector3`, `Matrix4`, `Quaternion`, ... | `Vector3f`, `Matrix4f`, `Quaternionf`, ... | +| `a.plus(b)`, `a.minus(b)` | `a + b`, `a - b` | +| `a.plusEquals(b)`, `a.minusEquals(b)` | `a += b`, `a -= b` | +| `a.times(2f)`, `a.timesEquals(2f)` | `a * 2`, `a *= 2` | +| `a.cross(b)`, `a.crossEquals(b)` | `a.cross(b)` or `a * b`, `a.crossEquals(b)` | +| `a.dot(b)` | `a.dot(b)` | +| `A.times(B)`, `A.times(v)`, `v.times(A)` | `A * B`, `A * v`, `v * A` (all A·V) | +| `a.equals(b)` | `a == b` (within EPSILON) | +| `new Vector3(v)`, `v.copy()` | copy constructor | +| `null` result | empty `std::optional` | +| `equals(Object)`, `hashCode()` | not needed | +| `float[][] getArray()`, `setArray(float[][])` | `std::vector>` | +| `Constants.EPSILON` | `EPSILON` | +| `Tools`, `GeometryTools` | `Tools`, `GeometryTools` | +| `Vector2.set(x, y)` | `setX(x); setY(y);` (a `set(x, y)` would be ambiguous with `set(i, v)`) | + +Not ported: the classes of the rest of the engine (`Rotation`, `Translation`, `Scaling`, projections), see the possible step P5 in [HISTORY.md](HISTORY.md). diff --git a/docs/HISTORY.md b/docs/HISTORY.md new file mode 100644 index 0000000..6cc26e1 --- /dev/null +++ b/docs/HISTORY.md @@ -0,0 +1,50 @@ +# History: from Aventura (Java) to VectorMatrixLib (C++) + +## Aventura, the origin + +[Aventura](https://github.com/obarry/Aventura) is a lightweight 3D rendering engine written by Olivier BARRY in 100% pure Java: no GPU, no native code, no third-party library. It builds a scene through a plain Java API (shapes, textures, lights, shadows, camera), then transforms, projects and rasterizes it on the CPU, with a Z-buffer, lighting and shadow mapping. + +Everything in such an engine rests on a small amount of linear algebra: points and vectors in homogeneous coordinates, 4x4 transformation matrices composed along a scene graph, matrix inversion to go back from camera to world space, and rotations. Aventura gathers it in its own package, `com.aventura.math.vector`: + +- `Vector2`, `Vector3`, `Vector4`, `Matrix2`, `Matrix3`, `Matrix4`; +- `Quaternion`, with `slerp`, used for rotations; +- `GaussJordanSolver`, the matrix inversion shared by `Matrix3` and `Matrix4`; +- `Tools` and `GeometryTools` (interpolation, center of a set of points); +- a common `Vector3DException` hierarchy, and float comparisons within `Constants.EPSILON`; +- a few hundred JUnit tests covering all of it. + +## The C++ port (2021 - 2023) + +VectorMatrixLib started in 2021 as the first brick of a C++ version of Aventura: every source file still says it is "part of the C++ Aventura Project". The math package was the natural place to start, since the rest of the engine depends on it and it depends on nothing. + +That first version translated the four core classes, `Vector3f`, `Vector4f`, `Matrix3f` and `Matrix4f` (the `f` suffix marks single precision `float`, as in many C++ graphics libraries), with C++ operators in place of Java's `plus`, `minus` and `times`, and a small console program, `TestMatrix.cpp`, to try them. It was built with Visual Studio only. Its last update dates from August 2023. + +In the meantime, the Java library kept evolving: new classes, the Gauss-Jordan solver, EPSILON comparisons, exceptions, many bug fixes, and a large test suite. By 2026 the two had drifted far apart. + +## Bringing it up to date (October 2026) + +In October 2026 the C++ library was compared class by class with `com.aventura.math.vector`, and brought up to date in five steps, each one built and tested before the next: + +| Step | Content | +|---|---| +| P0 | Fixed the bugs of the first version (`Vector4f::setW`, `Matrix3f::operator-=`, `Matrix3f * float` returning a `Matrix4f`, an out of bound read in `Matrix4f(Matrix3f)`, memory leaks from `new` in operators), const-correctness; added a CMake build, a first test program and CI on Linux, Windows and macOS. | +| P1 | `EPSILON` and `MathTools::equals`, `==` and `!=` within EPSILON, and the exception hierarchy of Aventura (`Vector3DException` and its subclasses). | +| P2 | Completed `Vector3f`, `Vector4f`, `Matrix3f` and `Matrix4f` with the rest of the Java API, including inversion through a shared `GaussJordanSolver`. | +| P3 | Added `Vector2f`, `Matrix2f` and `Quaternionf`. | +| P4 | Ported the JUnit tests, added `Tools` and `GeometryTools`, a console tour of the API, and this documentation. | + +A possible next step (P5) is to port Aventura's transformations and projections (rotation, translation, scaling, frustum and orthographic projection matrices) on top of this library. + +## What changed in the translation + +The C++ library keeps the names, the semantics and the numerical behavior of the Java one, so that a test written for one holds for the other. The differences come from the language: + +- **Operators instead of methods.** `a.plus(b)` is `a + b`, `a.timesEquals(2)` is `a *= 2`, `M.times(v)` is `M * v`. Java's `v.times(M)` keeps its meaning: `v * M` is also A·V. +- **Values instead of references.** C++ objects are copied by value, so Java's `copy()` is the copy constructor, and there is no `null`: a missing result is an empty `std::optional`. +- **No `equals(Object)` or `hashCode`**: `==` and `!=` compare within EPSILON. +- **Unchecked `Matrix get(i, j)` and `set(i, j, v)`**, for speed; rows, columns and vector coordinates are checked and throw `IndexOutOfBoundException`. +- **`Quaternionf`** instead of `Quaternion`, for consistency with the other `f` classes. +- **`Vector2f` has no `set(x, y)`**: it would be ambiguous with `set(int i, float v)`. +- **`GeometryTools::center` of a 2D set** counts every point, where Java assumes all rows have the length of the first one. + +[API.md](API.md) gives the full mapping, class by class. diff --git a/docs/TESTING.md b/docs/TESTING.md new file mode 100644 index 0000000..b708cb6 --- /dev/null +++ b/docs/TESTING.md @@ -0,0 +1,62 @@ +# Tests + +## Running them + +```bash +cmake -S . -B build +cmake --build build +ctest --test-dir build --output-on-failure +``` + +or run `build/vectormatrix_tests` directly to see the result of each suite. An argument runs only the suites whose name contains it: `vectormatrix_tests Quaternion`. The program returns a nonzero exit code when a test fails, which is what CTest and the CI check. + +## The framework + +`tests/TestFramework.h` is a minimal framework with no dependency, in the spirit of JUnit: + +```cpp +#include "TestFramework.h" +#include "Vector3f.h" + +using namespace vectormatrix; + +TEST(Vector3f, length) +{ + CHECK_NEAR(Vector3f(3, 4, 0).length(), 5.0f); +} +``` + +Each `TEST(Suite, name)` registers itself, and `tests/TestMain.cpp` runs them all, grouped by suite. A failed check prints its file, line and expression, and the test goes on with its next checks. + +| Macro | JUnit equivalent | Checks that | +|---|---|---| +| `CHECK(expr)` | `assertTrue` | `expr` is true | +| `CHECK_EQUAL(actual, expected)` | `assertEquals(expected, actual)` | `actual == expected` (within EPSILON for the library classes); prints both on failure | +| `CHECK_NEAR(actual, expected)` | `assertEquals(expected, actual, EPSILON)` | the floats differ by at most `EPSILON` | +| `CHECK_NEAR_TOL(actual, expected, tol)` | `assertEquals(expected, actual, tol)` | the floats differ by at most `tol` | +| `CHECK_THROWS(expr, Exception)` | `assertThrows` | `expr` throws `Exception` or a subclass | +| `CHECK_NO_THROW(expr)` | | `expr` throws nothing | + +An exception escaping a test is reported as a failure of that test. + +To add a test file, create it in `tests/` and add it to `vectormatrix_tests` in `CMakeLists.txt`. + +## Where the tests come from + +| File | Suite | Origin | +|---|---|---| +| `TestVector2f.cpp` ... `TestVector4f.cpp` | `Vector2f` ... `Vector4f` | Aventura `TestVector2.java` ... `TestVector4.java` | +| `TestMatrix2f.cpp` ... `TestMatrix4f.cpp` | `Matrix2f` ... `Matrix4f` | Aventura `TestMatrix2.java` ... `TestMatrix4.java` | +| `TestQuaternionf.cpp` | `Quaternionf` | Aventura `TestQuaternion.java` | +| `TestGaussJordanSolver.cpp` | `GaussJordanSolver` | Aventura `TestGaussJordanSolver.java` | +| `TestVector3fMatrix3f.cpp` | `Vector3fMatrix3f` | Aventura `TestVector3Matrix3.java` | +| `TestTools.cpp`, `TestGeometryTools.cpp` | `Tools`, `GeometryTools` | Aventura `TestTools.java`, `TestGeometryTools.java` | +| `TestVectorMatrix.cpp` | `CppSpecific` | written for the C++ library: EPSILON comparisons, exceptions, conversions between classes, and the behaviors that differ from Java | + +The JUnit tests were ported one for one, in the same order and with the same values, so that a failure can be compared with the Java side. A few could not be, and are marked `// Not ported:` with the reason where they would be: + +- tests of `equals(Object)`, `hashCode` and `null` arguments, which have no C++ counterpart; +- tests of protected Java helpers (`timesRow`, `swapRows`) and of their visibility; +- a static `Vector2.equals(v1, v2)` and a deprecated method. + +Checks that compare Java references (two calls returning different objects) were dropped from tests that otherwise were ported, since C++ returns values; the value checks around them were kept. The tests that relied on Aventura's `Rotation` class build the same matrix with a local helper using the formula of `Rotation.initRotation`. diff --git a/tests/TestFramework.h b/tests/TestFramework.h new file mode 100644 index 0000000..5543fcc --- /dev/null +++ b/tests/TestFramework.h @@ -0,0 +1,102 @@ +// +// MIT License +// Copyright(c) 2021 - 2026 Olivier BARRY +// +// This file is part of the C++ Aventura Project +// +// VectorMatrix Math Library +// +// Minimal unit test framework, without any third-party dependency. +// +// TEST(Vector3f, length) { CHECK_NEAR(Vector3f(3, 4, 0).length(), 5.0f); } +// +// Each TEST registers itself; TestMain.cpp runs them all, suite by suite. +// + +#ifndef TESTFRAMEWORK_H +#define TESTFRAMEWORK_H + +#include +#include +#include +#include +#include "MathTools.h" + +namespace testframework +{ + struct TestCase + { + const char* suite; + const char* name; + void (*function)(); + }; + + inline std::vector& registry() + { + static std::vector tests; + return tests; + } + + // Number of failed checks since the start of the run + inline int& failures() + { + static int count = 0; + return count; + } + + struct Registrar + { + Registrar(const char* suite, const char* name, void (*function)()) + { + registry().push_back({ suite, name, function }); + } + }; + + inline bool check(bool condition, const char* expression, const char* file, int line) + { + if (!condition) + { + std::cerr << " " << file << ":" << line << ": FAILED: " << expression << std::endl; + failures()++; + } + return condition; + } + + template + void checkEqual(const A& actual, const B& expected, const char* expression, const char* file, int line) + { + if (!check(actual == expected, expression, file, line)) + std::cerr << " actual: " << actual << std::endl + << " expected: " << expected << std::endl; + } + + inline void checkNear(double actual, double expected, double tolerance, const char* expression, const char* file, int line) + { + if (!check(std::fabs(actual - expected) <= tolerance, expression, file, line)) + std::cerr << " actual: " << actual << ", expected: " << expected << " (tolerance " << tolerance << ")" << std::endl; + } +} + +#define TEST(suite, name) \ + static void suite##_##name(); \ + static const testframework::Registrar suite##_##name##_registrar(#suite, #name, &suite##_##name); \ + static void suite##_##name() + +// expr must be true +#define CHECK(expr) testframework::check((expr), #expr, __FILE__, __LINE__) +// actual == expected (vectors, matrices and quaternions compare within EPSILON), both printed on failure +#define CHECK_EQUAL(actual, expected) testframework::checkEqual((actual), (expected), #actual " == " #expected, __FILE__, __LINE__) +// |actual - expected| <= EPSILON (same tolerance as MathTools::equals) +#define CHECK_NEAR(actual, expected) testframework::checkNear((actual), (expected), vectormatrix::EPSILON, #actual " ~= " #expected, __FILE__, __LINE__) +// |actual - expected| <= tolerance (same as JUnit assertEquals(expected, actual, delta)) +#define CHECK_NEAR_TOL(actual, expected, tolerance) testframework::checkNear((actual), (expected), (tolerance), #actual " ~= " #expected, __FILE__, __LINE__) +// expr must throw Exception (or a subclass) +#define CHECK_THROWS(expr, Exception) \ + do { bool thrown_ = false; try { (void)(expr); } catch (const Exception&) { thrown_ = true; } \ + testframework::check(thrown_, #expr " throws " #Exception, __FILE__, __LINE__); } while (0) +// expr must not throw anything +#define CHECK_NO_THROW(expr) \ + do { bool thrown_ = false; try { (void)(expr); } catch (...) { thrown_ = true; } \ + testframework::check(!thrown_, #expr " does not throw", __FILE__, __LINE__); } while (0) + +#endif // TESTFRAMEWORK_H diff --git a/tests/TestGaussJordanSolver.cpp b/tests/TestGaussJordanSolver.cpp new file mode 100644 index 0000000..b1900df --- /dev/null +++ b/tests/TestGaussJordanSolver.cpp @@ -0,0 +1,109 @@ +// +// MIT License +// Copyright(c) 2021 - 2026 Olivier BARRY +// +// This file is part of the C++ Aventura Project +// +// VectorMatrix Math Library +// +// Port of Aventura TestGaussJordanSolver.java + +#include "TestFramework.h" +#include "GaussJordanSolver.h" +#include "Matrix3f.h" +#include "Matrix4f.h" +#include "Exceptions.h" + +using namespace vectormatrix; + +TEST(GaussJordanSolver, PivotSelection_picksTrueMaxAbsRow_regression) +{ + // Column 0 has abs values [2, 5, 1] starting at pivot row 0: row 1 (value 5) must be selected, + // not row 0 itself by default (this was the bug: the search never compared against the pivot row). + const float m[3][3] = { + { 2.0f, 1.0f, 1.0f }, + { 5.0f, 1.0f, 1.0f }, + { 1.0f, 1.0f, 1.0f } + }; + int chosen = GaussJordanSolver::indiceOfMaxRowInColumn(m, 0, 0); + CHECK_EQUAL(chosen, 1); + + // And when the pivot row already holds the max value, it must be correctly kept (not overlooked). + const float m2[3][3] = { + { 9.0f, 1.0f, 1.0f }, + { 5.0f, 1.0f, 1.0f }, + { 1.0f, 1.0f, 1.0f } + }; + int chosen2 = GaussJordanSolver::indiceOfMaxRowInColumn(m2, 0, 0); + CHECK_EQUAL(chosen2, 0); +} + +TEST(GaussJordanSolver, PivotSelection_searchStartsAtGivenPivot) +{ + const float m[4][4] = { + { 9.0f, 1.0f, 1.0f, 1.0f }, + { 5.0f, 1.0f, 1.0f, 1.0f }, + { 7.0f, 1.0f, 1.0f, 1.0f }, + { 2.0f, 1.0f, 1.0f, 1.0f } + }; + // Starting the search at pivot=2: only rows 2 and 3 are candidates (values 7 and 2), row 2 wins, + // even though rows 0 and 1 (values 9 and 5) hold larger values overall. + int chosen = GaussJordanSolver::indiceOfMaxRowInColumn(m, 0, 2); + CHECK_EQUAL(chosen, 2); +} + +TEST(GaussJordanSolver, Invert_3x3_matchesIdentityRoundTrip) +{ + const float a[3][3] = { + { 4.0f, 7.0f, 2.0f }, + { 3.0f, 5.0f, 1.0f }, + { 2.0f, 3.0f, 1.0f } + }; + float inv[3][3]; + GaussJordanSolver::invert(a, inv, 1.0E-4f); + Matrix3f A(a); + Matrix3f invA(inv); + CHECK(A * invA == Matrix3f::identity()); +} + +TEST(GaussJordanSolver, Invert_4x4_matchesIdentityRoundTrip) +{ + const float a[4][4] = { + { 2.0f, 0.0f, 0.0f, 0.0f }, + { 0.0f, 3.0f, 0.0f, 0.0f }, + { 0.0f, 0.0f, 4.0f, 0.0f }, + { 1.0f, 1.0f, 1.0f, 1.0f } + }; + float inv[4][4]; + GaussJordanSolver::invert(a, inv, 1.0E-4f); + Matrix4f A(a); + Matrix4f invA(inv); + CHECK(A * invA == Matrix4f::identity()); +} + +TEST(GaussJordanSolver, Invert_singularMatrix_throws) +{ + // Third row is a linear combination (sum) of the first two: singular (determinant 0). + const float a[3][3] = { + { 1.0f, 2.0f, 3.0f }, + { 4.0f, 5.0f, 6.0f }, + { 5.0f, 7.0f, 9.0f } + }; + float inv[3][3]; + CHECK_THROWS(GaussJordanSolver::invert(a, inv, 1.0E-4f), NotInvertibleMatrixException); +} + +TEST(GaussJordanSolver, Invert_doesNotMutateInputArray) +{ + // a is deliberately non-const, so that a mutation would compile and be detected + float a[2][2] = { + { 4.0f, 7.0f }, + { 2.0f, 6.0f } + }; + const float original[2][2] = { { 4.0f, 7.0f }, { 2.0f, 6.0f } }; + float inv[2][2]; + GaussJordanSolver::invert(a, inv, 1.0E-4f); + for (int i = 0; i < 2; i++) + for (int j = 0; j < 2; j++) + CHECK_NEAR_TOL(a[i][j], original[i][j], 0.00001f); +} diff --git a/tests/TestGeometryTools.cpp b/tests/TestGeometryTools.cpp new file mode 100644 index 0000000..2319756 --- /dev/null +++ b/tests/TestGeometryTools.cpp @@ -0,0 +1,82 @@ +// +// MIT License +// Copyright(c) 2021 - 2026 Olivier BARRY +// +// This file is part of the C++ Aventura Project +// +// VectorMatrix Math Library +// +// Port of Aventura TestGeometryTools.java + +#include +#include +#include "TestFramework.h" +#include "GeometryTools.h" +#include "Vector4f.h" + +using namespace vectormatrix; + +TEST(GeometryTools, center_monoDimensional_normalCase) +{ + std::vector points = { + Vector4f(0.0f, 0.0f, 0.0f, 1.0f), + Vector4f(2.0f, 0.0f, 0.0f, 1.0f), + Vector4f(0.0f, 2.0f, 0.0f, 1.0f), + Vector4f(0.0f, 0.0f, 2.0f, 1.0f) + }; + std::optional center = GeometryTools::center(points); + CHECK(center.has_value()); + if (!center) return; + CHECK_NEAR_TOL(center->getX(), 0.5f, 0.00001f); + CHECK_NEAR_TOL(center->getY(), 0.5f, 0.00001f); + CHECK_NEAR_TOL(center->getZ(), 0.5f, 0.00001f); + CHECK_NEAR_TOL(center->getW(), 1.0f, 0.00001f); // center of points must remain a Point (w=1) +} + +TEST(GeometryTools, center_monoDimensional_singlePoint) +{ + std::vector points = { Vector4f(3.0f, 4.0f, 5.0f, 1.0f) }; + std::optional center = GeometryTools::center(points); + CHECK(center.has_value()); + if (!center) return; + CHECK_NEAR_TOL(center->getX(), 3.0f, 0.00001f); + CHECK_NEAR_TOL(center->getY(), 4.0f, 0.00001f); + CHECK_NEAR_TOL(center->getZ(), 5.0f, 0.00001f); +} + +TEST(GeometryTools, center_monoDimensional_emptyArray_returnsNull) +{ + std::optional center = GeometryTools::center(std::vector()); + CHECK(!center.has_value()); +} + +// Not ported: testCenter_monoDimensional_null_returnsNull (no null std::vector in C++) + +TEST(GeometryTools, center_biDimensional_normalCase) +{ + std::vector> points = { + { Vector4f(0.0f, 0.0f, 0.0f, 1.0f), Vector4f(4.0f, 0.0f, 0.0f, 1.0f) }, + { Vector4f(0.0f, 4.0f, 0.0f, 1.0f), Vector4f(0.0f, 0.0f, 4.0f, 1.0f) } + }; + std::optional center = GeometryTools::center(points); + CHECK(center.has_value()); + if (!center) return; + CHECK_NEAR_TOL(center->getX(), 1.0f, 0.00001f); + CHECK_NEAR_TOL(center->getY(), 1.0f, 0.00001f); + CHECK_NEAR_TOL(center->getZ(), 1.0f, 0.00001f); +} + +TEST(GeometryTools, center_biDimensional_emptyOuterArray_returnsNull) +{ + std::optional center = GeometryTools::center(std::vector>()); + CHECK(!center.has_value()); +} + +TEST(GeometryTools, center_biDimensional_emptyInnerArray_returnsNull) +{ + std::vector> points = { std::vector() }; + std::optional center = GeometryTools::center(points); + CHECK(!center.has_value()); +} + +// Not ported: testCenter_biDimensional_null_returnsNull (no null std::vector in C++) diff --git a/tests/TestMain.cpp b/tests/TestMain.cpp new file mode 100644 index 0000000..d3d6b2e --- /dev/null +++ b/tests/TestMain.cpp @@ -0,0 +1,69 @@ +// +// MIT License +// Copyright(c) 2021 - 2026 Olivier BARRY +// +// This file is part of the C++ Aventura Project +// +// VectorMatrix Math Library +// +// Runs all the registered tests, suite by suite. +// Run them with: ctest --test-dir build --output-on-failure +// An optional argument runs only the suites whose name contains it, e.g. vectormatrix_tests Matrix +// + +#include +#include +#include +#include +#include "TestFramework.h" + +int main(int argc, char* argv[]) +{ + using namespace testframework; + + std::string filter = argc > 1 ? argv[1] : ""; + + // Group by suite, keeping each suite's tests in registration order + std::map> suites; + for (const TestCase& test : registry()) + if (std::string(test.suite).find(filter) != std::string::npos) + suites[test.suite].push_back(test); + + int nbTests = 0; + int nbFailedTests = 0; + for (const auto& suite : suites) + { + int suiteFailed = 0; + for (const TestCase& test : suite.second) + { + int before = failures(); + try + { + test.function(); + } + catch (const std::exception& e) + { + std::cerr << " unexpected exception: " << e.what() << std::endl; + failures()++; + } + if (failures() != before) + { + std::cerr << " FAILED " << test.suite << "." << test.name << std::endl; + suiteFailed++; + } + } + nbTests += static_cast(suite.second.size()); + nbFailedTests += suiteFailed; + std::cout << (suiteFailed == 0 ? "[ OK ] " : "[FAILED] ") << suite.first + << " (" << suite.second.size() << " tests" << (suiteFailed ? ", " + std::to_string(suiteFailed) + " failed" : "") << ")" << std::endl; + } + + std::cout << std::endl; + if (nbFailedTests == 0) + { + std::cout << "All " << nbTests << " tests passed" << std::endl; + return 0; + } + std::cout << nbFailedTests << " of " << nbTests << " tests failed (" << failures() << " failed checks)" << std::endl; + return 1; +} diff --git a/tests/TestMatrix2f.cpp b/tests/TestMatrix2f.cpp new file mode 100644 index 0000000..9172686 --- /dev/null +++ b/tests/TestMatrix2f.cpp @@ -0,0 +1,480 @@ +// +// MIT License +// Copyright(c) 2021 - 2026 Olivier BARRY +// +// This file is part of the C++ Aventura Project +// +// VectorMatrix Math Library +// +// Port of Aventura TestMatrix2.java + +#include +#include "TestFramework.h" +#include "Vector2f.h" +#include "Matrix2f.h" +#include "Exceptions.h" + +using namespace vectormatrix; + +TEST(Matrix2f, Matrix2_0) +{ + Matrix2f A; + Matrix2f B(0.0f); + Matrix2f C(7.0f); + (void)C; + + CHECK_EQUAL(A, B); +} + +TEST(Matrix2f, array_0) +{ + float array[2][2]; + + for (int i = 0; i < 2; i++) + { + for (int j = 0; j < 2; j++) + { + array[i][j] = 0; + } + } + + Matrix2f A(array); + Matrix2f B; + + CHECK_EQUAL(A, B); +} + +TEST(Matrix2f, array_value) +{ + float array[2][2]; + + for (int i = 0; i < 2; i++) + { + for (int j = 0; j < 2; j++) + { + array[i][j] = 5; + } + } + array[1][0] = 22.3f; + + Matrix2f A(array); + Matrix2f B(5.0f); + B.set(1, 0, 22.3f); + + CHECK_EQUAL(A, B); +} + +TEST(Matrix2f, plus) +{ + float array[2][2]; + + for (int i = 0; i < 2; i++) + { + for (int j = 0; j < 2; j++) + { + array[i][j] = static_cast(i + j); + } + } + + Matrix2f A(array); + Matrix2f B(5.0f); + Matrix2f C = A + B; + + for (int i = 0; i < 2; i++) + { + for (int j = 0; j < 2; j++) + { + CHECK_NEAR_TOL(C.get(i, j), static_cast(i + j + 5), 0.0f); + } + } +} + +TEST(Matrix2f, minus) +{ + float array[2][2]; + + for (int i = 0; i < 2; i++) + { + for (int j = 0; j < 2; j++) + { + array[i][j] = static_cast(i + j); + } + } + + Matrix2f A(array); + Matrix2f B(2.0f); + Matrix2f C = A - B; + + for (int i = 0; i < 2; i++) + { + for (int j = 0; j < 2; j++) + { + CHECK_NEAR_TOL(C.get(i, j), static_cast(i + j - 2), 0.0f); + } + } +} + +TEST(Matrix2f, plusEquals) +{ + /* + * A=[[0.0, 1.0] + * [1.0, 2.0]] + * + * B=[[7.0, 6.0] + * [8.0, 7.0]] + */ + const float array1[2][2] = { { 0.0f, 1.0f }, { 1.0f, 2.0f } }; + const float array2[2][2] = { { 7.0f, 6.0f }, { 8.0f, 7.0f } }; + + Matrix2f A(array1); + Matrix2f B(array2); + A += B; + + CHECK_NEAR_TOL(A.get(0, 0), 7.0f, 0.0f); + CHECK_NEAR_TOL(A.get(0, 1), 7.0f, 0.0f); + CHECK_NEAR_TOL(A.get(1, 0), 9.0f, 0.0f); + CHECK_NEAR_TOL(A.get(1, 1), 9.0f, 0.0f); +} + +TEST(Matrix2f, minusEquals) +{ + const float array1[2][2] = { { 0.0f, 1.0f }, { 1.0f, 2.0f } }; + const float array2[2][2] = { { 7.0f, 6.0f }, { 8.0f, 7.0f } }; + + Matrix2f A(array1); + Matrix2f B(array2); + A -= B; + + CHECK_NEAR_TOL(A.get(0, 0), -7.0f, 0.0f); + CHECK_NEAR_TOL(A.get(0, 1), -5.0f, 0.0f); + CHECK_NEAR_TOL(A.get(1, 0), -7.0f, 0.0f); + CHECK_NEAR_TOL(A.get(1, 1), -5.0f, 0.0f); +} + +TEST(Matrix2f, times) +{ + /* + * A=[[0.0, 1.0] + * [1.0, 2.0]] + * + * B=[[7.0, 6.0] + * [8.0, 7.0]] + */ + const float array1[2][2] = { { 0.0f, 1.0f }, { 1.0f, 2.0f } }; + const float array2[2][2] = { { 7.0f, 6.0f }, { 8.0f, 7.0f } }; + + Matrix2f A(array1); + Matrix2f B(array2); + Matrix2f C = A * B; + + // Row0 = [0,1]: [0*7+1*8, 0*6+1*7] = [8, 7] + // Row1 = [1,2]: [1*7+2*8, 1*6+2*7] = [23, 20] + CHECK_NEAR_TOL(C.get(0, 0), 8.0f, 0.0f); + CHECK_NEAR_TOL(C.get(0, 1), 7.0f, 0.0f); + CHECK_NEAR_TOL(C.get(1, 0), 23.0f, 0.0f); + CHECK_NEAR_TOL(C.get(1, 1), 20.0f, 0.0f); +} + +TEST(Matrix2f, timesEquals) +{ + const float array1[2][2] = { { 0.0f, 1.0f }, { 1.0f, 2.0f } }; + const float array2[2][2] = { { 7.0f, 6.0f }, { 8.0f, 7.0f } }; + + Matrix2f A(array1); + Matrix2f B(array2); + Matrix2f expected = A * B; + A *= B; + + CHECK_EQUAL(A, expected); +} + +TEST(Matrix2f, times_scalar) +{ + const float array[2][2] = { { 1.0f, 2.0f }, { 3.0f, 4.0f } }; + Matrix2f A(array); + Matrix2f B = A * 2.0f; + CHECK_NEAR_TOL(B.get(0, 0), 2.0f, 0.0f); + CHECK_NEAR_TOL(B.get(0, 1), 4.0f, 0.0f); + CHECK_NEAR_TOL(B.get(1, 0), 6.0f, 0.0f); + CHECK_NEAR_TOL(B.get(1, 1), 8.0f, 0.0f); + + A *= 2.0f; + CHECK_EQUAL(A, B); +} + +TEST(Matrix2f, transpose1) +{ + const float array[2][2] = { { 2.0f, 1.0f }, { 3.0f, 2.0f } }; + Matrix2f A(array); + Matrix2f B = A.transpose(); + Matrix2f C = B.transpose(); + CHECK_EQUAL(C, A); +} + +TEST(Matrix2f, transpose2) +{ + const float array[2][2] = { { 2.0f, 1.0f }, { 3.0f, 2.0f } }; + Matrix2f A(array); + Matrix2f B(A); // Keep image of A before transposition + A.transposeEquals(); + Matrix2f C = A.transpose(); // Do not modify A for this transposition + CHECK_EQUAL(C, B); +} + +TEST(Matrix2f, inverse1) +{ + const float array[2][2] = { { 10.0f, 9.0f }, { 0.0f, 7.0f } }; + Matrix2f A(array); + Matrix2f B; + + CHECK_NO_THROW(B = A.inverse()); + Matrix2f C; + CHECK_NO_THROW(C = B.inverse()); + CHECK_EQUAL(C, A); +} + +TEST(Matrix2f, inverse2) +{ + const float array[2][2] = { { 10.0f, 9.0f }, { 0.0f, 7.0f } }; + Matrix2f A(array); + Matrix2f B; + CHECK_NO_THROW(B = A.inverse()); + Matrix2f C = B * A; // inverse(A).A = I + CHECK_EQUAL(C, Matrix2f::identity()); +} + +TEST(Matrix2f, inverse3) +{ + Matrix2f A(Matrix2f::identity()); + Matrix2f B; + CHECK_NO_THROW(B = A.inverse()); + CHECK_EQUAL(B, Matrix2f::identity()); +} + +TEST(Matrix2f, inverse_singularMatrix_throws) +{ + // Second row is 2x the first row: this matrix is singular (determinant 0). + const float array[2][2] = { { 1.0f, 2.0f }, { 2.0f, 4.0f } }; + Matrix2f singular(array); + CHECK_THROWS(singular.inverse(), NotInvertibleMatrixException); +} + +TEST(Matrix2f, inverse_precision_generalCase) +{ + const float array[2][2] = { { 4.0f, 7.0f }, { 2.0f, 6.0f } }; + Matrix2f a(array); + Matrix2f invA = a.inverse(); + Matrix2f product = a * invA; + CHECK_EQUAL(product, Matrix2f::identity()); +} + +TEST(Matrix2f, getRow_invalidIndex_throws) +{ + Matrix2f m(Matrix2f::identity()); + CHECK_THROWS(m.getRow(2), IndexOutOfBoundException); // valid indices are 0..1 +} + +TEST(Matrix2f, getColumn_invalidIndex_throws) +{ + Matrix2f m(Matrix2f::identity()); + CHECK_THROWS(m.getColumn(2), IndexOutOfBoundException); // valid indices are 0..1 +} + +// Not ported: testMatrix2_timesRow_invalidIndex_throws, timesRow is a protected Java helper absent from the C++ API + +TEST(Matrix2f, setRow_setColumn) +{ + Matrix2f m(0.0f); + m.setRow(1, Vector2f(1.0f, 2.0f)); + CHECK_NEAR_TOL(m.get(1, 0), 1.0f, 0.0f); + CHECK_NEAR_TOL(m.get(1, 1), 2.0f, 0.0f); + + m.setColumn(1, Vector2f(7.0f, 8.0f)); + CHECK_NEAR_TOL(m.get(0, 1), 7.0f, 0.0f); + CHECK_NEAR_TOL(m.get(1, 1), 8.0f, 0.0f); + + // round-trip: what we just set via setRow/setColumn must be readable via getRow/getColumn + Vector2f col1 = m.getColumn(1); + CHECK_NEAR_TOL(col1.getX(), 7.0f, 0.0f); + CHECK_NEAR_TOL(col1.getY(), 8.0f, 0.0f); +} + +TEST(Matrix2f, setRow_invalidIndex_throws) +{ + Matrix2f m(0.0f); + CHECK_THROWS(m.setRow(2, Vector2f(0.0f, 0.0f)), IndexOutOfBoundException); +} + +TEST(Matrix2f, setColumn_invalidIndex_throws) +{ + Matrix2f m(0.0f); + CHECK_THROWS(m.setColumn(2, Vector2f(0.0f, 0.0f)), IndexOutOfBoundException); +} + +TEST(Matrix2f, getArray_isDefensiveCopy) +{ + Matrix2f m(Matrix2f::identity()); + std::vector> arr = m.getArray(); + arr[0][0] = 999.0f; // mutate the returned array + + // The Matrix itself must be unaffected + CHECK_NEAR_TOL(m.get(0, 0), 1.0f, 0.0f); +} + +TEST(Matrix2f, trace) +{ + CHECK_NEAR_TOL(Matrix2f::identity().trace(), 2.0f, 0.00001f); + + const float array[2][2] = { { 2.0f, 0.0f }, { 0.0f, 5.0f } }; + Matrix2f m(array); + CHECK_NEAR_TOL(m.trace(), 7.0f, 0.00001f); +} + +TEST(Matrix2f, isIdentity) +{ + CHECK(Matrix2f::identity().isIdentity()); + CHECK(Matrix2f(Matrix2f::identity()).isIdentity()); + CHECK(!Matrix2f(0.0f).isIdentity()); + CHECK(!Matrix2f(1.0f).isIdentity()); // all-ones is not the Identity +} + +TEST(Matrix2f, equals_negativeCase) +{ + Matrix2f a(1.0f); + Matrix2f b(2.0f); + CHECK(!(a == b)); +} + +TEST(Matrix2f, times_isNotCommutative) +{ + const float arrayA[2][2] = { { 1.0f, 2.0f }, { 0.0f, 1.0f } }; + const float arrayB[2][2] = { { 1.0f, 0.0f }, { 3.0f, 1.0f } }; + Matrix2f a(arrayA); + Matrix2f b(arrayB); + + Matrix2f ab = a * b; + Matrix2f ba = b * a; + CHECK(!(ab == ba)); +} + +TEST(Matrix2f, determinant) +{ + CHECK_NEAR_TOL(Matrix2f::identity().determinant(), 1.0f, 0.00001f); + + const float diag[2][2] = { { 2.0f, 0.0f }, { 0.0f, 3.0f } }; + Matrix2f m(diag); + CHECK_NEAR_TOL(m.determinant(), 6.0f, 0.00001f); // diagonal matrix: det = product of diagonal + + // A known singular matrix (row1 = 2x row0) must have determinant 0 + const float sing[2][2] = { { 1.0f, 2.0f }, { 2.0f, 4.0f } }; + Matrix2f singular(sing); + CHECK_NEAR_TOL(singular.determinant(), 0.0f, 0.00001f); +} + +// Not ported: testMatrix2_equalsObject_and_hashCode, Java-only equals(Object)/hashCode contract + +TEST(Matrix2f, identity_isFreshIndependentCopyEachCall) +{ + // The reference identity check (i1 != i2 as Java references) is not meaningful in C++ + Matrix2f i1 = Matrix2f::identity(); + Matrix2f i2 = Matrix2f::identity(); + CHECK_EQUAL(i1, i2); + + // Mutating one call's result must never affect a later call's result. + i1.set(0, 0, 999.0f); + Matrix2f i3 = Matrix2f::identity(); + CHECK_NEAR_TOL(i3.get(0, 0), 1.0f, 0.0f); +} + +// Not ported: testMatrix2_swapRows_timesRow_areProtected_notPublicApi, protected Java API absent from C++ + +TEST(Matrix2f, constructor_array_isDefensiveCopy) +{ + float source[2][2] = { { 1.0f, 2.0f }, { 3.0f, 4.0f } }; + Matrix2f m(source); + + // Mutate the source array after construction: the Matrix must be unaffected. + source[0][0] = 999.0f; + CHECK_NEAR_TOL(m.get(0, 0), 1.0f, 0.0f); +} + +TEST(Matrix2f, setArray_isDefensiveCopy) +{ + Matrix2f m(0.0f); + std::vector> source = { { 1.0f, 2.0f }, { 3.0f, 4.0f } }; + m.setArray(source); + + source[0][0] = 999.0f; + CHECK_NEAR_TOL(m.get(0, 0), 1.0f, 0.0f); +} + +TEST(Matrix2f, setArray_wrongRowSize_throws) +{ + Matrix2f m(0.0f); + std::vector> a = { { 1.0f, 2.0f }, { 3.0f, 4.0f }, { 5.0f, 6.0f } }; // 3 rows instead of 2 + CHECK_THROWS(m.setArray(a), MatrixArrayWrongSizeException); +} + +TEST(Matrix2f, setArray_wrongColumnSize_throws) +{ + Matrix2f m(0.0f); + std::vector> a = { { 1.0f, 2.0f, 3.0f }, { 4.0f, 5.0f, 6.0f } }; // 3 columns instead of 2 + CHECK_THROWS(m.setArray(a), MatrixArrayWrongSizeException); +} + +TEST(Matrix2f, setDiagonal) +{ + Matrix2f m(0.0f); + m.setDiagonal(5.0f); + CHECK_NEAR_TOL(m.get(0, 0), 5.0f, 0.0f); + CHECK_NEAR_TOL(m.get(1, 1), 5.0f, 0.0f); + CHECK_NEAR_TOL(m.get(0, 1), 0.0f, 0.0f); + CHECK_NEAR_TOL(m.get(1, 0), 0.0f, 0.0f); +} + +// ----- times(Vector2f): Matrix2f's own version of the Matrix3f/Vector3fMatrix3f cross-check ----- + +TEST(Matrix2f, times_vector2_matchesManualComputation) +{ + const float array[2][2] = { { 2.0f, 3.0f }, { 4.0f, 5.0f } }; + Matrix2f A(array); + Vector2f v(1.0f, 2.0f); + + Vector2f w = A * v; + + // W = A.V : w.x = 2*1+3*2 = 8 ; w.y = 4*1+5*2 = 14 + CHECK_NEAR_TOL(w.getX(), 8.0f, 0.0001f); + CHECK_NEAR_TOL(w.getY(), 14.0f, 0.0001f); +} + +TEST(Matrix2f, times_vector2_identityIsNoOp) +{ + Vector2f v(3.5f, -2.25f); + Vector2f w = Matrix2f::identity() * v; + + CHECK_EQUAL(w, v); +} + +TEST(Matrix2f, times_vector2_matchesVector2TimesMatrix2) +{ + const float array[2][2] = { { 1.0f, 2.0f }, { 3.0f, 4.0f } }; + Matrix2f A(array); + Vector2f v(5.0f, 6.0f); + + Vector2f fromMatrix = A * v; + Vector2f fromVector = v * A; + + CHECK_EQUAL(fromMatrix, fromVector); +} + +TEST(Matrix2f, timesEquals_vector2_matchesTimes) +{ + const float array[2][2] = { { 2.0f, 0.0f }, { 0.0f, 3.0f } }; + Matrix2f A(array); + Vector2f v(4.0f, 5.0f); + + Vector2f expected = v * A; + v *= A; + + CHECK_EQUAL(v, expected); +} diff --git a/tests/TestMatrix3f.cpp b/tests/TestMatrix3f.cpp new file mode 100644 index 0000000..d3b4749 --- /dev/null +++ b/tests/TestMatrix3f.cpp @@ -0,0 +1,436 @@ +// +// MIT License +// Copyright(c) 2021 - 2026 Olivier BARRY +// +// This file is part of the C++ Aventura Project +// +// VectorMatrix Math Library +// +// Port of Aventura TestMatrix3.java + +#include +#include "TestFramework.h" +#include "Matrix3f.h" +#include "Vector3f.h" +#include "Exceptions.h" + +using namespace vectormatrix; + +TEST(Matrix3f, Matrix3_0) +{ + Matrix3f A; + Matrix3f B(0.0f); + Matrix3f C(7.0f); + (void)C; + + CHECK(A == B); +} + +TEST(Matrix3f, array_0) +{ + float array[3][3]; + for (int i = 0; i < 3; i++) + for (int j = 0; j < 3; j++) + array[i][j] = 0; + + Matrix3f A(array); + Matrix3f B; + + CHECK(A == B); +} + +TEST(Matrix3f, array_value) +{ + float array[3][3]; + for (int i = 0; i < 3; i++) + for (int j = 0; j < 3; j++) + array[i][j] = 5; + array[1][2] = 22.3f; + + Matrix3f A(array); + Matrix3f B(5.0f); + B.set(1, 2, 22.3f); + + CHECK(A == B); +} + +TEST(Matrix3f, plus) +{ + float array[3][3]; + for (int i = 0; i < 3; i++) + for (int j = 0; j < 3; j++) + array[i][j] = static_cast(i + j); + + Matrix3f A(array); + Matrix3f B(5.0f); + Matrix3f C = A + B; + + for (int i = 0; i < 3; i++) + for (int j = 0; j < 3; j++) + CHECK(C.get(i, j) == static_cast(i + j + 5)); +} + +TEST(Matrix3f, minus) +{ + float array[3][3]; + for (int i = 0; i < 3; i++) + for (int j = 0; j < 3; j++) + array[i][j] = static_cast(i + j); + + Matrix3f A(array); + Matrix3f B(2.0f); + Matrix3f C = A - B; + + for (int i = 0; i < 3; i++) + for (int j = 0; j < 3; j++) + CHECK(C.get(i, j) == static_cast(i + j - 2)); +} + +TEST(Matrix3f, plusEquals) +{ + float array1[3][3]; + float array2[3][3]; + for (int i = 0; i < 3; i++) + for (int j = 0; j < 3; j++) + { + array1[i][j] = static_cast(i + j); + array2[i][j] = static_cast(i - j + 7); + } + + Matrix3f A(array1); + Matrix3f B(array2); + A += B; + + CHECK(A.get(0, 0) == 7.0f && A.get(0, 1) == 7.0f && A.get(0, 2) == 7.0f); + CHECK(A.get(1, 0) == 9.0f && A.get(1, 1) == 9.0f && A.get(1, 2) == 9.0f); + CHECK(A.get(2, 0) == 11.0f && A.get(2, 1) == 11.0f && A.get(2, 2) == 11.0f); +} + +TEST(Matrix3f, minusEquals) +{ + float array1[3][3]; + float array2[3][3]; + for (int i = 0; i < 3; i++) + for (int j = 0; j < 3; j++) + { + array1[i][j] = static_cast(i + j); + array2[i][j] = static_cast(i - j + 7); + } + + Matrix3f A(array1); + Matrix3f B(array2); + A -= B; + + CHECK(A.get(0, 0) == -7.0f && A.get(0, 1) == -5.0f && A.get(0, 2) == -3.0f); + CHECK(A.get(1, 0) == -7.0f && A.get(1, 1) == -5.0f && A.get(1, 2) == -3.0f); + CHECK(A.get(2, 0) == -7.0f && A.get(2, 1) == -5.0f && A.get(2, 2) == -3.0f); +} + +TEST(Matrix3f, times) +{ + float array1[3][3]; + float array2[3][3]; + for (int i = 0; i < 3; i++) + for (int j = 0; j < 3; j++) + { + array1[i][j] = static_cast(i + j); + array2[i][j] = static_cast(i - j + 7); + } + + Matrix3f A(array1); + Matrix3f B(array2); + Matrix3f C = A * B; + + CHECK(C.get(0, 0) == 26.0f && C.get(0, 1) == 23.0f && C.get(0, 2) == 20.0f); + CHECK(C.get(1, 0) == 50.0f && C.get(1, 1) == 44.0f && C.get(1, 2) == 38.0f); + CHECK(C.get(2, 0) == 74.0f && C.get(2, 1) == 65.0f && C.get(2, 2) == 56.0f); +} + +TEST(Matrix3f, transpose1) +{ + float array[3][3]; + for (int i = 0; i < 3; i++) + for (int j = 0; j < 3; j++) + array[i][j] = static_cast(i - j + 2); + + Matrix3f A(array); + Matrix3f B = A.transpose(); + Matrix3f C = B.transpose(); + CHECK(A == C); +} + +TEST(Matrix3f, transpose2) +{ + float array[3][3]; + for (int i = 0; i < 3; i++) + for (int j = 0; j < 3; j++) + array[i][j] = static_cast(i - j + 2); + + Matrix3f A(array); + Matrix3f B(A); // Keep image of A before transposition + A.transposeEquals(); + Matrix3f C = A.transpose(); // Do not modify A for this transposition + CHECK(B == C); +} + +TEST(Matrix3f, inverse1) +{ + float array[3][3]; + for (int i = 0; i < 3; i++) + for (int j = 0; j < 3; j++) + array[i][j] = (i > j) ? 0.0f : static_cast(10 - 2 * i - j); + + Matrix3f A(array); + Matrix3f B; + bool invertible = true; + try + { + B = A.inverse(); // Calculate inverse + } + catch (const NotInvertibleMatrixException&) + { + invertible = false; + } + CHECK(invertible); + try + { + Matrix3f C = B.inverse(); // Inverse the inverse + CHECK(A == C); + } + catch (const NotInvertibleMatrixException&) + { + CHECK(!"Not invertible Matrix B"); + } +} + +TEST(Matrix3f, inverse2) +{ + float array[3][3]; + for (int i = 0; i < 3; i++) + for (int j = 0; j < 3; j++) + array[i][j] = (i > j) ? 0.0f : static_cast(10 - 2 * i - j); + + Matrix3f A(array); + try + { + Matrix3f B = A.inverse(); // Calculate inverse + Matrix3f C = B * A; // inverse(A).A = I + CHECK(C == Matrix3f::identity()); + } + catch (const NotInvertibleMatrixException&) + { + CHECK(!"Not invertible Matrix"); + } +} + +TEST(Matrix3f, inverse3) +{ + Matrix3f A(Matrix3f::identity()); + try + { + Matrix3f B = A.inverse(); // Calculate inverse + CHECK(B == Matrix3f::identity()); + } + catch (const NotInvertibleMatrixException&) + { + CHECK(!"Not invertible Matrix"); + } +} + +// ----- Additional tests (bounds regressions, new methods, singular matrix) ----- + +TEST(Matrix3f, getRow_invalidIndex_throws) +{ + Matrix3f m(Matrix3f::identity()); + CHECK_THROWS(m.getRow(3), IndexOutOfBoundException); // valid indices are 0..2 +} + +TEST(Matrix3f, getColumn_invalidIndex_throws) +{ + Matrix3f m(Matrix3f::identity()); + CHECK_THROWS(m.getColumn(3), IndexOutOfBoundException); // valid indices are 0..2 +} + +// Not ported: timesRow_invalidIndex_throws (timesRow is a protected Java helper, absent from the C++ API) + +TEST(Matrix3f, setRow_setColumn) +{ + Matrix3f m(0.0f); + m.setRow(1, Vector3f(1.0f, 2.0f, 3.0f)); + CHECK_NEAR_TOL(m.get(1, 0), 1.0f, 0.0f); + CHECK_NEAR_TOL(m.get(1, 1), 2.0f, 0.0f); + CHECK_NEAR_TOL(m.get(1, 2), 3.0f, 0.0f); + + m.setColumn(2, Vector3f(7.0f, 8.0f, 9.0f)); + CHECK_NEAR_TOL(m.get(0, 2), 7.0f, 0.0f); + CHECK_NEAR_TOL(m.get(1, 2), 8.0f, 0.0f); + CHECK_NEAR_TOL(m.get(2, 2), 9.0f, 0.0f); + + // round-trip: what we just set via setRow/setColumn must be readable via getRow/getColumn + Vector3f col2 = m.getColumn(2); + CHECK_NEAR_TOL(col2.getX(), 7.0f, 0.0f); + CHECK_NEAR_TOL(col2.getY(), 8.0f, 0.0f); + CHECK_NEAR_TOL(col2.getZ(), 9.0f, 0.0f); +} + +TEST(Matrix3f, setRow_invalidIndex_throws) +{ + Matrix3f m(0.0f); + CHECK_THROWS(m.setRow(3, Vector3f::zeroVector()), IndexOutOfBoundException); +} + +TEST(Matrix3f, setColumn_invalidIndex_throws) +{ + Matrix3f m(0.0f); + CHECK_THROWS(m.setColumn(3, Vector3f::zeroVector()), IndexOutOfBoundException); +} + +TEST(Matrix3f, getArray_isDefensiveCopy) +{ + Matrix3f m(Matrix3f::identity()); + std::vector> arr = m.getArray(); + arr[0][0] = 999.0f; // mutate the returned array + + // The Matrix itself must be unaffected + CHECK_NEAR_TOL(m.get(0, 0), 1.0f, 0.0f); +} + +TEST(Matrix3f, trace) +{ + CHECK_NEAR_TOL(Matrix3f::identity().trace(), 3.0f, 0.00001f); + + const float array[3][3] = { + { 2.0f, 0.0f, 0.0f }, + { 0.0f, 5.0f, 0.0f }, + { 0.0f, 0.0f, 9.0f } + }; + Matrix3f m(array); + CHECK_NEAR_TOL(m.trace(), 16.0f, 0.00001f); +} + +TEST(Matrix3f, isIdentity) +{ + CHECK(Matrix3f::identity().isIdentity()); + CHECK(Matrix3f(Matrix3f::identity()).isIdentity()); + CHECK(!Matrix3f(0.0f).isIdentity()); + CHECK(!Matrix3f(1.0f).isIdentity()); // all-ones is not the Identity +} + +TEST(Matrix3f, equals_negativeCase) +{ + Matrix3f a(1.0f); + Matrix3f b(2.0f); + CHECK(!(a == b)); +} + +TEST(Matrix3f, times_isNotCommutative) +{ + const float arrayA[3][3] = { + { 1.0f, 2.0f, 0.0f }, + { 0.0f, 1.0f, 0.0f }, + { 0.0f, 0.0f, 1.0f } + }; + const float arrayB[3][3] = { + { 1.0f, 0.0f, 0.0f }, + { 3.0f, 1.0f, 0.0f }, + { 0.0f, 0.0f, 1.0f } + }; + Matrix3f a(arrayA); + Matrix3f b(arrayB); + + Matrix3f ab = a * b; + Matrix3f ba = b * a; + CHECK(!(ab == ba)); +} + +TEST(Matrix3f, inverse_singularMatrix_throws) +{ + // Third row is a linear combination (sum) of the first two: this matrix is singular (determinant 0). + const float array[3][3] = { + { 1.0f, 2.0f, 3.0f }, + { 4.0f, 5.0f, 6.0f }, + { 5.0f, 7.0f, 9.0f } + }; + Matrix3f singular(array); + CHECK_THROWS(singular.inverse(), NotInvertibleMatrixException); +} + +TEST(Matrix3f, inverse_precision_generalCase) +{ + const float array[3][3] = { + { 4.0f, 7.0f, 2.0f }, + { 3.0f, 5.0f, 1.0f }, + { 2.0f, 3.0f, 1.0f } + }; + Matrix3f a(array); + Matrix3f invA = a.inverse(); + Matrix3f product = a * invA; + CHECK(product == Matrix3f::identity()); +} + +TEST(Matrix3f, constructor_array_isDefensiveCopy) +{ + float source[3][3] = { + { 1.0f, 2.0f, 3.0f }, + { 4.0f, 5.0f, 6.0f }, + { 7.0f, 8.0f, 9.0f } + }; + Matrix3f m(source); + + // Mutate the source array after construction: the Matrix must be unaffected. + source[0][0] = 999.0f; + CHECK_NEAR_TOL(m.get(0, 0), 1.0f, 0.0f); +} + +TEST(Matrix3f, setArray_isDefensiveCopy) +{ + Matrix3f m(0.0f); + std::vector> source = { + { 1.0f, 2.0f, 3.0f }, + { 4.0f, 5.0f, 6.0f }, + { 7.0f, 8.0f, 9.0f } + }; + m.setArray(source); + + source[0][0] = 999.0f; + CHECK_NEAR_TOL(m.get(0, 0), 1.0f, 0.0f); +} + +TEST(Matrix3f, determinant) +{ + CHECK_NEAR_TOL(Matrix3f::identity().determinant(), 1.0f, 0.00001f); + + const float diagonal[3][3] = { + { 2.0f, 0.0f, 0.0f }, + { 0.0f, 3.0f, 0.0f }, + { 0.0f, 0.0f, 4.0f } + }; + Matrix3f m(diagonal); + CHECK_NEAR_TOL(m.determinant(), 24.0f, 0.00001f); // diagonal matrix: det = product of diagonal + + // A known singular matrix (third row = row1+row2) must have determinant 0 + const float singularArray[3][3] = { + { 1.0f, 2.0f, 3.0f }, + { 4.0f, 5.0f, 6.0f }, + { 5.0f, 7.0f, 9.0f } + }; + Matrix3f singular(singularArray); + CHECK_NEAR_TOL(singular.determinant(), 0.0f, 0.00001f); +} + +// Not ported: equalsObject_and_hashCode (Java equals(Object)/null/other type and hashCode only) + +TEST(Matrix3f, identity_isFreshIndependentCopyEachCall) +{ + // The Java reference-identity check (i1 != i2) is meaningless by value; the value semantics are kept. + Matrix3f i1 = Matrix3f::identity(); + Matrix3f i2 = Matrix3f::identity(); + CHECK(i1 == i2); + + // Mutating one call's result must never affect a later call's result + i1.set(0, 0, 999.0f); + Matrix3f i3 = Matrix3f::identity(); + CHECK_NEAR_TOL(i3.get(0, 0), 1.0f, 0.0f); +} + +// Not ported: swapRows_timesRow_areProtected_notPublicApi (Java protected visibility test; swapRows absent from the C++ API) diff --git a/tests/TestMatrix4f.cpp b/tests/TestMatrix4f.cpp new file mode 100644 index 0000000..2a3eedb --- /dev/null +++ b/tests/TestMatrix4f.cpp @@ -0,0 +1,465 @@ +// +// MIT License +// Copyright(c) 2021 - 2026 Olivier BARRY +// +// This file is part of the C++ Aventura Project +// +// VectorMatrix Math Library +// +// Port of Aventura TestMatrix4.java + +#include +#include "TestFramework.h" +#include "Vector4f.h" +#include "Matrix3f.h" +#include "Matrix4f.h" +#include "Exceptions.h" + +using namespace vectormatrix; + +TEST(Matrix4f, Matrix4_0) +{ + Matrix4f A; + Matrix4f B(0.0f); + Matrix4f C(7.0f); + (void)C; + + CHECK_EQUAL(A, B); +} + +TEST(Matrix4f, array_0) +{ + float array[4][4]; + for (int i = 0; i < 4; i++) + for (int j = 0; j < 4; j++) + array[i][j] = 0; + + Matrix4f A(array); + Matrix4f B; + + CHECK_EQUAL(A, B); +} + +TEST(Matrix4f, array_value) +{ + float array[4][4]; + for (int i = 0; i < 4; i++) + for (int j = 0; j < 4; j++) + array[i][j] = 5; + array[1][2] = 22.3f; + + Matrix4f A(array); + Matrix4f B(5.0f); + B.set(1, 2, 22.3f); + + CHECK_EQUAL(A, B); +} + +TEST(Matrix4f, plus) +{ + float array[4][4]; + for (int i = 0; i < 4; i++) + for (int j = 0; j < 4; j++) + array[i][j] = static_cast(i + j); + + Matrix4f A(array); + Matrix4f B(5.0f); + Matrix4f C = A + B; + + for (int i = 0; i < 3; i++) + for (int j = 0; j < 3; j++) + CHECK_EQUAL(C.get(i, j), static_cast(i + j + 5)); +} + +TEST(Matrix4f, minus) +{ + float array[4][4]; + for (int i = 0; i < 4; i++) + for (int j = 0; j < 4; j++) + array[i][j] = static_cast(i + j); + + Matrix4f A(array); + Matrix4f B(2.0f); + Matrix4f C = A - B; + + for (int i = 0; i < 3; i++) + for (int j = 0; j < 3; j++) + CHECK_EQUAL(C.get(i, j), static_cast(i + j - 2)); +} + +TEST(Matrix4f, plusEquals) +{ + float array1[4][4]; + float array2[4][4]; + for (int i = 0; i < 4; i++) + { + for (int j = 0; j < 4; j++) + { + array1[i][j] = static_cast(i + j); + array2[i][j] = static_cast(i - j + 7); + } + } + + Matrix4f A(array1); + Matrix4f B(array2); + A += B; + + CHECK(A.get(0, 0) == 7.0f && A.get(0, 1) == 7.0f && A.get(0, 2) == 7.0f); + CHECK(A.get(1, 0) == 9.0f && A.get(1, 1) == 9.0f && A.get(1, 2) == 9.0f); + CHECK(A.get(2, 0) == 11.0f && A.get(2, 1) == 11.0f && A.get(2, 2) == 11.0f); +} + +TEST(Matrix4f, minusEquals) +{ + float array1[4][4]; + float array2[4][4]; + for (int i = 0; i < 4; i++) + { + for (int j = 0; j < 4; j++) + { + array1[i][j] = static_cast(i + j); + array2[i][j] = static_cast(i - j + 7); + } + } + + Matrix4f A(array1); + Matrix4f B(array2); + A -= B; + + CHECK(A.get(0, 0) == -7.0f && A.get(0, 1) == -5.0f && A.get(0, 2) == -3.0f); + CHECK(A.get(1, 0) == -7.0f && A.get(1, 1) == -5.0f && A.get(1, 2) == -3.0f); + CHECK(A.get(2, 0) == -7.0f && A.get(2, 1) == -5.0f && A.get(2, 2) == -3.0f); +} + +TEST(Matrix4f, times) +{ + float array1[4][4]; + float array2[4][4]; + for (int i = 0; i < 4; i++) + { + for (int j = 0; j < 4; j++) + { + array1[i][j] = static_cast(i + j); + array2[i][j] = static_cast(i - j + 7); + } + } + + Matrix4f A(array1); + Matrix4f B(array2); + Matrix4f C = A * B; + + CHECK(C.get(0, 0) == 56.0f && C.get(0, 1) == 50.0f && C.get(0, 2) == 44.0f && C.get(0, 3) == 38.0f); + CHECK(C.get(1, 0) == 90.0f && C.get(1, 1) == 80.0f && C.get(1, 2) == 70.0f && C.get(1, 3) == 60.0f); + CHECK(C.get(2, 0) == 124.0f && C.get(2, 1) == 110.0f && C.get(2, 2) == 96.0f && C.get(2, 3) == 82.0f); + CHECK(C.get(3, 0) == 158.0f && C.get(3, 1) == 140.0f && C.get(3, 2) == 122.0f && C.get(3, 3) == 104.0f); +} + +TEST(Matrix4f, transpose1) +{ + float array[4][4]; + for (int i = 0; i < 4; i++) + for (int j = 0; j < 4; j++) + array[i][j] = static_cast(i - j + 3); + + Matrix4f A(array); + Matrix4f B = A.transpose(); + Matrix4f C = B.transpose(); + + CHECK_EQUAL(A, C); +} + +TEST(Matrix4f, transpose2) +{ + float array[4][4]; + for (int i = 0; i < 4; i++) + for (int j = 0; j < 4; j++) + array[i][j] = static_cast(i - j + 2); + + Matrix4f A(array); + Matrix4f B(A); // Keep image of A before transposition + A.transposeEquals(); + Matrix4f C = A.transpose(); // Do not modify A for this transposition + + CHECK_EQUAL(B, C); +} + +TEST(Matrix4f, inverse1) +{ + float array[4][4]; + for (int i = 0; i < 4; i++) + for (int j = 0; j < 4; j++) + array[i][j] = (i > j) ? 0.0f : static_cast(10 - 2 * i - j); + + Matrix4f A(array); + Matrix4f B; + Matrix4f C; + + CHECK_NO_THROW(B = A.inverse()); // Calculate inverse + CHECK_NO_THROW(C = B.inverse()); // Inverse the inverse + CHECK_EQUAL(A, C); +} + +TEST(Matrix4f, inverse2) +{ + float array[4][4]; + for (int i = 0; i < 4; i++) + for (int j = 0; j < 4; j++) + array[i][j] = (i > j) ? 0.0f : static_cast(10 - 2 * i - j); + + Matrix4f A(array); + Matrix4f B; + + CHECK_NO_THROW(B = A.inverse()); // Calculate inverse + Matrix4f C = B * A; // inverse(A).A = I + CHECK_EQUAL(C, Matrix4f::identity()); +} + +TEST(Matrix4f, inverse3) +{ + Matrix4f A(Matrix4f::identity()); + Matrix4f B; + + CHECK_NO_THROW(B = A.inverse()); // Calculate inverse + CHECK_EQUAL(B, Matrix4f::identity()); +} + +TEST(Matrix4f, setDiagonal_regression_lastElement) +{ + Matrix4f m(0.0f); + m.setDiagonal(7.0f); + CHECK_NEAR_TOL(m.get(0, 0), 7.0f, 0.0f); + CHECK_NEAR_TOL(m.get(1, 1), 7.0f, 0.0f); + CHECK_NEAR_TOL(m.get(2, 2), 7.0f, 0.0f); + CHECK_NEAR_TOL(m.get(3, 3), 7.0f, 0.0f); // this element was left at 0 before the fix +} + +TEST(Matrix4f, getRow_invalidIndex_throws) +{ + Matrix4f m(Matrix4f::identity()); + CHECK_THROWS(m.getRow(4), IndexOutOfBoundException); // valid indices are 0..3 +} + +TEST(Matrix4f, getColumn_invalidIndex_throws) +{ + Matrix4f m(Matrix4f::identity()); + CHECK_THROWS(m.getColumn(4), IndexOutOfBoundException); // valid indices are 0..3 +} + +// Not ported: testMatrix4_timesRow_invalidIndex_throws (timesRow is protected in Java, absent from the C++ API) + +TEST(Matrix4f, setRow_invalidIndex_throws) +{ + Matrix4f m(0.0f); + CHECK_THROWS(m.setRow(4, Vector4f::zeroVector()), IndexOutOfBoundException); +} + +TEST(Matrix4f, setColumn_invalidIndex_throws) +{ + Matrix4f m(0.0f); + CHECK_THROWS(m.setColumn(4, Vector4f::zeroVector()), IndexOutOfBoundException); +} + +TEST(Matrix4f, setRow_setColumn_roundTrip) +{ + Matrix4f m(0.0f); + m.setRow(2, Vector4f(1.0f, 2.0f, 3.0f, 4.0f)); + Vector4f row2 = m.getRow(2); + CHECK_NEAR_TOL(row2.getX(), 1.0f, 0.0f); + CHECK_NEAR_TOL(row2.getW(), 4.0f, 0.0f); + + m.setColumn(3, Vector4f(5.0f, 6.0f, 7.0f, 8.0f)); + Vector4f col3 = m.getColumn(3); + CHECK_NEAR_TOL(col3.getX(), 5.0f, 0.0f); + CHECK_NEAR_TOL(col3.getW(), 8.0f, 0.0f); +} + +TEST(Matrix4f, getMatrix3_subMatrix) +{ + const float array[4][4] = { + { 1.0f, 2.0f, 3.0f, 99.0f }, + { 4.0f, 5.0f, 6.0f, 99.0f }, + { 7.0f, 8.0f, 9.0f, 99.0f }, + { 0.0f, 0.0f, 0.0f, 1.0f } + }; + Matrix4f m(array); + Matrix3f sub = m.getMatrix3(); + CHECK_NEAR_TOL(sub.get(0, 0), 1.0f, 0.0f); + CHECK_NEAR_TOL(sub.get(1, 1), 5.0f, 0.0f); + CHECK_NEAR_TOL(sub.get(2, 2), 9.0f, 0.0f); + CHECK_NEAR_TOL(sub.get(2, 1), 8.0f, 0.0f); +} + +TEST(Matrix4f, trace) +{ + CHECK_NEAR_TOL(Matrix4f::identity().trace(), 4.0f, 0.00001f); + + const float array[4][4] = { + { 2.0f, 0.0f, 0.0f, 0.0f }, + { 0.0f, 3.0f, 0.0f, 0.0f }, + { 0.0f, 0.0f, 4.0f, 0.0f }, + { 0.0f, 0.0f, 0.0f, 5.0f } + }; + Matrix4f m(array); + CHECK_NEAR_TOL(m.trace(), 14.0f, 0.00001f); +} + +TEST(Matrix4f, isIdentity) +{ + CHECK(Matrix4f::identity().isIdentity()); + CHECK(Matrix4f(Matrix4f::identity()).isIdentity()); + CHECK(!Matrix4f(0.0f).isIdentity()); +} + +TEST(Matrix4f, equals_negativeCase) +{ + Matrix4f a(1.0f); + Matrix4f b(2.0f); + CHECK(!(a == b)); +} + +TEST(Matrix4f, times_isNotCommutative) +{ + const float arrayA[4][4] = { + { 1.0f, 2.0f, 0.0f, 0.0f }, + { 0.0f, 1.0f, 0.0f, 0.0f }, + { 0.0f, 0.0f, 1.0f, 0.0f }, + { 0.0f, 0.0f, 0.0f, 1.0f } + }; + const float arrayB[4][4] = { + { 1.0f, 0.0f, 0.0f, 0.0f }, + { 3.0f, 1.0f, 0.0f, 0.0f }, + { 0.0f, 0.0f, 1.0f, 0.0f }, + { 0.0f, 0.0f, 0.0f, 1.0f } + }; + Matrix4f a(arrayA); + Matrix4f b(arrayB); + + Matrix4f ab = a * b; + Matrix4f ba = b * a; + CHECK(!(ab == ba)); +} + +TEST(Matrix4f, inverse_singularMatrix_throws) +{ + // Last row is entirely 0: this matrix is singular (determinant 0). + const float array[4][4] = { + { 1.0f, 2.0f, 3.0f, 4.0f }, + { 5.0f, 6.0f, 7.0f, 8.0f }, + { 9.0f, 10.0f, 11.0f, 12.0f }, + { 0.0f, 0.0f, 0.0f, 0.0f } + }; + Matrix4f singular(array); + CHECK_THROWS(singular.inverse(), NotInvertibleMatrixException); +} + +TEST(Matrix4f, inverse_precision_generalCase) +{ + const float array[4][4] = { + { 4.0f, 7.0f, 2.0f, 1.0f }, + { 3.0f, 5.0f, 1.0f, 2.0f }, + { 2.0f, 3.0f, 1.0f, 0.0f }, + { 1.0f, 0.0f, 2.0f, 3.0f } + }; + Matrix4f a(array); + Matrix4f invA = a.inverse(); + Matrix4f product = a * invA; + CHECK_EQUAL(product, Matrix4f::identity()); +} + +TEST(Matrix4f, constructor_array_isDefensiveCopy) +{ + float source[4][4] = { + { 1.0f, 0.0f, 0.0f, 0.0f }, + { 0.0f, 1.0f, 0.0f, 0.0f }, + { 0.0f, 0.0f, 1.0f, 0.0f }, + { 0.0f, 0.0f, 0.0f, 1.0f } + }; + Matrix4f m(source); + + source[0][0] = 999.0f; + CHECK_NEAR_TOL(m.get(0, 0), 1.0f, 0.0f); +} + +TEST(Matrix4f, setArray_isDefensiveCopy) +{ + Matrix4f m(0.0f); + std::vector> source = { + { 1.0f, 2.0f, 3.0f, 4.0f }, + { 5.0f, 6.0f, 7.0f, 8.0f }, + { 9.0f, 10.0f, 11.0f, 12.0f }, + { 13.0f, 14.0f, 15.0f, 16.0f } + }; + m.setArray(source); + + source[0][0] = 999.0f; + CHECK_NEAR_TOL(m.get(0, 0), 1.0f, 0.0f); +} + +TEST(Matrix4f, getArray_isDefensiveCopy) +{ + Matrix4f m(Matrix4f::identity()); + std::vector> arr = m.getArray(); + arr[0][0] = 999.0f; + + CHECK_NEAR_TOL(m.get(0, 0), 1.0f, 0.0f); +} + +TEST(Matrix4f, determinant) +{ + CHECK_NEAR_TOL(Matrix4f::identity().determinant(), 1.0f, 0.00001f); + + const float array[4][4] = { + { 2.0f, 0.0f, 0.0f, 0.0f }, + { 0.0f, 3.0f, 0.0f, 0.0f }, + { 0.0f, 0.0f, 4.0f, 0.0f }, + { 0.0f, 0.0f, 0.0f, 5.0f } + }; + Matrix4f m(array); + CHECK_NEAR_TOL(m.determinant(), 120.0f, 0.0001f); // diagonal matrix: det = product of diagonal + + // Last row all zero -> singular -> determinant 0 + const float arraySingular[4][4] = { + { 1.0f, 2.0f, 3.0f, 4.0f }, + { 5.0f, 6.0f, 7.0f, 8.0f }, + { 9.0f, 10.0f, 11.0f, 12.0f }, + { 0.0f, 0.0f, 0.0f, 0.0f } + }; + Matrix4f singular(arraySingular); + CHECK_NEAR_TOL(singular.determinant(), 0.0f, 0.0001f); +} + +TEST(Matrix4f, setArrayOfGetArray_realWorldPattern) +{ + // Mirrors the pattern used in LookAt: combine two matrices, then adopt the result's array + // into a third matrix via getArray()/setArray(). + const float arrayB[4][4] = { + { 1.0f, 0.0f, 0.0f, 2.0f }, + { 0.0f, 1.0f, 0.0f, 3.0f }, + { 0.0f, 0.0f, 1.0f, 4.0f }, + { 0.0f, 0.0f, 0.0f, 1.0f } + }; + Matrix4f b(arrayB); + Matrix4f c(Matrix4f::identity()); + + Matrix4f a(0.0f); + a.setArray((b * c).getArray()); + + CHECK_EQUAL(a, b); +} + +// Not ported: testMatrix4_equalsObject_and_hashCode (Java-only equals(Object)/null/other type and hashCode) + +TEST(Matrix4f, identity_isFreshIndependentCopyEachCall) +{ + // The Java reference-identity check (i1 != i2) is not applicable to C++ values; the by-value part is ported. + Matrix4f i1 = Matrix4f::identity(); + Matrix4f i2 = Matrix4f::identity(); + CHECK_EQUAL(i1, i2); + + // Mutating one call's result must never affect a later call's result + i1.set(0, 0, 999.0f); + Matrix4f i3 = Matrix4f::identity(); + CHECK_NEAR_TOL(i3.get(0, 0), 1.0f, 0.0f); +} + +// Not ported: testMatrix4_swapRows_timesRow_areProtected_notPublicApi (protected API visibility test, swapRows absent from the C++ API) diff --git a/tests/TestQuaternionf.cpp b/tests/TestQuaternionf.cpp new file mode 100644 index 0000000..4da928e --- /dev/null +++ b/tests/TestQuaternionf.cpp @@ -0,0 +1,348 @@ +// +// MIT License +// Copyright(c) 2021 - 2026 Olivier BARRY +// +// This file is part of the C++ Aventura Project +// +// VectorMatrix Math Library +// +// Port of Aventura TestQuaternion.java +// + +#include +#include "TestFramework.h" +#include "Vector3f.h" +#include "Matrix3f.h" +#include "Matrix4f.h" +#include "Quaternionf.h" +#include "Exceptions.h" + +using namespace vectormatrix; + +static const float DELTA = 0.0001f; +static const float PI_F = static_cast(3.14159265358979323846); + +// Replacement for Aventura Rotation(a, v) (class absent from the C++ lib): same formula as +// Rotation.initRotation(a, v), applied on an identity Matrix4f +static Matrix4f rotationMatrix(float a, const Vector3f& v) +{ + Vector3f v1(v); + v1.normalize(); + float x = v1.getX(); + float y = v1.getY(); + float z = v1.getZ(); + float c = std::cos(a); + float s = std::sin(a); + + Matrix4f r = Matrix4f::identity(); + // First row + r.set(0, 0, x * x + (1 - x * x) * c); + r.set(0, 1, x * y * (1 - c) - z * s); + r.set(0, 2, x * z * (1 - c) + y * s); + // Second row + r.set(1, 0, x * y * (1 - c) + z * s); + r.set(1, 1, y * y + (1 - y * y) * c); + r.set(1, 2, y * z * (1 - c) - x * s); + // Third row + r.set(2, 0, x * z * (1 - c) - y * s); + r.set(2, 1, y * z * (1 - c) + x * s); + r.set(2, 2, z * z + (1 - z * z) * c); + return r; +} + +TEST(Quaternionf, identity) +{ + Quaternionf q; + CHECK_NEAR_TOL(q.getX(), 0.0f, 0.0f); + CHECK_NEAR_TOL(q.getY(), 0.0f, 0.0f); + CHECK_NEAR_TOL(q.getZ(), 0.0f, 0.0f); + CHECK_NEAR_TOL(q.getW(), 1.0f, 0.0f); +} + +TEST(Quaternionf, identity_toMatrix4IsIdentity) +{ + Quaternionf q; + CHECK(q.toMatrix4().isIdentity()); +} + +TEST(Quaternionf, constructor_fourArgs) +{ + Quaternionf q(1.0f, 2.0f, 3.0f, 4.0f); + CHECK_NEAR_TOL(q.getX(), 1.0f, 0.0f); + CHECK_NEAR_TOL(q.getY(), 2.0f, 0.0f); + CHECK_NEAR_TOL(q.getZ(), 3.0f, 0.0f); + CHECK_NEAR_TOL(q.getW(), 4.0f, 0.0f); +} + +TEST(Quaternionf, copyConstructor) +{ + Quaternionf q1(1.0f, 2.0f, 3.0f, 4.0f); + Quaternionf q2(q1); + CHECK(q1 == q2); + // Verify it's a real copy + q2.setX(99.0f); + CHECK_NEAR_TOL(q1.getX(), 1.0f, 0.0f); +} + +TEST(Quaternionf, getSet_byIndex) +{ + Quaternionf q; + q.set(0, 10.0f); + q.set(1, 20.0f); + q.set(2, 30.0f); + q.set(3, 40.0f); + CHECK_NEAR_TOL(q.get(0), 10.0f, 0.0f); + CHECK_NEAR_TOL(q.get(1), 20.0f, 0.0f); + CHECK_NEAR_TOL(q.get(2), 30.0f, 0.0f); + CHECK_NEAR_TOL(q.get(3), 40.0f, 0.0f); + CHECK_NEAR_TOL(q.get(0), q.getX(), 0.0f); + CHECK_NEAR_TOL(q.get(3), q.getW(), 0.0f); +} + +TEST(Quaternionf, get_invalidIndex_throws) +{ + CHECK_THROWS(Quaternionf().get(4), IndexOutOfBoundException); +} + +TEST(Quaternionf, set_invalidIndex_throws) +{ + Quaternionf q; + CHECK_THROWS(q.set(4, 1.0f), IndexOutOfBoundException); +} + +TEST(Quaternionf, length_ofIdentityIsOne) +{ + CHECK_NEAR_TOL(Quaternionf().length(), 1.0f, DELTA); +} + +TEST(Quaternionf, lengthSquared_matchesLengthSquared) +{ + Quaternionf q(1.0f, 2.0f, 2.0f, 4.0f); // length = sqrt(1+4+4+16) = 5 + CHECK_NEAR_TOL(q.lengthSquared(), 25.0f, DELTA); + CHECK_NEAR_TOL(q.length(), 5.0f, DELTA); +} + +TEST(Quaternionf, normalize) +{ + Quaternionf q(0.0f, 0.0f, 0.0f, 5.0f); + q.normalize(); + CHECK_NEAR_TOL(q.length(), 1.0f, DELTA); + CHECK_NEAR_TOL(q.getW(), 1.0f, DELTA); +} + +TEST(Quaternionf, conjugate) +{ + Quaternionf q(1.0f, 2.0f, 3.0f, 4.0f); + Quaternionf c = q.conjugate(); + CHECK_NEAR_TOL(c.getX(), -1.0f, 0.0f); + CHECK_NEAR_TOL(c.getY(), -2.0f, 0.0f); + CHECK_NEAR_TOL(c.getZ(), -3.0f, 0.0f); + CHECK_NEAR_TOL(c.getW(), 4.0f, 0.0f); + // Original must not be modified + CHECK_NEAR_TOL(q.getX(), 1.0f, 0.0f); +} + +TEST(Quaternionf, inverse_ofUnitQuaternionEqualsConjugate) +{ + Quaternionf q(Vector3f::xAxis(), PI_F / 3); + Quaternionf inv = q.inverse(); + Quaternionf conj = q.conjugate(); + CHECK(inv == conj); +} + +TEST(Quaternionf, inverse_timesOriginalIsIdentity) +{ + Quaternionf q(Vector3f::yAxis(), PI_F / 5); + Quaternionf result = q * q.inverse(); + CHECK(result == Quaternionf()); +} + +TEST(Quaternionf, times_withIdentityIsUnchanged) +{ + Quaternionf q(Vector3f::zAxis(), PI_F / 4); + Quaternionf result = q * Quaternionf(); + CHECK(result == q); +} + +TEST(Quaternionf, timesEquals_matchesTimes) +{ + Quaternionf q1(Vector3f::xAxis(), PI_F / 6); + Quaternionf q2(Vector3f::yAxis(), PI_F / 7); + Quaternionf expected = q1 * q2; + q1 *= q2; + CHECK(expected == q1); +} + +TEST(Quaternionf, dot_ofIdenticalUnitQuaternionIsOne) +{ + Quaternionf q(Vector3f::xAxis(), PI_F / 3); + CHECK_NEAR_TOL(q.dot(q), 1.0f, DELTA); +} + +TEST(Quaternionf, axisAngleConstructor_matchesRotationClass) +{ + float angle = PI_F / 3; + Quaternionf q(Vector3f::xAxis(), angle); + Matrix4f r = rotationMatrix(angle, Vector3f::xAxis()); + CHECK(q.toMatrix4() == r); +} + +TEST(Quaternionf, axisAngleConstructor_matchesRotationClass_yAxis) +{ + float angle = PI_F / 4; + Quaternionf q(Vector3f::yAxis(), angle); + Matrix4f r = rotationMatrix(angle, Vector3f::yAxis()); + CHECK(q.toMatrix4() == r); +} + +TEST(Quaternionf, axisAngleConstructor_matchesRotationClass_zAxis) +{ + float angle = PI_F / 5; + Quaternionf q(Vector3f::zAxis(), angle); + Matrix4f r = rotationMatrix(angle, Vector3f::zAxis()); + CHECK(q.toMatrix4() == r); +} + +TEST(Quaternionf, toMatrix3_matchesToMatrix4UpperLeft) +{ + Quaternionf q(Vector3f::xAxis(), PI_F / 3); + Matrix3f m3 = q.toMatrix3(); + Matrix4f m4 = q.toMatrix4(); + for (int i = 0; i < 3; i++) + for (int j = 0; j < 3; j++) + CHECK_NEAR_TOL(m3.get(i, j), m4.get(i, j), DELTA); +} + +TEST(Quaternionf, matrix3Constructor_roundTrip) +{ + Quaternionf original(Vector3f::xAxis(), PI_F / 3); + Matrix3f m = original.toMatrix3(); + Quaternionf rebuilt(m); + // May differ by an overall sign (q and -q represent the same rotation matrix): compare the matrices + CHECK(original.toMatrix3() == rebuilt.toMatrix3()); +} + +TEST(Quaternionf, matrix4Constructor_roundTrip) +{ + Quaternionf original(Vector3f::yAxis(), PI_F / 4); + Matrix4f m = original.toMatrix4(); + Quaternionf rebuilt(m); + CHECK(original.toMatrix4() == rebuilt.toMatrix4()); +} + +TEST(Quaternionf, matrix3Constructor_matchesMatrix4Constructor) +{ + float angle = PI_F / 3; + Matrix4f r = rotationMatrix(angle, Vector3f::zAxis()); + + float upperLeft3x3[3][3]; + for (int i = 0; i < 3; i++) + for (int j = 0; j < 3; j++) + upperLeft3x3[i][j] = r.get(i, j); + Matrix3f m3(upperLeft3x3); + + Quaternionf fromMatrix3(m3); + Quaternionf fromMatrix4(r); + + // May differ by an overall sign (q and -q represent the same rotation): compare the matrices + CHECK(fromMatrix3.toMatrix4() == fromMatrix4.toMatrix4()); +} + +TEST(Quaternionf, toAxisAngle_roundTrip) +{ + Vector3f originalAxis = Vector3f::xAxis(); + float originalAngle = PI_F / 3; + Quaternionf q(originalAxis, originalAngle); + + Vector3f recoveredAxis; + float recoveredAngle = q.toAxisAngle(recoveredAxis); + + CHECK_NEAR_TOL(recoveredAngle, originalAngle, DELTA); + CHECK(originalAxis == recoveredAxis); +} + +TEST(Quaternionf, toAxisAngle_identityHasZeroAngle) +{ + Quaternionf q; + Vector3f axis; + float angle = q.toAxisAngle(axis); + CHECK_NEAR_TOL(angle, 0.0f, DELTA); +} + +TEST(Quaternionf, slerp_atZeroReturnsStart) +{ + Quaternionf q1(Vector3f::xAxis(), PI_F / 6); + Quaternionf q2(Vector3f::xAxis(), PI_F / 2); + Quaternionf result = Quaternionf::slerp(q1, q2, 0.0f); + CHECK(q1 == result); +} + +TEST(Quaternionf, slerp_atOneReturnsEnd) +{ + Quaternionf q1(Vector3f::xAxis(), PI_F / 6); + Quaternionf q2(Vector3f::xAxis(), PI_F / 2); + Quaternionf result = Quaternionf::slerp(q1, q2, 1.0f); + CHECK(q2 == result); +} + +TEST(Quaternionf, slerp_atHalfway_sameAxis_isAverageAngle) +{ + Quaternionf q1(Vector3f::xAxis(), PI_F / 6); + Quaternionf q2(Vector3f::xAxis(), PI_F / 2); + Quaternionf expected(Vector3f::xAxis(), (PI_F / 6 + PI_F / 2) / 2); + Quaternionf result = Quaternionf::slerp(q1, q2, 0.5f); + CHECK(expected == result); +} + +TEST(Quaternionf, slerp_resultIsUnitLength) +{ + Quaternionf q1(Vector3f::xAxis(), PI_F / 6); + Quaternionf q2(Vector3f::yAxis(), PI_F / 2); + Quaternionf result = Quaternionf::slerp(q1, q2, 0.37f); + CHECK_NEAR_TOL(result.length(), 1.0f, DELTA); +} + +TEST(Quaternionf, slerp_takesShorterPath) +{ + Quaternionf q1(Vector3f::xAxis(), PI_F / 6); + // -q1 represents the exact same rotation as q1, but dot(q1, -q1) = -1: slerp must still + // produce a valid, unit-length interpolation rather than blowing up or taking the long way + Quaternionf negQ1(-q1.getX(), -q1.getY(), -q1.getZ(), -q1.getW()); + Quaternionf q2(Vector3f::xAxis(), PI_F / 2); + Quaternionf result = Quaternionf::slerp(negQ1, q2, 0.5f); + CHECK_NEAR_TOL(result.length(), 1.0f, DELTA); + // The rotation it represents should still land at the same matrix as slerping q1->q2 directly + Quaternionf directResult = Quaternionf::slerp(q1, q2, 0.5f); + CHECK(directResult.toMatrix4() == result.toMatrix4()); +} + +TEST(Quaternionf, slerp_nearIdenticalQuaternions_fallsBackToLerp) +{ + Quaternionf q1(Vector3f::xAxis(), PI_F / 6); + Quaternionf q2(Vector3f::xAxis(), PI_F / 6 + 0.00001f); + Quaternionf result = Quaternionf::slerp(q1, q2, 0.5f); + CHECK(!std::isnan(result.getX())); + CHECK(!std::isnan(result.getW())); + CHECK_NEAR_TOL(result.length(), 1.0f, DELTA); +} + +TEST(Quaternionf, equals_negativeCase) +{ + Quaternionf q1(1.0f, 2.0f, 3.0f, 4.0f); + Quaternionf q2(9.0f, 9.0f, 9.0f, 9.0f); + CHECK(!(q1 == q2)); +} + +// Not ported: testEquals_object_negativeCase_null (Java-only equals(Object) with null / other type) + +// Not ported: testHashCode_consistentWithEquals (Java-only hashCode) + +// Rotation(Quaternion) is Rotation(q.toMatrix4()) in Aventura: compare toMatrix4() with Rotation(angle, axis) +TEST(Quaternionf, rotationConstructor_fromQuaternion) +{ + float angle = PI_F / 4; + Quaternionf q(Vector3f::yAxis(), angle); + Matrix4f fromQuaternion = q.toMatrix4(); + Matrix4f fromAngleAxis = rotationMatrix(angle, Vector3f::yAxis()); + CHECK(fromQuaternion == fromAngleAxis); +} diff --git a/tests/TestTools.cpp b/tests/TestTools.cpp new file mode 100644 index 0000000..b7606a6 --- /dev/null +++ b/tests/TestTools.cpp @@ -0,0 +1,96 @@ +// +// MIT License +// Copyright(c) 2021 - 2026 Olivier BARRY +// +// This file is part of the C++ Aventura Project +// +// VectorMatrix Math Library +// +// Port of Aventura TestTools.java + +#include "TestFramework.h" +#include "Tools.h" +#include "Vector2f.h" +#include "Vector3f.h" +#include "Vector4f.h" + +using namespace vectormatrix; + +TEST(Tools, interpolate_vector4_endpoints) +{ + Vector4f a(0.0f, 0.0f, 0.0f, 1.0f); + Vector4f b(10.0f, 20.0f, 30.0f, 1.0f); + + Vector4f atStart = Tools::interpolate(a, b, 0.0f); + CHECK(atStart == a); + + Vector4f atEnd = Tools::interpolate(a, b, 1.0f); + CHECK(atEnd == b); +} + +TEST(Tools, interpolate_vector4_midpoint) +{ + Vector4f a(0.0f, 0.0f, 0.0f, 1.0f); + Vector4f b(10.0f, 20.0f, 30.0f, 1.0f); + + Vector4f mid = Tools::interpolate(a, b, 0.5f); + CHECK_NEAR_TOL(mid.getX(), 5.0f, 0.00001f); + CHECK_NEAR_TOL(mid.getY(), 10.0f, 0.00001f); + CHECK_NEAR_TOL(mid.getZ(), 15.0f, 0.00001f); +} + +TEST(Tools, interpolate_vector4_extrapolation) +{ + Vector4f a(0.0f, 0.0f, 0.0f, 1.0f); + Vector4f b(10.0f, 0.0f, 0.0f, 1.0f); + + Vector4f beyondB = Tools::interpolate(a, b, 2.0f); + CHECK_NEAR_TOL(beyondB.getX(), 20.0f, 0.00001f); // beyond B, on the (AB) line + + Vector4f beforeA = Tools::interpolate(a, b, -1.0f); + CHECK_NEAR_TOL(beforeA.getX(), -10.0f, 0.00001f); // beyond A, on the (AB) line +} + +TEST(Tools, interpolate_vector3_endpointsAndMidpoint) +{ + Vector3f a(0.0f, 0.0f, 0.0f); + Vector3f b(4.0f, 8.0f, 12.0f); + + CHECK(Tools::interpolate(a, b, 0.0f) == a); + CHECK(Tools::interpolate(a, b, 1.0f) == b); + + Vector3f mid = Tools::interpolate(a, b, 0.5f); + CHECK_NEAR_TOL(mid.getX(), 2.0f, 0.00001f); + CHECK_NEAR_TOL(mid.getY(), 4.0f, 0.00001f); + CHECK_NEAR_TOL(mid.getZ(), 6.0f, 0.00001f); +} + +TEST(Tools, interpolate_vector2_endpointsAndMidpoint) +{ + // Java passes t as a double here; the C++ Vector2f overload takes a float + Vector2f a(0.0f, 0.0f); + Vector2f b(6.0f, 10.0f); + + CHECK(Tools::interpolate(a, b, 0.0f) == a); + CHECK(Tools::interpolate(a, b, 1.0f) == b); + + Vector2f mid = Tools::interpolate(a, b, 0.5f); + CHECK_NEAR_TOL(mid.getX(), 3.0f, 0.00001f); + CHECK_NEAR_TOL(mid.getY(), 5.0f, 0.00001f); +} + +TEST(Tools, interpolate_scalar_double) +{ + CHECK_NEAR_TOL(Tools::interpolate(0.0, 10.0, 0.0), 0.0, 0.00001); + CHECK_NEAR_TOL(Tools::interpolate(0.0, 10.0, 1.0), 10.0, 0.00001); + CHECK_NEAR_TOL(Tools::interpolate(0.0, 10.0, 0.5), 5.0, 0.00001); + CHECK_NEAR_TOL(Tools::interpolate(0.0, 10.0, 2.0), 20.0, 0.00001); // extrapolation beyond b +} + +TEST(Tools, interpolate_scalar_float) +{ + CHECK_NEAR_TOL(Tools::interpolate(0.0f, 10.0f, 0.0f), 0.0f, 0.00001f); + CHECK_NEAR_TOL(Tools::interpolate(0.0f, 10.0f, 1.0f), 10.0f, 0.00001f); + CHECK_NEAR_TOL(Tools::interpolate(0.0f, 10.0f, 0.5f), 5.0f, 0.00001f); + CHECK_NEAR_TOL(Tools::interpolate(0.0f, 10.0f, -1.0f), -10.0f, 0.00001f); // extrapolation before a +} diff --git a/tests/TestVector2f.cpp b/tests/TestVector2f.cpp new file mode 100644 index 0000000..51733de --- /dev/null +++ b/tests/TestVector2f.cpp @@ -0,0 +1,206 @@ +// +// MIT License +// Copyright(c) 2021 - 2026 Olivier BARRY +// +// This file is part of the C++ Aventura Project +// +// VectorMatrix Math Library +// +// Port of Aventura TestVector2.java + +#include +#include +#include "TestFramework.h" +#include "Vector2f.h" +#include "Exceptions.h" + +using namespace vectormatrix; + +TEST(Vector2f, constructors) +{ + Vector2f v0; + CHECK_NEAR_TOL(v0.getX(), 0.0f, 0.0f); + CHECK_NEAR_TOL(v0.getY(), 0.0f, 0.0f); + + Vector2f v1(3.0f, 4.0f); + CHECK_NEAR_TOL(v1.getX(), 3.0f, 0.0f); + CHECK_NEAR_TOL(v1.getY(), 4.0f, 0.0f); + + // Copy constructor (the "distinct instance" check is Java reference identity, not ported) + Vector2f v2(v1); + CHECK_EQUAL(v2, v1); +} + +TEST(Vector2f, settersGetters) +{ + Vector2f v; + v.setX(5.0f); + v.setY(-2.0f); + CHECK_NEAR_TOL(v.getX(), 5.0f, 0.0f); + CHECK_NEAR_TOL(v.getY(), -2.0f, 0.0f); +} + +TEST(Vector2f, length) +{ + Vector2f v(3.0f, 4.0f); + CHECK_NEAR_TOL(v.length(), 5.0f, 0.00001f); +} + +TEST(Vector2f, lengthSquared) +{ + Vector2f v(3.0f, 4.0f); + CHECK_NEAR_TOL(v.lengthSquared(), 25.0f, 0.00001f); + // Consistency check: lengthSquared must always equal length()^2 + CHECK_NEAR_TOL(v.lengthSquared(), v.length() * v.length(), 0.001f); +} + +TEST(Vector2f, normalize) +{ + Vector2f v(6.0f, -8.0f); + v.normalize(); + CHECK_NEAR_TOL(v.length(), 1.0f, 0.00001f); + // direction must be preserved: still pointing in the same quadrant + CHECK(v.getX() > 0); + CHECK(v.getY() < 0); +} + +TEST(Vector2f, dot) +{ + Vector2f v1(1.0f, 0.0f); + Vector2f v2(0.0f, 1.0f); + // Orthogonal vectors: dot product is 0 + CHECK_NEAR_TOL(v1.dot(v2), 0.0f, 0.00001f); + + Vector2f v3(2.0f, 3.0f); + Vector2f v4(4.0f, 5.0f); + CHECK_NEAR_TOL(v3.dot(v4), 23.0f, 0.00001f); // 2*4 + 3*5 +} + +TEST(Vector2f, timesScalar) +{ + Vector2f v(2.0f, -3.0f); + Vector2f r = v * 2.0f; + CHECK_NEAR_TOL(r.getX(), 4.0f, 0.0f); + CHECK_NEAR_TOL(r.getY(), -6.0f, 0.0f); + + v *= 2.0f; + CHECK_NEAR_TOL(v.getX(), 4.0f, 0.0f); + CHECK_NEAR_TOL(v.getY(), -6.0f, 0.0f); +} + +TEST(Vector2f, plus) +{ + Vector2f v1(1.0f, 2.0f); + Vector2f v2(3.0f, 4.0f); + Vector2f v3 = v1 + v2; + CHECK_NEAR_TOL(v3.getX(), 4.0f, 0.0f); + CHECK_NEAR_TOL(v3.getY(), 6.0f, 0.0f); + + v1 += v2; + CHECK_EQUAL(v1, v3); +} + +TEST(Vector2f, minus) +{ + Vector2f v1(5.0f, 7.0f); + Vector2f v2(2.0f, 3.0f); + Vector2f v3 = v1 - v2; + CHECK_NEAR_TOL(v3.getX(), 3.0f, 0.0f); + CHECK_NEAR_TOL(v3.getY(), 4.0f, 0.0f); + + v1 -= v2; + CHECK_EQUAL(v1, v3); +} + +TEST(Vector2f, equals_instance) +{ + Vector2f v1(1.0f, 2.0f); + Vector2f v2(1.0f, 2.0f); + Vector2f v3(9.0f, 9.0f); + + CHECK(v1 == v2); + CHECK(!(v1 == v3)); +} + +// Not ported: testVector2_equals_static, no static equals(v1, v2) in C++ (operator== is covered by equals_instance) + +TEST(Vector2f, copy) +{ + // Java copy() maps to the C++ copy constructor + Vector2f v1(1.0f, 2.0f); + Vector2f v2(v1); + + CHECK_EQUAL(v2, v1); + + // Mutating the copy must not affect the original + v2.setX(999.0f); + CHECK_NEAR_TOL(v1.getX(), 1.0f, 0.0f); +} + +// Not ported: testVector2_legacyEqualsNoArg_stillWorksAsCopy, legacy deprecated Java method + +TEST(Vector2f, constructor_array_ok) +{ + std::vector array = { 3.0f, 4.0f }; + Vector2f v(array); + CHECK_NEAR_TOL(v.getX(), 3.0f, 0.0f); + CHECK_NEAR_TOL(v.getY(), 4.0f, 0.0f); + + float carray[2] = { 3.0f, 4.0f }; + Vector2f w(carray); + CHECK_NEAR_TOL(w.getX(), 3.0f, 0.0f); + CHECK_NEAR_TOL(w.getY(), 4.0f, 0.0f); +} + +TEST(Vector2f, constructor_array_tooShort) +{ + std::vector array = { 3.0f }; // only 1 element, Vector2f needs 2 + CHECK_THROWS(Vector2f(array), VectorArrayWrongSizeException); +} + +TEST(Vector2f, indexedGetSet_valid) +{ + Vector2f v; + v.set(0, 5.0f); + v.set(1, 6.0f); + CHECK_NEAR_TOL(v.get(0), 5.0f, 0.0f); + CHECK_NEAR_TOL(v.get(1), 6.0f, 0.0f); +} + +TEST(Vector2f, get_invalidIndex_throws) +{ + Vector2f v; + CHECK_THROWS(v.get(2), IndexOutOfBoundException); // valid indices are 0..1 +} + +TEST(Vector2f, set_invalidIndex_throws) +{ + Vector2f v; + CHECK_THROWS(v.set(2, 1.0f), IndexOutOfBoundException); // valid indices are 0..1 +} + +TEST(Vector2f, distance_distanceSquared) +{ + Vector2f v1(0.0f, 0.0f); + Vector2f v2(3.0f, 4.0f); + + CHECK_NEAR_TOL(v1.distanceSquared(v2), 25.0f, 0.00001f); + CHECK_NEAR_TOL(v1.distance(v2), 5.0f, 0.00001f); + CHECK_NEAR_TOL(v1.distance(v2), (v1 - v2).length(), 0.0001f); +} + +TEST(Vector2f, toArray) +{ + Vector2f v(1.0f, 2.0f); + std::array a = v.toArray(); + CHECK_NEAR_TOL(a[0], 1.0f, 0.00001f); + CHECK_NEAR_TOL(a[1], 2.0f, 0.00001f); + + float dest[2] = { 0.0f, 0.0f }; + float* r = v.toArray(dest); + CHECK(r == dest); + CHECK_NEAR_TOL(dest[0], 1.0f, 0.00001f); + CHECK_NEAR_TOL(dest[1], 2.0f, 0.00001f); +} + +// Not ported: testVector2_equalsObject_and_hashCode, Java-only equals(Object)/hashCode contract diff --git a/tests/TestVector3f.cpp b/tests/TestVector3f.cpp new file mode 100644 index 0000000..982dde4 --- /dev/null +++ b/tests/TestVector3f.cpp @@ -0,0 +1,348 @@ +// +// MIT License +// Copyright(c) 2021 - 2026 Olivier BARRY +// +// This file is part of the C++ Aventura Project +// +// VectorMatrix Math Library +// +// Port of Aventura TestVector3.java + +#include +#include "TestFramework.h" +#include "Vector3f.h" +#include "Vector4f.h" +#include "Matrix3f.h" +#include "Exceptions.h" + +using namespace vectormatrix; + +TEST(Vector3f, equals) +{ + Vector3f V1; + Vector3f V2(0.0f); + Vector3f V3(7.0f); + (void)V3; + + CHECK(V1 == V2); +} + +TEST(Vector3f, axis) +{ + const float x_axis[3] = { 1.0f, 0.0f, 0.0f }; + const float y_axis[3] = { 0.0f, 1.0f, 0.0f }; + const float z_axis[3] = { 0.0f, 0.0f, 1.0f }; + + Vector3f V1 = Vector3f::xAxis(); + Vector3f V2 = Vector3f::yAxis(); + Vector3f V3 = Vector3f::zAxis(); + + CHECK(V1 == Vector3f(x_axis)); + CHECK(V2 == Vector3f(y_axis)); + CHECK(V3 == Vector3f(z_axis)); +} + +TEST(Vector3f, length) +{ + const float values[3] = { 1.0f, 2.0f, 3.0f }; + Vector3f vector(values); + CHECK_NEAR_TOL(vector.length(), (float)std::sqrt(1 * 1 + 2 * 2 + 3 * 3), 0.00001f); +} + +TEST(Vector3f, normalize) +{ + Vector3f V1(12.0f, -12.0f, 24.0f); + CHECK(!(V1.getX() != 12.0f || V1.getY() != -12.0f || V1.getZ() != 24.0f)); + + V1.normalize(); + CHECK_NEAR_TOL(V1.length(), 1, 0.00001f); +} + +TEST(Vector3f, array_0) +{ + float array[3]; + for (int i = 0; i < 3; i++) + array[i] = 0; + + Vector3f V1(array); + Vector3f V2; + CHECK(V1 == V2); +} + +TEST(Vector3f, array_value) +{ + float array[3]; + for (int i = 0; i < 3; i++) + array[i] = 5; + array[1] = 22.3f; + + Vector3f V1(array); + Vector3f V2(5.0f); + V2.set(1, 22.3f); + CHECK(V1 == V2); +} + +TEST(Vector3f, plus) +{ + float array[3]; + for (int i = 0; i < 3; i++) + array[i] = (float)i; + + Vector3f V1(array); + Vector3f V2(5.0f); + Vector3f V3 = V1 + V2; + + for (int i = 0; i < 3; i++) + CHECK(V3.get(i) == i + 5); +} + +TEST(Vector3f, plusEquals) +{ + float array1[3]; + float array2[3]; + for (int i = 0; i < 3; i++) + array1[i] = (float)(7 - i); // (7, 6, 5) + for (int i = 0; i < 3; i++) + array2[i] = (float)(i * 2); // (0, 2, 4) + + Vector3f V1(array1); + Vector3f V2(array2); + V1 += V2; + + CHECK(V1.get(0) == 7.0f && V1.get(1) == 8.0f && V1.get(2) == 9.0f); +} + +TEST(Vector3f, minus) +{ + float array[3]; + for (int i = 0; i < 3; i++) + array[i] = (float)i; + + Vector3f V1(array); + Vector3f V2(2.0f); + Vector3f V3 = V1 - V2; + + for (int i = 0; i < 3; i++) + CHECK(V3.get(i) == i - 2); +} + +TEST(Vector3f, minusEquals) +{ + float array1[3]; + float array2[3]; + for (int i = 0; i < 3; i++) + array1[i] = (float)(7 - i); // (7, 6, 5) + for (int i = 0; i < 3; i++) + array2[i] = (float)(i * 2 - 3); // (-3, -1, 1) + + Vector3f V1(array1); + Vector3f V2(array2); + V1 -= V2; + + CHECK(V1.get(0) == 10.0f && V1.get(1) == 7.0f && V1.get(2) == 4.0f); +} + +TEST(Vector3f, times) +{ + float array[3]; + for (int i = 0; i < 3; i++) + array[i] = (float)(i + 1); // (1, 2, 3) + + Vector3f V1(array); + Vector3f V2 = V1 * 7.0f; + + CHECK(V2.get(0) == 7.0f && V2.get(1) == 14.0f && V2.get(2) == 21.0f); +} + +TEST(Vector3f, timesEquals) +{ + float array[3]; + for (int i = 0; i < 3; i++) + array[i] = (float)(i - 1); // (-1, 0, 1) + + Vector3f V1(array); + V1 *= 3.0f; + + CHECK(V1.get(0) == -3.0f && V1.get(1) == 0.0f && V1.get(2) == 3.0f); +} + +TEST(Vector3f, scalar) +{ + float array[3]; + for (int i = 0; i < 3; i++) + array[i] = (float)i; + + Vector3f V1(array); + Vector3f V2(2.0f); + float scal = V1.dot(V2); + + CHECK(scal == 6); +} + +TEST(Vector3f, timesVector) +{ + // a=(a1,a2,a3) and b=(b1,b2,b3) then a^b=(a2b3-a3b2, a3b1-a1b3, a1b2-a2b1) + float array1[3]; + float array2[3]; + for (int i = 0; i < 3; i++) + { + array1[i] = (float)i; + array2[i] = (float)(3 - i); + } + + Vector3f V1(array1); // V1=(0,1,2) + Vector3f V2(array2); // V2=(3,2,1) + Vector3f V3 = V1.cross(V2); // V3=(1x1-2x2, 2x3-0x1, 0x2-1x3)=(-3,6,-3) + + CHECK(V3.get(0) == -3.0f && V3.get(1) == 6.0f && V3.get(2) == -3.0f); +} + +TEST(Vector3f, timesEqualsVector) +{ + // a=(a1,a2,a3) and b=(b1,b2,b3) then a^b=(a2b3-a3b2, a3b1-a1b3, a1b2-a2b1) + float array1[3]; + float array2[3]; + for (int i = 0; i < 3; i++) + { + array1[i] = (float)(i + 1); + array2[i] = (float)(3 - i); + } + + Vector3f V1(array1); // V1=(1,2,3) + Vector3f V2(array2); // V2=(3,2,1) + V1.crossEquals(V2); // Result=(2x1-3x2, 3x3-1x1, 1x2-2x3)=(-4,8,-4) + + CHECK(V1.get(0) == -4.0f && V1.get(1) == 8.0f && V1.get(2) == -4.0f); +} + +TEST(Vector3f, get_invalidIndex_throws) +{ + Vector3f v; + CHECK_THROWS(v.get(3), IndexOutOfBoundException); // valid indices are 0..2 +} + +TEST(Vector3f, set_invalidIndex_throws) +{ + Vector3f v; + CHECK_THROWS(v.set(3, 1.0f), IndexOutOfBoundException); // valid indices are 0..2 +} + +TEST(Vector3f, equals_negativeCase) +{ + Vector3f v1(1.0f, 2.0f, 3.0f); + Vector3f v2(9.0f, 9.0f, 9.0f); + CHECK(!(v1 == v2)); +} + +TEST(Vector3f, dot_orthogonalIsZero) +{ + CHECK_NEAR_TOL(Vector3f::xAxis().dot(Vector3f::yAxis()), 0.0f, 0.00001f); + CHECK_NEAR_TOL(Vector3f::yAxis().dot(Vector3f::zAxis()), 0.0f, 0.00001f); +} + +TEST(Vector3f, cross_isAnticommutative) +{ + Vector3f v1(1.0f, 2.0f, 3.0f); + Vector3f v2(4.0f, 5.0f, 6.0f); + + Vector3f cross12 = v1.cross(v2); + Vector3f cross21 = v2.cross(v1); + + CHECK(cross12 == cross21 * -1.0f); +} + +TEST(Vector3f, constructor_fromVector4_dropsW) +{ + Vector4f v4(1.0f, 2.0f, 3.0f, 99.0f); + Vector3f v3(v4); + CHECK_NEAR_TOL(v3.getX(), 1.0f, 0.0f); + CHECK_NEAR_TOL(v3.getY(), 2.0f, 0.0f); + CHECK_NEAR_TOL(v3.getZ(), 3.0f, 0.0f); +} + +TEST(Vector3f, constructor_fromTwoVector4Points) +{ + Vector4f a(1.0f, 1.0f, 1.0f, 1.0f); + Vector4f b(4.0f, 6.0f, 8.0f, 1.0f); + Vector3f ab(a, b); + + CHECK_NEAR_TOL(ab.getX(), 3.0f, 0.0f); + CHECK_NEAR_TOL(ab.getY(), 5.0f, 0.0f); + CHECK_NEAR_TOL(ab.getZ(), 7.0f, 0.0f); +} + +TEST(Vector3f, constructor_fromMatrix3RowColumn) +{ + Matrix3f m(Matrix3f::identity()); + Vector3f row0(0, m); + CHECK_NEAR_TOL(row0.getX(), 1.0f, 0.0f); + CHECK_NEAR_TOL(row0.getY(), 0.0f, 0.0f); + + Vector3f col1(m, 1); + CHECK_NEAR_TOL(col1.getX(), 0.0f, 0.0f); + CHECK_NEAR_TOL(col1.getY(), 1.0f, 0.0f); +} + +TEST(Vector3f, V4_conversion) +{ + Vector3f v3(1.0f, 2.0f, 3.0f); + Vector4f v4 = v3.V4(); + CHECK_NEAR_TOL(v4.getX(), 1.0f, 0.0f); + CHECK_NEAR_TOL(v4.getY(), 2.0f, 0.0f); + CHECK_NEAR_TOL(v4.getZ(), 3.0f, 0.0f); +} + +TEST(Vector3f, lengthSquared) +{ + Vector3f v(2.0f, 3.0f, 6.0f); + CHECK_NEAR_TOL(v.lengthSquared(), 49.0f, 0.00001f); // 4+9+36 + CHECK_NEAR_TOL(v.lengthSquared(), v.length() * v.length(), 0.001f); +} + +TEST(Vector3f, distance_distanceSquared) +{ + Vector3f v1(0.0f, 0.0f, 0.0f); + Vector3f v2(2.0f, 3.0f, 6.0f); + + CHECK_NEAR_TOL(v1.distanceSquared(v2), 49.0f, 0.00001f); + CHECK_NEAR_TOL(v1.distance(v2), 7.0f, 0.00001f); + CHECK_NEAR_TOL(v1.distance(v2), (v1 - v2).length(), 0.0001f); +} + +TEST(Vector3f, toArray) +{ + Vector3f v(1.0f, 2.0f, 3.0f); + std::array a = v.toArray(); + CHECK_NEAR_TOL(a[0], 1.0f, 0.00001f); + CHECK_NEAR_TOL(a[1], 2.0f, 0.00001f); + CHECK_NEAR_TOL(a[2], 3.0f, 0.00001f); + + float dest[3] = { 0.0f, 0.0f, 0.0f }; + float* r = v.toArray(dest); + CHECK(r == dest); + CHECK_NEAR_TOL(dest[0], 1.0f, 0.00001f); + CHECK_NEAR_TOL(dest[1], 2.0f, 0.00001f); + CHECK_NEAR_TOL(dest[2], 3.0f, 0.00001f); +} + +// Not ported: testVector3_equalsObject_and_hashCode (Java-only equals(Object)/null/other type and hashCode) + +TEST(Vector3f, accessorConstants_areFreshIndependentCopies) +{ + // Java also checks that two calls return different references: meaningless in C++ (returned by value) + Vector3f a1 = Vector3f::xAxis(); + Vector3f a2 = Vector3f::xAxis(); + CHECK(a1 == a2); + + // Mutating one call's result must never affect a later call's result + a1.setX(999.0f); + Vector3f a3 = Vector3f::xAxis(); + CHECK_NEAR_TOL(a3.getX(), 1.0f, 0.0f); + + CHECK(Vector3f::yAxis() == Vector3f(0.0f, 1.0f, 0.0f)); + CHECK(Vector3f::zAxis() == Vector3f(0.0f, 0.0f, 1.0f)); + CHECK(Vector3f::xOppAxis() == Vector3f(-1.0f, 0.0f, 0.0f)); + CHECK(Vector3f::yOppAxis() == Vector3f(0.0f, -1.0f, 0.0f)); + CHECK(Vector3f::zOppAxis() == Vector3f(0.0f, 0.0f, -1.0f)); + CHECK(Vector3f::zeroVector() == Vector3f(0.0f, 0.0f, 0.0f)); +} diff --git a/tests/TestVector3fMatrix3f.cpp b/tests/TestVector3fMatrix3f.cpp new file mode 100644 index 0000000..1045342 --- /dev/null +++ b/tests/TestVector3fMatrix3f.cpp @@ -0,0 +1,57 @@ +// +// MIT License +// Copyright(c) 2021 - 2026 Olivier BARRY +// +// This file is part of the C++ Aventura Project +// +// VectorMatrix Math Library +// +// Port of Aventura TestVector3Matrix3.java + +#include "TestFramework.h" +#include "Vector3f.h" +#include "Matrix3f.h" + +using namespace vectormatrix; + +TEST(Vector3fMatrix3f, MatrixTimesVector) +{ + float array1[3]; + float array2[3][3]; + for (int i = 0; i < 3; i++) + array1[i] = (float)(i + 1); + for (int i = 0; i < 3; i++) + for (int j = 0; j < 3; j++) + array2[i][j] = (float)(i + j + 1); + /* + * A=[[1.0, 2.0, 3.0] + * [2.0, 3.0, 4.0] + * [3.0, 4.0, 5.0]] + */ + + Vector3f V1(array1); + Matrix3f A(array2); + Vector3f V2 = A * V1; + + CHECK(V2.get(0) == 14.0f && V2.get(1) == 20.0f && V2.get(2) == 26.0f); +} + +TEST(Vector3fMatrix3f, MatrixGetVector) +{ + float array[3][3]; + for (int i = 0; i < 3; i++) + for (int j = 0; j < 3; j++) + array[i][j] = (float)(i + j + 1); + /* + * A=[[1.0, 2.0, 3.0] + * [2.0, 3.0, 4.0] + * [3.0, 4.0, 5.0]] + */ + + Matrix3f A(array); + Vector3f V1 = A.getRow(1); // Second row + CHECK(V1.get(0) == 2.0f && V1.get(1) == 3.0f && V1.get(2) == 4.0f); + + Vector3f V2 = A.getColumn(2); // Third column + CHECK(V2.get(0) == 3.0f && V2.get(1) == 4.0f && V2.get(2) == 5.0f); +} diff --git a/tests/TestVector4f.cpp b/tests/TestVector4f.cpp new file mode 100644 index 0000000..e978e4c --- /dev/null +++ b/tests/TestVector4f.cpp @@ -0,0 +1,323 @@ +// +// MIT License +// Copyright(c) 2021 - 2026 Olivier BARRY +// +// This file is part of the C++ Aventura Project +// +// VectorMatrix Math Library +// +// Port of Aventura TestVector4.java +// + +#include +#include +#include +#include "TestFramework.h" +#include "Vector3f.h" +#include "Vector4f.h" +#include "Matrix4f.h" +#include "Exceptions.h" + +using namespace vectormatrix; + +TEST(Vector4f, constructors_basic) +{ + Vector4f v0; + CHECK_NEAR_TOL(v0.getX(), 0.0f, 0.0f); + CHECK_NEAR_TOL(v0.getY(), 0.0f, 0.0f); + CHECK_NEAR_TOL(v0.getZ(), 0.0f, 0.0f); + CHECK_NEAR_TOL(v0.getW(), 0.0f, 0.0f); + + Vector4f v1(2.0f); + CHECK_NEAR_TOL(v1.getX(), 2.0f, 0.0f); + CHECK_NEAR_TOL(v1.getW(), 2.0f, 0.0f); + + Vector4f v2(1.0f, 2.0f, 3.0f, 1.0f); + CHECK_NEAR_TOL(v2.getX(), 1.0f, 0.0f); + CHECK_NEAR_TOL(v2.getY(), 2.0f, 0.0f); + CHECK_NEAR_TOL(v2.getZ(), 3.0f, 0.0f); + CHECK_NEAR_TOL(v2.getW(), 1.0f, 0.0f); + + // Copy constructor (reference identity check of the Java test is meaningless by value) + Vector4f v3(v2); + CHECK_EQUAL(v3, v2); +} + +TEST(Vector4f, constructor_array_ok) +{ + float array[] = { 1.0f, 2.0f, 3.0f, 4.0f }; + Vector4f v(array); + CHECK_NEAR_TOL(v.getX(), 1.0f, 0.0f); + CHECK_NEAR_TOL(v.getW(), 4.0f, 0.0f); + + std::vector vec = { 1.0f, 2.0f, 3.0f, 4.0f }; + Vector4f w(vec); + CHECK_NEAR_TOL(w.getX(), 1.0f, 0.0f); + CHECK_NEAR_TOL(w.getW(), 4.0f, 0.0f); +} + +TEST(Vector4f, constructor_array_tooShort) +{ + std::vector array = { 1.0f, 2.0f, 3.0f }; // only 3 elements, Vector4f needs 4 + CHECK_THROWS(Vector4f(array), VectorArrayWrongSizeException); +} + +TEST(Vector4f, constructor_fromVector3) +{ + Vector3f v3(1.0f, 2.0f, 3.0f); + Vector4f v4(v3); + CHECK_NEAR_TOL(v4.getX(), 1.0f, 0.0f); + CHECK_NEAR_TOL(v4.getY(), 2.0f, 0.0f); + CHECK_NEAR_TOL(v4.getZ(), 3.0f, 0.0f); + CHECK_NEAR_TOL(v4.getW(), 0.0f, 0.0f); // built from a Vector3f: w defaults to 0 (a vector, not a point) +} + +TEST(Vector4f, constructor_fromTwoPoints) +{ + Vector4f a(1.0f, 1.0f, 1.0f, 1.0f); + Vector4f b(4.0f, 5.0f, 6.0f, 1.0f); + Vector4f ab(a, b); + + CHECK_NEAR_TOL(ab.getX(), 3.0f, 0.0f); + CHECK_NEAR_TOL(ab.getY(), 4.0f, 0.0f); + CHECK_NEAR_TOL(ab.getZ(), 5.0f, 0.0f); + CHECK_NEAR_TOL(ab.getW(), 0.0f, 0.0f); // both points have w=1: ab is a direction, not a point +} + +TEST(Vector4f, constructor_fromMatrixRowColumn) +{ + Matrix4f m(Matrix4f::identity()); + Vector4f row0(0, m); + CHECK_NEAR_TOL(row0.getX(), 1.0f, 0.0f); + CHECK_NEAR_TOL(row0.getY(), 0.0f, 0.0f); + + Vector4f col0(m, 0); + CHECK_NEAR_TOL(col0.getX(), 1.0f, 0.0f); + CHECK_NEAR_TOL(col0.getY(), 0.0f, 0.0f); +} + +TEST(Vector4f, indexedGetSet_valid) +{ + Vector4f v; + v.set(0, 1.0f); + v.set(1, 2.0f); + v.set(2, 3.0f); + v.set(3, 4.0f); + + CHECK_NEAR_TOL(v.get(0), 1.0f, 0.0f); + CHECK_NEAR_TOL(v.get(1), 2.0f, 0.0f); + CHECK_NEAR_TOL(v.get(2), 3.0f, 0.0f); + CHECK_NEAR_TOL(v.get(3), 4.0f, 0.0f); +} + +TEST(Vector4f, get_invalidIndex_throws) +{ + Vector4f v; + CHECK_THROWS(v.get(4), IndexOutOfBoundException); // valid indices are 0..3 +} + +TEST(Vector4f, set_invalidIndex_throws) +{ + Vector4f v; + CHECK_THROWS(v.set(4, 1.0f), IndexOutOfBoundException); // valid indices are 0..3 +} + +TEST(Vector4f, get3DConversions) +{ + Vector4f v(4.0f, 6.0f, 8.0f, 2.0f); + CHECK_NEAR_TOL(v.get3DX(), 2.0f, 0.00001f); + CHECK_NEAR_TOL(v.get3DY(), 3.0f, 0.00001f); + CHECK_NEAR_TOL(v.get3DZ(), 4.0f, 0.00001f); + + std::optional p = v.get3DPoint(); + if (CHECK(p.has_value())) + { + CHECK_NEAR_TOL(p->getX(), 2.0f, 0.00001f); + CHECK_NEAR_TOL(p->getY(), 3.0f, 0.00001f); + CHECK_NEAR_TOL(p->getZ(), 4.0f, 0.00001f); + } +} + +TEST(Vector4f, get3DPoint_nullWhenW0) +{ + Vector4f v(1.0f, 2.0f, 3.0f, 0.0f); + CHECK(!v.get3DPoint().has_value()); +} + +TEST(Vector4f, length_normalize) +{ + Vector4f v(1.0f, 2.0f, 2.0f, 0.0f); + CHECK_NEAR_TOL(v.length(), 3.0f, 0.00001f); // sqrt(1+4+4) = 3 + + v.normalize(); + CHECK_NEAR_TOL(v.length(), 1.0f, 0.00001f); +} + +TEST(Vector4f, equals) +{ + Vector4f v1(1.0f, 2.0f, 3.0f, 4.0f); + Vector4f v2(1.0f, 2.0f, 3.0f, 4.0f); + Vector4f v3(9.0f, 9.0f, 9.0f, 9.0f); + + CHECK(v1 == v2); + CHECK(!(v1 == v3)); +} + +TEST(Vector4f, plusMinus_vector4) +{ + Vector4f v1(1.0f, 2.0f, 3.0f, 1.0f); + Vector4f v2(4.0f, 5.0f, 6.0f, 0.0f); + + Vector4f sum = v1 + v2; + CHECK_NEAR_TOL(sum.getX(), 5.0f, 0.0f); + CHECK_NEAR_TOL(sum.getY(), 7.0f, 0.0f); + CHECK_NEAR_TOL(sum.getZ(), 9.0f, 0.0f); + CHECK_NEAR_TOL(sum.getW(), 1.0f, 0.0f); + + Vector4f diff = v1 - v2; + CHECK_NEAR_TOL(diff.getX(), -3.0f, 0.0f); + + v1 += v2; + CHECK_EQUAL(v1, sum); +} + +TEST(Vector4f, plusMinus_vector3_movesPointKeepingW) +{ + Vector4f point(1.0f, 1.0f, 1.0f, 1.0f); // a point + Vector3f displacement(2.0f, 3.0f, 4.0f); + + Vector4f moved = point + displacement; + CHECK_NEAR_TOL(moved.getX(), 3.0f, 0.0f); + CHECK_NEAR_TOL(moved.getY(), 4.0f, 0.0f); + CHECK_NEAR_TOL(moved.getZ(), 5.0f, 0.0f); + CHECK_NEAR_TOL(moved.getW(), 1.0f, 0.0f); // w must be left unchanged (still a point) + + Vector4f movedBack = moved - displacement; + CHECK_EQUAL(movedBack, point); +} + +TEST(Vector4f, dot) +{ + Vector4f v1(1.0f, 2.0f, 3.0f, 4.0f); + Vector4f v2(5.0f, 6.0f, 7.0f, 8.0f); + CHECK_NEAR_TOL(v1.dot(v2), 70.0f, 0.00001f); // 5+12+21+32 +} + +TEST(Vector4f, timesScalar) +{ + Vector4f v(1.0f, 2.0f, 3.0f, 4.0f); + Vector4f r = v * 2.0f; + CHECK_NEAR_TOL(r.getX(), 2.0f, 0.0f); + CHECK_NEAR_TOL(r.getW(), 8.0f, 0.0f); + + v *= 2.0f; + CHECK_EQUAL(v, r); +} + +TEST(Vector4f, crossProduct_forcesW0) +{ + Vector4f x(1.0f, 0.0f, 0.0f, 0.0f); + Vector4f y(0.0f, 1.0f, 0.0f, 0.0f); + Vector4f z = x.cross(y); + + CHECK_NEAR_TOL(z.getX(), 0.0f, 0.00001f); + CHECK_NEAR_TOL(z.getY(), 0.0f, 0.00001f); + CHECK_NEAR_TOL(z.getZ(), 1.0f, 0.00001f); + CHECK_NEAR_TOL(z.getW(), 0.0f, 0.0f); +} + +TEST(Vector4f, timesMatrix4_identity) +{ + Vector4f v(3.0f, -2.0f, 5.0f, 1.0f); + Vector4f r = v * Matrix4f::identity(); + CHECK_EQUAL(r, v); + + v *= Matrix4f::identity(); + CHECK_EQUAL(v, r); +} + +TEST(Vector4f, V3_conversion) +{ + Vector4f v4(1.0f, 2.0f, 3.0f, 9.0f); + Vector3f v3 = v4.V3(); + CHECK_NEAR_TOL(v3.getX(), 1.0f, 0.0f); + CHECK_NEAR_TOL(v3.getY(), 2.0f, 0.0f); + CHECK_NEAR_TOL(v3.getZ(), 3.0f, 0.0f); // w is dropped +} + +TEST(Vector4f, pointVectorSemantics) +{ + Vector4f v(1.0f, 2.0f, 3.0f, 0.0f); + CHECK(v.isVector()); + CHECK(!v.isPoint()); + + v.point(); + CHECK(v.isPoint()); + CHECK(!v.isVector()); + + v.vector(); + CHECK(v.isVector()); + + v.point(); + CHECK(v.isPoint()); + + v.vector(); + CHECK(v.isVector()); + CHECK_NEAR_TOL(v.getW(), 0.0f, 0.0f); +} + +TEST(Vector4f, lengthSquared) +{ + Vector4f v(1.0f, 2.0f, 2.0f, 0.0f); + CHECK_NEAR_TOL(v.lengthSquared(), 9.0f, 0.00001f); // 1+4+4 + CHECK_NEAR_TOL(v.lengthSquared(), v.length() * v.length(), 0.001f); +} + +TEST(Vector4f, distance_distanceSquared) +{ + Vector4f v1(0.0f, 0.0f, 0.0f, 0.0f); + Vector4f v2(1.0f, 2.0f, 2.0f, 0.0f); + + CHECK_NEAR_TOL(v1.distanceSquared(v2), 9.0f, 0.00001f); + CHECK_NEAR_TOL(v1.distance(v2), 3.0f, 0.00001f); + + // Consistency with the allocating equivalent (v1 - v2).length() + CHECK_NEAR_TOL(v1.distance(v2), (v1 - v2).length(), 0.0001f); +} + +TEST(Vector4f, toArray) +{ + Vector4f v(1.0f, 2.0f, 3.0f, 4.0f); + std::array a = v.toArray(); + const float expected[] = { 1.0f, 2.0f, 3.0f, 4.0f }; + for (int i = 0; i < 4; i++) + CHECK_NEAR_TOL(a[i], expected[i], 0.00001f); + + float dest[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; + float* r = v.toArray(dest); + CHECK(r == dest); // must fill and return the same array, not allocate a new one + for (int i = 0; i < 4; i++) + CHECK_NEAR_TOL(dest[i], expected[i], 0.00001f); +} + +// Not ported: equalsObject_and_hashCode (Java-only equals(Object)/null/other type and hashCode) + +TEST(Vector4f, accessorConstants_areFreshIndependentCopies) +{ + Vector4f a1 = Vector4f::xAxis(); + Vector4f a2 = Vector4f::xAxis(); + CHECK(a1 == a2); + + // Mutating one call's result must never affect a later call's result + a1.setX(999.0f); + Vector4f a3 = Vector4f::xAxis(); + CHECK_NEAR_TOL(a3.getX(), 1.0f, 0.0f); + + CHECK(Vector4f::yAxis() == Vector4f(0.0f, 1.0f, 0.0f, 0.0f)); + CHECK(Vector4f::zAxis() == Vector4f(0.0f, 0.0f, 1.0f, 0.0f)); + CHECK(Vector4f::xOppAxis() == Vector4f(-1.0f, 0.0f, 0.0f, 0.0f)); + CHECK(Vector4f::yOppAxis() == Vector4f(0.0f, -1.0f, 0.0f, 0.0f)); + CHECK(Vector4f::zOppAxis() == Vector4f(0.0f, 0.0f, -1.0f, 0.0f)); + CHECK(Vector4f::zeroVector() == Vector4f(0.0f, 0.0f, 0.0f, 0.0f)); + CHECK(Vector4f::zeroPoint() == Vector4f(0.0f, 0.0f, 0.0f, 1.0f)); +} diff --git a/tests/TestVectorMatrix.cpp b/tests/TestVectorMatrix.cpp index 5641fcd..471d12d 100644 --- a/tests/TestVectorMatrix.cpp +++ b/tests/TestVectorMatrix.cpp @@ -6,8 +6,9 @@ // // VectorMatrix Math Library // -// Unit tests, without any third-party dependency. -// Run them with: ctest --test-dir build --output-on-failure +// Tests written for the C++ library itself during the upgrade (P0 to P3): +// C++ specific behaviors (EPSILON comparisons, exceptions, conversions between classes) +// that complement the tests ported from Aventura JUnit tests (TestVector3f.cpp, etc.). // #include @@ -24,26 +25,12 @@ #include "Quaternionf.h" #include "MathTools.h" #include "Exceptions.h" +#include "GeometryTools.h" +#include "TestFramework.h" using namespace vectormatrix; -static int nb_failures = 0; - -static void check(bool condition, const char* expression, const char* file, int line) -{ - if (!condition) - { - std::cerr << file << ":" << line << ": FAILED: " << expression << std::endl; - nb_failures++; - } -} - -#define CHECK(expr) check((expr), #expr, __FILE__, __LINE__) -#define CHECK_THROWS(expr, Exception) \ - do { bool thrown = false; try { (void)(expr); } catch (const Exception&) { thrown = true; } check(thrown, #expr " throws " #Exception, __FILE__, __LINE__); } while (0) -#define CHECK_NEAR(a, b) check(MathTools::equals((a), (b)), #a " == " #b, __FILE__, __LINE__) - -static void testMathTools() +TEST(CppSpecific, MathTools) { CHECK(MathTools::equals(1.0f, 1.0f + EPSILON / 2)); CHECK(!MathTools::equals(1.0f, 1.0f + EPSILON * 2)); @@ -73,7 +60,7 @@ static void testMathTools() } } -static void testVector3f() +TEST(CppSpecific, Vector3f) { Vector3f a(1, 2, 3); Vector3f b(4, 5, 6); @@ -109,7 +96,7 @@ static void testVector3f() CHECK(a.V4() == Vector4f(1, 2, 3, 0)); } -static void testVector4f() +TEST(CppSpecific, Vector4f) { Vector4f a(1, 2, 3, 4); Vector4f b(5, 6, 7, 8); @@ -142,7 +129,7 @@ static void testVector4f() CHECK(Vector4f::interpolate(a, b, 0.25f) == Vector4f(2, 3, 4, 5)); } -static void testMatrix3f() +TEST(CppSpecific, Matrix3f) { float va[3][3] = { { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } }; float vb[3][3] = { { 9, 8, 7 }, { 6, 5, 4 }, { 3, 2, 1 } }; @@ -173,7 +160,7 @@ static void testMatrix3f() CHECK(v == Vector3f(-2, -2, -2)); } -static void testMatrix4f() +TEST(CppSpecific, Matrix4f) { float va[4][4] = { { 1, 2, 3, 4 }, { 5, 6, 7, 8 }, { 9, 10, 11, 12 }, { 13, 14, 15, 16 } }; Matrix4f a(va); @@ -211,7 +198,7 @@ static void testMatrix4f() } } -static void testVector3fMethods() +TEST(CppSpecific, Vector3fMethods) { CHECK(Vector3f::xAxis() == Vector3f(1, 0, 0)); CHECK(Vector3f::zOppAxis() == Vector3f(0, 0, -1)); @@ -251,7 +238,7 @@ static void testVector3fMethods() CHECK(Vector3f(m, 1) == Vector3f(2, 5, 8)); } -static void testVector4fMethods() +TEST(CppSpecific, Vector4fMethods) { CHECK(Vector4f::yAxis() == Vector4f(0, 1, 0, 0)); CHECK(Vector4f::xOppAxis() == Vector4f(-1, 0, 0, 0)); @@ -310,7 +297,7 @@ static void testVector4fMethods() CHECK(Vector4f(m, 2) == Vector4f(3, 7, 11, 15)); } -static void testMatrix3fMethods() +TEST(CppSpecific, Matrix3fMethods) { float va[3][3] = { { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } }; Matrix3f a(va); @@ -366,7 +353,7 @@ static void testMatrix3fMethods() CHECK_THROWS(a.inverse(), NotInvertibleMatrixException); } -static void testMatrix4fMethods() +TEST(CppSpecific, Matrix4fMethods) { CHECK(Matrix4f::identity().isIdentity()); CHECK_NEAR(Matrix4f::identity().trace(), 4.0f); @@ -414,7 +401,7 @@ static void testMatrix4fMethods() CHECK_THROWS(b.setArray(std::vector>(3, std::vector(3))), MatrixArrayWrongSizeException); } -static void testVector2f() +TEST(CppSpecific, Vector2f) { Vector2f a(1, 2); Vector2f b(3, 5); @@ -470,7 +457,7 @@ static void testVector2f() CHECK(c == Vector2f(5, 11)); } -static void testMatrix2f() +TEST(CppSpecific, Matrix2f) { float va[2][2] = { { 1, 2 }, { 3, 4 } }; float vb[2][2] = { { 5, 6 }, { 7, 8 } }; @@ -525,7 +512,7 @@ static void testMatrix2f() CHECK_THROWS(Matrix2f(vs).inverse(), NotInvertibleMatrixException); } -static void testQuaternionf() +TEST(CppSpecific, Quaternionf) { const float PI = 3.14159265f; @@ -595,26 +582,17 @@ static void testQuaternionf() CHECK_THROWS(q.set(-1, 0), IndexOutOfBoundException); } -int main() +TEST(CppSpecific, GeometryToolsRaggedRows) { - testMathTools(); - testVector3f(); - testVector4f(); - testMatrix3f(); - testMatrix4f(); - testVector3fMethods(); - testVector4fMethods(); - testMatrix3fMethods(); - testMatrix4fMethods(); - testVector2f(); - testMatrix2f(); - testQuaternionf(); - - if (nb_failures == 0) - { - std::cout << "All tests passed" << std::endl; - return 0; - } - std::cerr << nb_failures << " check(s) failed" << std::endl; - return 1; + // Unlike Aventura, which assumes all rows have the length of the first one, + // the 2D center counts every point, so rows may have different lengths + std::vector> points = { + { Vector4f(0, 0, 0, 1) }, + {}, + { Vector4f(3, 0, 0, 1), Vector4f(0, 6, 9, 1) } + }; + std::optional center = GeometryTools::center(points); + CHECK(center.has_value()); + if (center) CHECK_EQUAL(*center, Vector4f(1, 2, 3, 1)); + CHECK(!GeometryTools::center(std::vector>{ {}, {} }).has_value()); }