Compare commits

...

5 Commits

Author SHA1 Message Date
Jan Mrna
f64dd339e4 WIP new vec class and tests 2025-10-02 19:29:08 +02:00
Jan Mrna
8f71cbf3d5 Add test target 2025-10-02 17:56:37 +02:00
Jan Mrna
90808c1f9c Added .aider to gitignore 2025-09-30 14:23:00 +02:00
Jan Mrna
ee8087037d Moved TODOs from Readme to GitHub project 2025-09-30 14:00:08 +02:00
Jan Mrna
56697cdc2c Window member variables set as private 2025-09-29 14:15:08 +02:00
6 changed files with 571 additions and 172 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@ tags
python/.ipynb_checkpoints
cpp/build
cpp/pathfinding
.aider*

View File

@ -52,55 +52,3 @@ make -j $(nproc)
Run the `pathfinding` binary in the [cpp](./cpp/) folder.
## TODO
- [x] python
- [x] get jupyter lab running
- [x] drawing utility
- [x] interface for pathfinding
- [x] research methods
- [x] implement methods
- [x] DFS
- [x] BFS
- [x] Dijsktra
- [x] GBFS
- [x] A*
- [x] performance measurement: time/visited nodes
- [x] finalize the script and copy back to the jupyter notebook
- [x] finish text on the page
- [x] create a dedicated python script
- [ ] C++
- [x] re-use 2D game engine
- [x] add tiles (with cost) to map
- [x] conversion functions from tile coords to world coords
- [x] drawing tiles
- [x] add "terrain tiles" with different costs
- [x] add mouse-click action
- [x] add direct movement (through mouse click action, no pathfinding)
- [x] implement pathfinding
- [x] BFS
- [x] GBFS
- [x] Dijkstra
- [ ] A*
- [ ] windows build?
- [x] VS solution
- [ ] merge to master
- [ ] cmake?
- [x] add screenshot
- [ ] zoom + pan of the map
- [ ] maze generator?
- [ ] collisions
- [ ] multiple units
- change from single unit (player) to RTS-style multiple units
- [ ] unit selection
- selection rectangle?
- [ ] pathfinding for multiple units
- [ ] pathfinding for multiple units
- group formation, local cohesion, etc
- [ ] cpython interface
- control the game through the interpreter
- [ ] clang-format config
- [ ] git hooks?
- [ ] [gcovr](https://gcovr.com/en/stable/)
- [ ] [clang-tidy](https://clang.llvm.org/extra/clang-tidy/)

View File

@ -9,16 +9,23 @@ LDLIBS := -lSDL3 -lSDL3_image -lGLEW -lGL
SRC_DIR := src
BUILD_DIR:= build
TARGET := pathfinding
TEST_TARGET := unittest
#----------------------------------
SOURCES := $(shell find $(SRC_DIR) -name '*.cpp')
OBJECTS := $(SOURCES:$(SRC_DIR)/%.cpp=$(BUILD_DIR)/%.o)
#----------------------------------
.PHONY: all clean
.PHONY: all clean test
all: $(TARGET)
test: $(TEST_TARGET)
./$(TEST_TARGET)
$(TEST_TARGET): test/test.cpp
$(CXX) -std=c++23 -lgtest -Isrc -o $@ $<
# link step
$(TARGET): $(OBJECTS)
$(CXX) $(LDFLAGS) -o $@ $^ $(LDLIBS)
@ -32,4 +39,4 @@ $(BUILD_DIR):
mkdir -p $@
clean:
rm -rf $(BUILD_DIR) $(TARGET)
rm -rf $(BUILD_DIR) $(TARGET) $(TEST_TARGET)

View File

@ -6,9 +6,211 @@
#include <initializer_list>
#include <iostream>
#include <utility>
#include <algorithm>
#include <ranges>
#include <numeric>
template <typename T>
requires std::floating_point<T>
static inline bool equalEpsilon(const T& a, const T& b)
{
constexpr auto epsilon = [](){
if constexpr (std::is_same_v<T, float>) {
return T{1e-5};
} else {
return T{1e-12}; // double, long double
}
}();
if (a == b) {
// handle special cases: bit equality, Inf...
return true;
}
return std::abs(a - b) < epsilon;
}
template <typename T, size_t N>
class vec {
public:
vec() : m_Array{} {}
template <typename... ArgsT>
requires (std::same_as<ArgsT, T> && ...) && (sizeof...(ArgsT) == N)
vec(ArgsT... args) : m_Array{args...} {}
const T& operator[](size_t index) const
{
// we leave run-time checks to the underlying std::array
return m_Array[index];
}
T& operator[](size_t index)
{
// we leave run-time checks to the underlying std::array
return m_Array[index];
}
friend std::ostream &operator<<(std::ostream &os, const vec &obj)
{
os << "( ";
for (const auto& element : obj.m_Array) {
os << element << " ";
}
os << ")";
return os;
}
//
// binary operators
//
friend bool operator==(const vec& a, const vec& b)
{
for (const auto& [u, v] : std::views::zip(a.m_Array,b.m_Array)) {
if (!equalEpsilon(u, v)) {
return false;
}
}
return true;
}
friend bool operator!=(const vec& a, const vec& b)
{
return !(a == b);
}
friend vec operator+(const vec& a, const vec& b)
{
vec<T,N> c;
std::ranges::transform(a.m_Array, b.m_Array, c.m_Array.begin(), std::plus{});
return c;
}
friend vec operator-(const vec& a, const vec& b)
{
vec<T,N> c;
std::ranges::transform(a.m_Array, b.m_Array, c.m_Array.begin(), std::minus{});
return c;
}
friend vec operator*(const vec& a, const T& scalar)
{
vec<T,N> c;
std::ranges::transform(a.m_Array, std::views::repeat(scalar), c.m_Array.begin(), std::multiplies{});
return c;
}
friend vec operator*(const T& scalar, const vec& a) {
return a * scalar;
}
friend vec operator/(const vec& a, const T& scalar)
{
vec<T,N> c;
std::ranges::transform(a.m_Array, std::views::repeat(scalar), c.m_Array.begin(), std::divides{});
return c;
}
//
// compound-assignment operators
//
vec& operator+=(const vec& b)
{
vec& a = *this;
std::ranges::transform(a.m_Array, b.m_Array, a.m_Array.begin(), std::plus{});
return a;
}
vec& operator-=(const vec& b)
{
vec& a = *this;
std::ranges::transform(a.m_Array, b.m_Array, a.m_Array.begin(), std::minus{});
return a;
}
//
// Utility functions
//
T LengthSquared() const
{
return std::transform_reduce(
m_Array.begin(), m_Array.end(),
T{0.0},
std::plus{},
[](T x){
return x*x;
}
);
}
T Length() const
{
return std::sqrt(LengthSquared());
}
//
// In-place vector operations
//
void Normalize()
{
T length = Length();
if (equalEpsilon(length, T{0}))
return;
std::ranges::transform(m_Array, std::views::repeat(length), m_Array.begin(), std::divides{});
}
//
// Methods returning new object
//
vec GetNormalized() const
{
vec tmp = *this;
tmp.Normalize();
return tmp;
}
vec GetOrthogonal() const requires (N == 2)
{
vec tmp = *this;
std::swap(tmp.m_Array[0], tmp.m_Array[1]);
tmp.m_Array[0] *= -1;
return tmp;
}
// const T& x = m_Array[0];
// const T& y = m_Array[1];
// const T& z = m_Array[2];
private:
std::array<T,N> m_Array;
};
using vec2 = vec<float, 2>;
using vec3 = vec<float, 3>;
using vec4 = vec<float, 4>;
using dvec2 = vec<double, 2>;
using dvec3 = vec<double, 3>;
using dvec4 = vec<double, 4>;
using ivec2 = vec<std::int32_t, 2>;
using ivec3 = vec<std::int32_t, 3>;
using ivec4 = vec<std::int32_t, 4>;
using uvec2 = vec<std::uint32_t, 2>;
using uvec3 = vec<std::uint32_t, 3>;
using uvec4 = vec<std::uint32_t, 4>;
constexpr double EQUALITY_LIMIT = 1e-6;
template <typename T> struct Vec2D {
public:
Vec2D() = default;
@ -127,6 +329,7 @@ public:
using TilePos = Vec2D<int>;
using WorldPos = Vec2D<float>;
using WindowPos = Vec2D<float>;
struct TilePosHash {
std::size_t operator()(const TilePos& p) const noexcept {

View File

@ -30,11 +30,11 @@ public:
void DrawCircle(const WorldPos &position, float radius);
void DrawLine(const WorldPos &A, const WorldPos &B);
private:
uint32_t m_Width;
uint32_t m_Height;
std::shared_ptr<SDL_Renderer> m_Renderer = nullptr;
SDL_Window *m_Window;
SDL_GLContext m_Context;
private:
uint32_t m_Width;
uint32_t m_Height;
};

View File

@ -5,151 +5,391 @@
#include <sstream>
#include <unordered_set>
#include "array.hpp"
#include "log.hpp"
#include "math.hpp"
// Vec2D Tests
TEST(Vec2D, DefaultConstruction) {
Vec2D<int> v;
// Default values are uninitialized, but we can test basic functionality
v.x = 0;
v.y = 0;
ASSERT_EQ(v.x, 0);
ASSERT_EQ(v.y, 0);
TEST(vec, DefaultConstruction) {
// Test that default-constucted vector
// has all elements equal to zero
vec3 v1;
ASSERT_EQ(v1[0], 0.0);
ASSERT_EQ(v1[1], 0.0);
ASSERT_EQ(v1[2], 0.0);
}
TEST(Vec2D, InitializerListConstruction) {
Vec2D<int> v{3, 4};
ASSERT_EQ(v.x, 3);
ASSERT_EQ(v.y, 4);
Vec2D<float> vf{1.5f, 2.5f};
ASSERT_FLOAT_EQ(vf.x, 1.5f);
ASSERT_FLOAT_EQ(vf.y, 2.5f);
// Test type conversion
Vec2D<float> vd{1, 2}; // int to float
ASSERT_FLOAT_EQ(vd.x, 1.0f);
ASSERT_FLOAT_EQ(vd.y, 2.0f);
TEST(vec, GetElements) {
// Test operator[]
ivec3 v1{12, 34, 56};
ASSERT_EQ(v1[0], 12);
ASSERT_EQ(v1[1], 34);
ASSERT_EQ(v1[2], 56);
}
TEST(Vec2D, Addition) {
Vec2D<int> a{1, 2};
Vec2D<int> b{3, 4};
Vec2D<int> c = a + b;
ASSERT_EQ(c.x, 4);
ASSERT_EQ(c.y, 6);
TEST(vec, equalEpsilon) {
// Test equalEpsilon
// TODO just an ad-hoc test,
// can possibly fail for other machines.
// This needs some work
vec3 v1{1.0f, 2.0f, 3.0f};
vec3 v2{0.999999f, 1.9999999f, 2.9999999f};
ASSERT_EQ(v1, v2);
}
TEST(vec, nonEqualEpsilon) {
// Test operator!=
vec3 v1{1.0f, 2.0f, 3.0f};
vec3 v2{2.0f, 4.0f, 6.0f};
ASSERT_NE(v1, v2);
}
TEST(vec, LogPrint) {
// Test that logger can print the vector of different types
// and sizes
vec2 v2(1.2f, 3.4f);
vec3 v3(1.2f, 3.4f, 5.6f);
vec4 v4(1.2f, 3.4f, 5.6f, 7.8f);
dvec2 dv2(1.2, 3.4);
dvec3 dv3(1.2, 3.4, 5.6);
dvec4 dv4(1.2, 3.4, 5.6, 7.8);
ivec2 iv2(1, 3);
ivec3 iv3(1, 3, 5);
ivec4 iv4(1, 3, 5, 7);
uvec2 uv2(1u, 3u);
uvec3 uv3(1u, 3u, 5u);
uvec4 uv4(1u, 3u, 5u, 7u);
LOG_DEBUG("vec2 ", v2);
LOG_DEBUG("vec3 ", v3);
LOG_DEBUG("vec4 ", v4);
LOG_DEBUG("dvec2 ", dv2);
LOG_DEBUG("dvec3 ", dv3);
LOG_DEBUG("dvec4 ", dv4);
LOG_DEBUG("ivec2 ", iv2);
LOG_DEBUG("ivec3 ", iv3);
LOG_DEBUG("ivec4 ", iv4);
LOG_DEBUG("uvec2 ", uv2);
LOG_DEBUG("uvec3 ", uv3);
LOG_DEBUG("uvec4 ", uv4);
}
TEST(vec, Add)
{
// Test operator+ with float vectors
vec3 v1{1.0f, 2.0f, 3.0f};
vec3 v2{4.0f, 5.0f, 6.0f};
vec3 result = v1 + v2;
ASSERT_FLOAT_EQ(result[0], 5.0f);
ASSERT_FLOAT_EQ(result[1], 7.0f);
ASSERT_FLOAT_EQ(result[2], 9.0f);
// Test operator+ with integer vectors
ivec3 iv1{1, 2, 3};
ivec3 iv2{10, 20, 30};
ivec3 iresult = iv1 + iv2;
ASSERT_EQ(iresult[0], 11);
ASSERT_EQ(iresult[1], 22);
ASSERT_EQ(iresult[2], 33);
// Test that original vectors are unchanged
ASSERT_EQ(a.x, 1);
ASSERT_EQ(a.y, 2);
ASSERT_EQ(b.x, 3);
ASSERT_EQ(b.y, 4);
ASSERT_FLOAT_EQ(v1[0], 1.0f);
ASSERT_FLOAT_EQ(v1[1], 2.0f);
ASSERT_FLOAT_EQ(v1[2], 3.0f);
}
TEST(Vec2D, AdditionAssignment) {
Vec2D<int> a{1, 2};
Vec2D<int> b{3, 4};
TEST(vec, Sub)
{
// Test operator- with float vectors
vec3 v1{5.0f, 7.0f, 9.0f};
vec3 v2{1.0f, 2.0f, 3.0f};
vec3 result = v1 - v2;
a += b;
ASSERT_EQ(a.x, 4);
ASSERT_EQ(a.y, 6);
ASSERT_FLOAT_EQ(result[0], 4.0f);
ASSERT_FLOAT_EQ(result[1], 5.0f);
ASSERT_FLOAT_EQ(result[2], 6.0f);
// Test that b is unchanged
ASSERT_EQ(b.x, 3);
ASSERT_EQ(b.y, 4);
// Test operator- with integer vectors
ivec3 iv1{30, 20, 10};
ivec3 iv2{5, 3, 1};
ivec3 iresult = iv1 - iv2;
ASSERT_EQ(iresult[0], 25);
ASSERT_EQ(iresult[1], 17);
ASSERT_EQ(iresult[2], 9);
// Test that original vectors are unchanged
ASSERT_FLOAT_EQ(v1[0], 5.0f);
ASSERT_FLOAT_EQ(v1[1], 7.0f);
ASSERT_FLOAT_EQ(v1[2], 9.0f);
// Test subtraction resulting in negative values
vec3 v3{1.0f, 2.0f, 3.0f};
vec3 v4{4.0f, 5.0f, 6.0f};
vec3 negative_result = v3 - v4;
ASSERT_FLOAT_EQ(negative_result[0], -3.0f);
ASSERT_FLOAT_EQ(negative_result[1], -3.0f);
ASSERT_FLOAT_EQ(negative_result[2], -3.0f);
}
TEST(Vec2D, ScalarMultiplication) {
Vec2D<int> v{2, 3};
TEST(vec, ScalarMultiplication)
{
// Test scalar * vector with float vectors
vec3 v1{2.0f, 3.0f, 4.0f};
vec3 result = v1 * 2.5f;
Vec2D<int> result = v * 2.0f;
ASSERT_EQ(result.x, 4);
ASSERT_EQ(result.y, 6);
ASSERT_FLOAT_EQ(result[0], 5.0f);
ASSERT_FLOAT_EQ(result[1], 7.5f);
ASSERT_FLOAT_EQ(result[2], 10.0f);
// Test with float vector
Vec2D<float> vf{1.5f, 2.5f};
Vec2D<float> resultf = vf * 2.0f;
ASSERT_FLOAT_EQ(resultf.x, 3.0f);
ASSERT_FLOAT_EQ(resultf.y, 5.0f);
// Test scalar * vector with integer vectors
ivec3 iv1{3, 5, 7};
ivec3 iresult = iv1 * 2;
ASSERT_EQ(iresult[0], 6);
ASSERT_EQ(iresult[1], 10);
ASSERT_EQ(iresult[2], 14);
// Test that original vector is unchanged
ASSERT_FLOAT_EQ(v1[0], 2.0f);
ASSERT_FLOAT_EQ(v1[1], 3.0f);
ASSERT_FLOAT_EQ(v1[2], 4.0f);
// Test multiplication by zero
vec3 v2{1.0f, 2.0f, 3.0f};
vec3 zero_result = v2 * 0.0f;
ASSERT_FLOAT_EQ(zero_result[0], 0.0f);
ASSERT_FLOAT_EQ(zero_result[1], 0.0f);
ASSERT_FLOAT_EQ(zero_result[2], 0.0f);
// Test multiplication by negative scalar (and different ordering)
vec3 v3{1.0f, -2.0f, 3.0f};
vec3 negative_result = -2.0f * v3;
ASSERT_FLOAT_EQ(negative_result[0], -2.0f);
ASSERT_FLOAT_EQ(negative_result[1], 4.0f);
ASSERT_FLOAT_EQ(negative_result[2], -6.0f);
}
TEST(Vec2D, Normalization) {
Vec2D<float> v{3.0f, 4.0f}; // Length = 5
TEST(vec, ScalarDivision)
{
// Test vector / scalar with float vectors
vec3 v1{10.0f, 15.0f, 20.0f};
vec3 result = v1 / 2.5f;
v.normalize();
ASSERT_FLOAT_EQ(v.x, 0.6f);
ASSERT_FLOAT_EQ(v.y, 0.8f);
ASSERT_FLOAT_EQ(result[0], 4.0f);
ASSERT_FLOAT_EQ(result[1], 6.0f);
ASSERT_FLOAT_EQ(result[2], 8.0f);
// Check that length is approximately 1
float length = sqrt(v.x * v.x + v.y * v.y);
ASSERT_NEAR(length, 1.0f, 1e-6f);
// Test vector / scalar with integer vectors
ivec3 iv1{12, 18, 24};
ivec3 iresult = iv1 / 2;
ASSERT_EQ(iresult[0], 6);
ASSERT_EQ(iresult[1], 9);
ASSERT_EQ(iresult[2], 12);
// Test that original vector is unchanged
ASSERT_FLOAT_EQ(v1[0], 10.0f);
ASSERT_FLOAT_EQ(v1[1], 15.0f);
ASSERT_FLOAT_EQ(v1[2], 20.0f);
// Test division by negative scalar
vec3 v2{6.0f, -9.0f, 12.0f};
vec3 negative_result = v2 / -3.0f;
ASSERT_FLOAT_EQ(negative_result[0], -2.0f);
ASSERT_FLOAT_EQ(negative_result[1], 3.0f);
ASSERT_FLOAT_EQ(negative_result[2], -4.0f);
// Test division by fractional scalar
vec3 v3{1.0f, 2.0f, 3.0f};
vec3 fractional_result = v3 / 0.5f;
ASSERT_FLOAT_EQ(fractional_result[0], 2.0f);
ASSERT_FLOAT_EQ(fractional_result[1], 4.0f);
ASSERT_FLOAT_EQ(fractional_result[2], 6.0f);
}
TEST(Vec2D, NormalizedCopy) {
Vec2D<float> v{3.0f, 4.0f};
Vec2D<float> normalized = v.normalized();
TEST(vec, AdditionAssignment)
{
// Test operator+= with float vectors
vec3 v1{1.0f, 2.0f, 3.0f};
vec3 v2{4.0f, 5.0f, 6.0f};
v1 += v2;
// Original should be unchanged
ASSERT_FLOAT_EQ(v.x, 3.0f);
ASSERT_FLOAT_EQ(v.y, 4.0f);
ASSERT_FLOAT_EQ(v1[0], 5.0f);
ASSERT_FLOAT_EQ(v1[1], 7.0f);
ASSERT_FLOAT_EQ(v1[2], 9.0f);
// Test that v2 is unchanged
ASSERT_FLOAT_EQ(v2[0], 4.0f);
ASSERT_FLOAT_EQ(v2[1], 5.0f);
ASSERT_FLOAT_EQ(v2[2], 6.0f);
// Test operator+= with integer vectors
ivec3 iv1{10, 20, 30};
ivec3 iv2{1, 2, 3};
iv1 += iv2;
ASSERT_EQ(iv1[0], 11);
ASSERT_EQ(iv1[1], 22);
ASSERT_EQ(iv1[2], 33);
// Test chaining
vec3 v3{1.0f, 1.0f, 1.0f};
vec3 v4{2.0f, 2.0f, 2.0f};
vec3 v5{3.0f, 3.0f, 3.0f};
v3 += v4 += v5;
ASSERT_FLOAT_EQ(v3[0], 6.0f);
ASSERT_FLOAT_EQ(v3[1], 6.0f);
ASSERT_FLOAT_EQ(v3[2], 6.0f);
ASSERT_FLOAT_EQ(v4[0], 5.0f);
ASSERT_FLOAT_EQ(v4[1], 5.0f);
ASSERT_FLOAT_EQ(v4[2], 5.0f);
}
TEST(vec, SubtractionAssignment)
{
// Test operator-= with float vectors
vec3 v1{10.0f, 15.0f, 20.0f};
vec3 v2{3.0f, 5.0f, 7.0f};
v1 -= v2;
ASSERT_FLOAT_EQ(v1[0], 7.0f);
ASSERT_FLOAT_EQ(v1[1], 10.0f);
ASSERT_FLOAT_EQ(v1[2], 13.0f);
// Test that v2 is unchanged
ASSERT_FLOAT_EQ(v2[0], 3.0f);
ASSERT_FLOAT_EQ(v2[1], 5.0f);
ASSERT_FLOAT_EQ(v2[2], 7.0f);
// Test operator-= with integer vectors
ivec3 iv1{50, 40, 30};
ivec3 iv2{5, 10, 15};
iv1 -= iv2;
ASSERT_EQ(iv1[0], 45);
ASSERT_EQ(iv1[1], 30);
ASSERT_EQ(iv1[2], 15);
// Test subtraction resulting in negative values
vec3 v3{1.0f, 2.0f, 3.0f};
vec3 v4{4.0f, 5.0f, 6.0f};
v3 -= v4;
ASSERT_FLOAT_EQ(v3[0], -3.0f);
ASSERT_FLOAT_EQ(v3[1], -3.0f);
ASSERT_FLOAT_EQ(v3[2], -3.0f);
}
TEST(vec, LengthSquared)
{
// Test LengthSquared with float vectors
vec3 v1{3.0f, 4.0f, 0.0f};
ASSERT_FLOAT_EQ(v1.LengthSquared(), 25.0f); // 3² + 4² + 0² = 25
vec2 v2{1.0f, 1.0f};
ASSERT_FLOAT_EQ(v2.LengthSquared(), 2.0f); // 1² + 1² = 2
// Test with zero vector
vec3 zero{0.0f, 0.0f, 0.0f};
ASSERT_FLOAT_EQ(zero.LengthSquared(), 0.0f);
}
TEST(vec, Length)
{
// Test Length with float vectors
vec3 v1{3.0f, 4.0f, 0.0f};
ASSERT_FLOAT_EQ(v1.Length(), 5.0f); // sqrt(3² + 4² + 0²) = 5
vec2 v2{1.0f, 1.0f};
ASSERT_NEAR(v2.Length(), 1.414213f, 1e-5f); // sqrt(2) ≈ 1.414213
// Test with zero vector
vec3 zero{0.0f, 0.0f, 0.0f};
ASSERT_FLOAT_EQ(zero.Length(), 0.0f);
}
TEST(vec, Normalize)
{
// Test Normalize with float vectors
vec3 v1{3.0f, 4.0f, 0.0f};
v1.Normalize();
ASSERT_FLOAT_EQ(v1[0], 0.6f); // 3/5
ASSERT_FLOAT_EQ(v1[1], 0.8f); // 4/5
ASSERT_FLOAT_EQ(v1[2], 0.0f);
ASSERT_NEAR(v1.Length(), 1.0f, 1e-6f);
// Test with zero vector (may produce NaN - implementation dependent)
vec3 zero{0.0f, 0.0f, 0.0f};
zero.Normalize();
// Check if result is NaN (which is expected for zero vector normalization)
ASSERT_TRUE(zero[0] == 0.0f);
ASSERT_TRUE(zero[1] == 0.0f);
ASSERT_TRUE(zero[2] == 0.0f);
}
TEST(vec, GetNormalized)
{
// Test GetNormalized with float vectors
const vec3 v1{3.0f, 4.0f, 0.0f};
vec3 normalized = v1.GetNormalized();
// Original vector should be unchanged
ASSERT_FLOAT_EQ(v1[0], 3.0f);
ASSERT_FLOAT_EQ(v1[1], 4.0f);
ASSERT_FLOAT_EQ(v1[2], 0.0f);
// Normalized copy should be unit length
ASSERT_FLOAT_EQ(normalized.x, 0.6f);
ASSERT_FLOAT_EQ(normalized.y, 0.8f);
ASSERT_FLOAT_EQ(normalized[0], 0.6f); // 3/5
ASSERT_FLOAT_EQ(normalized[1], 0.8f); // 4/5
ASSERT_FLOAT_EQ(normalized[2], 0.0f);
ASSERT_NEAR(normalized.Length(), 1.0f, 1e-6f);
float length =
sqrt(normalized.x * normalized.x + normalized.y * normalized.y);
ASSERT_NEAR(length, 1.0f, 1e-6f);
// Test with zero vector
vec3 zero{0.0f, 0.0f, 0.0f};
vec3 zero_normalized = zero.GetNormalized();
ASSERT_FLOAT_EQ(zero_normalized[0], 0.0f);
ASSERT_FLOAT_EQ(zero_normalized[1], 0.0f);
ASSERT_FLOAT_EQ(zero_normalized[2], 0.0f);
// Original zero vector should be unchanged
ASSERT_FLOAT_EQ(zero[0], 0.0f);
ASSERT_FLOAT_EQ(zero[1], 0.0f);
ASSERT_FLOAT_EQ(zero[2], 0.0f);
}
TEST(Vec2D, ZeroVectorNormalization) {
Vec2D<float> v{0.0f, 0.0f};
v.normalize();
ASSERT_FLOAT_EQ(v.x, 0.0f);
ASSERT_FLOAT_EQ(v.y, 0.0f);
// Test normalized() as well
Vec2D<float> v2{0.0f, 0.0f};
Vec2D<float> normalized = v2.normalized();
ASSERT_FLOAT_EQ(normalized.x, 0.0f);
ASSERT_FLOAT_EQ(normalized.y, 0.0f);
TEST(vec, GetOrthogonal)
{
const vec2 v1{5.0f, 1.0f};
auto v2 = v1.GetOrthogonal();
ASSERT_FLOAT_EQ(v2[0], -1.0f);
ASSERT_FLOAT_EQ(v2[1], 5.0f);
}
TEST(Vec2D, VerySmallVectorNormalization) {
Vec2D<float> v{1e-7f, 1e-7f}; // Very small vector
v.normalize();
// Should be treated as zero vector
ASSERT_FLOAT_EQ(v.x, 0.0f);
ASSERT_FLOAT_EQ(v.y, 0.0f);
}
TEST(Vec2D, OutputOperator) {
Vec2D<int> v{42, 24};
std::ostringstream oss;
oss << v;
ASSERT_EQ(oss.str(), "( 42, 24)");
}
TEST(Vec2D, ChainedOperations) {
Vec2D<float> a{1.0f, 2.0f};
Vec2D<float> b{3.0f, 4.0f};
TEST(vec, ChainedOperations) {
vec2 a{1.0f, 2.0f};
vec2 b{3.0f, 4.0f};
// Test chaining: (a + b) * 2.0f
Vec2D<float> result = (a + b) * 2.0f;
ASSERT_FLOAT_EQ(result.x, 8.0f);
ASSERT_FLOAT_EQ(result.y, 12.0f);
auto result = (a + b) * 2.0f;
ASSERT_FLOAT_EQ(result[0], 8.0f);
ASSERT_FLOAT_EQ(result[1], 12.0f);
// Test chaining with assignment
a += b;
a = a * 0.5f;
ASSERT_FLOAT_EQ(a.x, 2.0f);
ASSERT_FLOAT_EQ(a.y, 3.0f);
ASSERT_FLOAT_EQ(a[0], 2.0f);
ASSERT_FLOAT_EQ(a[1], 3.0f);
}
int main(int argc, char **argv) {