diff --git a/util/CMakeLists.txt b/util/CMakeLists.txt index f1aeda5..ead2889 100644 --- a/util/CMakeLists.txt +++ b/util/CMakeLists.txt @@ -4,6 +4,7 @@ cmake_minimum_required(VERSION 3.27) # Define util library sources add_library(istd_util STATIC src/vec2.cpp + src/tile_geometry.cpp ) target_include_directories(istd_util PUBLIC include) target_compile_features(istd_util PUBLIC cxx_std_23) diff --git a/util/include/istd_util/tile_geometry.h b/util/include/istd_util/tile_geometry.h new file mode 100644 index 0000000..17794de --- /dev/null +++ b/util/include/istd_util/tile_geometry.h @@ -0,0 +1,33 @@ +#ifndef ISTD_UTIL_TILE_GEOMETRY_H +#define ISTD_UTIL_TILE_GEOMETRY_H + +#include "istd_util/vec2.h" +#include +#include +#include + +namespace istd { + +/** + * @brief Iterates all tile coordinates traversed by a line segment on a + * tilemap. + * + * Uses the Amanatides-Woo algorithm to efficiently enumerate all integer tile + * positions that a segment from p1 to p2 passes through, including both + * endpoints. + * + * @note x points downward, y points rightward, + * i.e. x is row index, y is column index. + * + * @param p1 The starting point of the segment (floating point coordinates). + * @param p2 The ending point of the segment (floating point coordinates). + * @return Generator yielding (i, j) tuples for each tile crossed by the + * segment. + */ +std::generator> tiles_on_segment( + Vec2 p1, Vec2 p2 +) noexcept; + +} // namespace istd + +#endif \ No newline at end of file diff --git a/util/include/istd_util/vec2.h b/util/include/istd_util/vec2.h index 3c4cd6c..7ff5f59 100644 --- a/util/include/istd_util/vec2.h +++ b/util/include/istd_util/vec2.h @@ -28,10 +28,12 @@ struct Vec2 { * @param y Y component */ Vec2(float x, float y) noexcept; + /** * @brief Returns a zero vector (0, 0). */ static Vec2 zero() noexcept; + /** * @brief Returns a vector rotated by the given angle. * @param rad Angle in radians @@ -40,9 +42,15 @@ struct Vec2 { static Vec2 rotated(float rad, float len = 1.0) noexcept; /** - * @name Symmetric operations - * @{ + * @brief Returns a vector with infinite components. */ + static Vec2 inf() noexcept; + + /** + * @brief Returns a vector with NaN components. + */ + static Vec2 invalid() noexcept; + /** * @brief Vector addition. */ @@ -67,12 +75,7 @@ struct Vec2 { * @brief Three-way comparison operator. */ friend std::strong_ordering operator<=>(Vec2 a, Vec2 b) noexcept; - /** @} */ - /** - * @name Assignment operations - * @{ - */ /** * @brief Adds another vector to this vector. */ @@ -89,7 +92,6 @@ struct Vec2 { * @brief Divides this vector by a scalar. */ Vec2 &operator/=(float k) noexcept; - /** @} */ /** * @brief Access vector components by index. @@ -123,22 +125,31 @@ struct Vec2 { throw std::out_of_range("Index out of range for Vec2"); } + /** + * @brief Checks if the vector is valid (not NaN). + */ + bool is_valid(this const Vec2 self) noexcept; + /** * @brief Returns the length (magnitude) of the vector. */ float length(this const Vec2 self) noexcept; + /** * @brief Returns the squared length of the vector. */ float length_squared(this const Vec2 self) noexcept; + /** * @brief Returns a normalized (unit length) vector. */ Vec2 normalized(this const Vec2 self) noexcept; + /** * @brief Returns a tuple of floored components. */ std::tuple floor(this const Vec2 self) noexcept; + /** * @brief Returns a tuple of rounded components. */ @@ -147,11 +158,12 @@ struct Vec2 { /** * @brief Returns the dot product of two vectors. */ - friend float dot(Vec2 a, Vec2 b) noexcept; + static float dot(Vec2 a, Vec2 b) noexcept; + /** * @brief Returns the cross product of two vectors. */ - friend float cross(Vec2 a, Vec2 b) noexcept; + static float cross(Vec2 a, Vec2 b) noexcept; }; } // namespace istd diff --git a/util/include/util.h b/util/include/util.h index da7952e..7c87f9d 100644 --- a/util/include/util.h +++ b/util/include/util.h @@ -1 +1,3 @@ #include "istd_util/small_map.h" +#include "istd_util/tile_geometry.h" +#include "istd_util/vec2.h" diff --git a/util/src/tile_geometry.cpp b/util/src/tile_geometry.cpp new file mode 100644 index 0000000..4a04c0c --- /dev/null +++ b/util/src/tile_geometry.cpp @@ -0,0 +1,52 @@ +#include "istd_util/tile_geometry.h" + +namespace istd { + +// Amanatides-Woo Algorithm +std::generator> tiles_on_segment( + Vec2 p1, Vec2 p2 +) noexcept { + auto [i, j] = p1.floor(); + co_yield {i, j}; + if (p1.floor() == p2.floor()) { + co_return; + } + + auto delta = p2 - p1; + int step_x = 0, step_y = 0; + auto t_max = Vec2::inf(), t_delta = Vec2::inf(); + + if (delta.x > 0) { + step_x = 1; + t_max.x = (i + 1 - p1.x) / delta.x; + t_delta.x = 1.0f / delta.x; + } else if (delta.x < 0) { + step_x = -1; + t_max.x = (i - p1.x) / delta.x; + t_delta.x = -1.0f / delta.x; + } + + if (delta.y > 0) { + step_y = 1; + t_max.y = (j + 1 - p1.y) / delta.y; + t_delta.y = 1.0f / delta.y; + } else if (delta.y < 0) { + step_y = -1; + t_max.y = (j - p1.y) / delta.y; + t_delta.y = -1.0f / delta.y; + } + + auto [end_i, end_j] = p2.floor(); + while (i != end_i || j != end_j) { + if (t_max.x < t_max.y) { + i += step_x; + t_max.x += t_delta.x; + } else { + j += step_y; + t_max.y += t_delta.y; + } + co_yield {i, j}; + } +} + +} // namespace istd diff --git a/util/src/vec2.cpp b/util/src/vec2.cpp index 8084ec3..b66f539 100644 --- a/util/src/vec2.cpp +++ b/util/src/vec2.cpp @@ -7,11 +7,25 @@ namespace istd { Vec2::Vec2(float x_, float y_) noexcept: x(x_), y(y_) {} Vec2 Vec2::zero() noexcept { - return Vec2(0.0f, 0.0f); + return {0.0f, 0.0f}; } Vec2 Vec2::rotated(float rad, float len) noexcept { - return Vec2(std::cos(rad) * len, std::sin(rad) * len); + return {std::cos(rad) * len, std::sin(rad) * len}; +} + +Vec2 Vec2::inf() noexcept { + return Vec2( + std::numeric_limits::infinity(), + std::numeric_limits::infinity() + ); +} + +Vec2 Vec2::invalid() noexcept { + return Vec2( + std::numeric_limits::quiet_NaN(), + std::numeric_limits::quiet_NaN() + ); } Vec2 operator+(Vec2 a, Vec2 b) noexcept { @@ -74,6 +88,10 @@ Vec2 &Vec2::operator/=(float k) noexcept { return *this; } +bool Vec2::is_valid(this const Vec2 self) noexcept { + return !std::isnan(self.x) && !std::isnan(self.y); +} + float Vec2::length(this const Vec2 self) noexcept { return std::sqrt(self.x * self.x + self.y * self.y); } @@ -104,11 +122,11 @@ std::tuple Vec2::round(this const Vec2 self) noexcept { ); } -float dot(Vec2 a, Vec2 b) noexcept { +float Vec2::dot(Vec2 a, Vec2 b) noexcept { return a.x * b.x + a.y * b.y; } -float cross(Vec2 a, Vec2 b) noexcept { +float Vec2::cross(Vec2 a, Vec2 b) noexcept { return a.x * b.y - a.y * b.x; } diff --git a/util/test/CMakeLists.txt b/util/test/CMakeLists.txt index 677ed0a..394a031 100644 --- a/util/test/CMakeLists.txt +++ b/util/test/CMakeLists.txt @@ -1,16 +1,15 @@ cmake_minimum_required(VERSION 3.27) - -add_executable(test_small_map small_map.cpp) -target_link_libraries(test_small_map PRIVATE istd_util) -target_compile_features(test_small_map PRIVATE cxx_std_23) - -add_executable(test_vec2 test_vec2.cpp) -target_link_libraries(test_vec2 PRIVATE istd_util) -target_compile_features(test_vec2 PRIVATE cxx_std_23) - - include(CTest) enable_testing() -add_test(NAME util_small_map COMMAND test_small_map) -add_test(NAME util_vec2 COMMAND test_vec2) + +function(declare_istd_util_test name src) + add_executable(${name} ${src}) + target_link_libraries(${name} PRIVATE istd_util) + target_compile_features(${name} PRIVATE cxx_std_23) + add_test(NAME ${name} COMMAND ${name}) +endfunction() + +declare_istd_util_test(test_small_map small_map.cpp) +declare_istd_util_test(test_vec2 test_vec2.cpp) +declare_istd_util_test(test_tile_geometry test_tile_geometry.cpp) diff --git a/util/test/test_tile_geometry.cpp b/util/test/test_tile_geometry.cpp new file mode 100644 index 0000000..f77a20b --- /dev/null +++ b/util/test/test_tile_geometry.cpp @@ -0,0 +1,60 @@ +#include "istd_util/tile_geometry.h" +#include +#include +#include +using namespace istd; + +int main() { + // Test a simple horizontal segment + Vec2 p1(0.5f, 1.2f); + Vec2 p2(0.5f, 4.8f); + std::vector> result; + for (auto [i, j] : tiles_on_segment(p1, p2)) { + result.emplace_back(i, j); + } + // Should traverse columns 1 to 4, row 0 + assert(result.size() == 4); + assert(result[0] == std::make_tuple(0, 1)); + assert(result[1] == std::make_tuple(0, 2)); + assert(result[2] == std::make_tuple(0, 3)); + assert(result[3] == std::make_tuple(0, 4)); + + // Test a diagonal segment + p1 = Vec2(1.1f, 1.1f); + p2 = Vec2(3.9f, 3.9f); + result.clear(); + for (auto [i, j] : tiles_on_segment(p1, p2)) { + result.emplace_back(i, j); + } + // Should traverse (1,1), (2,2), (3,3) + assert(result.size() == 3); + assert(result[0] == std::make_tuple(1, 1)); + assert(result[1] == std::make_tuple(2, 2)); + assert(result[2] == std::make_tuple(3, 3)); + + // Test vertical segment + p1 = Vec2(2.2f, 0.5f); + p2 = Vec2(5.7f, 0.5f); + result.clear(); + for (auto [i, j] : tiles_on_segment(p1, p2)) { + result.emplace_back(i, j); + } + // Should traverse rows 2 to 5, column 0 + assert(result.size() == 4); + assert(result[0] == std::make_tuple(2, 0)); + assert(result[1] == std::make_tuple(3, 0)); + assert(result[2] == std::make_tuple(4, 0)); + assert(result[3] == std::make_tuple(5, 0)); + + // Test single tile + p1 = Vec2(7.3f, 8.9f); + p2 = Vec2(7.7f, 8.1f); + result.clear(); + for (auto [i, j] : tiles_on_segment(p1, p2)) { + result.emplace_back(i, j); + } + assert(result.size() == 1); + assert(result[0] == std::make_tuple(7, 8)); + + return 0; +} diff --git a/util/test/test_vec2.cpp b/util/test/test_vec2.cpp index 31253e3..96cbebf 100644 --- a/util/test/test_vec2.cpp +++ b/util/test/test_vec2.cpp @@ -20,15 +20,25 @@ int main() { Vec2 v4 = v1 - v2; assert(v4.x == 2.0f && v4.y == 2.0f); - // Test dot and cross - assert(dot(v1, v2) == 11.0f); - assert(cross(v1, v2) == 2.0f); - // Test floor and round Vec2 v5(1.7f, -2.3f); auto f = v5.floor(); auto r = v5.round(); assert(f == std::make_tuple(1, -3)); assert(r == std::make_tuple(2, -2)); + + // Test inf and invalid + Vec2 vinf = Vec2::inf(); + assert(std::isinf(vinf.x) && std::isinf(vinf.y)); + Vec2 vinvalid = Vec2::invalid(); + assert(std::isnan(vinvalid.x) && std::isnan(vinvalid.y)); + + // Test is_valid + assert(v1.is_valid()); + assert(!vinvalid.is_valid()); + + // Test static dot and cross + assert(Vec2::dot(v1, v2) == 11.0f); + assert(Vec2::cross(v1, v2) == 2.0f); return 0; }