diff --git a/include/omath/linear_algebra/mat.hpp b/include/omath/linear_algebra/mat.hpp index 060492fc..948e9df0 100644 --- a/include/omath/linear_algebra/mat.hpp +++ b/include/omath/linear_algebra/mat.hpp @@ -123,7 +123,7 @@ namespace omath } [[nodiscard]] - static consteval MatSize size() noexcept + static constexpr MatSize size() noexcept { return {Rows, Columns}; } @@ -854,4 +854,4 @@ struct std::formatter> // NOLINT(*-dc if constexpr (std::is_same_v) return std::format_to(ctx.out(), u8"{}", mat.to_u8string()); } -}; \ No newline at end of file +}; diff --git a/include/omath/lua/lua.hpp b/include/omath/lua/lua.hpp index 45c0942f..1110b253 100644 --- a/include/omath/lua/lua.hpp +++ b/include/omath/lua/lua.hpp @@ -15,11 +15,16 @@ namespace omath::lua static void register_vec2(sol::table& omath_table); static void register_vec3(sol::table& omath_table); static void register_vec4(sol::table& omath_table); + static void register_matrices(sol::table& omath_table); + static void register_quaternion(sol::table& omath_table); static void register_color(sol::table& omath_table); + static void register_hud(sol::table& omath_table); static void register_triangle(sol::table& omath_table); + static void register_3d_primitives(sol::table& omath_table); + static void register_collision(sol::table& omath_table); static void register_shared_types(sol::table& omath_table); static void register_engines(sol::table& omath_table); static void register_pattern_scan(sol::table& omath_table); }; } -#endif \ No newline at end of file +#endif diff --git a/include/omath/projection/camera.hpp b/include/omath/projection/camera.hpp index f5c66b67..30da3547 100644 --- a/include/omath/projection/camera.hpp +++ b/include/omath/projection/camera.hpp @@ -107,7 +107,8 @@ namespace omath::projection // m[1,1] == 1 / tan(fov/2) => fov = 2 * atan(1 / m[1,1]) const auto f = proj_matrix.at(1, 1); // m[0,0] == m[1,1] / aspect_ratio => aspect = m[1,1] / m[0,0] - return {FieldOfView::from_radians(NumericType{2} * std::atan(NumericType{1} / f)), + const auto fov_radians = NumericType{2} * std::atan(NumericType{1} / f); + return {FieldOfView::from_radians(static_cast(fov_radians)), f / proj_matrix.at(0, 0)}; } diff --git a/source/lua/lua.cpp b/source/lua/lua.cpp index 0d226cea..d76b8abd 100644 --- a/source/lua/lua.cpp +++ b/source/lua/lua.cpp @@ -17,8 +17,13 @@ namespace omath::lua register_vec2(omath_table); register_vec3(omath_table); register_vec4(omath_table); + register_matrices(omath_table); + register_quaternion(omath_table); register_color(omath_table); + register_hud(omath_table); register_triangle(omath_table); + register_3d_primitives(omath_table); + register_collision(omath_table); register_shared_types(omath_table); register_engines(omath_table); register_pattern_scan(omath_table); diff --git a/source/lua/lua_collision.cpp b/source/lua/lua_collision.cpp new file mode 100644 index 00000000..00e55d34 --- /dev/null +++ b/source/lua/lua_collision.cpp @@ -0,0 +1,304 @@ +// +// Created by orange on 07.03.2026. +// +#ifdef OMATH_ENABLE_LUA +#include "omath/lua/lua.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + using Vec3f = omath::Vector3; + using Triangle3f = omath::Triangle; + using Ray3f = omath::collision::Ray; + using LineTracer3f = omath::collision::LineTracer; + using Aabbf = omath::primitives::Aabb; + using Obbf = omath::primitives::Obb; + + template + auto lua_field(Value Object::* member) + { + return sol::property( + [member](const Object& object) -> const Value& + { + return object.*member; + }, + [member](Object& object, const Value& value) + { + object.*member = value; + }); + } + + class LuaConvexCollider final : public omath::collision::ColliderInterface + { + public: + using VectorType = Vec3f; + + explicit LuaConvexCollider(std::vector vertices, const Vec3f& origin = {}) + : m_vertices(std::move(vertices)), m_origin(origin) + { + if (m_vertices.empty()) + throw std::invalid_argument("convex collider must contain at least one vertex"); + } + + [[nodiscard]] + Vec3f find_abs_furthest_vertex_position(const Vec3f& direction) const override + { + const auto furthest = std::ranges::max_element( + m_vertices, + [&direction](const Vec3f& first, const Vec3f& second) + { + return first.dot(direction) < second.dot(direction); + }); + + return m_origin + *furthest; + } + + [[nodiscard]] + const Vec3f& get_origin() const override + { + return m_origin; + } + + void set_origin(const Vec3f& new_origin) override + { + m_origin = new_origin; + } + + [[nodiscard]] + std::size_t vertex_count() const noexcept + { + return m_vertices.size(); + } + + [[nodiscard]] + const std::vector& vertices() const noexcept + { + return m_vertices; + } + + private: + std::vector m_vertices; + Vec3f m_origin; + }; + + std::vector vec3_table_to_vector(const sol::table& points) + { + std::vector result; + for (std::size_t i = 1;; ++i) + { + const auto point = points[i].get>(); + if (!point) + break; + result.push_back(*point); + } + return result; + } + + sol::table vec3_array_to_table(const auto& points, sol::this_state state) + { + sol::state_view lua(state); + sol::table result = lua.create_table(static_cast(points.size()), 0); + for (std::size_t i = 0; i < points.size(); ++i) + result[i + 1] = points[i]; + return result; + } + + Vec3f aabb_top(const Aabbf& aabb, const omath::primitives::UpAxis axis) + { + switch (axis) + { + case omath::primitives::UpAxis::X: + return aabb.top(); + case omath::primitives::UpAxis::Y: + return aabb.top(); + case omath::primitives::UpAxis::Z: + return aabb.top(); + } + std::unreachable(); + } + + Vec3f aabb_bottom(const Aabbf& aabb, const omath::primitives::UpAxis axis) + { + switch (axis) + { + case omath::primitives::UpAxis::X: + return aabb.bottom(); + case omath::primitives::UpAxis::Y: + return aabb.bottom(); + case omath::primitives::UpAxis::Z: + return aabb.bottom(); + } + std::unreachable(); + } + + bool ray_hits_triangle(const Ray3f& ray, const Triangle3f& triangle) + { + return !(LineTracer3f::get_ray_hit_point(ray, triangle) == ray.end); + } + + bool ray_hits_aabb(const Ray3f& ray, const Aabbf& aabb) + { + return !(LineTracer3f::get_ray_hit_point(ray, aabb) == ray.end); + } + + bool ray_hits_obb(const Ray3f& ray, const Obbf& obb) + { + return !(LineTracer3f::get_ray_hit_point(ray, obb) == ray.end); + } +} // namespace + +namespace omath::lua +{ + void LuaInterpreter::register_3d_primitives(sol::table& omath_table) + { + auto primitives_table = omath_table["primitives"].get_or_create(); + + primitives_table.new_enum("UpAxis", "X", omath::primitives::UpAxis::X, "Y", omath::primitives::UpAxis::Y, "Z", + omath::primitives::UpAxis::Z); + + primitives_table.new_usertype( + "Aabb", + sol::factories([]() { return Aabbf{}; }, + [](const Vec3f& min, const Vec3f& max) { return Aabbf{min, max}; }), + + "min", lua_field(&Aabbf::min), "max", lua_field(&Aabbf::max), "center", &Aabbf::center, "extents", + &Aabbf::extents, + "top", + [](const Aabbf& aabb, sol::optional axis) + { + return aabb_top(aabb, axis.value_or(omath::primitives::UpAxis::Y)); + }, + "bottom", + [](const Aabbf& aabb, sol::optional axis) + { + return aabb_bottom(aabb, axis.value_or(omath::primitives::UpAxis::Y)); + }, + "is_collide", &Aabbf::is_collide, + "as_table", + [](const Aabbf& aabb, sol::this_state state) -> sol::table + { + sol::state_view lua(state); + sol::table result = lua.create_table(); + result["min"] = aabb.min; + result["max"] = aabb.max; + return result; + }); + + primitives_table.new_usertype( + "Obb", + sol::factories( + []() + { + return Obbf{}; + }, + [](const Vec3f& center, const Vec3f& axis_x, const Vec3f& axis_y, const Vec3f& axis_z, + const Vec3f& half_extents) + { + return Obbf{center, axis_x, axis_y, axis_z, half_extents}; + }), + + "center", lua_field(&Obbf::center), "axis_x", lua_field(&Obbf::axis_x), "axis_y", + lua_field(&Obbf::axis_y), "axis_z", lua_field(&Obbf::axis_z), "half_extents", + lua_field(&Obbf::half_extents), + "vertices", + [](const Obbf& obb, sol::this_state state) + { + return vec3_array_to_table(obb.vertices(), state); + }); + } + + void LuaInterpreter::register_collision(sol::table& omath_table) + { + auto collision_table = omath_table["collision"].get_or_create(); + + collision_table.new_usertype( + "Ray", + sol::factories([]() { return Ray3f{}; }, + [](const Vec3f& start, const Vec3f& end) { return Ray3f{start, end}; }, + [](const Vec3f& start, const Vec3f& end, const bool infinite_length) + { return Ray3f{start, end, infinite_length}; }), + + "start", lua_field(&Ray3f::start), "end", lua_field(&Ray3f::end), "infinite_length", + lua_field(&Ray3f::infinite_length), + "direction_vector", &Ray3f::direction_vector, "direction_vector_normalized", + &Ray3f::direction_vector_normalized); + + collision_table.new_usertype( + "ConvexCollider", + sol::factories([](const sol::table& vertices) { return LuaConvexCollider(vec3_table_to_vector(vertices)); }, + [](const sol::table& vertices, const Vec3f& origin) + { return LuaConvexCollider(vec3_table_to_vector(vertices), origin); }), + + "find_abs_furthest_vertex_position", &LuaConvexCollider::find_abs_furthest_vertex_position, + "get_origin", &LuaConvexCollider::get_origin, "set_origin", &LuaConvexCollider::set_origin, + "vertex_count", &LuaConvexCollider::vertex_count, + "vertices", + [](const LuaConvexCollider& collider, sol::this_state state) + { + return vec3_array_to_table(collider.vertices(), state); + }); + + collision_table.new_usertype( + "LineTracer", sol::no_constructor, "can_trace_line", &LineTracer3f::can_trace_line, + "get_ray_hit_point", + sol::overload( + [](const Ray3f& ray, const Triangle3f& triangle) + { + return LineTracer3f::get_ray_hit_point(ray, triangle); + }, + [](const Ray3f& ray, const Aabbf& aabb) + { + return LineTracer3f::get_ray_hit_point(ray, aabb); + }, + [](const Ray3f& ray, const Obbf& obb) + { + return LineTracer3f::get_ray_hit_point(ray, obb); + }), + "ray_hits_triangle", &ray_hits_triangle, "ray_hits_aabb", &ray_hits_aabb, "ray_hits_obb", + &ray_hits_obb); + + collision_table["gjk_support_vertex"] = + [](const LuaConvexCollider& collider_a, const LuaConvexCollider& collider_b, const Vec3f& direction) + { return omath::collision::GjkAlgorithm::find_support_vertex(collider_a, collider_b, direction); }; + + collision_table["gjk_collide"] = [](const LuaConvexCollider& collider_a, const LuaConvexCollider& collider_b) + { return omath::collision::GjkAlgorithm::is_collide(collider_a, collider_b); }; + + collision_table["epa_solve"] = [](const LuaConvexCollider& collider_a, const LuaConvexCollider& collider_b, + sol::this_state state) -> sol::object + { + using Gjk = omath::collision::GjkAlgorithm; + using Epa = omath::collision::Epa; + + sol::state_view lua(state); + const auto gjk = Gjk::is_collide_with_simplex_info(collider_a, collider_b); + if (!gjk.hit) + return sol::nil; + + const auto epa = Epa::solve(collider_a, collider_b, gjk.simplex); + if (!epa) + return sol::nil; + + sol::table result = lua.create_table(); + result["normal"] = epa->normal; + result["penetration_vector"] = epa->penetration_vector; + result["depth"] = epa->depth; + result["iterations"] = epa->iterations; + result["num_vertices"] = epa->num_vertices; + result["num_faces"] = epa->num_faces; + return sol::make_object(lua, result); + }; + } +} // namespace omath::lua +#endif diff --git a/source/lua/lua_engines.cpp b/source/lua/lua_engines.cpp index 76a6c816..31a49994 100644 --- a/source/lua/lua_engines.cpp +++ b/source/lua/lua_engines.cpp @@ -14,6 +14,8 @@ #include #include #include +#include +#include namespace { @@ -85,44 +87,72 @@ namespace using PitchAngle = typename EngineTraits::PitchAngle; using ViewAngles = typename EngineTraits::ViewAngles; using Camera = typename EngineTraits::Camera; + using Mat4X4 = std::remove_cvref_t().get_view_matrix())>; auto engine_table = omath_table[subtable_name].get_or_create(); auto types = omath_table["_types"].get(); set_engine_aliases(engine_table, types); - engine_table.new_usertype( + auto camera_type = engine_table.new_usertype( "Camera", sol::constructors&, const ViewAngles&, const omath::projection::ViewPort&, const omath::projection::FieldOfView&, - ArithmeticType, ArithmeticType)>(), - "look_at", &Camera::look_at, "get_forward", &Camera::get_forward, "get_right", &Camera::get_right, - "get_up", &Camera::get_up, "get_origin", &Camera::get_origin, "get_view_angles", - &Camera::get_view_angles, "get_near_plane", &Camera::get_near_plane, "get_far_plane", - &Camera::get_far_plane, "get_field_of_view", &Camera::get_field_of_view, "set_origin", - &Camera::set_origin, "set_view_angles", &Camera::set_view_angles, "set_view_port", - &Camera::set_view_port, "set_field_of_view", &Camera::set_field_of_view, "set_near_plane", - &Camera::set_near_plane, "set_far_plane", &Camera::set_far_plane, - - "world_to_screen", - [](const Camera& cam, const omath::Vector3& pos) - -> std::tuple>, sol::optional> - { - auto result = cam.world_to_screen(pos); - if (result) - return {*result, sol::nullopt}; - return {sol::nullopt, projection_error_to_string(result.error())}; - }, - - "screen_to_world", - [](const Camera& cam, const omath::Vector3& pos) - -> std::tuple>, sol::optional> - { - auto result = cam.screen_to_world(pos); - if (result) - return {*result, sol::nullopt}; - return {sol::nullopt, projection_error_to_string(result.error())}; - }); + ArithmeticType, ArithmeticType)>()); + + camera_type["look_at"] = &Camera::look_at; + camera_type["get_forward"] = &Camera::get_forward; + camera_type["get_right"] = &Camera::get_right; + camera_type["get_up"] = &Camera::get_up; + camera_type["get_origin"] = &Camera::get_origin; + camera_type["get_view_angles"] = &Camera::get_view_angles; + camera_type["get_near_plane"] = &Camera::get_near_plane; + camera_type["get_far_plane"] = &Camera::get_far_plane; + camera_type["get_field_of_view"] = &Camera::get_field_of_view; + camera_type["set_origin"] = &Camera::set_origin; + camera_type["set_view_angles"] = &Camera::set_view_angles; + camera_type["set_view_port"] = &Camera::set_view_port; + camera_type["set_field_of_view"] = &Camera::set_field_of_view; + camera_type["set_near_plane"] = &Camera::set_near_plane; + camera_type["set_far_plane"] = &Camera::set_far_plane; + + camera_type["get_view_matrix"] = [](const Camera& cam) -> Mat4X4 + { + return cam.get_view_matrix(); + }; + camera_type["get_projection_matrix"] = [](const Camera& cam) -> Mat4X4 + { + return cam.get_projection_matrix(); + }; + camera_type["get_view_projection_matrix"] = [](const Camera& cam) -> Mat4X4 + { + return cam.get_view_projection_matrix(); + }; + camera_type["extract_projection_params"] = [](const Mat4X4& projection_matrix) + { + const auto params = Camera::extract_projection_params(projection_matrix); + return std::make_tuple(params.fov, params.aspect_ratio); + }; + camera_type["calc_view_angles_from_view_matrix"] = &Camera::calc_view_angles_from_view_matrix; + camera_type["calc_origin_from_view_matrix"] = &Camera::calc_origin_from_view_matrix; + + camera_type["world_to_screen"] = [](const Camera& cam, const omath::Vector3& pos) + -> std::tuple>, sol::optional> + { + auto result = cam.world_to_screen(pos); + if (result) + return {*result, sol::nullopt}; + return {sol::nullopt, projection_error_to_string(result.error())}; + }; + + camera_type["screen_to_world"] = [](const Camera& cam, const omath::Vector3& pos) + -> std::tuple>, sol::optional> + { + auto result = cam.screen_to_world(pos); + if (result) + return {*result, sol::nullopt}; + return {sol::nullopt, projection_error_to_string(result.error())}; + }; } // ---- Engine trait structs ----------------------------------------------- diff --git a/source/lua/lua_hud.cpp b/source/lua/lua_hud.cpp new file mode 100644 index 00000000..3a790240 --- /dev/null +++ b/source/lua/lua_hud.cpp @@ -0,0 +1,516 @@ +// +// Created by orange on 07.03.2026. +// +#ifdef OMATH_ENABLE_LUA +#include "omath/lua/lua.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + [[noreturn]] + void throw_lua_error(sol::protected_function_result& result) + { + sol::error err = result; + throw err; + } + + class LuaHudRenderer final : public omath::hud::HudRendererInterface + { + public: + explicit LuaHudRenderer(sol::table callbacks): m_callbacks(std::move(callbacks)) + { + } + + void add_line(const omath::Vector2& line_start, const omath::Vector2& line_end, + const omath::Color& color, const float thickness) override + { + call_optional("add_line", line_start, line_end, color, thickness); + } + + void add_polyline(const std::span>& vertexes, const omath::Color& color, + const float thickness) override + { + call_optional("add_polyline", make_points_table(vertexes), color, thickness); + } + + void add_filled_polyline(const std::span>& vertexes, + const omath::Color& color) override + { + call_optional("add_filled_polyline", make_points_table(vertexes), color); + } + + void add_rectangle(const omath::Vector2& min, const omath::Vector2& max, + const omath::Color& color) override + { + call_optional("add_rectangle", min, max, color); + } + + void add_filled_rectangle(const omath::Vector2& min, const omath::Vector2& max, + const omath::Color& color) override + { + call_optional("add_filled_rectangle", min, max, color); + } + + void add_circle(const omath::Vector2& center, const float radius, const omath::Color& color, + const float thickness, const int segments) override + { + call_optional("add_circle", center, radius, color, thickness, segments); + } + + void add_filled_circle(const omath::Vector2& center, const float radius, const omath::Color& color, + const int segments) override + { + call_optional("add_filled_circle", center, radius, color, segments); + } + + void add_arc(const omath::Vector2& center, const float radius, const float a_min, const float a_max, + const omath::Color& color, const float thickness, const int segments) override + { + call_optional("add_arc", center, radius, a_min, a_max, color, thickness, segments); + } + + void add_image(const std::any& texture_id, const omath::Vector2& min, const omath::Vector2& max, + const omath::Color& tint) override + { + const auto callback = callback_for("add_image"); + if (!callback) + return; + + sol::object texture = sol::nil; + if (const auto lua_object = std::any_cast(&texture_id)) + texture = *lua_object; + + auto result = (*callback)(texture, min, max, tint); + if (!result.valid()) + throw_lua_error(result); + } + + void add_text(const omath::Vector2& position, const omath::Color& color, + const std::string_view& text) override + { + call_optional("add_text", position, color, std::string{text}); + } + + [[nodiscard]] + omath::Vector2 calc_text_size(const std::string_view& text) override + { + const auto callback = callback_for("calc_text_size"); + if (!callback) + return {}; + + auto result = (*callback)(std::string{text}); + if (!result.valid()) + throw_lua_error(result); + + return result.get>(); + } + + private: + sol::main_table m_callbacks; + + [[nodiscard]] + sol::optional callback_for(const char* name) const + { + const sol::object callback = m_callbacks[name]; + if (!callback.valid() || callback == sol::nil) + return sol::nullopt; + return callback.as(); + } + + template + void call_optional(const char* name, Args&&... args) const + { + const auto callback = callback_for(name); + if (!callback) + return; + + auto result = (*callback)(std::forward(args)...); + if (!result.valid()) + throw_lua_error(result); + } + + [[nodiscard]] + sol::table make_points_table(const std::span>& vertexes) const + { + sol::state_view lua(m_callbacks.lua_state()); + sol::table points = lua.create_table(static_cast(vertexes.size()), 0); + for (std::size_t i = 0; i < vertexes.size(); ++i) + points[i + 1] = vertexes[i]; + return points; + } + }; + + [[nodiscard]] + omath::Color transparent_black() + { + return {0.f, 0.f, 0.f, 0.f}; + } + + [[nodiscard]] + omath::Color opaque_white() + { + return {1.f, 1.f, 1.f, 1.f}; + } + + [[nodiscard]] + omath::hud::EntityOverlay make_overlay(const omath::Vector2& top, const omath::Vector2& bottom, + const std::shared_ptr& renderer) + { + if (!renderer) + throw std::invalid_argument("hud renderer must not be nil"); + return {top, bottom, renderer}; + } + + [[nodiscard]] + std::any texture_id_from_lua_object(const sol::object& texture_id) + { + return texture_id; + } + + std::vector> points_from_table(const sol::table& points) + { + std::vector> result; + for (std::size_t i = 1;; ++i) + { + const auto point = points[i].get>>(); + if (!point) + break; + result.push_back(*point); + } + return result; + } + + template + auto lua_field(Value Object::* member) + { + return sol::property( + [member](const Object& object) -> const Value& + { + return object.*member; + }, + [member](Object& object, const Value& value) + { + object.*member = value; + }); + } +} // namespace + +namespace omath::lua +{ + void LuaInterpreter::register_hud(sol::table& omath_table) + { + auto hud_table = omath_table["hud"].get_or_create(); + + hud_table.new_usertype( + "CanvasBox", + sol::factories([](const omath::Vector2& top, const omath::Vector2& bottom) + { return omath::hud::CanvasBox(top, bottom); }, + [](const omath::Vector2& top, const omath::Vector2& bottom, + const float ratio) { return omath::hud::CanvasBox(top, bottom, ratio); }), + + "top_left_corner", lua_field(&omath::hud::CanvasBox::top_left_corner), "top_right_corner", + lua_field(&omath::hud::CanvasBox::top_right_corner), "bottom_left_corner", + lua_field(&omath::hud::CanvasBox::bottom_left_corner), "bottom_right_corner", + lua_field(&omath::hud::CanvasBox::bottom_right_corner), + + "as_table", + [](const omath::hud::CanvasBox& box, sol::this_state s) -> sol::table + { + sol::state_view lua(s); + sol::table t = lua.create_table(4, 0); + const auto points = box.as_array(); + for (std::size_t i = 0; i < points.size(); ++i) + t[i + 1] = points[i]; + return t; + }); + + hud_table.new_usertype( + "Renderer", + sol::factories([](const sol::table& callbacks) + { return std::make_shared(callbacks); }), + + "add_line", &LuaHudRenderer::add_line, + "add_polyline", + [](LuaHudRenderer& renderer, const sol::table& points, const omath::Color& color, + const float thickness) + { + const auto vertices = points_from_table(points); + renderer.add_polyline({vertices.data(), vertices.size()}, color, thickness); + }, + "add_filled_polyline", + [](LuaHudRenderer& renderer, const sol::table& points, const omath::Color& color) + { + const auto vertices = points_from_table(points); + renderer.add_filled_polyline({vertices.data(), vertices.size()}, color); + }, + "add_rectangle", &LuaHudRenderer::add_rectangle, "add_filled_rectangle", + &LuaHudRenderer::add_filled_rectangle, "add_circle", + [](LuaHudRenderer& renderer, const omath::Vector2& center, const float radius, + const omath::Color& color, const float thickness, sol::optional segments) + { + renderer.add_circle(center, radius, color, thickness, segments.value_or(0)); + }, + "add_filled_circle", + [](LuaHudRenderer& renderer, const omath::Vector2& center, const float radius, + const omath::Color& color, sol::optional segments) + { + renderer.add_filled_circle(center, radius, color, segments.value_or(0)); + }, + "add_arc", + [](LuaHudRenderer& renderer, const omath::Vector2& center, const float radius, + const float a_min, const float a_max, const omath::Color& color, const float thickness, + sol::optional segments) + { + renderer.add_arc(center, radius, a_min, a_max, color, thickness, segments.value_or(0)); + }, + "add_image", + [](LuaHudRenderer& renderer, const sol::object& texture_id, const omath::Vector2& min, + const omath::Vector2& max, sol::optional tint) + { + renderer.add_image(texture_id_from_lua_object(texture_id), min, max, + tint.value_or(opaque_white())); + }, + "add_text", &LuaHudRenderer::add_text, "calc_text_size", &LuaHudRenderer::calc_text_size); + + hud_table.new_usertype( + "EntityOverlay", + sol::factories(&make_overlay), + + "add_2d_box", + [](omath::hud::EntityOverlay& overlay, const omath::Color& box_color, + sol::optional fill_color, sol::optional thickness) -> omath::hud::EntityOverlay& + { + return overlay.add_2d_box(box_color, fill_color.value_or(transparent_black()), + thickness.value_or(1.f)); + }, + "add_cornered_2d_box", + [](omath::hud::EntityOverlay& overlay, const omath::Color& box_color, + sol::optional fill_color, sol::optional corner_ratio_len, + sol::optional thickness) -> omath::hud::EntityOverlay& + { + return overlay.add_cornered_2d_box(box_color, fill_color.value_or(transparent_black()), + corner_ratio_len.value_or(0.2f), thickness.value_or(1.f)); + }, + "add_dashed_box", + [](omath::hud::EntityOverlay& overlay, const omath::Color& color, sol::optional dash_len, + sol::optional gap_len, sol::optional thickness) -> omath::hud::EntityOverlay& + { + return overlay.add_dashed_box(color, dash_len.value_or(8.f), gap_len.value_or(5.f), + thickness.value_or(1.f)); + }, + + "add_right_bar", + [](omath::hud::EntityOverlay& overlay, const omath::Color& color, const omath::Color& outline_color, + const omath::Color& bg_color, const float width, const float ratio, + sol::optional offset) -> omath::hud::EntityOverlay& + { + return overlay.add_right_bar(color, outline_color, bg_color, width, ratio, offset.value_or(5.f)); + }, + "add_left_bar", + [](omath::hud::EntityOverlay& overlay, const omath::Color& color, const omath::Color& outline_color, + const omath::Color& bg_color, const float width, const float ratio, + sol::optional offset) -> omath::hud::EntityOverlay& + { + return overlay.add_left_bar(color, outline_color, bg_color, width, ratio, offset.value_or(5.f)); + }, + "add_top_bar", + [](omath::hud::EntityOverlay& overlay, const omath::Color& color, const omath::Color& outline_color, + const omath::Color& bg_color, const float height, const float ratio, + sol::optional offset) -> omath::hud::EntityOverlay& + { + return overlay.add_top_bar(color, outline_color, bg_color, height, ratio, offset.value_or(5.f)); + }, + "add_bottom_bar", + [](omath::hud::EntityOverlay& overlay, const omath::Color& color, const omath::Color& outline_color, + const omath::Color& bg_color, const float height, const float ratio, + sol::optional offset) -> omath::hud::EntityOverlay& + { + return overlay.add_bottom_bar(color, outline_color, bg_color, height, ratio, offset.value_or(5.f)); + }, + "add_right_dashed_bar", + [](omath::hud::EntityOverlay& overlay, const omath::Color& color, const omath::Color& outline_color, + const omath::Color& bg_color, const float width, const float ratio, const float dash_len, + const float gap_len, sol::optional offset) -> omath::hud::EntityOverlay& + { + return overlay.add_right_dashed_bar(color, outline_color, bg_color, width, ratio, dash_len, gap_len, + offset.value_or(5.f)); + }, + "add_left_dashed_bar", + [](omath::hud::EntityOverlay& overlay, const omath::Color& color, const omath::Color& outline_color, + const omath::Color& bg_color, const float width, const float ratio, const float dash_len, + const float gap_len, sol::optional offset) -> omath::hud::EntityOverlay& + { + return overlay.add_left_dashed_bar(color, outline_color, bg_color, width, ratio, dash_len, gap_len, + offset.value_or(5.f)); + }, + "add_top_dashed_bar", + [](omath::hud::EntityOverlay& overlay, const omath::Color& color, const omath::Color& outline_color, + const omath::Color& bg_color, const float height, const float ratio, const float dash_len, + const float gap_len, sol::optional offset) -> omath::hud::EntityOverlay& + { + return overlay.add_top_dashed_bar(color, outline_color, bg_color, height, ratio, dash_len, gap_len, + offset.value_or(5.f)); + }, + "add_bottom_dashed_bar", + [](omath::hud::EntityOverlay& overlay, const omath::Color& color, const omath::Color& outline_color, + const omath::Color& bg_color, const float height, const float ratio, const float dash_len, + const float gap_len, sol::optional offset) -> omath::hud::EntityOverlay& + { + return overlay.add_bottom_dashed_bar(color, outline_color, bg_color, height, ratio, dash_len, + gap_len, offset.value_or(5.f)); + }, + + "add_right_label", + [](omath::hud::EntityOverlay& overlay, const omath::Color& color, const float offset, + const bool outlined, const std::string& text) -> omath::hud::EntityOverlay& + { + return overlay.add_right_label(color, offset, outlined, text); + }, + "add_left_label", + [](omath::hud::EntityOverlay& overlay, const omath::Color& color, const float offset, + const bool outlined, const std::string& text) -> omath::hud::EntityOverlay& + { + return overlay.add_left_label(color, offset, outlined, text); + }, + "add_top_label", + [](omath::hud::EntityOverlay& overlay, const omath::Color& color, const float offset, + const bool outlined, const std::string& text) -> omath::hud::EntityOverlay& + { + return overlay.add_top_label(color, offset, outlined, text); + }, + "add_bottom_label", + [](omath::hud::EntityOverlay& overlay, const omath::Color& color, const float offset, + const bool outlined, const std::string& text) -> omath::hud::EntityOverlay& + { + return overlay.add_bottom_label(color, offset, outlined, text); + }, + "add_centered_top_label", + [](omath::hud::EntityOverlay& overlay, const omath::Color& color, const float offset, + const bool outlined, const std::string& text) -> omath::hud::EntityOverlay& + { + return overlay.add_centered_top_label(color, offset, outlined, text); + }, + "add_centered_bottom_label", + [](omath::hud::EntityOverlay& overlay, const omath::Color& color, const float offset, + const bool outlined, const std::string& text) -> omath::hud::EntityOverlay& + { + return overlay.add_centered_bottom_label(color, offset, outlined, text); + }, + + "add_right_space_vertical", &omath::hud::EntityOverlay::add_right_space_vertical, + "add_right_space_horizontal", &omath::hud::EntityOverlay::add_right_space_horizontal, + "add_left_space_vertical", &omath::hud::EntityOverlay::add_left_space_vertical, + "add_left_space_horizontal", &omath::hud::EntityOverlay::add_left_space_horizontal, + "add_top_space_vertical", &omath::hud::EntityOverlay::add_top_space_vertical, + "add_top_space_horizontal", &omath::hud::EntityOverlay::add_top_space_horizontal, + "add_bottom_space_vertical", &omath::hud::EntityOverlay::add_bottom_space_vertical, + "add_bottom_space_horizontal", &omath::hud::EntityOverlay::add_bottom_space_horizontal, + + "add_right_progress_ring", + [](omath::hud::EntityOverlay& overlay, const omath::Color& color, const omath::Color& bg, + const float radius, const float ratio, sol::optional thickness, sol::optional offset, + sol::optional segments) -> omath::hud::EntityOverlay& + { + return overlay.add_right_progress_ring(color, bg, radius, ratio, thickness.value_or(2.f), + offset.value_or(5.f), segments.value_or(0)); + }, + "add_left_progress_ring", + [](omath::hud::EntityOverlay& overlay, const omath::Color& color, const omath::Color& bg, + const float radius, const float ratio, sol::optional thickness, sol::optional offset, + sol::optional segments) -> omath::hud::EntityOverlay& + { + return overlay.add_left_progress_ring(color, bg, radius, ratio, thickness.value_or(2.f), + offset.value_or(5.f), segments.value_or(0)); + }, + "add_top_progress_ring", + [](omath::hud::EntityOverlay& overlay, const omath::Color& color, const omath::Color& bg, + const float radius, const float ratio, sol::optional thickness, sol::optional offset, + sol::optional segments) -> omath::hud::EntityOverlay& + { + return overlay.add_top_progress_ring(color, bg, radius, ratio, thickness.value_or(2.f), + offset.value_or(5.f), segments.value_or(0)); + }, + "add_bottom_progress_ring", + [](omath::hud::EntityOverlay& overlay, const omath::Color& color, const omath::Color& bg, + const float radius, const float ratio, sol::optional thickness, sol::optional offset, + sol::optional segments) -> omath::hud::EntityOverlay& + { + return overlay.add_bottom_progress_ring(color, bg, radius, ratio, thickness.value_or(2.f), + offset.value_or(5.f), segments.value_or(0)); + }, + + "add_right_icon", + [](omath::hud::EntityOverlay& overlay, const sol::object& texture_id, const float width, + const float height, sol::optional tint, + sol::optional offset) -> omath::hud::EntityOverlay& + { + return overlay.add_right_icon(texture_id_from_lua_object(texture_id), width, height, + tint.value_or(opaque_white()), offset.value_or(5.f)); + }, + "add_left_icon", + [](omath::hud::EntityOverlay& overlay, const sol::object& texture_id, const float width, + const float height, sol::optional tint, + sol::optional offset) -> omath::hud::EntityOverlay& + { + return overlay.add_left_icon(texture_id_from_lua_object(texture_id), width, height, + tint.value_or(opaque_white()), offset.value_or(5.f)); + }, + "add_top_icon", + [](omath::hud::EntityOverlay& overlay, const sol::object& texture_id, const float width, + const float height, sol::optional tint, + sol::optional offset) -> omath::hud::EntityOverlay& + { + return overlay.add_top_icon(texture_id_from_lua_object(texture_id), width, height, + tint.value_or(opaque_white()), offset.value_or(5.f)); + }, + "add_bottom_icon", + [](omath::hud::EntityOverlay& overlay, const sol::object& texture_id, const float width, + const float height, sol::optional tint, + sol::optional offset) -> omath::hud::EntityOverlay& + { + return overlay.add_bottom_icon(texture_id_from_lua_object(texture_id), width, height, + tint.value_or(opaque_white()), offset.value_or(5.f)); + }, + + "add_snap_line", &omath::hud::EntityOverlay::add_snap_line, "add_skeleton", + [](omath::hud::EntityOverlay& overlay, const omath::Color& color, + sol::optional thickness) -> omath::hud::EntityOverlay& + { + return overlay.add_skeleton(color, thickness.value_or(1.f)); + }, + "add_scan_marker", + [](omath::hud::EntityOverlay& overlay, const omath::Color& color, + sol::optional outline, sol::optional outline_thickness) + -> omath::hud::EntityOverlay& + { + return overlay.contents(omath::hud::widget::ScanMarker{ + color, outline.value_or(transparent_black()), outline_thickness.value_or(1.f)}); + }, + "add_aim_dot", + [](omath::hud::EntityOverlay& overlay, const omath::Vector2& position, + const omath::Color& color, sol::optional radius) -> omath::hud::EntityOverlay& + { + return overlay.contents(omath::hud::widget::AimDot{position, color, radius.value_or(3.f)}); + }, + "add_projectile_aim", + [](omath::hud::EntityOverlay& overlay, const omath::Vector2& position, + const omath::Color& color, sol::optional size, sol::optional line_size, + sol::optional figure) -> omath::hud::EntityOverlay& + { + return overlay.contents(omath::hud::widget::ProjectileAim{ + position, color, size.value_or(3.f), line_size.value_or(1.f), + figure.value_or(omath::hud::widget::ProjectileAim::Figure::SQUARE)}); + }); + + hud_table.new_enum("ProjectileAimFigure", "CIRCLE", omath::hud::widget::ProjectileAim::Figure::CIRCLE, + "SQUARE", omath::hud::widget::ProjectileAim::Figure::SQUARE); + } +} // namespace omath::lua +#endif diff --git a/source/lua/lua_mat.cpp b/source/lua/lua_mat.cpp new file mode 100644 index 00000000..3442567c --- /dev/null +++ b/source/lua/lua_mat.cpp @@ -0,0 +1,156 @@ +// +// Created by orange on 07.03.2026. +// +#ifdef OMATH_ENABLE_LUA +#include "omath/lua/lua.hpp" +#include +#include +#include + +namespace +{ + template + std::size_t checked_index(const int index) + { + if (index < 0 || index >= static_cast(Limit)) + throw std::out_of_range("matrix index is out of range"); + return static_cast(index); + } + + template + omath::Mat mat_from_rows(const sol::table& rows) + { + omath::Mat result; + + for (std::size_t row = 0; row < Rows; ++row) + { + const auto row_table = rows[row + 1].get>(); + if (!row_table) + throw std::invalid_argument("matrix rows must be tables"); + + for (std::size_t column = 0; column < Columns; ++column) + { + const auto value = (*row_table)[column + 1].get>(); + if (!value) + throw std::invalid_argument("matrix row has missing value"); + result.at(row, column) = *value; + } + } + + return result; + } + + template + sol::table mat_as_table(const omath::Mat& mat, sol::this_state state) + { + sol::state_view lua(state); + sol::table rows = lua.create_table(); + + for (std::size_t row = 0; row < Rows; ++row) + { + sol::table row_table = lua.create_table(); + for (std::size_t column = 0; column < Columns; ++column) + row_table[column + 1] = mat.at(row, column); + rows[row + 1] = row_table; + } + + return rows; + } + + template + void register_square_mat(sol::table& omath_table, const char* name) + { + using MatType = omath::Mat; + + auto type = omath_table.new_usertype( + name, sol::constructors(), + + "from_rows", &mat_from_rows, "row_count", + []() + { + return Size; + }, + "columns_count", + []() + { + return Size; + }, + + "at", + [](const MatType& mat, const int row, const int column) + { + return mat.at(checked_index(row), checked_index(column)); + }, + "set_at", + [](MatType& mat, const int row, const int column, const Type value) + { + mat.at(checked_index(row), checked_index(column)) = value; + return mat; + }, + + sol::meta_function::multiplication, + sol::overload( + [](const MatType& lhs, const MatType& rhs) + { + return lhs * rhs; + }, + [](const MatType& mat, const Type scalar) + { + return mat * scalar; + }, + [](const Type scalar, const MatType& mat) + { + return mat * scalar; + }), + sol::meta_function::division, + [](const MatType& mat, const Type scalar) + { + return mat / scalar; + }, + sol::meta_function::equal_to, &MatType::operator==, sol::meta_function::to_string, &MatType::to_string, + + "transposed", &MatType::transposed, "determinant", &MatType::determinant, "sum", &MatType::sum, "clear", + &MatType::clear, "set", &MatType::set, "inverted", + [](const MatType& mat) -> sol::optional + { + auto result = mat.inverted(); + if (!result) + return sol::nullopt; + return *result; + }, + "as_table", &mat_as_table); + + if constexpr (RegisterMat4Helpers) + { + type["to_screen_mat"] = [](const Type screen_width, const Type screen_height) + { + return MatType{ + {screen_width / Type{2}, Type{0}, Type{0}, Type{0}}, + {Type{0}, -screen_height / Type{2}, Type{0}, Type{0}}, + {Type{0}, Type{0}, Type{1}, Type{0}}, + {screen_width / Type{2}, screen_height / Type{2}, Type{0}, Type{1}}, + }; + }; + type["translation"] = &omath::mat_translation; + type["scale"] = &omath::mat_scale; + type["look_at_left_handed"] = &omath::mat_look_at_left_handed; + type["look_at_right_handed"] = &omath::mat_look_at_right_handed; + type["perspective_left_handed_vertical_fov"] = + &omath::mat_perspective_left_handed_vertical_fov; + type["perspective_right_handed_vertical_fov"] = + &omath::mat_perspective_right_handed_vertical_fov; + } + } +} // namespace + +namespace omath::lua +{ + void LuaInterpreter::register_matrices(sol::table& omath_table) + { + register_square_mat<3, float, omath::MatStoreType::ROW_MAJOR>(omath_table, "Mat3"); + register_square_mat<4, float, omath::MatStoreType::ROW_MAJOR, true>(omath_table, "Mat4"); + register_square_mat<4, float, omath::MatStoreType::COLUMN_MAJOR, true>(omath_table, "Mat4ColumnMajor"); + register_square_mat<4, double, omath::MatStoreType::ROW_MAJOR>(omath_table, "Mat4d"); + } +} // namespace omath::lua +#endif diff --git a/source/lua/lua_quaternion.cpp b/source/lua/lua_quaternion.cpp new file mode 100644 index 00000000..165fbcd0 --- /dev/null +++ b/source/lua/lua_quaternion.cpp @@ -0,0 +1,95 @@ +// +// Created by orange on 07.03.2026. +// +#ifdef OMATH_ENABLE_LUA +#include "omath/lua/lua.hpp" +#include +#include + +namespace omath::lua +{ + void LuaInterpreter::register_quaternion(sol::table& omath_table) + { + using Quatf = omath::Quaternion; + using Vec3f = omath::Vector3; + + omath_table.new_usertype( + "Quaternion", sol::constructors(), + + "from_axis_angle", &Quatf::from_axis_angle, + + "x", + sol::property( + [](const Quatf& q) + { + return q.x; + }, + [](Quatf& q, float val) + { + q.x = val; + }), + "y", + sol::property( + [](const Quatf& q) + { + return q.y; + }, + [](Quatf& q, float val) + { + q.y = val; + }), + "z", + sol::property( + [](const Quatf& q) + { + return q.z; + }, + [](Quatf& q, float val) + { + q.z = val; + }), + "w", + sol::property( + [](const Quatf& q) + { + return q.w; + }, + [](Quatf& q, float val) + { + q.w = val; + }), + + sol::meta_function::addition, sol::resolve(&Quatf::operator+), + sol::meta_function::multiplication, + sol::overload(sol::resolve(&Quatf::operator*), + sol::resolve(&Quatf::operator*), + [](float s, const Quatf& q) + { + return q * s; + }), + sol::meta_function::unary_minus, sol::resolve(&Quatf::operator-), + sol::meta_function::equal_to, &Quatf::operator==, sol::meta_function::to_string, + [](const Quatf& q) + { + return std::format("Quaternion({}, {}, {}, {})", q.x, q.y, q.z, q.w); + }, + + "conjugate", &Quatf::conjugate, "dot", &Quatf::dot, "length", &Quatf::length, "length_sqr", + &Quatf::length_sqr, "normalized", &Quatf::normalized, "inverse", &Quatf::inverse, "rotate", + sol::resolve(&Quatf::rotate), "to_rotation_matrix3", + &Quatf::to_rotation_matrix3, "to_rotation_matrix4", &Quatf::to_rotation_matrix4, + + "as_table", + [](const Quatf& q, sol::this_state s) -> sol::table + { + sol::state_view lua(s); + sol::table t = lua.create_table(); + t["x"] = q.x; + t["y"] = q.y; + t["z"] = q.z; + t["w"] = q.w; + return t; + }); + } +} // namespace omath::lua +#endif diff --git a/tests/lua/collision_tests.lua b/tests/lua/collision_tests.lua new file mode 100644 index 00000000..cbd01e11 --- /dev/null +++ b/tests/lua/collision_tests.lua @@ -0,0 +1,169 @@ +local function approx(a, b, eps) return math.abs(a - b) < (eps or 1e-4) end + +local function cube_points() + return { + omath.Vec3.new(-1, -1, -1), + omath.Vec3.new(1, -1, -1), + omath.Vec3.new(-1, 1, -1), + omath.Vec3.new(1, 1, -1), + omath.Vec3.new(-1, -1, 1), + omath.Vec3.new(1, -1, 1), + omath.Vec3.new(-1, 1, 1), + omath.Vec3.new(1, 1, 1), + } +end + +function Collision_Aabb_constructor_and_fields() + local aabb = omath.primitives.Aabb.new(omath.Vec3.new(-1, -2, -3), omath.Vec3.new(1, 2, 3)) + assert(aabb.min.x == -1 and aabb.max.z == 3) + aabb.max = omath.Vec3.new(2, 3, 4) + assert(aabb.max.x == 2 and aabb.max.y == 3 and aabb.max.z == 4) +end + +function Collision_Aabb_center_extents() + local aabb = omath.primitives.Aabb.new(omath.Vec3.new(-1, -2, -3), omath.Vec3.new(3, 6, 9)) + local center = aabb:center() + local extents = aabb:extents() + assert(center.x == 1 and center.y == 2 and center.z == 3) + assert(extents.x == 2 and extents.y == 4 and extents.z == 6) +end + +function Collision_Aabb_top_bottom_default_axis() + local aabb = omath.primitives.Aabb.new(omath.Vec3.new(-1, -2, -3), omath.Vec3.new(3, 6, 9)) + assert(aabb:top().y == 6) + assert(aabb:bottom().y == -2) +end + +function Collision_Aabb_top_bottom_explicit_axis() + local aabb = omath.primitives.Aabb.new(omath.Vec3.new(-1, -2, -3), omath.Vec3.new(3, 6, 9)) + assert(aabb:top(omath.primitives.UpAxis.X).x == 3) + assert(aabb:bottom(omath.primitives.UpAxis.Z).z == -3) +end + +function Collision_Aabb_is_collide() + local a = omath.primitives.Aabb.new(omath.Vec3.new(-1, -1, -1), omath.Vec3.new(1, 1, 1)) + local b = omath.primitives.Aabb.new(omath.Vec3.new(0, 0, 0), omath.Vec3.new(2, 2, 2)) + local c = omath.primitives.Aabb.new(omath.Vec3.new(3, 3, 3), omath.Vec3.new(4, 4, 4)) + assert(a:is_collide(b)) + assert(not a:is_collide(c)) +end + +function Collision_Aabb_as_table() + local aabb = omath.primitives.Aabb.new(omath.Vec3.new(-1, -2, -3), omath.Vec3.new(1, 2, 3)) + local t = aabb:as_table() + assert(t.min.x == -1 and t.max.z == 3) +end + +function Collision_Obb_constructor_and_vertices() + local obb = omath.primitives.Obb.new( + omath.Vec3.new(0, 0, 0), + omath.Vec3.new(1, 0, 0), + omath.Vec3.new(0, 1, 0), + omath.Vec3.new(0, 0, 1), + omath.Vec3.new(1, 2, 3) + ) + local vertices = obb:vertices() + assert(#vertices == 8) + assert(vertices[1].x == -1 and vertices[1].y == -2 and vertices[1].z == -3) + assert(vertices[8].x == 1 and vertices[8].y == 2 and vertices[8].z == 3) +end + +function Collision_Ray_constructor_and_direction() + local ray = omath.collision.Ray.new(omath.Vec3.new(1, 2, 3), omath.Vec3.new(4, 6, 3), true) + local dir = ray:direction_vector() + local normal = ray:direction_vector_normalized() + assert(ray.infinite_length) + assert(dir.x == 3 and dir.y == 4 and dir.z == 0) + assert(approx(normal:length(), 1)) +end + +function Collision_LineTracer_triangle_hit() + local tri = omath.Triangle.new(omath.Vec3.new(0, 0, 0), omath.Vec3.new(2, 0, 0), omath.Vec3.new(0, 2, 0)) + local ray = omath.collision.Ray.new(omath.Vec3.new(0.5, 0.5, -1), omath.Vec3.new(0.5, 0.5, 1)) + local hit = omath.collision.LineTracer.get_ray_hit_point(ray, tri) + assert(omath.collision.LineTracer.ray_hits_triangle(ray, tri)) + assert(not omath.collision.LineTracer.can_trace_line(ray, tri)) + assert(approx(hit.x, 0.5) and approx(hit.y, 0.5) and approx(hit.z, 0)) +end + +function Collision_LineTracer_triangle_miss() + local tri = omath.Triangle.new(omath.Vec3.new(0, 0, 0), omath.Vec3.new(1, 0, 0), omath.Vec3.new(0, 1, 0)) + local ray = omath.collision.Ray.new(omath.Vec3.new(2, 2, -1), omath.Vec3.new(2, 2, 1)) + local hit = omath.collision.LineTracer.get_ray_hit_point(ray, tri) + assert(not omath.collision.LineTracer.ray_hits_triangle(ray, tri)) + assert(omath.collision.LineTracer.can_trace_line(ray, tri)) + assert(hit == ray["end"]) +end + +function Collision_LineTracer_aabb_hit() + local aabb = omath.primitives.Aabb.new(omath.Vec3.new(-1, -1, -1), omath.Vec3.new(1, 1, 1)) + local ray = omath.collision.Ray.new(omath.Vec3.new(0, 0, -3), omath.Vec3.new(0, 0, 3)) + local hit = omath.collision.LineTracer.get_ray_hit_point(ray, aabb) + assert(omath.collision.LineTracer.ray_hits_aabb(ray, aabb)) + assert(approx(hit.x, 0) and approx(hit.y, 0) and approx(hit.z, -1)) +end + +function Collision_LineTracer_aabb_miss() + local aabb = omath.primitives.Aabb.new(omath.Vec3.new(-1, -1, -1), omath.Vec3.new(1, 1, 1)) + local ray = omath.collision.Ray.new(omath.Vec3.new(0, 3, -3), omath.Vec3.new(0, 3, 3)) + local hit = omath.collision.LineTracer.get_ray_hit_point(ray, aabb) + assert(not omath.collision.LineTracer.ray_hits_aabb(ray, aabb)) + assert(hit == ray["end"]) +end + +function Collision_LineTracer_obb_hit() + local obb = omath.primitives.Obb.new( + omath.Vec3.new(0, 0, 0), + omath.Vec3.new(1, 0, 0), + omath.Vec3.new(0, 1, 0), + omath.Vec3.new(0, 0, 1), + omath.Vec3.new(1, 1, 1) + ) + local ray = omath.collision.Ray.new(omath.Vec3.new(0, 0, -3), omath.Vec3.new(0, 0, 3)) + local hit = omath.collision.LineTracer.get_ray_hit_point(ray, obb) + assert(omath.collision.LineTracer.ray_hits_obb(ray, obb)) + assert(approx(hit.z, -1)) +end + +function Collision_ConvexCollider_vertices_and_support() + local collider = omath.collision.ConvexCollider.new(cube_points(), omath.Vec3.new(2, 0, 0)) + assert(collider:vertex_count() == 8) + assert(#collider:vertices() == 8) + local support = collider:find_abs_furthest_vertex_position(omath.Vec3.new(1, 0, 0)) + assert(support.x == 3) +end + +function Collision_Gjk_support_vertex() + local a = omath.collision.ConvexCollider.new(cube_points()) + local b = omath.collision.ConvexCollider.new(cube_points()) + local support = omath.collision.gjk_support_vertex(a, b, omath.Vec3.new(1, 0, 0)) + assert(support.x == 2 and support.y == 0 and support.z == 0) +end + +function Collision_Gjk_collide_true() + local a = omath.collision.ConvexCollider.new(cube_points()) + local b = omath.collision.ConvexCollider.new(cube_points(), omath.Vec3.new(1, 0, 0)) + assert(omath.collision.gjk_collide(a, b)) +end + +function Collision_Gjk_collide_false() + local a = omath.collision.ConvexCollider.new(cube_points()) + local b = omath.collision.ConvexCollider.new(cube_points(), omath.Vec3.new(5, 0, 0)) + assert(not omath.collision.gjk_collide(a, b)) +end + +function Collision_Epa_solve_hit() + local a = omath.collision.ConvexCollider.new(cube_points()) + local b = omath.collision.ConvexCollider.new(cube_points(), omath.Vec3.new(1.5, 0, 0)) + local result = omath.collision.epa_solve(a, b) + assert(result ~= nil) + assert(result.depth > 0) + assert(approx(result.normal:length(), 1)) + assert(approx(result.penetration_vector:length(), result.depth)) +end + +function Collision_Epa_solve_miss() + local a = omath.collision.ConvexCollider.new(cube_points()) + local b = omath.collision.ConvexCollider.new(cube_points(), omath.Vec3.new(5, 0, 0)) + assert(omath.collision.epa_solve(a, b) == nil) +end diff --git a/tests/lua/hud_tests.lua b/tests/lua/hud_tests.lua new file mode 100644 index 00000000..d3f10fb7 --- /dev/null +++ b/tests/lua/hud_tests.lua @@ -0,0 +1,244 @@ +local function approx(a, b, eps) return math.abs(a - b) < (eps or 1e-4) end + +local function color() return omath.Color.new(1, 0, 0, 1) end +local function fill() return omath.Color.new(0, 0, 0, 0.5) end +local function outline() return omath.Color.new(0, 0, 0, 1) end +local function bg() return omath.Color.new(0.2, 0.2, 0.2, 1) end + +local function renderer_with(commands) + return omath.hud.Renderer.new({ + add_line = function(a, b, c, thickness) + table.insert(commands, { kind = "line", a = a, b = b, color = c, thickness = thickness }) + end, + add_polyline = function(points, c, thickness) + table.insert(commands, { kind = "polyline", points = points, color = c, thickness = thickness }) + end, + add_filled_polyline = function(points, c) + table.insert(commands, { kind = "filled_polyline", points = points, color = c }) + end, + add_rectangle = function(min, max, c) + table.insert(commands, { kind = "rectangle", min = min, max = max, color = c }) + end, + add_filled_rectangle = function(min, max, c) + table.insert(commands, { kind = "filled_rectangle", min = min, max = max, color = c }) + end, + add_circle = function(center, radius, c, thickness, segments) + table.insert(commands, { + kind = "circle", + center = center, + radius = radius, + color = c, + thickness = thickness, + segments = segments, + }) + end, + add_filled_circle = function(center, radius, c, segments) + table.insert(commands, { kind = "filled_circle", center = center, radius = radius, color = c, segments = segments }) + end, + add_arc = function(center, radius, a_min, a_max, c, thickness, segments) + table.insert(commands, { + kind = "arc", + center = center, + radius = radius, + a_min = a_min, + a_max = a_max, + color = c, + thickness = thickness, + segments = segments, + }) + end, + add_image = function(texture, min, max, tint) + table.insert(commands, { kind = "image", texture = texture, min = min, max = max, tint = tint }) + end, + add_text = function(pos, c, text) + table.insert(commands, { kind = "text", pos = pos, color = c, text = text }) + end, + calc_text_size = function(text) + return omath.Vec2.new(#text * 6, 10) + end, + }) +end + +local function overlay(commands) + return omath.hud.EntityOverlay.new(omath.Vec2.new(100, 10), omath.Vec2.new(100, 50), renderer_with(commands)) +end + +function Hud_CanvasBox_default_ratio() + local box = omath.hud.CanvasBox.new(omath.Vec2.new(100, 10), omath.Vec2.new(100, 50)) + assert(approx(box.top_left_corner.x, 90) and approx(box.top_left_corner.y, 10)) + assert(approx(box.top_right_corner.x, 110) and approx(box.bottom_right_corner.y, 50)) +end + +function Hud_CanvasBox_custom_ratio() + local box = omath.hud.CanvasBox.new(omath.Vec2.new(100, 10), omath.Vec2.new(100, 50), 2) + assert(approx(box.top_left_corner.x, 80) and approx(box.bottom_right_corner.x, 120)) +end + +function Hud_CanvasBox_as_table() + local points = omath.hud.CanvasBox.new(omath.Vec2.new(100, 10), omath.Vec2.new(100, 50)):as_table() + assert(#points == 4) + assert(approx(points[1].x, 90) and approx(points[3].x, 110)) +end + +function Hud_Renderer_callbacks() + local commands = {} + local renderer = renderer_with(commands) + renderer:add_line(omath.Vec2.new(1, 2), omath.Vec2.new(3, 4), color(), 2) + renderer:add_text(omath.Vec2.new(5, 6), color(), "hp") + local size = renderer:calc_text_size("abcd") + assert(#commands == 2) + assert(commands[1].kind == "line" and commands[2].text == "hp") + assert(size.x == 24 and size.y == 10) +end + +function Hud_Renderer_polyline_table() + local commands = {} + local renderer = renderer_with(commands) + renderer:add_polyline({ omath.Vec2.new(1, 1), omath.Vec2.new(2, 2) }, color(), 3) + assert(commands[1].kind == "polyline") + assert(#commands[1].points == 2 and commands[1].points[2].x == 2) +end + +function Hud_Renderer_circle_defaults() + local commands = {} + local renderer = renderer_with(commands) + renderer:add_circle(omath.Vec2.new(1, 2), 5, color(), 2) + renderer:add_filled_circle(omath.Vec2.new(3, 4), 6, color()) + assert(commands[1].kind == "circle" and commands[1].segments == 0) + assert(commands[2].kind == "filled_circle" and commands[2].segments == 0) +end + +function Hud_EntityOverlay_add_2d_box() + local commands = {} + overlay(commands):add_2d_box(color(), fill(), 2) + assert(#commands == 2) + assert(commands[1].kind == "polyline" and #commands[1].points == 4) + assert(commands[2].kind == "filled_polyline") +end + +function Hud_EntityOverlay_add_cornered_2d_box() + local commands = {} + overlay(commands):add_cornered_2d_box(color(), fill(), 0.25, 2) + assert(#commands == 10) + assert(commands[1].kind == "polyline" and commands[10].kind == "line") +end + +function Hud_EntityOverlay_add_dashed_box() + local commands = {} + overlay(commands):add_dashed_box(color(), 8, 5, 1) + assert(#commands > 4) + assert(commands[1].kind == "line") +end + +function Hud_EntityOverlay_add_bar() + local commands = {} + overlay(commands):add_right_bar(color(), outline(), bg(), 4, 0.5) + assert(#commands == 3) + assert(commands[1].kind == "filled_rectangle" and commands[2].kind == "filled_rectangle") + assert(commands[3].kind == "rectangle") +end + +function Hud_EntityOverlay_add_dashed_bar() + local commands = {} + overlay(commands):add_bottom_dashed_bar(color(), outline(), bg(), 4, 0.5, 8, 5) + assert(#commands >= 3) + assert(commands[1].kind == "filled_rectangle") +end + +function Hud_EntityOverlay_add_label() + local commands = {} + overlay(commands):add_right_label(color(), 5, false, "HP") + assert(#commands == 1) + assert(commands[1].kind == "text" and commands[1].text == "HP") + assert(approx(commands[1].pos.x, 115)) +end + +function Hud_EntityOverlay_add_outlined_label() + local commands = {} + overlay(commands):add_left_label(color(), 5, true, "HP") + assert(#commands == 9) + assert(commands[9].kind == "text" and commands[9].text == "HP") +end + +function Hud_EntityOverlay_add_centered_label() + local commands = {} + overlay(commands):add_centered_top_label(color(), 5, false, "HP") + assert(#commands == 1) + assert(commands[1].kind == "text") + assert(approx(commands[1].pos.x, 94)) +end + +function Hud_EntityOverlay_add_spaces() + local commands = {} + overlay(commands):add_right_space_vertical(10):add_right_label(color(), 5, false, "HP") + assert(#commands == 1) + assert(approx(commands[1].pos.y, 20)) +end + +function Hud_EntityOverlay_add_progress_ring() + local commands = {} + overlay(commands):add_right_progress_ring(color(), bg(), 6, 0.25, 2, 5, 16) + assert(#commands == 2) + assert(commands[1].kind == "circle" and commands[2].kind == "arc") + assert(commands[2].segments == 16) +end + +function Hud_EntityOverlay_add_progress_ring_defaults() + local commands = {} + overlay(commands):add_right_progress_ring(color(), bg(), 6, 0.25) + assert(#commands == 2) + assert(commands[1].thickness == 2 and commands[1].segments == 0) +end + +function Hud_EntityOverlay_add_icon() + local commands = {} + overlay(commands):add_right_icon("texture-id", 8, 6, color(), 5) + assert(#commands == 1) + assert(commands[1].kind == "image" and commands[1].texture == "texture-id") +end + +function Hud_EntityOverlay_add_snap_line() + local commands = {} + overlay(commands):add_snap_line(omath.Vec2.new(0, 0), color(), 2) + assert(#commands == 1) + assert(commands[1].kind == "line") + assert(approx(commands[1].b.x, 100) and approx(commands[1].b.y, 50)) +end + +function Hud_EntityOverlay_add_skeleton() + local commands = {} + overlay(commands):add_skeleton(color()) + assert(#commands == 15) + assert(commands[1].kind == "line") +end + +function Hud_EntityOverlay_add_scan_marker() + local commands = {} + overlay(commands):add_scan_marker(color(), outline(), 2) + assert(#commands == 2) + assert(commands[1].kind == "filled_polyline" and #commands[1].points == 3) + assert(commands[2].kind == "polyline" and commands[2].thickness == 2) +end + +function Hud_EntityOverlay_add_aim_dot() + local commands = {} + overlay(commands):add_aim_dot(omath.Vec2.new(5, 6), color()) + assert(#commands == 1) + assert(commands[1].kind == "filled_circle" and commands[1].radius == 3) +end + +function Hud_EntityOverlay_add_projectile_aim_circle() + local commands = {} + overlay(commands):add_projectile_aim(omath.Vec2.new(5, 6), color(), 4, 2, omath.hud.ProjectileAimFigure.CIRCLE) + assert(#commands == 2) + assert(commands[1].kind == "line") + assert(commands[2].kind == "filled_circle" and commands[2].radius == 4) +end + +function Hud_EntityOverlay_add_projectile_aim_square_default() + local commands = {} + overlay(commands):add_projectile_aim(omath.Vec2.new(5, 6), color()) + assert(#commands == 2) + assert(commands[1].kind == "line") + assert(commands[2].kind == "filled_rectangle") +end diff --git a/tests/lua/mat_tests.lua b/tests/lua/mat_tests.lua new file mode 100644 index 00000000..81523fb5 --- /dev/null +++ b/tests/lua/mat_tests.lua @@ -0,0 +1,195 @@ +local function approx(a, b, eps) return math.abs(a - b) < (eps or 1e-5) end + +function Mat4_Constructor_default() + local m = omath.Mat4.new() + assert(m:row_count() == 4 and m:columns_count() == 4) + assert(m:at(0, 0) == 0 and m:at(3, 3) == 0) +end + +function Mat4_FromRows() + local m = omath.Mat4.from_rows({ + {1, 2, 3, 4}, + {5, 6, 7, 8}, + {9, 10, 11, 12}, + {13, 14, 15, 16}, + }) + assert(m:at(0, 0) == 1 and m:at(3, 3) == 16) +end + +function Mat4_SetAt() + local m = omath.Mat4.new() + m:set_at(2, 3, 42) + assert(m:at(2, 3) == 42) +end + +function Mat4_SetAndClear() + local m = omath.Mat4.new() + m:set(2) + assert(m:sum() == 32) + m:clear() + assert(m:sum() == 0) +end + +function Mat4_Multiplication_matrix() + local identity = omath.Mat4.from_rows({ + {1, 0, 0, 0}, + {0, 1, 0, 0}, + {0, 0, 1, 0}, + {0, 0, 0, 1}, + }) + local m = omath.Mat4.from_rows({ + {1, 2, 3, 4}, + {5, 6, 7, 8}, + {9, 10, 11, 12}, + {13, 14, 15, 16}, + }) + local result = m * identity + assert(result == m) +end + +function Mat4_Multiplication_scalar() + local m = omath.Mat4.from_rows({ + {1, 0, 0, 0}, + {0, 1, 0, 0}, + {0, 0, 1, 0}, + {0, 0, 0, 1}, + }) * 2 + assert(m:at(0, 0) == 2 and m:at(3, 3) == 2) +end + +function Mat4_Multiplication_scalar_reversed() + local m = 2 * omath.Mat4.from_rows({ + {1, 0, 0, 0}, + {0, 1, 0, 0}, + {0, 0, 1, 0}, + {0, 0, 0, 1}, + }) + assert(m:at(0, 0) == 2 and m:at(3, 3) == 2) +end + +function Mat4_Division_scalar() + local m = omath.Mat4.from_rows({ + {2, 0, 0, 0}, + {0, 2, 0, 0}, + {0, 0, 2, 0}, + {0, 0, 0, 2}, + }) / 2 + assert(m:at(0, 0) == 1 and m:at(3, 3) == 1) +end + +function Mat4_Transposed() + local m = omath.Mat4.from_rows({ + {1, 2, 0, 0}, + {3, 4, 0, 0}, + {0, 0, 1, 0}, + {0, 0, 0, 1}, + }):transposed() + assert(m:at(0, 1) == 3 and m:at(1, 0) == 2) +end + +function Mat4_Determinant() + local m = omath.Mat4.from_rows({ + {1, 0, 0, 0}, + {0, 2, 0, 0}, + {0, 0, 3, 0}, + {0, 0, 0, 4}, + }) + assert(m:determinant() == 24) +end + +function Mat4_Inverted_success() + local m = omath.Mat4.from_rows({ + {2, 0, 0, 0}, + {0, 2, 0, 0}, + {0, 0, 2, 0}, + {0, 0, 0, 2}, + }) + local inv = m:inverted() + assert(inv ~= nil) + assert(approx(inv:at(0, 0), 0.5) and approx(inv:at(3, 3), 0.5)) +end + +function Mat4_Inverted_singular() + local m = omath.Mat4.new() + assert(m:inverted() == nil) +end + +function Mat4_ToScreenMat() + local m = omath.Mat4.to_screen_mat(800, 600) + assert(m:at(0, 0) == 400 and m:at(1, 1) == -300 and m:at(3, 1) == 300) +end + +function Mat4_Translation() + local m = omath.Mat4.translation(omath.Vec3.new(1, 2, 3)) + assert(m:at(0, 3) == 1 and m:at(1, 3) == 2 and m:at(2, 3) == 3) +end + +function Mat4_Scale() + local m = omath.Mat4.scale(omath.Vec3.new(2, 3, 4)) + assert(m:at(0, 0) == 2 and m:at(1, 1) == 3 and m:at(2, 2) == 4) +end + +function Mat4_Perspective() + local m = omath.Mat4.perspective_left_handed_vertical_fov(90, 16 / 9, 0.1, 1000) + assert(m:at(0, 0) > 0 and m:at(1, 1) > 0) +end + +function Mat4_AsTable() + local m = omath.Mat4.from_rows({ + {1, 2, 3, 4}, + {5, 6, 7, 8}, + {9, 10, 11, 12}, + {13, 14, 15, 16}, + }) + local t = m:as_table() + assert(t[1][1] == 1 and t[4][4] == 16) +end + +function Mat4_ToString() + local m = omath.Mat4.from_rows({ + {1, 2, 0, 0}, + {0, 1, 0, 0}, + {0, 0, 1, 0}, + {0, 0, 0, 1}, + }) + assert(string.find(tostring(m), "%[%[") ~= nil) +end + +function Mat3_FromRows() + local m = omath.Mat3.from_rows({ + {1, 2, 3}, + {0, 1, 4}, + {5, 6, 0}, + }) + assert(m:determinant() == 1) +end + +function Mat4ColumnMajor_FromRows() + local m = omath.Mat4ColumnMajor.from_rows({ + {1, 2, 3, 4}, + {5, 6, 7, 8}, + {9, 10, 11, 12}, + {13, 14, 15, 16}, + }) + assert(m:at(0, 1) == 2 and m:at(3, 2) == 15) +end + +function Mat4ColumnMajor_ToScreenMat() + local m = omath.Mat4ColumnMajor.to_screen_mat(800, 600) + assert(m:at(0, 0) == 400 and m:at(1, 1) == -300 and m:at(3, 1) == 300) +end + +function Mat4ColumnMajor_Translation() + local m = omath.Mat4ColumnMajor.translation(omath.Vec3.new(1, 2, 3)) + assert(m:at(0, 3) == 1 and m:at(1, 3) == 2 and m:at(2, 3) == 3) +end + +function Mat4d_FromRows() + local m = omath.Mat4d.from_rows({ + {1, 0, 0, 0}, + {0, 2, 0, 0}, + {0, 0, 3, 0}, + {0, 0, 0, 4}, + }) + assert(m:determinant() == 24) +end diff --git a/tests/lua/quaternion_tests.lua b/tests/lua/quaternion_tests.lua new file mode 100644 index 00000000..b6d0fbab --- /dev/null +++ b/tests/lua/quaternion_tests.lua @@ -0,0 +1,111 @@ +local function approx(a, b, eps) return math.abs(a - b) < (eps or 1e-5) end + +function Quaternion_Constructor_default() + local q = omath.Quaternion.new() + assert(q.x == 0 and q.y == 0 and q.z == 0 and q.w == 1) +end + +function Quaternion_Constructor_xyzw() + local q = omath.Quaternion.new(1, 2, 3, 4) + assert(q.x == 1 and q.y == 2 and q.z == 3 and q.w == 4) +end + +function Quaternion_Field_mutation() + local q = omath.Quaternion.new() + q.x = 1; q.y = 2; q.z = 3; q.w = 4 + assert(q.x == 1 and q.y == 2 and q.z == 3 and q.w == 4) +end + +function Quaternion_FromAxisAngle_zero_angle() + local q = omath.Quaternion.from_axis_angle(omath.Vec3.new(0, 0, 1), 0) + assert(approx(q.x, 0) and approx(q.y, 0) and approx(q.z, 0) and approx(q.w, 1)) +end + +function Quaternion_Addition() + local q = omath.Quaternion.new(1, 2, 3, 4) + omath.Quaternion.new(4, 3, 2, 1) + assert(q.x == 5 and q.y == 5 and q.z == 5 and q.w == 5) +end + +function Quaternion_Multiplication_scalar() + local q = omath.Quaternion.new(1, 2, 3, 4) * 2 + assert(q.x == 2 and q.y == 4 and q.z == 6 and q.w == 8) +end + +function Quaternion_Multiplication_scalar_reversed() + local q = 2 * omath.Quaternion.new(1, 2, 3, 4) + assert(q.x == 2 and q.y == 4 and q.z == 6 and q.w == 8) +end + +function Quaternion_Multiplication_quaternion() + local i = omath.Quaternion.new(1, 0, 0, 0) + local j = omath.Quaternion.new(0, 1, 0, 0) + local k = i * j + assert(k.x == 0 and k.y == 0 and k.z == 1 and k.w == 0) +end + +function Quaternion_UnaryMinus() + local q = -omath.Quaternion.new(1, -2, 3, -4) + assert(q.x == -1 and q.y == 2 and q.z == -3 and q.w == 4) +end + +function Quaternion_EqualTo_true() + assert(omath.Quaternion.new(1, 2, 3, 4) == omath.Quaternion.new(1, 2, 3, 4)) +end + +function Quaternion_EqualTo_false() + assert(not (omath.Quaternion.new(1, 2, 3, 4) == omath.Quaternion.new(1, 2, 3, 5))) +end + +function Quaternion_ToString() + assert(tostring(omath.Quaternion.new(1, 2, 3, 4)) == "Quaternion(1, 2, 3, 4)") +end + +function Quaternion_Conjugate() + local q = omath.Quaternion.new(1, 2, 3, 4):conjugate() + assert(q.x == -1 and q.y == -2 and q.z == -3 and q.w == 4) +end + +function Quaternion_Dot() + assert(omath.Quaternion.new(1, 0, 0, 0):dot(omath.Quaternion.new(0, 1, 0, 0)) == 0) +end + +function Quaternion_Length() + assert(approx(omath.Quaternion.new(1, 1, 1, 1):length(), 2)) +end + +function Quaternion_LengthSqr() + assert(omath.Quaternion.new(1, 2, 3, 4):length_sqr() == 30) +end + +function Quaternion_Normalized() + local q = omath.Quaternion.new(1, 1, 1, 1):normalized() + assert(approx(q:length(), 1)) + assert(approx(q.x, 0.5) and approx(q.y, 0.5) and approx(q.z, 0.5) and approx(q.w, 0.5)) +end + +function Quaternion_Inverse() + local q = omath.Quaternion.from_axis_angle(omath.Vec3.new(0, 0, 1), math.pi / 3) + local identity = q * q:inverse() + assert(approx(identity.x, 0) and approx(identity.y, 0) and approx(identity.z, 0) and approx(identity.w, 1)) +end + +function Quaternion_Rotate() + local q = omath.Quaternion.from_axis_angle(omath.Vec3.new(0, 0, 1), math.pi / 2) + local v = q:rotate(omath.Vec3.new(1, 0, 0)) + assert(approx(v.x, 0, 1e-4) and approx(v.y, 1, 1e-4) and approx(v.z, 0, 1e-4)) +end + +function Quaternion_ToRotationMatrix3() + local m = omath.Quaternion.new():to_rotation_matrix3() + assert(m:at(0, 0) == 1 and m:at(1, 1) == 1 and m:at(2, 2) == 1) +end + +function Quaternion_ToRotationMatrix4() + local m = omath.Quaternion.new():to_rotation_matrix4() + assert(m:at(0, 0) == 1 and m:at(1, 1) == 1 and m:at(2, 2) == 1 and m:at(3, 3) == 1) +end + +function Quaternion_AsTable() + local t = omath.Quaternion.new(1, 2, 3, 4):as_table() + assert(t.x == 1 and t.y == 2 and t.z == 3 and t.w == 4) +end diff --git a/tests/lua/source_engine_tests.lua b/tests/lua/source_engine_tests.lua index 26a10af8..c370ed1c 100644 --- a/tests/lua/source_engine_tests.lua +++ b/tests/lua/source_engine_tests.lua @@ -175,6 +175,46 @@ function Source_Camera_get_up() assert(approx(make_camera():get_up():length(), 1.0)) end +function Source_Camera_get_view_matrix() + local view = make_camera():get_view_matrix() + assert(view ~= nil) + assert(view:row_count() == 4 and view:columns_count() == 4) +end + +function Source_Camera_get_projection_matrix() + local projection = make_camera():get_projection_matrix() + assert(projection ~= nil) + assert(projection:at(0, 0) > 0 and projection:at(1, 1) > 0) +end + +function Source_Camera_get_view_projection_matrix() + local view_projection = make_camera():get_view_projection_matrix() + assert(view_projection ~= nil) + assert(view_projection:row_count() == 4 and view_projection:columns_count() == 4) +end + +function Source_Camera_extract_projection_params() + local projection = make_camera():get_projection_matrix() + local fov, aspect_ratio = omath.source.Camera.extract_projection_params(projection) + assert(fov ~= nil) + assert(approx(aspect_ratio, 1920 / 1080)) +end + +function Source_Camera_calc_view_angles_from_view_matrix() + local cam = make_camera() + local view = cam:get_view_matrix() + local angles = omath.source.Camera.calc_view_angles_from_view_matrix(view) + assert(approx(angles.pitch:as_degrees(), 0)) + assert(approx(angles.yaw:as_degrees(), 0)) +end + +function Source_Camera_calc_origin_from_view_matrix() + local cam = make_camera() + cam:set_origin(omath.Vec3.new(1, 2, 3)) + local origin = omath.source.Camera.calc_origin_from_view_matrix(cam:get_view_matrix()) + assert(approx(origin.x, 1) and approx(origin.y, 2) and approx(origin.z, 3)) +end + function Source_Camera_world_to_screen_success() local cam = make_camera() cam:look_at(omath.Vec3.new(1, 0, 0)) diff --git a/tests/lua/unit_test_lua_collision.cpp b/tests/lua/unit_test_lua_collision.cpp new file mode 100644 index 00000000..f5d1078b --- /dev/null +++ b/tests/lua/unit_test_lua_collision.cpp @@ -0,0 +1,53 @@ +// +// Created by orange on 07.03.2026. +// +#include +#include +#include + +class LuaCollision : public ::testing::Test +{ +protected: + lua_State* L = nullptr; + + void SetUp() override + { + L = luaL_newstate(); + luaL_openlibs(L); + omath::lua::LuaInterpreter::register_lib(L); + if (luaL_dofile(L, LUA_SCRIPTS_DIR "/collision_tests.lua") != LUA_OK) + FAIL() << lua_tostring(L, -1); + } + + void TearDown() override { lua_close(L); } + + void check(const char* func_name) + { + lua_getglobal(L, func_name); + if (lua_pcall(L, 0, 0, 0) != LUA_OK) + { + FAIL() << lua_tostring(L, -1); + lua_pop(L, 1); + } + } +}; + +TEST_F(LuaCollision, Aabb_constructor_and_fields) { check("Collision_Aabb_constructor_and_fields"); } +TEST_F(LuaCollision, Aabb_center_extents) { check("Collision_Aabb_center_extents"); } +TEST_F(LuaCollision, Aabb_top_bottom_default_axis) { check("Collision_Aabb_top_bottom_default_axis"); } +TEST_F(LuaCollision, Aabb_top_bottom_explicit_axis) { check("Collision_Aabb_top_bottom_explicit_axis"); } +TEST_F(LuaCollision, Aabb_is_collide) { check("Collision_Aabb_is_collide"); } +TEST_F(LuaCollision, Aabb_as_table) { check("Collision_Aabb_as_table"); } +TEST_F(LuaCollision, Obb_constructor_and_vertices) { check("Collision_Obb_constructor_and_vertices"); } +TEST_F(LuaCollision, Ray_constructor_and_direction) { check("Collision_Ray_constructor_and_direction"); } +TEST_F(LuaCollision, LineTracer_triangle_hit) { check("Collision_LineTracer_triangle_hit"); } +TEST_F(LuaCollision, LineTracer_triangle_miss) { check("Collision_LineTracer_triangle_miss"); } +TEST_F(LuaCollision, LineTracer_aabb_hit) { check("Collision_LineTracer_aabb_hit"); } +TEST_F(LuaCollision, LineTracer_aabb_miss) { check("Collision_LineTracer_aabb_miss"); } +TEST_F(LuaCollision, LineTracer_obb_hit) { check("Collision_LineTracer_obb_hit"); } +TEST_F(LuaCollision, ConvexCollider_vertices_support) { check("Collision_ConvexCollider_vertices_and_support"); } +TEST_F(LuaCollision, Gjk_support_vertex) { check("Collision_Gjk_support_vertex"); } +TEST_F(LuaCollision, Gjk_collide_true) { check("Collision_Gjk_collide_true"); } +TEST_F(LuaCollision, Gjk_collide_false) { check("Collision_Gjk_collide_false"); } +TEST_F(LuaCollision, Epa_solve_hit) { check("Collision_Epa_solve_hit"); } +TEST_F(LuaCollision, Epa_solve_miss) { check("Collision_Epa_solve_miss"); } diff --git a/tests/lua/unit_test_lua_hud.cpp b/tests/lua/unit_test_lua_hud.cpp new file mode 100644 index 00000000..1514c7df --- /dev/null +++ b/tests/lua/unit_test_lua_hud.cpp @@ -0,0 +1,58 @@ +// +// Created by orange on 07.03.2026. +// +#include +#include +#include + +class LuaHud : public ::testing::Test +{ +protected: + lua_State* L = nullptr; + + void SetUp() override + { + L = luaL_newstate(); + luaL_openlibs(L); + omath::lua::LuaInterpreter::register_lib(L); + if (luaL_dofile(L, LUA_SCRIPTS_DIR "/hud_tests.lua") != LUA_OK) + FAIL() << lua_tostring(L, -1); + } + + void TearDown() override { lua_close(L); } + + void check(const char* func_name) + { + lua_getglobal(L, func_name); + if (lua_pcall(L, 0, 0, 0) != LUA_OK) + { + FAIL() << lua_tostring(L, -1); + lua_pop(L, 1); + } + } +}; + +TEST_F(LuaHud, CanvasBox_default_ratio) { check("Hud_CanvasBox_default_ratio"); } +TEST_F(LuaHud, CanvasBox_custom_ratio) { check("Hud_CanvasBox_custom_ratio"); } +TEST_F(LuaHud, CanvasBox_as_table) { check("Hud_CanvasBox_as_table"); } +TEST_F(LuaHud, Renderer_callbacks) { check("Hud_Renderer_callbacks"); } +TEST_F(LuaHud, Renderer_polyline_table) { check("Hud_Renderer_polyline_table"); } +TEST_F(LuaHud, Renderer_circle_defaults) { check("Hud_Renderer_circle_defaults"); } +TEST_F(LuaHud, EntityOverlay_add_2d_box) { check("Hud_EntityOverlay_add_2d_box"); } +TEST_F(LuaHud, EntityOverlay_add_cornered_2d_box) { check("Hud_EntityOverlay_add_cornered_2d_box"); } +TEST_F(LuaHud, EntityOverlay_add_dashed_box) { check("Hud_EntityOverlay_add_dashed_box"); } +TEST_F(LuaHud, EntityOverlay_add_bar) { check("Hud_EntityOverlay_add_bar"); } +TEST_F(LuaHud, EntityOverlay_add_dashed_bar) { check("Hud_EntityOverlay_add_dashed_bar"); } +TEST_F(LuaHud, EntityOverlay_add_label) { check("Hud_EntityOverlay_add_label"); } +TEST_F(LuaHud, EntityOverlay_add_outlined_label) { check("Hud_EntityOverlay_add_outlined_label"); } +TEST_F(LuaHud, EntityOverlay_add_centered_label) { check("Hud_EntityOverlay_add_centered_label"); } +TEST_F(LuaHud, EntityOverlay_add_spaces) { check("Hud_EntityOverlay_add_spaces"); } +TEST_F(LuaHud, EntityOverlay_add_progress_ring) { check("Hud_EntityOverlay_add_progress_ring"); } +TEST_F(LuaHud, EntityOverlay_add_progress_ring_defaults) { check("Hud_EntityOverlay_add_progress_ring_defaults"); } +TEST_F(LuaHud, EntityOverlay_add_icon) { check("Hud_EntityOverlay_add_icon"); } +TEST_F(LuaHud, EntityOverlay_add_snap_line) { check("Hud_EntityOverlay_add_snap_line"); } +TEST_F(LuaHud, EntityOverlay_add_skeleton) { check("Hud_EntityOverlay_add_skeleton"); } +TEST_F(LuaHud, EntityOverlay_add_scan_marker) { check("Hud_EntityOverlay_add_scan_marker"); } +TEST_F(LuaHud, EntityOverlay_add_aim_dot) { check("Hud_EntityOverlay_add_aim_dot"); } +TEST_F(LuaHud, EntityOverlay_add_projectile_aim_circle) { check("Hud_EntityOverlay_add_projectile_aim_circle"); } +TEST_F(LuaHud, EntityOverlay_add_projectile_aim_square_default) { check("Hud_EntityOverlay_add_projectile_aim_square_default"); } diff --git a/tests/lua/unit_test_lua_mat.cpp b/tests/lua/unit_test_lua_mat.cpp new file mode 100644 index 00000000..994c25e6 --- /dev/null +++ b/tests/lua/unit_test_lua_mat.cpp @@ -0,0 +1,129 @@ +// +// Created by orange on 07.03.2026. +// +#include +#include +#include + +class LuaMat : public ::testing::Test +{ +protected: + lua_State* L = nullptr; + + void SetUp() override + { + L = luaL_newstate(); + luaL_openlibs(L); + omath::lua::LuaInterpreter::register_lib(L); + if (luaL_dofile(L, LUA_SCRIPTS_DIR "/mat_tests.lua") != LUA_OK) + FAIL() << lua_tostring(L, -1); + } + + void TearDown() override + { + lua_close(L); + } + + void check(const char* func_name) + { + lua_getglobal(L, func_name); + if (lua_pcall(L, 0, 0, 0) != LUA_OK) + { + FAIL() << lua_tostring(L, -1); + lua_pop(L, 1); + } + } +}; + +TEST_F(LuaMat, Constructor_default) +{ + check("Mat4_Constructor_default"); +} +TEST_F(LuaMat, FromRows) +{ + check("Mat4_FromRows"); +} +TEST_F(LuaMat, SetAt) +{ + check("Mat4_SetAt"); +} +TEST_F(LuaMat, SetAndClear) +{ + check("Mat4_SetAndClear"); +} +TEST_F(LuaMat, Multiplication_matrix) +{ + check("Mat4_Multiplication_matrix"); +} +TEST_F(LuaMat, Multiplication_scalar) +{ + check("Mat4_Multiplication_scalar"); +} +TEST_F(LuaMat, Multiplication_scalar_reversed) +{ + check("Mat4_Multiplication_scalar_reversed"); +} +TEST_F(LuaMat, Division_scalar) +{ + check("Mat4_Division_scalar"); +} +TEST_F(LuaMat, Transposed) +{ + check("Mat4_Transposed"); +} +TEST_F(LuaMat, Determinant) +{ + check("Mat4_Determinant"); +} +TEST_F(LuaMat, Inverted_success) +{ + check("Mat4_Inverted_success"); +} +TEST_F(LuaMat, Inverted_singular) +{ + check("Mat4_Inverted_singular"); +} +TEST_F(LuaMat, ToScreenMat) +{ + check("Mat4_ToScreenMat"); +} +TEST_F(LuaMat, Translation) +{ + check("Mat4_Translation"); +} +TEST_F(LuaMat, Scale) +{ + check("Mat4_Scale"); +} +TEST_F(LuaMat, Perspective) +{ + check("Mat4_Perspective"); +} +TEST_F(LuaMat, AsTable) +{ + check("Mat4_AsTable"); +} +TEST_F(LuaMat, ToString) +{ + check("Mat4_ToString"); +} +TEST_F(LuaMat, Mat3_FromRows) +{ + check("Mat3_FromRows"); +} +TEST_F(LuaMat, Mat4ColumnMajor_FromRows) +{ + check("Mat4ColumnMajor_FromRows"); +} +TEST_F(LuaMat, Mat4ColumnMajor_ToScreenMat) +{ + check("Mat4ColumnMajor_ToScreenMat"); +} +TEST_F(LuaMat, Mat4ColumnMajor_Translation) +{ + check("Mat4ColumnMajor_Translation"); +} +TEST_F(LuaMat, Mat4d_FromRows) +{ + check("Mat4d_FromRows"); +} diff --git a/tests/lua/unit_test_lua_quaternion.cpp b/tests/lua/unit_test_lua_quaternion.cpp new file mode 100644 index 00000000..264ecea5 --- /dev/null +++ b/tests/lua/unit_test_lua_quaternion.cpp @@ -0,0 +1,125 @@ +// +// Created by orange on 07.03.2026. +// +#include +#include +#include + +class LuaQuaternion : public ::testing::Test +{ +protected: + lua_State* L = nullptr; + + void SetUp() override + { + L = luaL_newstate(); + luaL_openlibs(L); + omath::lua::LuaInterpreter::register_lib(L); + if (luaL_dofile(L, LUA_SCRIPTS_DIR "/quaternion_tests.lua") != LUA_OK) + FAIL() << lua_tostring(L, -1); + } + + void TearDown() override + { + lua_close(L); + } + + void check(const char* func_name) + { + lua_getglobal(L, func_name); + if (lua_pcall(L, 0, 0, 0) != LUA_OK) + { + FAIL() << lua_tostring(L, -1); + lua_pop(L, 1); + } + } +}; + +TEST_F(LuaQuaternion, Constructor_default) +{ + check("Quaternion_Constructor_default"); +} +TEST_F(LuaQuaternion, Constructor_xyzw) +{ + check("Quaternion_Constructor_xyzw"); +} +TEST_F(LuaQuaternion, Field_mutation) +{ + check("Quaternion_Field_mutation"); +} +TEST_F(LuaQuaternion, FromAxisAngle_zero_angle) +{ + check("Quaternion_FromAxisAngle_zero_angle"); +} +TEST_F(LuaQuaternion, Addition) +{ + check("Quaternion_Addition"); +} +TEST_F(LuaQuaternion, Multiplication_scalar) +{ + check("Quaternion_Multiplication_scalar"); +} +TEST_F(LuaQuaternion, Multiplication_scalar_reversed) +{ + check("Quaternion_Multiplication_scalar_reversed"); +} +TEST_F(LuaQuaternion, Multiplication_quaternion) +{ + check("Quaternion_Multiplication_quaternion"); +} +TEST_F(LuaQuaternion, UnaryMinus) +{ + check("Quaternion_UnaryMinus"); +} +TEST_F(LuaQuaternion, EqualTo_true) +{ + check("Quaternion_EqualTo_true"); +} +TEST_F(LuaQuaternion, EqualTo_false) +{ + check("Quaternion_EqualTo_false"); +} +TEST_F(LuaQuaternion, ToString) +{ + check("Quaternion_ToString"); +} +TEST_F(LuaQuaternion, Conjugate) +{ + check("Quaternion_Conjugate"); +} +TEST_F(LuaQuaternion, Dot) +{ + check("Quaternion_Dot"); +} +TEST_F(LuaQuaternion, Length) +{ + check("Quaternion_Length"); +} +TEST_F(LuaQuaternion, LengthSqr) +{ + check("Quaternion_LengthSqr"); +} +TEST_F(LuaQuaternion, Normalized) +{ + check("Quaternion_Normalized"); +} +TEST_F(LuaQuaternion, Inverse) +{ + check("Quaternion_Inverse"); +} +TEST_F(LuaQuaternion, Rotate) +{ + check("Quaternion_Rotate"); +} +TEST_F(LuaQuaternion, ToRotationMatrix3) +{ + check("Quaternion_ToRotationMatrix3"); +} +TEST_F(LuaQuaternion, ToRotationMatrix4) +{ + check("Quaternion_ToRotationMatrix4"); +} +TEST_F(LuaQuaternion, AsTable) +{ + check("Quaternion_AsTable"); +} diff --git a/tests/lua/unit_test_lua_source_engine.cpp b/tests/lua/unit_test_lua_source_engine.cpp index 102644c3..89251065 100644 --- a/tests/lua/unit_test_lua_source_engine.cpp +++ b/tests/lua/unit_test_lua_source_engine.cpp @@ -74,6 +74,12 @@ TEST_F(LuaSourceEngine, Camera_look_at) { check("Source_Camera_l TEST_F(LuaSourceEngine, Camera_get_forward) { check("Source_Camera_get_forward"); } TEST_F(LuaSourceEngine, Camera_get_right) { check("Source_Camera_get_right"); } TEST_F(LuaSourceEngine, Camera_get_up) { check("Source_Camera_get_up"); } +TEST_F(LuaSourceEngine, Camera_get_view_matrix) { check("Source_Camera_get_view_matrix"); } +TEST_F(LuaSourceEngine, Camera_get_projection_matrix) { check("Source_Camera_get_projection_matrix"); } +TEST_F(LuaSourceEngine, Camera_get_view_projection_matrix) { check("Source_Camera_get_view_projection_matrix"); } +TEST_F(LuaSourceEngine, Camera_extract_projection_params) { check("Source_Camera_extract_projection_params"); } +TEST_F(LuaSourceEngine, Camera_calc_view_angles_from_view_matrix) { check("Source_Camera_calc_view_angles_from_view_matrix"); } +TEST_F(LuaSourceEngine, Camera_calc_origin_from_view_matrix) { check("Source_Camera_calc_origin_from_view_matrix"); } TEST_F(LuaSourceEngine, Camera_world_to_screen_success) { check("Source_Camera_world_to_screen_success"); } TEST_F(LuaSourceEngine, Camera_world_to_screen_error) { check("Source_Camera_world_to_screen_error"); } TEST_F(LuaSourceEngine, Camera_screen_to_world) { check("Source_Camera_screen_to_world"); }