diff --git a/.claude/settings.local.json b/.claude/settings.local.json
deleted file mode 100644
index 63da4213..00000000
--- a/.claude/settings.local.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "permissions": {
- "allow": [
- "Bash(ls:*)"
- ]
- }
-}
diff --git a/.idea/editor.xml b/.idea/editor.xml
index 2eff1aff..9b5acf8e 100644
--- a/.idea/editor.xml
+++ b/.idea/editor.xml
@@ -17,7 +17,7 @@
-
+
@@ -103,14 +103,14 @@
-
+
-
+
@@ -202,7 +202,7 @@
-
+
@@ -216,7 +216,7 @@
-
+
diff --git a/include/omath/collision/line_tracer.hpp b/include/omath/collision/line_tracer.hpp
index 3bba86e4..65b253e3 100644
--- a/include/omath/collision/line_tracer.hpp
+++ b/include/omath/collision/line_tracer.hpp
@@ -3,6 +3,7 @@
//
#pragma once
+#include "omath/linear_algebra/aabb.hpp"
#include "omath/linear_algebra/triangle.hpp"
#include "omath/linear_algebra/vector3.hpp"
@@ -34,6 +35,7 @@ namespace omath::collision
class LineTracer final
{
using TriangleType = Triangle;
+ using AABBType = AABB;
public:
LineTracer() = delete;
@@ -87,6 +89,54 @@ namespace omath::collision
return ray.start + ray_dir * t_hit;
}
+ // Slab method ray-AABB intersection
+ // Returns the hit point on the AABB surface, or ray.end if no intersection
+ [[nodiscard]]
+ constexpr static auto get_ray_hit_point(const RayType& ray, const AABBType& aabb) noexcept
+ {
+ using T = typename RayType::VectorType::ContainedType;
+ const auto dir = ray.direction_vector();
+
+ auto t_min = -std::numeric_limits::infinity();
+ auto t_max = std::numeric_limits::infinity();
+
+ const auto process_axis = [&](const T& d, const T& origin, const T& box_min,
+ const T& box_max) -> bool
+ {
+ constexpr T k_epsilon = std::numeric_limits::epsilon();
+ if (std::abs(d) < k_epsilon)
+ return origin >= box_min && origin <= box_max;
+
+ const T inv = T(1) / d;
+ T t0 = (box_min - origin) * inv;
+ T t1 = (box_max - origin) * inv;
+ if (t0 > t1)
+ std::swap(t0, t1);
+
+ t_min = std::max(t_min, t0);
+ t_max = std::min(t_max, t1);
+ return t_min <= t_max;
+ };
+
+ if (!process_axis(dir.x, ray.start.x, aabb.min.x, aabb.max.x))
+ return ray.end;
+ if (!process_axis(dir.y, ray.start.y, aabb.min.y, aabb.max.y))
+ return ray.end;
+ if (!process_axis(dir.z, ray.start.z, aabb.min.z, aabb.max.z))
+ return ray.end;
+
+ // t_hit: use entry point if in front of origin, otherwise 0 (started inside)
+ const T t_hit = std::max(T(0), t_min);
+
+ if (t_max < T(0))
+ return ray.end; // box entirely behind origin
+
+ if (!ray.infinite_length && t_hit > T(1))
+ return ray.end; // box beyond ray endpoint
+
+ return ray.start + dir * t_hit;
+ }
+
template
[[nodiscard]]
constexpr static auto get_ray_hit_point(const RayType& ray, const MeshType& mesh) noexcept
diff --git a/include/omath/linear_algebra/aabb.hpp b/include/omath/linear_algebra/aabb.hpp
new file mode 100644
index 00000000..4faac54a
--- /dev/null
+++ b/include/omath/linear_algebra/aabb.hpp
@@ -0,0 +1,33 @@
+//
+// Created by Vlad on 3/25/2025.
+//
+#pragma once
+
+#include "omath/linear_algebra/vector3.hpp"
+
+namespace omath
+{
+ template>
+ class AABB final
+ {
+ public:
+ using VectorType = Vector;
+
+ VectorType min;
+ VectorType max;
+
+ constexpr AABB(const VectorType& min, const VectorType& max) noexcept : min(min), max(max) {}
+
+ [[nodiscard]]
+ constexpr VectorType center() const noexcept
+ {
+ return (min + max) / static_cast(2);
+ }
+
+ [[nodiscard]]
+ constexpr VectorType extents() const noexcept
+ {
+ return (max - min) / static_cast(2);
+ }
+ };
+} // namespace omath
diff --git a/tests/general/unit_test_line_tracer_aabb.cpp b/tests/general/unit_test_line_tracer_aabb.cpp
new file mode 100644
index 00000000..00b77880
--- /dev/null
+++ b/tests/general/unit_test_line_tracer_aabb.cpp
@@ -0,0 +1,126 @@
+//
+// Created by Vlad on 3/25/2025.
+//
+#include "omath/collision/line_tracer.hpp"
+#include "omath/linear_algebra/aabb.hpp"
+#include "omath/linear_algebra/vector3.hpp"
+#include
+
+using Vec3 = omath::Vector3;
+using Ray = omath::collision::Ray<>;
+using LineTracer = omath::collision::LineTracer<>;
+using AABB = omath::AABB;
+
+static Ray make_ray(Vec3 start, Vec3 end, bool infinite = false)
+{
+ Ray r;
+ r.start = start;
+ r.end = end;
+ r.infinite_length = infinite;
+ return r;
+}
+
+// Ray passing straight through the center along Z axis
+TEST(LineTracerAABBTests, HitCenterAlongZ)
+{
+ const AABB box{{-1.f, -1.f, -1.f}, {1.f, 1.f, 1.f}};
+ const auto ray = make_ray({0.f, 0.f, -5.f}, {0.f, 0.f, 5.f});
+
+ const auto hit = LineTracer::get_ray_hit_point(ray, box);
+ EXPECT_NE(hit, ray.end);
+ EXPECT_NEAR(hit.z, -1.f, 1e-4f);
+ EXPECT_NEAR(hit.x, 0.f, 1e-4f);
+ EXPECT_NEAR(hit.y, 0.f, 1e-4f);
+}
+
+// Ray passing straight through the center along X axis
+TEST(LineTracerAABBTests, HitCenterAlongX)
+{
+ const AABB box{{-1.f, -1.f, -1.f}, {1.f, 1.f, 1.f}};
+ const auto ray = make_ray({-5.f, 0.f, 0.f}, {5.f, 0.f, 0.f});
+
+ const auto hit = LineTracer::get_ray_hit_point(ray, box);
+ EXPECT_NE(hit, ray.end);
+ EXPECT_NEAR(hit.x, -1.f, 1e-4f);
+}
+
+// Ray that misses entirely (too far in Y)
+TEST(LineTracerAABBTests, MissReturnsEnd)
+{
+ const AABB box{{-1.f, -1.f, -1.f}, {1.f, 1.f, 1.f}};
+ const auto ray = make_ray({0.f, 5.f, -5.f}, {0.f, 5.f, 5.f});
+
+ const auto hit = LineTracer::get_ray_hit_point(ray, box);
+ EXPECT_EQ(hit, ray.end);
+}
+
+// Ray that stops short before reaching the box
+TEST(LineTracerAABBTests, RayTooShortReturnsEnd)
+{
+ const AABB box{{3.f, -1.f, -1.f}, {5.f, 1.f, 1.f}};
+ const auto ray = make_ray({0.f, 0.f, 0.f}, {2.f, 0.f, 0.f});
+
+ const auto hit = LineTracer::get_ray_hit_point(ray, box);
+ EXPECT_EQ(hit, ray.end);
+}
+
+// Infinite ray that starts before the box should hit
+TEST(LineTracerAABBTests, InfiniteRayHits)
+{
+ const AABB box{{3.f, -1.f, -1.f}, {5.f, 1.f, 1.f}};
+ const auto ray = make_ray({0.f, 0.f, 0.f}, {2.f, 0.f, 0.f}, true);
+
+ const auto hit = LineTracer::get_ray_hit_point(ray, box);
+ EXPECT_NE(hit, ray.end);
+ EXPECT_NEAR(hit.x, 3.f, 1e-4f);
+}
+
+// Ray starting inside the box — t_min=0, so hit point equals ray.start
+TEST(LineTracerAABBTests, RayStartsInsideBox)
+{
+ const AABB box{{-1.f, -1.f, -1.f}, {1.f, 1.f, 1.f}};
+ const auto ray = make_ray({0.f, 0.f, 0.f}, {0.f, 0.f, 5.f});
+
+ const auto hit = LineTracer::get_ray_hit_point(ray, box);
+ EXPECT_NE(hit, ray.end);
+ // t_min is clamped to 0, so hit == start
+ EXPECT_NEAR(hit.x, 0.f, 1e-4f);
+ EXPECT_NEAR(hit.y, 0.f, 1e-4f);
+ EXPECT_NEAR(hit.z, 0.f, 1e-4f);
+}
+
+// Ray parallel to XY plane, pointing along X, at Z outside the box
+TEST(LineTracerAABBTests, ParallelRayOutsideSlabMisses)
+{
+ const AABB box{{-1.f, -1.f, -1.f}, {1.f, 1.f, 1.f}};
+ // Z component of ray is 3.0 — outside box's Z slab
+ const auto ray = make_ray({-5.f, 0.f, 3.f}, {5.f, 0.f, 3.f});
+
+ const auto hit = LineTracer::get_ray_hit_point(ray, box);
+ EXPECT_EQ(hit, ray.end);
+}
+
+// Ray parallel to XY plane, pointing along X, at Z inside the box
+TEST(LineTracerAABBTests, ParallelRayInsideSlabHits)
+{
+ const AABB box{{-1.f, -1.f, -1.f}, {1.f, 1.f, 1.f}};
+ const auto ray = make_ray({-5.f, 0.f, 0.f}, {5.f, 0.f, 0.f});
+
+ const auto hit = LineTracer::get_ray_hit_point(ray, box);
+ EXPECT_NE(hit, ray.end);
+ EXPECT_NEAR(hit.x, -1.f, 1e-4f);
+}
+
+// Diagonal ray hitting a corner region
+TEST(LineTracerAABBTests, DiagonalRayHits)
+{
+ const AABB box{{0.f, 0.f, 0.f}, {2.f, 2.f, 2.f}};
+ const auto ray = make_ray({-1.f, -1.f, -1.f}, {3.f, 3.f, 3.f});
+
+ const auto hit = LineTracer::get_ray_hit_point(ray, box);
+ EXPECT_NE(hit, ray.end);
+ // Entry point should be at (0,0,0)
+ EXPECT_NEAR(hit.x, 0.f, 1e-4f);
+ EXPECT_NEAR(hit.y, 0.f, 1e-4f);
+ EXPECT_NEAR(hit.z, 0.f, 1e-4f);
+}