diff --git a/util/include/istd_util/tile_geometry.h b/util/include/istd_util/tile_geometry.h index 17794de..5f9c6e6 100644 --- a/util/include/istd_util/tile_geometry.h +++ b/util/include/istd_util/tile_geometry.h @@ -2,9 +2,9 @@ #define ISTD_UTIL_TILE_GEOMETRY_H #include "istd_util/vec2.h" +#include #include #include -#include namespace istd { @@ -24,10 +24,30 @@ namespace istd { * @return Generator yielding (i, j) tuples for each tile crossed by the * segment. */ -std::generator> tiles_on_segment( +std::generator> tiles_on_segment( Vec2 p1, Vec2 p2 ) noexcept; +/** + * @brief Computes the first intersection point between a line segment and a + * tile in the tilemap. + * + * Uses a coordinate system where x points downward (row index) and y points + * rightward (column index). Finds the intersection point (closest to p1) + * between the segment from p1 to p2 and the square tile specified by (i, j), + * where the tile is defined as the region from (i, j) to (i+1, j+1). + * + * @param p1 The starting point of the segment (floating point coordinates). + * @param p2 The ending point of the segment (floating point coordinates). + * @param tile The tile indices (i, j) representing the square from (i, j) to + * (i+1, j+1). + * @return The intersection point as a Vec2, or an undefined value if there is + * no intersection. + */ +Vec2 tile_segment_intersection( + Vec2 p1, Vec2 p2, std::array tile +) noexcept; + } // namespace istd #endif \ No newline at end of file diff --git a/util/src/tile_geometry.cpp b/util/src/tile_geometry.cpp index 4a04c0c..78b2b63 100644 --- a/util/src/tile_geometry.cpp +++ b/util/src/tile_geometry.cpp @@ -3,7 +3,7 @@ namespace istd { // Amanatides-Woo Algorithm -std::generator> tiles_on_segment( +std::generator> tiles_on_segment( Vec2 p1, Vec2 p2 ) noexcept { auto [i, j] = p1.floor(); @@ -38,7 +38,13 @@ std::generator> tiles_on_segment( auto [end_i, end_j] = p2.floor(); while (i != end_i || j != end_j) { - if (t_max.x < t_max.y) { + if (std::abs(t_max.x - t_max.y) < 1e-6f) { + // Both directions are equal, choose one arbitrarily + i += step_x; + j += step_y; + t_max.x += t_delta.x; + t_max.y += t_delta.y; + } else if (t_max.x < t_max.y) { i += step_x; t_max.x += t_delta.x; } else { @@ -49,4 +55,48 @@ std::generator> tiles_on_segment( } } +Vec2 tile_segment_intersection( + Vec2 p1, Vec2 p2, std::array tile +) noexcept { + // Tile bounds: [i, i+1) x [j, j+1) + float i = static_cast(tile[0]); + float j = static_cast(tile[1]); + float min_x = i, max_x = i + 1; + float min_y = j, max_y = j + 1; + + // Parametric line: p = p1 + t * (p2 - p1), t in [0, 1] + Vec2 d = p2 - p1; + float t_min = 0.0f, t_max = 1.0f; + + // For each slab (x and y), compute intersection interval + for (int axis = 0; axis < 2; ++axis) { + float p = axis == 0 ? p1.x : p1.y; + float q = axis == 0 ? d.x : d.y; + float slab_min = axis == 0 ? min_x : min_y; + float slab_max = axis == 0 ? max_x : max_y; + if (std::abs(q) < 1e-8f) { + // Parallel to slab, outside + if (p < slab_min || p > slab_max) { + return Vec2::invalid(); + } + } else { + float t1 = (slab_min - p) / q; + float t2 = (slab_max - p) / q; + if (t1 > t2) { + std::swap(t1, t2); + } + t_min = std::max(t_min, t1); + t_max = std::min(t_max, t2); + if (t_min > t_max) { + return Vec2::invalid(); + } + } + } + // Intersection exists in [t_min, t_max], want closest to p1 (t_min >= 0) + if (t_min < 0.0f || t_min > 1.0f) { + return Vec2::invalid(); + } + return p1 + d * t_min; +} + } // namespace istd diff --git a/util/test/test_tile_geometry.cpp b/util/test/test_tile_geometry.cpp index f77a20b..b6066c7 100644 --- a/util/test/test_tile_geometry.cpp +++ b/util/test/test_tile_geometry.cpp @@ -56,5 +56,43 @@ int main() { assert(result.size() == 1); assert(result[0] == std::make_tuple(7, 8)); + // Test tile_segment_intersection: horizontal segment + p1 = Vec2(0.5f, 1.2f); + p2 = Vec2(0.5f, 4.8f); + { + Vec2 inter = tile_segment_intersection(p1, p2, {0, 2}); + assert(inter.is_valid()); + assert(std::abs(inter.x - 0.5f) < 1e-6); + assert(inter.y >= 2.0f && inter.y <= 3.0f); + } + + // Test tile_segment_intersection: diagonal segment + p1 = Vec2(1.1f, 1.1f); + p2 = Vec2(3.9f, 3.9f); + { + Vec2 inter = tile_segment_intersection(p1, p2, {2, 2}); + assert(inter.is_valid()); + assert(inter.x >= 2.0f && inter.x <= 3.0f); + assert(inter.y >= 2.0f && inter.y <= 3.0f); + } + + // Test tile_segment_intersection: no intersection + p1 = Vec2(0.0f, 0.0f); + p2 = Vec2(0.5f, 0.5f); + { + Vec2 inter = tile_segment_intersection(p1, p2, {2, 2}); + assert(!inter.is_valid()); + } + + // Test tile_segment_intersection: segment starts inside tile + p1 = Vec2(2.2f, 2.2f); + p2 = Vec2(5.0f, 5.0f); + { + Vec2 inter = tile_segment_intersection(p1, p2, {2, 2}); + assert(inter.is_valid()); + assert(std::abs(inter.x - 2.2f) < 1e-6); + assert(std::abs(inter.y - 2.2f) < 1e-6); + } + return 0; } diff --git a/util/test/test_vec2.cpp b/util/test/test_vec2.cpp index 96cbebf..54deca4 100644 --- a/util/test/test_vec2.cpp +++ b/util/test/test_vec2.cpp @@ -1,5 +1,6 @@ #include "istd_util/vec2.h" #include +#include using namespace istd; int main() {