Compare commits

...

29 Commits

Author SHA1 Message Date
3ccbb0b25b Merge pull request #161 from orange-cpp/feature/lua-bindings
Feature/lua bindings
2026-03-10 19:41:34 +03:00
68993db48a updated read me 2026-03-10 19:22:39 +03:00
337204b3bf fixed missing source 2026-03-10 19:09:29 +03:00
1e601d2d8f android fix 2026-03-10 19:01:54 +03:00
2b4a75d011 added lua status to build logs 2026-03-10 19:00:36 +03:00
99a30e8fdf added pattern scan to lua 2026-03-10 18:55:55 +03:00
0cdd1d021f added triangle to lua 2026-03-10 18:39:20 +03:00
a79ac9743a added badge, added triangle 2026-03-10 18:39:10 +03:00
f26afb703b added bigobj for msvc and mingw 2026-03-10 18:29:13 +03:00
f237ee5f6a removed useless headers 2026-03-10 18:15:54 +03:00
9058ea9b39 refactored to class 2026-03-10 18:14:29 +03:00
f707ac1adb fixed forward decl 2026-03-10 18:04:31 +03:00
cbdabd3fc2 removed useless header 2026-03-10 17:57:11 +03:00
30e3feb4f8 fix 2026-03-10 16:38:00 +03:00
0726fdef32 webasm fix 2026-03-10 16:31:49 +03:00
0ffe0c2bdc potential fix 2026-03-10 16:24:21 +03:00
e9600ad42b splited lua into multiple sources 2026-03-10 16:15:02 +03:00
673835618c restructurized stuff 2026-03-10 15:57:22 +03:00
afb2a13dd6 added color 2026-03-08 23:08:23 +03:00
943472cf64 migrated to sol2
decomposed method

added vector2, vector4

refactored tests

added opengl engine to lua

added other engines

added source tests

removed tons of lua files
2026-03-08 12:55:35 +03:00
9752accb14 Merge pull request #160 from orange-cpp/feaure/gjk-epa-improvement
Feaure/gjk epa improvement
2026-03-03 18:25:37 +03:00
7373e6d3df added std namspace to int64_t type 2026-03-03 10:00:46 +03:00
68f4c8cc72 added nodiscard 2026-03-03 09:38:05 +03:00
2dafc8a49d added additional method 2026-03-03 09:22:11 +03:00
11fe49e801 added const 2026-03-03 08:51:13 +03:00
dee705a391 improvement 2026-03-03 08:43:30 +03:00
bfe147ef80 Merge remote-tracking branch 'orange-cpp/feaure/gjk-epa-improvement' into feaure/gjk-epa-improvement 2026-03-03 08:27:50 +03:00
2c70288a8f added epa tests 2026-03-03 08:27:26 +03:00
529322fe34 decomposed method 2026-03-03 08:14:12 +03:00
34 changed files with 2507 additions and 51 deletions

View File

@@ -107,7 +107,7 @@ jobs:
-DOMATH_BUILD_TESTS=ON \ -DOMATH_BUILD_TESTS=ON \
-DOMATH_BUILD_BENCHMARK=OFF \ -DOMATH_BUILD_BENCHMARK=OFF \
-DOMATH_ENABLE_COVERAGE=${{ matrix.coverage == true && 'ON' || 'OFF' }} \ -DOMATH_ENABLE_COVERAGE=${{ matrix.coverage == true && 'ON' || 'OFF' }} \
-DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests" -DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests;lua"
- name: Build - name: Build
shell: bash shell: bash
@@ -193,7 +193,7 @@ jobs:
-DOMATH_BUILD_TESTS=ON \ -DOMATH_BUILD_TESTS=ON \
-DOMATH_BUILD_BENCHMARK=OFF \ -DOMATH_BUILD_BENCHMARK=OFF \
-DOMATH_ENABLE_COVERAGE=OFF \ -DOMATH_ENABLE_COVERAGE=OFF \
-DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests" -DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests;lua"
- name: Build - name: Build
shell: bash shell: bash
@@ -234,7 +234,7 @@ jobs:
-DOMATH_ENABLE_COVERAGE=ON \ -DOMATH_ENABLE_COVERAGE=ON \
-DOMATH_THREAT_WARNING_AS_ERROR=OFF \ -DOMATH_THREAT_WARNING_AS_ERROR=OFF \
-DCMAKE_BUILD_TYPE=Debug \ -DCMAKE_BUILD_TYPE=Debug \
-DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests" -DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests;lua"
cmake --build cmake-build/build/${{ matrix.preset }} --config Debug --target unit_tests omath cmake --build cmake-build/build/${{ matrix.preset }} --config Debug --target unit_tests omath
- name: Run Tests (Generates .profraw) - name: Run Tests (Generates .profraw)
@@ -373,7 +373,7 @@ jobs:
-DOMATH_BUILD_TESTS=ON \ -DOMATH_BUILD_TESTS=ON \
-DOMATH_BUILD_BENCHMARK=OFF \ -DOMATH_BUILD_BENCHMARK=OFF \
-DOMATH_ENABLE_COVERAGE=${{ matrix.coverage == true && 'ON' || 'OFF' }} \ -DOMATH_ENABLE_COVERAGE=${{ matrix.coverage == true && 'ON' || 'OFF' }} \
-DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests" -DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests;lua"
- name: Build - name: Build
shell: bash shell: bash
@@ -450,7 +450,7 @@ jobs:
-DVCPKG_INSTALL_OPTIONS="--allow-unsupported" \ -DVCPKG_INSTALL_OPTIONS="--allow-unsupported" \
-DOMATH_BUILD_TESTS=ON \ -DOMATH_BUILD_TESTS=ON \
-DOMATH_BUILD_BENCHMARK=OFF \ -DOMATH_BUILD_BENCHMARK=OFF \
-DVCPKG_MANIFEST_FEATURES="imgui;tests" -DVCPKG_MANIFEST_FEATURES="imgui;tests;lua"
- name: Build - name: Build
shell: bash shell: bash
@@ -509,7 +509,7 @@ jobs:
cmake --preset ${{ matrix.preset }} \ cmake --preset ${{ matrix.preset }} \
-DOMATH_BUILD_TESTS=ON \ -DOMATH_BUILD_TESTS=ON \
-DOMATH_BUILD_BENCHMARK=OFF \ -DOMATH_BUILD_BENCHMARK=OFF \
-DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests" \ -DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests;lua" \
-DVCPKG_INSTALL_OPTIONS="--allow-unsupported" -DVCPKG_INSTALL_OPTIONS="--allow-unsupported"
cmake --build cmake-build/build/${{ matrix.preset }} --target unit_tests omath cmake --build cmake-build/build/${{ matrix.preset }} --target unit_tests omath
./out/Release/unit_tests ./out/Release/unit_tests
@@ -581,7 +581,7 @@ jobs:
-DVCPKG_INSTALL_OPTIONS="--allow-unsupported" \ -DVCPKG_INSTALL_OPTIONS="--allow-unsupported" \
-DOMATH_BUILD_TESTS=ON \ -DOMATH_BUILD_TESTS=ON \
-DOMATH_BUILD_BENCHMARK=OFF \ -DOMATH_BUILD_BENCHMARK=OFF \
-DVCPKG_MANIFEST_FEATURES="imgui;tests" -DVCPKG_MANIFEST_FEATURES="imgui;tests;lua"
- name: Build - name: Build
shell: bash shell: bash
@@ -650,7 +650,7 @@ jobs:
-DVCPKG_INSTALL_OPTIONS="--allow-unsupported" \ -DVCPKG_INSTALL_OPTIONS="--allow-unsupported" \
-DOMATH_BUILD_TESTS=ON \ -DOMATH_BUILD_TESTS=ON \
-DOMATH_BUILD_BENCHMARK=OFF \ -DOMATH_BUILD_BENCHMARK=OFF \
-DVCPKG_MANIFEST_FEATURES="imgui;tests" -DVCPKG_MANIFEST_FEATURES="imgui;tests;lua"
- name: Build - name: Build
shell: bash shell: bash
@@ -735,7 +735,7 @@ jobs:
-DVCPKG_INSTALL_OPTIONS="--allow-unsupported" \ -DVCPKG_INSTALL_OPTIONS="--allow-unsupported" \
-DOMATH_BUILD_TESTS=ON \ -DOMATH_BUILD_TESTS=ON \
-DOMATH_BUILD_BENCHMARK=OFF \ -DOMATH_BUILD_BENCHMARK=OFF \
-DVCPKG_MANIFEST_FEATURES="imgui;tests" -DVCPKG_MANIFEST_FEATURES="imgui;tests;lua"
- name: Build - name: Build
run: | run: |
@@ -800,7 +800,7 @@ jobs:
-DOMATH_BUILD_TESTS=ON \ -DOMATH_BUILD_TESTS=ON \
-DOMATH_BUILD_BENCHMARK=ON \ -DOMATH_BUILD_BENCHMARK=ON \
-DOMATH_ENABLE_VALGRIND=ON \ -DOMATH_ENABLE_VALGRIND=ON \
-DVCPKG_MANIFEST_FEATURES="imgui;avx2;tests;benchmark" -DVCPKG_MANIFEST_FEATURES="imgui;avx2;lua;tests;benchmark"
- name: Build All Targets - name: Build All Targets
shell: bash shell: bash

5
.luarc.json Normal file
View File

@@ -0,0 +1,5 @@
{
"diagnostics.globals": [
"omath"
]
}

View File

@@ -31,6 +31,9 @@ option(OMATH_SUPRESS_SAFETY_CHECKS
option(OMATH_ENABLE_COVERAGE "Enable coverage" OFF) option(OMATH_ENABLE_COVERAGE "Enable coverage" OFF)
option(OMATH_ENABLE_FORCE_INLINE option(OMATH_ENABLE_FORCE_INLINE
"Will for compiler to make some functions to be force inlined no matter what" ON) "Will for compiler to make some functions to be force inlined no matter what" ON)
option(OMATH_ENABLE_LUA
"omath bindings for lua" OFF)
if(VCPKG_MANIFEST_FEATURES) if(VCPKG_MANIFEST_FEATURES)
foreach(omath_feature IN LISTS VCPKG_MANIFEST_FEATURES) foreach(omath_feature IN LISTS VCPKG_MANIFEST_FEATURES)
if(omath_feature STREQUAL "imgui") if(omath_feature STREQUAL "imgui")
@@ -43,6 +46,8 @@ if(VCPKG_MANIFEST_FEATURES)
set(OMATH_BUILD_BENCHMARK ON) set(OMATH_BUILD_BENCHMARK ON)
elseif(omath_feature STREQUAL "examples") elseif(omath_feature STREQUAL "examples")
set(OMATH_BUILD_EXAMPLES ON) set(OMATH_BUILD_EXAMPLES ON)
elseif(omath_feature STREQUAL "lua")
set(OMATH_ENABLE_LUA ON)
endif() endif()
endforeach() endforeach()
@@ -72,6 +77,7 @@ if(${PROJECT_IS_TOP_LEVEL})
message(STATUS "[${PROJECT_NAME}]: Building using vcpkg ${OMATH_BUILD_VIA_VCPKG}") message(STATUS "[${PROJECT_NAME}]: Building using vcpkg ${OMATH_BUILD_VIA_VCPKG}")
message(STATUS "[${PROJECT_NAME}]: Coverage feature status ${OMATH_ENABLE_COVERAGE}") message(STATUS "[${PROJECT_NAME}]: Coverage feature status ${OMATH_ENABLE_COVERAGE}")
message(STATUS "[${PROJECT_NAME}]: Valgrind feature status ${OMATH_ENABLE_VALGRIND}") message(STATUS "[${PROJECT_NAME}]: Valgrind feature status ${OMATH_ENABLE_VALGRIND}")
message(STATUS "[${PROJECT_NAME}]: Lua feature status ${OMATH_ENABLE_LUA}")
endif() endif()
file(GLOB_RECURSE OMATH_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/source/*.cpp") file(GLOB_RECURSE OMATH_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/source/*.cpp")
@@ -83,6 +89,17 @@ else()
add_library(${PROJECT_NAME} STATIC ${OMATH_SOURCES} ${OMATH_HEADERS}) add_library(${PROJECT_NAME} STATIC ${OMATH_SOURCES} ${OMATH_HEADERS})
endif() endif()
if (OMATH_ENABLE_LUA)
target_compile_definitions(${PROJECT_NAME} PUBLIC OMATH_ENABLE_LUA)
find_package(Lua REQUIRED)
target_include_directories(${PROJECT_NAME} PRIVATE ${LUA_INCLUDE_DIR})
target_link_libraries(${PROJECT_NAME} PRIVATE ${LUA_LIBRARIES})
find_path(SOL2_INCLUDE_DIRS "sol/abort.hpp")
target_include_directories(${PROJECT_NAME} PRIVATE ${SOL2_INCLUDE_DIRS})
endif ()
add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME})
target_compile_definitions(${PROJECT_NAME} PUBLIC OMATH_VERSION="${PROJECT_VERSION}") target_compile_definitions(${PROJECT_NAME} PUBLIC OMATH_VERSION="${PROJECT_VERSION}")
@@ -174,6 +191,12 @@ elseif(OMATH_THREAT_WARNING_AS_ERROR)
target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic -Werror) target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic -Werror)
endif() endif()
if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
target_compile_options(${PROJECT_NAME} PRIVATE /bigobj)
endif()
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_HOST_SYSTEM_NAME EQUAL "Windows")
target_compile_options(${PROJECT_NAME} PRIVATE -mbig-obj)
endif()
# Windows SDK redefine min/max via preprocessor and break std::min and std::max # Windows SDK redefine min/max via preprocessor and break std::min and std::max
if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
target_compile_definitions(${PROJECT_NAME} INTERFACE NOMINMAX) target_compile_definitions(${PROJECT_NAME} INTERFACE NOMINMAX)

View File

@@ -145,7 +145,7 @@
"hidden": true, "hidden": true,
"inherits": ["linux-base", "vcpkg-base"], "inherits": ["linux-base", "vcpkg-base"],
"cacheVariables": { "cacheVariables": {
"VCPKG_MANIFEST_FEATURES": "tests;imgui;avx2" "VCPKG_MANIFEST_FEATURES": "tests;imgui;avx2;lua"
} }
}, },
{ {
@@ -235,7 +235,7 @@
"hidden": true, "hidden": true,
"inherits": ["darwin-base", "vcpkg-base"], "inherits": ["darwin-base", "vcpkg-base"],
"cacheVariables": { "cacheVariables": {
"VCPKG_MANIFEST_FEATURES": "tests;imgui;avx2;examples" "VCPKG_MANIFEST_FEATURES": "tests;imgui;avx2;examples;lua"
} }
}, },
{ {

View File

@@ -9,6 +9,7 @@
[![CodeFactor](https://www.codefactor.io/repository/github/orange-cpp/omath/badge)](https://www.codefactor.io/repository/github/orange-cpp/omath) [![CodeFactor](https://www.codefactor.io/repository/github/orange-cpp/omath/badge)](https://www.codefactor.io/repository/github/orange-cpp/omath)
![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/orange-cpp/omath/cmake-multi-platform.yml) ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/orange-cpp/omath/cmake-multi-platform.yml)
[![Vcpkg package](https://repology.org/badge/version-for-repo/vcpkg/orange-math.svg)](https://repology.org/project/orange-math/versions) [![Vcpkg package](https://repology.org/badge/version-for-repo/vcpkg/orange-math.svg)](https://repology.org/project/orange-math/versions)
![Conan Center](https://img.shields.io/conan/v/omath)
![GitHub forks](https://img.shields.io/github/forks/orange-cpp/omath) ![GitHub forks](https://img.shields.io/github/forks/orange-cpp/omath)
[![discord badge](https://dcbadge.limes.pink/api/server/https://discord.gg/eDgdaWbqwZ?style=flat)](https://discord.gg/eDgdaWbqwZ) [![discord badge](https://dcbadge.limes.pink/api/server/https://discord.gg/eDgdaWbqwZ?style=flat)](https://discord.gg/eDgdaWbqwZ)
[![telegram badge](https://img.shields.io/badge/Telegram-2CA5E0?style=flat-squeare&logo=telegram&logoColor=white)](https://t.me/orangennotes) [![telegram badge](https://img.shields.io/badge/Telegram-2CA5E0?style=flat-squeare&logo=telegram&logoColor=white)](https://t.me/orangennotes)
@@ -83,6 +84,7 @@ if (auto screen = camera.world_to_screen(world_position)) {
- **Engine support**: Supports coordinate systems of **Source, Unity, Unreal, Frostbite, IWEngine, CryEngine and canonical OpenGL**. - **Engine support**: Supports coordinate systems of **Source, Unity, Unreal, Frostbite, IWEngine, CryEngine and canonical OpenGL**.
- **Cross platform**: Supports Windows, MacOS and Linux. - **Cross platform**: Supports Windows, MacOS and Linux.
- **Algorithms**: Has ability to scan for byte pattern with wildcards in ELF/Mach-O/PE files/modules, binary slices, works even with Wine apps. - **Algorithms**: Has ability to scan for byte pattern with wildcards in ELF/Mach-O/PE files/modules, binary slices, works even with Wine apps.
- **Scripting**: Supports to make scripts in Lua out of box
- **Battle tested**: It's already used by some big players on the market like wraith.su and bluedream.ltd - **Battle tested**: It's already used by some big players on the market like wraith.su and bluedream.ltd
<div align = center> <div align = center>

View File

@@ -8,6 +8,7 @@
#include <memory> #include <memory>
#include <memory_resource> #include <memory_resource>
#include <queue> #include <queue>
#include <unordered_map>
#include <utility> #include <utility>
#include <vector> #include <vector>
@@ -50,7 +51,6 @@ namespace omath::collision
int max_iterations{64}; int max_iterations{64};
FloatingType tolerance{1e-4}; // absolute tolerance on distance growth FloatingType tolerance{1e-4}; // absolute tolerance on distance growth
}; };
// Precondition: simplex.size()==4 and contains the origin. // Precondition: simplex.size()==4 and contains the origin.
[[nodiscard]] [[nodiscard]]
static std::optional<Result> solve(const ColliderInterfaceType& a, const ColliderInterfaceType& b, static std::optional<Result> solve(const ColliderInterfaceType& a, const ColliderInterfaceType& b,
@@ -65,20 +65,14 @@ namespace omath::collision
Result out{}; Result out{};
// Hoisted outside the loop to reuse the allocation across iterations. // Hoisted outside the loop to reuse bucket allocation across iterations.
std::pmr::vector<Edge> boundary{&mem_resource}; // Initial bucket count 16 covers a typical horizon without rehashing.
boundary.reserve(16); BoundaryMap boundary{16, &mem_resource};
for (int it = 0; it < params.max_iterations; ++it) for (int it = 0; it < params.max_iterations; ++it)
{ {
// Lazily discard stale (deleted or index-mismatched) heap entries. // Lazily discard stale (deleted or index-mismatched) heap entries.
while (!heap.empty()) discard_stale_heap_entries(faces, heap);
{
const auto& top = heap.top();
if (!faces[top.idx].deleted && faces[top.idx].d == top.d)
break;
heap.pop();
}
if (heap.empty()) if (heap.empty())
break; break;
@@ -105,21 +99,11 @@ namespace omath::collision
// Tombstone visible faces and collect the horizon boundary. // Tombstone visible faces and collect the horizon boundary.
// This avoids copying the faces array (O(n)) each iteration. // This avoids copying the faces array (O(n)) each iteration.
boundary.clear(); tombstone_visible_faces(faces, boundary, p);
for (auto& f : faces)
{
if (!f.deleted && visible_from(f, p))
{
f.deleted = true;
add_edge_boundary(boundary, f.i0, f.i1);
add_edge_boundary(boundary, f.i1, f.i2);
add_edge_boundary(boundary, f.i2, f.i0);
}
}
// Stitch new faces around the horizon and push them directly onto the // Stitch new faces around the horizon and push them directly onto the
// heap — no full O(n log n) rebuild needed. // heap — no full O(n log n) rebuild needed.
for (const auto& e : boundary) for (const auto& [key, e] : boundary)
{ {
const int fi = static_cast<int>(faces.size()); const int fi = static_cast<int>(faces.size());
faces.emplace_back(make_face(vertexes, e.a, e.b, new_idx)); faces.emplace_back(make_face(vertexes, e.a, e.b, new_idx));
@@ -133,10 +117,7 @@ namespace omath::collision
} }
// Find the best surviving (non-deleted) face. // Find the best surviving (non-deleted) face.
const Face* best = nullptr; const Face* best = find_best_surviving_face(faces);
for (const auto& f : faces)
if (!f.deleted && (best == nullptr || f.d < best->d))
best = &f;
if (!best) if (!best)
return std::nullopt; return std::nullopt;
@@ -153,8 +134,8 @@ namespace omath::collision
struct Face final struct Face final
{ {
int i0, i1, i2; int i0, i1, i2;
VectorType n; // unit outward normal VectorType n; // unit outward normal
FloatingType d; // n · v0 (>= 0 ideally because origin is inside) FloatingType d; // n · v0 (>= 0 ideally because origin is inside)
bool deleted{false}; // tombstone flag — avoids O(n) compaction per iteration bool deleted{false}; // tombstone flag — avoids O(n) compaction per iteration
}; };
@@ -180,6 +161,16 @@ namespace omath::collision
using Heap = std::priority_queue<HeapItem, std::pmr::vector<HeapItem>, HeapCmp>; using Heap = std::priority_queue<HeapItem, std::pmr::vector<HeapItem>, HeapCmp>;
// Horizon boundary: maps packed(a,b) → Edge.
// Opposite edges cancel in O(1) via hash lookup instead of O(h) linear scan.
using BoundaryMap = std::pmr::unordered_map<std::int64_t, Edge>;
[[nodiscard]]
static constexpr std::int64_t pack_edge(const int a, const int b) noexcept
{
return (static_cast<std::int64_t>(a) << 32) | static_cast<std::uint32_t>(b);
}
[[nodiscard]] [[nodiscard]]
static Heap rebuild_heap(const std::pmr::vector<Face>& faces, auto& memory_resource) static Heap rebuild_heap(const std::pmr::vector<Face>& faces, auto& memory_resource)
{ {
@@ -198,14 +189,16 @@ namespace omath::collision
return f.n.dot(p) - f.d > static_cast<FloatingType>(1e-7); return f.n.dot(p) - f.d > static_cast<FloatingType>(1e-7);
} }
static void add_edge_boundary(std::pmr::vector<Edge>& boundary, int a, int b) static void add_edge_boundary(BoundaryMap& boundary, int a, int b)
{ {
// Keep edges that appear only once; cancel if opposite already present. // O(1) cancel: if the opposite edge (b→a) is already in the map it is an
auto itb = std::ranges::find_if(boundary, [&](const Edge& e) { return e.a == b && e.b == a; }); // internal edge shared by two visible faces and must be removed.
if (itb != boundary.end()) // Otherwise this is a horizon edge and we insert it.
boundary.erase(itb); const std::int64_t rev = pack_edge(b, a);
if (const auto it = boundary.find(rev); it != boundary.end())
boundary.erase(it);
else else
boundary.emplace_back(a, b); boundary.emplace(pack_edge(a, b), Edge{a, b});
} }
[[nodiscard]] [[nodiscard]]
@@ -277,5 +270,41 @@ namespace omath::collision
vertexes.emplace_back(simplex[i]); vertexes.emplace_back(simplex[i]);
return vertexes; return vertexes;
} }
static const Face* find_best_surviving_face(const std::pmr::vector<Face>& faces)
{
const Face* best = nullptr;
for (const auto& f : faces)
if (!f.deleted && (best == nullptr || f.d < best->d))
best = &f;
return best;
}
static void tombstone_visible_faces(std::pmr::vector<Face>& faces, BoundaryMap& boundary,
const VectorType& p)
{
boundary.clear();
for (auto& f : faces)
{
if (!f.deleted && visible_from(f, p))
{
f.deleted = true;
add_edge_boundary(boundary, f.i0, f.i1);
add_edge_boundary(boundary, f.i1, f.i2);
add_edge_boundary(boundary, f.i2, f.i0);
}
}
}
static void discard_stale_heap_entries(const std::pmr::vector<Face>& faces,
std::priority_queue<HeapItem, std::pmr::vector<HeapItem>, HeapCmp>& heap)
{
while (!heap.empty())
{
const auto& top = heap.top();
if (!faces[top.idx].deleted && faces[top.idx].d == top.d)
break;
heap.pop();
}
}
}; };
} // namespace omath::collision } // namespace omath::collision

View File

@@ -42,6 +42,16 @@ namespace omath::collision
m_mesh.set_origin(new_origin); m_mesh.set_origin(new_origin);
} }
[[nodiscard]]
const MeshType& get_mesh() const
{
return m_mesh;
}
[[nodiscard]]
MeshType& get_mesh()
{
return m_mesh;
}
private: private:
[[nodiscard]] [[nodiscard]]
const VertexType& find_furthest_vertex(const VectorType& direction) const const VertexType& find_furthest_vertex(const VectorType& direction) const

25
include/omath/lua/lua.hpp Normal file
View File

@@ -0,0 +1,25 @@
//
// Created by orange on 07.03.2026.
//
#pragma once
#ifdef OMATH_ENABLE_LUA
#include <sol/forward.hpp>
namespace omath::lua
{
class LuaInterpreter final
{
public:
static void register_lib(lua_State* lua_state);
private:
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_color(sol::table& omath_table);
static void register_triangle(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

27
source/lua/lua.cpp Normal file
View File

@@ -0,0 +1,27 @@
//
// Created by orange on 07.03.2026.
//
#ifdef OMATH_ENABLE_LUA
#include "lua.hpp"
#include <sol/sol.hpp>
#include "omath/lua/lua.hpp"
namespace omath::lua
{
void LuaInterpreter::register_lib(lua_State* lua_state)
{
sol::state_view lua(lua_state);
auto omath_table = lua["omath"].get_or_create<sol::table>();
register_vec2(omath_table);
register_vec3(omath_table);
register_vec4(omath_table);
register_color(omath_table);
register_triangle(omath_table);
register_shared_types(omath_table);
register_engines(omath_table);
register_pattern_scan(omath_table);
}
} // namespace omath::lua
#endif

46
source/lua/lua_color.cpp Normal file
View File

@@ -0,0 +1,46 @@
//
// Created by orange on 07.03.2026.
//
#ifdef OMATH_ENABLE_LUA
#include "omath/lua/lua.hpp"
#include <sol/sol.hpp>
#include <omath/utility/color.hpp>
namespace omath::lua
{
void LuaInterpreter::register_color(sol::table& omath_table)
{
omath_table.new_usertype<omath::Color>(
"Color",
sol::factories([](float r, float g, float b, float a) { return omath::Color(r, g, b, a); },
[]() { return omath::Color(); }),
"from_rgba", [](uint8_t r, uint8_t g, uint8_t b, uint8_t a)
{ return omath::Color::from_rgba(r, g, b, a); }, "from_hsv",
sol::overload([](float h, float s, float v) { return omath::Color::from_hsv(h, s, v); },
[](const omath::Hsv& hsv) { return omath::Color::from_hsv(hsv); }),
"red", []() { return omath::Color::red(); }, "green", []() { return omath::Color::green(); }, "blue",
[]() { return omath::Color::blue(); },
"r", sol::property([](const omath::Color& c) { return c.value().x; }), "g",
sol::property([](const omath::Color& c) { return c.value().y; }), "b",
sol::property([](const omath::Color& c) { return c.value().z; }), "a",
sol::property([](const omath::Color& c) { return c.value().w; }),
"to_hsv", &omath::Color::to_hsv, "set_hue", &omath::Color::set_hue, "set_saturation",
&omath::Color::set_saturation, "set_value", &omath::Color::set_value, "blend", &omath::Color::blend,
sol::meta_function::to_string, &omath::Color::to_string);
omath_table.new_usertype<omath::Hsv>(
"Hsv", sol::constructors<omath::Hsv()>(), "hue",
sol::property([](const omath::Hsv& h) { return h.hue; }, [](omath::Hsv& h, float val) { h.hue = val; }),
"saturation",
sol::property([](const omath::Hsv& h) { return h.saturation; },
[](omath::Hsv& h, float val) { h.saturation = val; }),
"value",
sol::property([](const omath::Hsv& h) { return h.value; },
[](omath::Hsv& h, float val) { h.value = val; }));
}
} // namespace omath::lua::detail
#endif

227
source/lua/lua_engines.cpp Normal file
View File

@@ -0,0 +1,227 @@
//
// Created by orange on 07.03.2026.
//
#ifdef OMATH_ENABLE_LUA
#include "omath/lua/lua.hpp"
#include <omath/engines/cry_engine/camera.hpp>
#include <omath/engines/frostbite_engine/camera.hpp>
#include <omath/engines/iw_engine/camera.hpp>
#include <omath/engines/opengl_engine/camera.hpp>
#include <omath/engines/source_engine/camera.hpp>
#include <omath/engines/unity_engine/camera.hpp>
#include <omath/engines/unreal_engine/camera.hpp>
#include <sol/sol.hpp>
#include <string_view>
namespace
{
// ---- Canonical shared C++ type aliases ----------------------------------
// Each unique template instantiation must be registered exactly once.
using PitchAngle90 = omath::Angle<float, -90.f, 90.f, omath::AngleFlags::Clamped>;
using PitchAngle89 = omath::Angle<float, -89.f, 89.f, omath::AngleFlags::Clamped>;
using SharedYawRoll = omath::Angle<float, -180.f, 180.f, omath::AngleFlags::Normalized>;
using SharedFoV = omath::Angle<float, 0.f, 180.f, omath::AngleFlags::Clamped>;
using ViewAngles90 = omath::ViewAngles<PitchAngle90, SharedYawRoll, SharedYawRoll>;
using ViewAngles89 = omath::ViewAngles<PitchAngle89, SharedYawRoll, SharedYawRoll>;
std::string projection_error_to_string(omath::projection::Error e)
{
switch (e)
{
case omath::projection::Error::WORLD_POSITION_IS_OUT_OF_SCREEN_BOUNDS:
return "world position is out of screen bounds";
case omath::projection::Error::INV_VIEW_PROJ_MAT_DET_EQ_ZERO:
return "inverse view-projection matrix determinant is zero";
}
return "unknown error";
}
template<class AngleType>
void register_angle(sol::table& table, const char* name)
{
table.new_usertype<AngleType>(
name, sol::no_constructor, "from_degrees", &AngleType::from_degrees, "from_radians",
&AngleType::from_radians, "as_degrees", &AngleType::as_degrees, "as_radians", &AngleType::as_radians,
"sin", &AngleType::sin, "cos", &AngleType::cos, "tan", &AngleType::tan, "cot", &AngleType::cot,
sol::meta_function::addition, [](const AngleType& a, const AngleType& b)
{ return AngleType::from_degrees(a.as_degrees() + b.as_degrees()); }, sol::meta_function::subtraction,
[](const AngleType& a, const AngleType& b)
{ return AngleType::from_degrees(a.as_degrees() - b.as_degrees()); }, sol::meta_function::unary_minus,
[](const AngleType& a) { return AngleType::from_degrees(-a.as_degrees()); },
sol::meta_function::equal_to, [](const AngleType& a, const AngleType& b) { return a == b; },
sol::meta_function::to_string, [](const AngleType& a) { return std::format("{}deg", a.as_degrees()); });
}
// Set aliases in an engine subtable pointing to the already-registered shared types
template<class PitchAngleType, class ViewAnglesType>
void set_engine_aliases(sol::table& engine_table, sol::table& types)
{
if constexpr (std::is_same_v<PitchAngleType, PitchAngle90>)
engine_table["PitchAngle"] = types["PitchAngle90"];
else
engine_table["PitchAngle"] = types["PitchAngle89"];
engine_table["YawAngle"] = types["YawRoll"];
engine_table["RollAngle"] = types["YawRoll"];
engine_table["FieldOfView"] = types["FieldOfView"];
engine_table["ViewPort"] = types["ViewPort"];
if constexpr (std::is_same_v<ViewAnglesType, ViewAngles90>)
engine_table["ViewAngles"] = types["ViewAngles90"];
else
engine_table["ViewAngles"] = types["ViewAngles89"];
}
// Register an engine: alias shared types, register unique Camera
template<class EngineTraits>
void register_engine(sol::table& omath_table, const char* subtable_name)
{
using PitchAngle = typename EngineTraits::PitchAngle;
using ViewAngles = typename EngineTraits::ViewAngles;
using Camera = typename EngineTraits::Camera;
auto engine_table = omath_table[subtable_name].get_or_create<sol::table>();
auto types = omath_table["_types"].get<sol::table>();
set_engine_aliases<PitchAngle, ViewAngles>(engine_table, types);
engine_table.new_usertype<Camera>(
"Camera",
sol::constructors<Camera(const omath::Vector3<float>&, const ViewAngles&,
const omath::projection::ViewPort&, const omath::projection::FieldOfView&,
float, float)>(),
"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<float>& pos)
-> std::tuple<sol::optional<omath::Vector3<float>>, sol::optional<std::string>>
{
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<float>& pos)
-> std::tuple<sol::optional<omath::Vector3<float>>, sol::optional<std::string>>
{
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 -----------------------------------------------
struct OpenGLEngineTraits
{
using PitchAngle = omath::opengl_engine::PitchAngle;
using ViewAngles = omath::opengl_engine::ViewAngles;
using Camera = omath::opengl_engine::Camera;
};
struct FrostbiteEngineTraits
{
using PitchAngle = omath::frostbite_engine::PitchAngle;
using ViewAngles = omath::frostbite_engine::ViewAngles;
using Camera = omath::frostbite_engine::Camera;
};
struct IWEngineTraits
{
using PitchAngle = omath::iw_engine::PitchAngle;
using ViewAngles = omath::iw_engine::ViewAngles;
using Camera = omath::iw_engine::Camera;
};
struct SourceEngineTraits
{
using PitchAngle = omath::source_engine::PitchAngle;
using ViewAngles = omath::source_engine::ViewAngles;
using Camera = omath::source_engine::Camera;
};
struct UnityEngineTraits
{
using PitchAngle = omath::unity_engine::PitchAngle;
using ViewAngles = omath::unity_engine::ViewAngles;
using Camera = omath::unity_engine::Camera;
};
struct UnrealEngineTraits
{
using PitchAngle = omath::unreal_engine::PitchAngle;
using ViewAngles = omath::unreal_engine::ViewAngles;
using Camera = omath::unreal_engine::Camera;
};
struct CryEngineTraits
{
using PitchAngle = omath::cry_engine::PitchAngle;
using ViewAngles = omath::cry_engine::ViewAngles;
using Camera = omath::cry_engine::Camera;
};
} // namespace
namespace omath::lua
{
void LuaInterpreter::register_shared_types(sol::table& omath_table)
{
auto t = omath_table["_types"].get_or_create<sol::table>();
register_angle<PitchAngle90>(t, "PitchAngle90");
register_angle<PitchAngle89>(t, "PitchAngle89");
register_angle<SharedYawRoll>(t, "YawRoll");
register_angle<SharedFoV>(t, "FieldOfView");
t.new_usertype<omath::projection::ViewPort>(
"ViewPort", sol::factories([](float w, float h) { return omath::projection::ViewPort{w, h}; }), "width",
sol::property([](const omath::projection::ViewPort& vp) { return vp.m_width; },
[](omath::projection::ViewPort& vp, float val) { vp.m_width = val; }),
"height",
sol::property([](const omath::projection::ViewPort& vp) { return vp.m_height; },
[](omath::projection::ViewPort& vp, float val) { vp.m_height = val; }),
"aspect_ratio", &omath::projection::ViewPort::aspect_ratio);
t.new_usertype<ViewAngles90>(
"ViewAngles90",
sol::factories([](PitchAngle90 p, SharedYawRoll y, SharedYawRoll r) { return ViewAngles90{p, y, r}; }),
"pitch",
sol::property([](const ViewAngles90& va) { return va.pitch; },
[](ViewAngles90& va, const PitchAngle90& val) { va.pitch = val; }),
"yaw",
sol::property([](const ViewAngles90& va) { return va.yaw; },
[](ViewAngles90& va, const SharedYawRoll& val) { va.yaw = val; }),
"roll",
sol::property([](const ViewAngles90& va) { return va.roll; },
[](ViewAngles90& va, const SharedYawRoll& val) { va.roll = val; }));
t.new_usertype<ViewAngles89>(
"ViewAngles89",
sol::factories([](PitchAngle89 p, SharedYawRoll y, SharedYawRoll r) { return ViewAngles89{p, y, r}; }),
"pitch",
sol::property([](const ViewAngles89& va) { return va.pitch; },
[](ViewAngles89& va, const PitchAngle89& val) { va.pitch = val; }),
"yaw",
sol::property([](const ViewAngles89& va) { return va.yaw; },
[](ViewAngles89& va, const SharedYawRoll& val) { va.yaw = val; }),
"roll",
sol::property([](const ViewAngles89& va) { return va.roll; },
[](ViewAngles89& va, const SharedYawRoll& val) { va.roll = val; }));
}
void LuaInterpreter::register_engines(sol::table& omath_table)
{
register_engine<OpenGLEngineTraits>(omath_table, "opengl");
register_engine<FrostbiteEngineTraits>(omath_table, "frostbite");
register_engine<IWEngineTraits>(omath_table, "iw");
register_engine<SourceEngineTraits>(omath_table, "source");
register_engine<UnityEngineTraits>(omath_table, "unity");
register_engine<UnrealEngineTraits>(omath_table, "unreal");
register_engine<CryEngineTraits>(omath_table, "cry");
}
} // namespace omath::lua::detail
#endif

View File

@@ -0,0 +1,104 @@
//
// Created by orange on 10.03.2026.
//
#ifdef OMATH_ENABLE_LUA
#include "omath/lua/lua.hpp"
#include <format>
#include <omath/utility/elf_pattern_scan.hpp>
#include <omath/utility/macho_pattern_scan.hpp>
#include <omath/utility/pattern_scan.hpp>
#include <omath/utility/pe_pattern_scan.hpp>
#include <omath/utility/section_scan_result.hpp>
#include <sol/sol.hpp>
#endif
namespace omath::lua
{
void LuaInterpreter::register_pattern_scan(sol::table& omath_table)
{
omath_table.new_usertype<SectionScanResult>(
"SectionScanResult", sol::no_constructor,
"virtual_base_addr",
sol::property([](const SectionScanResult& r) { return r.virtual_base_addr; }),
"raw_base_addr",
sol::property([](const SectionScanResult& r) { return r.raw_base_addr; }),
"target_offset",
sol::property([](const SectionScanResult& r) { return r.target_offset; }),
sol::meta_function::to_string,
[](const SectionScanResult& r)
{
return std::format("SectionScanResult(vbase=0x{:X}, raw_base=0x{:X}, offset={})",
r.virtual_base_addr, r.raw_base_addr, r.target_offset);
});
// Generic scanner: accepts a Lua string as a byte buffer
auto ps_table = omath_table["PatternScanner"].get_or_create<sol::table>();
ps_table["scan"] = [](const std::string& data, const std::string& pattern) -> sol::optional<std::ptrdiff_t>
{
const auto* begin = reinterpret_cast<const std::byte*>(data.data());
const auto* end = begin + data.size();
const auto* result = PatternScanner::scan_for_pattern(begin, end, pattern);
if (result == end)
return sol::nullopt;
return std::distance(begin, result);
};
auto pe_table = omath_table["PePatternScanner"].get_or_create<sol::table>();
pe_table["scan_in_module"] = [](std::uintptr_t base_addr, const std::string& pattern,
sol::optional<std::string> section) -> sol::optional<std::uintptr_t>
{
auto result = PePatternScanner::scan_for_pattern_in_loaded_module(reinterpret_cast<const void*>(base_addr),
pattern, section.value_or(".text"));
if (!result)
return sol::nullopt;
return *result;
};
pe_table["scan_in_file"] = [](const std::string& path, const std::string& pattern,
sol::optional<std::string> section) -> sol::optional<SectionScanResult>
{
auto result = PePatternScanner::scan_for_pattern_in_file(std::filesystem::path(path), pattern,
section.value_or(".text"));
if (!result)
return sol::nullopt;
return *result;
};
auto elf_table = omath_table["ElfPatternScanner"].get_or_create<sol::table>();
elf_table["scan_in_module"] = [](std::uintptr_t base_addr, const std::string& pattern,
sol::optional<std::string> section) -> sol::optional<std::uintptr_t>
{
auto result = ElfPatternScanner::scan_for_pattern_in_loaded_module(reinterpret_cast<const void*>(base_addr),
pattern, section.value_or(".text"));
if (!result)
return sol::nullopt;
return *result;
};
elf_table["scan_in_file"] = [](const std::string& path, const std::string& pattern,
sol::optional<std::string> section) -> sol::optional<SectionScanResult>
{
auto result = ElfPatternScanner::scan_for_pattern_in_file(std::filesystem::path(path), pattern,
section.value_or(".text"));
if (!result)
return sol::nullopt;
return *result;
};
auto macho_table = omath_table["MachOPatternScanner"].get_or_create<sol::table>();
macho_table["scan_in_module"] = [](std::uintptr_t base_addr, const std::string& pattern,
sol::optional<std::string> section) -> sol::optional<std::uintptr_t>
{
auto result = MachOPatternScanner::scan_for_pattern_in_loaded_module(
reinterpret_cast<const void*>(base_addr), pattern, section.value_or("__text"));
if (!result)
return sol::nullopt;
return *result;
};
macho_table["scan_in_file"] = [](const std::string& path, const std::string& pattern,
sol::optional<std::string> section) -> sol::optional<SectionScanResult>
{
auto result = MachOPatternScanner::scan_for_pattern_in_file(std::filesystem::path(path), pattern,
section.value_or("__text"));
if (!result)
return sol::nullopt;
return *result;
};
}
} // namespace omath::lua

View File

@@ -0,0 +1,48 @@
//
// Created by orange on 10.03.2026.
//
#ifdef OMATH_ENABLE_LUA
#include "omath/lua/lua.hpp"
#include <sol/sol.hpp>
#include <omath/linear_algebra/triangle.hpp>
namespace omath::lua
{
void LuaInterpreter::register_triangle(sol::table& omath_table)
{
using Vec3f = omath::Vector3<float>;
using Tri3f = omath::Triangle<Vec3f>;
omath_table.new_usertype<Tri3f>(
"Triangle", sol::constructors<Tri3f(), Tri3f(const Vec3f&, const Vec3f&, const Vec3f&)>(),
"vertex1",
sol::property([](const Tri3f& t) { return t.m_vertex1; },
[](Tri3f& t, const Vec3f& v) { t.m_vertex1 = v; }),
"vertex2",
sol::property([](const Tri3f& t) { return t.m_vertex2; },
[](Tri3f& t, const Vec3f& v) { t.m_vertex2 = v; }),
"vertex3",
sol::property([](const Tri3f& t) { return t.m_vertex3; },
[](Tri3f& t, const Vec3f& v) { t.m_vertex3 = v; }),
"calculate_normal", &Tri3f::calculate_normal,
"side_a_length", &Tri3f::side_a_length,
"side_b_length", &Tri3f::side_b_length,
"side_a_vector", &Tri3f::side_a_vector,
"side_b_vector", &Tri3f::side_b_vector,
"hypot", &Tri3f::hypot,
"is_rectangular", &Tri3f::is_rectangular,
"mid_point", &Tri3f::mid_point,
sol::meta_function::to_string,
[](const Tri3f& t)
{
return std::format("Triangle(({}, {}, {}), ({}, {}, {}), ({}, {}, {}))",
t.m_vertex1.x, t.m_vertex1.y, t.m_vertex1.z,
t.m_vertex2.x, t.m_vertex2.y, t.m_vertex2.z,
t.m_vertex3.x, t.m_vertex3.y, t.m_vertex3.z);
});
}
} // namespace omath::lua
#endif

54
source/lua/lua_vec2.cpp Normal file
View File

@@ -0,0 +1,54 @@
//
// Created by orange on 07.03.2026.
//
#ifdef OMATH_ENABLE_LUA
#include "omath/lua/lua.hpp"
#include <omath/linear_algebra/vector2.hpp>
#include <sol/sol.hpp>
namespace omath::lua
{
void LuaInterpreter::register_vec2(sol::table& omath_table)
{
using Vec2f = omath::Vector2<float>;
omath_table.new_usertype<Vec2f>(
"Vec2", sol::constructors<Vec2f(), Vec2f(float, float)>(),
"x", sol::property([](const Vec2f& v) { return v.x; }, [](Vec2f& v, const float val) { v.x = val; }),
"y", sol::property([](const Vec2f& v) { return v.y; }, [](Vec2f& v, const float val) { v.y = val; }),
sol::meta_function::addition, sol::resolve<Vec2f(const Vec2f&) const>(&Vec2f::operator+),
sol::meta_function::subtraction, sol::resolve<Vec2f(const Vec2f&) const>(&Vec2f::operator-),
sol::meta_function::unary_minus, sol::resolve<Vec2f() const>(&Vec2f::operator-),
sol::meta_function::equal_to, &Vec2f::operator==,
sol::meta_function::less_than, sol::resolve<bool(const Vec2f&) const>(&Vec2f::operator<),
sol::meta_function::less_than_or_equal_to, sol::resolve<bool(const Vec2f&) const>(&Vec2f::operator<=),
sol::meta_function::to_string,
[](const Vec2f& v) { return std::format("Vec2({}, {})", v.x, v.y); },
sol::meta_function::multiplication,
sol::overload(sol::resolve<Vec2f(const float&) const>(&Vec2f::operator*),
[](const float s, const Vec2f& v) { return v * s; }),
sol::meta_function::division,
sol::resolve<Vec2f(const float&) const>(&Vec2f::operator/),
"length", &Vec2f::length,
"length_sqr", &Vec2f::length_sqr,
"normalized", &Vec2f::normalized,
"dot", &Vec2f::dot,
"distance_to", &Vec2f::distance_to,
"distance_to_sqr", &Vec2f::distance_to_sqr,
"sum", &Vec2f::sum,
"abs",
[](const Vec2f& v)
{
Vec2f copy = v;
copy.abs();
return copy;
});
}
} // namespace omath::lua::detail
#endif

81
source/lua/lua_vec3.cpp Normal file
View File

@@ -0,0 +1,81 @@
//
// Created by orange on 07.03.2026.
//
#ifdef OMATH_ENABLE_LUA
#include "omath/lua/lua.hpp"
#include <sol/sol.hpp>
#include <omath/linear_algebra/vector3.hpp>
namespace omath::lua
{
void LuaInterpreter::register_vec3(sol::table& omath_table)
{
using Vec3f = omath::Vector3<float>;
omath_table.new_usertype<Vec3f>(
"Vec3", sol::constructors<Vec3f(), Vec3f(float, float, float)>(),
"x", sol::property([](const Vec3f& v) { return v.x; }, [](Vec3f& v, float val) { v.x = val; }),
"y", sol::property([](const Vec3f& v) { return v.y; }, [](Vec3f& v, float val) { v.y = val; }),
"z", sol::property([](const Vec3f& v) { return v.z; }, [](Vec3f& v, float val) { v.z = val; }),
sol::meta_function::addition, sol::resolve<Vec3f(const Vec3f&) const>(&Vec3f::operator+),
sol::meta_function::subtraction, sol::resolve<Vec3f(const Vec3f&) const>(&Vec3f::operator-),
sol::meta_function::unary_minus, sol::resolve<Vec3f() const>(&Vec3f::operator-),
sol::meta_function::equal_to, &Vec3f::operator==, sol::meta_function::less_than,
sol::resolve<bool(const Vec3f&) const>(&Vec3f::operator<), sol::meta_function::less_than_or_equal_to,
sol::resolve<bool(const Vec3f&) const>(&Vec3f::operator<=), sol::meta_function::to_string,
[](const Vec3f& v) { return std::format("Vec3({}, {}, {})", v.x, v.y, v.z); },
sol::meta_function::multiplication,
sol::overload(sol::resolve<Vec3f(const float&) const>(&Vec3f::operator*),
sol::resolve<Vec3f(const Vec3f&) const>(&Vec3f::operator*),
[](const float s, const Vec3f& v) { return v * s; }),
sol::meta_function::division,
sol::overload(sol::resolve<Vec3f(const float&) const>(&Vec3f::operator/),
sol::resolve<Vec3f(const Vec3f&) const>(&Vec3f::operator/)),
"length", &Vec3f::length, "length_2d", &Vec3f::length_2d, "length_sqr", &Vec3f::length_sqr,
"normalized", &Vec3f::normalized, "dot", &Vec3f::dot, "cross", &Vec3f::cross, "distance_to",
&Vec3f::distance_to, "distance_to_sqr", &Vec3f::distance_to_sqr, "sum",
sol::resolve<float() const>(&Vec3f::sum), "sum_2d", &Vec3f::sum_2d, "point_to_same_direction",
&Vec3f::point_to_same_direction, "as_array", &Vec3f::as_array,
"abs",
[](const Vec3f& v)
{
Vec3f copy = v;
copy.abs();
return copy;
},
"angle_between",
[](const Vec3f& self,
const Vec3f& other) -> std::tuple<sol::optional<float>, sol::optional<std::string>>
{
auto result = self.angle_between(other);
if (result)
return std::make_tuple(sol::optional<float>(result->as_degrees()),
sol::optional<std::string>(sol::nullopt));
return std::make_tuple(sol::optional<float>(sol::nullopt),
sol::optional<std::string>("impossible angle (zero-length vector)"));
},
"is_perpendicular",
[](const Vec3f& self, const Vec3f& other, sol::optional<float> eps)
{ return self.is_perpendicular(other, eps.value_or(0.0001f)); },
"as_table",
[](const Vec3f& v, sol::this_state s) -> sol::table
{
sol::state_view lua(s);
sol::table t = lua.create_table();
t["x"] = v.x;
t["y"] = v.y;
t["z"] = v.z;
return t;
});
}
} // namespace omath::lua::detail
#endif

62
source/lua/lua_vec4.cpp Normal file
View File

@@ -0,0 +1,62 @@
//
// Created by orange on 07.03.2026.
//
#ifdef OMATH_ENABLE_LUA
#include "omath/lua/lua.hpp"
#include <sol/sol.hpp>
#include <omath/linear_algebra/vector4.hpp>
namespace omath::lua
{
void LuaInterpreter::register_vec4(sol::table& omath_table)
{
using Vec4f = omath::Vector4<float>;
omath_table.new_usertype<Vec4f>(
"Vec4", sol::constructors<Vec4f(), Vec4f(float, float, float, float)>(),
"x", sol::property([](const Vec4f& v) { return v.x; }, [](Vec4f& v, float val) { v.x = val; }),
"y", sol::property([](const Vec4f& v) { return v.y; }, [](Vec4f& v, float val) { v.y = val; }),
"z", sol::property([](const Vec4f& v) { return v.z; }, [](Vec4f& v, float val) { v.z = val; }),
"w", sol::property([](const Vec4f& v) { return v.w; }, [](Vec4f& v, float val) { v.w = val; }),
sol::meta_function::addition, sol::resolve<Vec4f(const Vec4f&) const>(&Vec4f::operator+),
sol::meta_function::subtraction, sol::resolve<Vec4f(const Vec4f&) const>(&Vec4f::operator-),
sol::meta_function::unary_minus, sol::resolve<Vec4f() const>(&Vec4f::operator-),
sol::meta_function::equal_to, &Vec4f::operator==,
sol::meta_function::less_than, sol::resolve<bool(const Vec4f&) const>(&Vec4f::operator<),
sol::meta_function::less_than_or_equal_to, sol::resolve<bool(const Vec4f&) const>(&Vec4f::operator<=),
sol::meta_function::to_string,
[](const Vec4f& v) { return std::format("Vec4({}, {}, {}, {})", v.x, v.y, v.z, v.w); },
sol::meta_function::multiplication,
sol::overload(sol::resolve<Vec4f(const float&) const>(&Vec4f::operator*),
sol::resolve<Vec4f(const Vec4f&) const>(&Vec4f::operator*),
[](const float s, const Vec4f& v) { return v * s; }),
sol::meta_function::division,
sol::overload(sol::resolve<Vec4f(const float&) const>(&Vec4f::operator/),
sol::resolve<Vec4f(const Vec4f&) const>(&Vec4f::operator/)),
"length", &Vec4f::length,
"length_sqr", &Vec4f::length_sqr,
"dot", &Vec4f::dot,
"sum", &Vec4f::sum,
"abs",
[](const Vec4f& v)
{
Vec4f copy = v;
copy.abs();
return copy;
},
"clamp",
[](Vec4f& v, float mn, float mx)
{
v.clamp(mn, mx);
return v;
});
}
} // namespace omath::lua::detail
#endif

View File

@@ -4,8 +4,8 @@ project(unit_tests)
include(GoogleTest) include(GoogleTest)
file(GLOB_RECURSE UNIT_TESTS_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp") file(GLOB_RECURSE UNIT_TESTS_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/general/*.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/engines/*.cpp")
add_executable(${PROJECT_NAME} ${UNIT_TESTS_SOURCES}) add_executable(${PROJECT_NAME} ${UNIT_TESTS_SOURCES} main.cpp)
set_target_properties( set_target_properties(
${PROJECT_NAME} ${PROJECT_NAME}
@@ -22,6 +22,16 @@ else() # GTest is being linked as vcpkg package
target_link_libraries(${PROJECT_NAME} PRIVATE GTest::gtest GTest::gtest_main omath::omath) target_link_libraries(${PROJECT_NAME} PRIVATE GTest::gtest GTest::gtest_main omath::omath)
endif() endif()
if (OMATH_ENABLE_LUA)
file(GLOB_RECURSE UNIT_TESTS_SOURCES_LUA CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/lua/*.cpp")
target_compile_definitions(${PROJECT_NAME} PRIVATE LUA_SCRIPTS_DIR="${CMAKE_CURRENT_SOURCE_DIR}/lua")
target_sources(${PROJECT_NAME} PRIVATE ${UNIT_TESTS_SOURCES_LUA})
if (EMSCRIPTEN)
target_link_options(${PROJECT_NAME} PRIVATE
"SHELL:--embed-file ${CMAKE_CURRENT_SOURCE_DIR}/lua@${CMAKE_CURRENT_SOURCE_DIR}/lua")
endif()
endif()
if(OMATH_ENABLE_COVERAGE) if(OMATH_ENABLE_COVERAGE)
include(${CMAKE_SOURCE_DIR}/cmake/Coverage.cmake) include(${CMAKE_SOURCE_DIR}/cmake/Coverage.cmake)
omath_setup_coverage(${PROJECT_NAME}) omath_setup_coverage(${PROJECT_NAME})
@@ -36,3 +46,4 @@ endif()
if(NOT (ANDROID OR IOS OR EMSCRIPTEN)) if(NOT (ANDROID OR IOS OR EMSCRIPTEN))
gtest_discover_tests(${PROJECT_NAME}) gtest_discover_tests(${PROJECT_NAME})
endif() endif()

View File

@@ -0,0 +1,471 @@
//
// Comprehensive EPA tests.
// Covers: all 3 axis directions, multiple depth levels, penetration-vector
// round-trips, depth monotonicity, symmetry, asymmetric sizes, memory
// resource variants, tolerance sensitivity, and iteration bookkeeping.
//
#include <cmath>
#include <gtest/gtest.h>
#include <memory_resource>
#include <omath/collision/epa_algorithm.hpp>
#include <omath/collision/gjk_algorithm.hpp>
#include <omath/engines/source_engine/collider.hpp>
#include <omath/engines/source_engine/mesh.hpp>
using Mesh = omath::source_engine::Mesh;
using Collider = omath::source_engine::MeshCollider;
using Gjk = omath::collision::GjkAlgorithm<Collider>;
using Epa = omath::collision::Epa<Collider>;
using Vec3 = omath::Vector3<float>;
namespace
{
const std::vector<omath::primitives::Vertex<>> k_cube_vbo = {
{ { -1.f, -1.f, -1.f }, {}, {} },
{ { -1.f, -1.f, 1.f }, {}, {} },
{ { -1.f, 1.f, -1.f }, {}, {} },
{ { -1.f, 1.f, 1.f }, {}, {} },
{ { 1.f, 1.f, 1.f }, {}, {} },
{ { 1.f, 1.f, -1.f }, {}, {} },
{ { 1.f, -1.f, 1.f }, {}, {} },
{ { 1.f, -1.f, -1.f }, {}, {} },
};
const std::vector<omath::Vector3<std::uint32_t>> k_empty_ebo{};
constexpr Epa::Params k_default_params{ .max_iterations = 64, .tolerance = 1e-4f };
Collider make_cube(const Vec3& origin = {}, const Vec3& scale = { 1, 1, 1 })
{
Mesh m{ k_cube_vbo, k_empty_ebo, scale };
m.set_origin(origin);
return Collider{ m };
}
// Run GJK then EPA; asserts GJK hit and EPA converged.
Epa::Result solve(const Collider& a, const Collider& b,
const Epa::Params& params = k_default_params)
{
const auto [hit, simplex] = Gjk::is_collide_with_simplex_info(a, b);
EXPECT_TRUE(hit) << "GJK must detect collision before EPA can run";
auto result = Epa::solve(a, b, simplex, params);
EXPECT_TRUE(result.has_value()) << "EPA must converge";
return *result;
}
} // namespace
// ---------------------------------------------------------------------------
// Normal direction per axis
// ---------------------------------------------------------------------------
// For two unit cubes (half-extent 1) with B offset by d along an axis:
// depth = 2 - d (distance from origin to nearest face of Minkowski diff)
// normal component along that axis ≈ ±1
TEST(EpaComprehensive, NormalAlongX_Positive)
{
const auto r = solve(make_cube({ 0, 0, 0 }), make_cube({ 0.5f, 0, 0 }));
EXPECT_NEAR(std::abs(r.normal.x), 1.f, 1e-3f);
EXPECT_NEAR(r.normal.y, 0.f, 1e-3f);
EXPECT_NEAR(r.normal.z, 0.f, 1e-3f);
}
TEST(EpaComprehensive, NormalAlongX_Negative)
{
const auto r = solve(make_cube({ 0, 0, 0 }), make_cube({ -0.5f, 0, 0 }));
EXPECT_NEAR(std::abs(r.normal.x), 1.f, 1e-3f);
EXPECT_NEAR(r.normal.y, 0.f, 1e-3f);
EXPECT_NEAR(r.normal.z, 0.f, 1e-3f);
}
TEST(EpaComprehensive, NormalAlongY_Positive)
{
const auto r = solve(make_cube({ 0, 0, 0 }), make_cube({ 0, 0.5f, 0 }));
EXPECT_NEAR(r.normal.x, 0.f, 1e-3f);
EXPECT_NEAR(std::abs(r.normal.y), 1.f, 1e-3f);
EXPECT_NEAR(r.normal.z, 0.f, 1e-3f);
}
TEST(EpaComprehensive, NormalAlongY_Negative)
{
const auto r = solve(make_cube({ 0, 0, 0 }), make_cube({ 0, -0.5f, 0 }));
EXPECT_NEAR(r.normal.x, 0.f, 1e-3f);
EXPECT_NEAR(std::abs(r.normal.y), 1.f, 1e-3f);
EXPECT_NEAR(r.normal.z, 0.f, 1e-3f);
}
TEST(EpaComprehensive, NormalAlongZ_Positive)
{
const auto r = solve(make_cube({ 0, 0, 0 }), make_cube({ 0, 0, 0.5f }));
EXPECT_NEAR(r.normal.x, 0.f, 1e-3f);
EXPECT_NEAR(r.normal.y, 0.f, 1e-3f);
EXPECT_NEAR(std::abs(r.normal.z), 1.f, 1e-3f);
}
TEST(EpaComprehensive, NormalAlongZ_Negative)
{
const auto r = solve(make_cube({ 0, 0, 0 }), make_cube({ 0, 0, -0.5f }));
EXPECT_NEAR(r.normal.x, 0.f, 1e-3f);
EXPECT_NEAR(r.normal.y, 0.f, 1e-3f);
EXPECT_NEAR(std::abs(r.normal.z), 1.f, 1e-3f);
}
// ---------------------------------------------------------------------------
// Depth correctness (depth = 2 - offset for unit cubes)
// ---------------------------------------------------------------------------
TEST(EpaComprehensive, Depth_ShallowOverlap)
{
// offset 1.9 → depth 0.1
const auto r = solve(make_cube({ 0, 0, 0 }), make_cube({ 1.9f, 0, 0 }));
EXPECT_NEAR(r.depth, 0.1f, 1e-2f);
}
TEST(EpaComprehensive, Depth_QuarterOverlap)
{
// offset 1.5 → depth 0.5
const auto r = solve(make_cube({ 0, 0, 0 }), make_cube({ 1.5f, 0, 0 }));
EXPECT_NEAR(r.depth, 0.5f, 1e-2f);
}
TEST(EpaComprehensive, Depth_HalfOverlap)
{
// offset 1.0 → depth 1.0
const auto r = solve(make_cube({ 0, 0, 0 }), make_cube({ 1.0f, 0, 0 }));
EXPECT_NEAR(r.depth, 1.0f, 1e-2f);
}
TEST(EpaComprehensive, Depth_ThreeQuarterOverlap)
{
// offset 0.5 → depth 1.5
const auto r = solve(make_cube({ 0, 0, 0 }), make_cube({ 0.5f, 0, 0 }));
EXPECT_NEAR(r.depth, 1.5f, 1e-2f);
}
TEST(EpaComprehensive, Depth_AlongY_HalfOverlap)
{
const auto r = solve(make_cube({ 0, 0, 0 }), make_cube({ 0, 1.0f, 0 }));
EXPECT_NEAR(r.depth, 1.0f, 1e-2f);
}
TEST(EpaComprehensive, Depth_AlongZ_HalfOverlap)
{
const auto r = solve(make_cube({ 0, 0, 0 }), make_cube({ 0, 0, 1.0f }));
EXPECT_NEAR(r.depth, 1.0f, 1e-2f);
}
// ---------------------------------------------------------------------------
// Depth monotonicity — deeper overlap → larger depth
// ---------------------------------------------------------------------------
TEST(EpaComprehensive, DepthMonotonic_AlongX)
{
const float d1 = solve(make_cube({ 0, 0, 0 }), make_cube({ 1.9f, 0, 0 })).depth; // ~0.1
const float d2 = solve(make_cube({ 0, 0, 0 }), make_cube({ 1.5f, 0, 0 })).depth; // ~0.5
const float d3 = solve(make_cube({ 0, 0, 0 }), make_cube({ 1.0f, 0, 0 })).depth; // ~1.0
const float d4 = solve(make_cube({ 0, 0, 0 }), make_cube({ 0.5f, 0, 0 })).depth; // ~1.5
EXPECT_LT(d1, d2);
EXPECT_LT(d2, d3);
EXPECT_LT(d3, d4);
}
// ---------------------------------------------------------------------------
// Normal is a unit vector
// ---------------------------------------------------------------------------
TEST(EpaComprehensive, NormalIsUnit_AlongX)
{
const auto r = solve(make_cube({ 0, 0, 0 }), make_cube({ 0.5f, 0, 0 }));
EXPECT_NEAR(r.normal.dot(r.normal), 1.f, 1e-5f);
}
TEST(EpaComprehensive, NormalIsUnit_AlongY)
{
const auto r = solve(make_cube({ 0, 0, 0 }), make_cube({ 0, 1.2f, 0 }));
EXPECT_NEAR(r.normal.dot(r.normal), 1.f, 1e-5f);
}
TEST(EpaComprehensive, NormalIsUnit_AlongZ)
{
const auto r = solve(make_cube({ 0, 0, 0 }), make_cube({ 0, 0, 0.8f }));
EXPECT_NEAR(r.normal.dot(r.normal), 1.f, 1e-5f);
}
// ---------------------------------------------------------------------------
// Penetration vector = normal * depth
// ---------------------------------------------------------------------------
TEST(EpaComprehensive, PenetrationVectorLength_EqualsDepth)
{
const auto r = solve(make_cube({ 0, 0, 0 }), make_cube({ 0.5f, 0, 0 }));
const float pen_len = std::sqrt(r.penetration_vector.dot(r.penetration_vector));
EXPECT_NEAR(pen_len, r.depth, 1e-5f);
}
TEST(EpaComprehensive, PenetrationVectorDirection_ParallelToNormal)
{
const auto r = solve(make_cube({ 0, 0, 0 }), make_cube({ 0, 1.0f, 0 }));
// penetration_vector = normal * depth → cross product must be ~zero
const auto cross = r.penetration_vector.cross(r.normal);
EXPECT_NEAR(cross.dot(cross), 0.f, 1e-8f);
}
// ---------------------------------------------------------------------------
// Round-trip: applying penetration_vector separates the shapes
// ---------------------------------------------------------------------------
TEST(EpaComprehensive, RoundTrip_AlongX)
{
const auto a = make_cube({ 0, 0, 0 });
Mesh mesh_b{ k_cube_vbo, k_empty_ebo };
mesh_b.set_origin({ 0.5f, 0, 0 });
const auto b = Collider{ mesh_b };
const auto r = solve(a, b);
constexpr float margin = 1.f + 1e-3f;
// Move B along the penetration vector; it should separate from A
Mesh mesh_sep{ k_cube_vbo, k_empty_ebo };
mesh_sep.set_origin(mesh_b.get_origin() + r.penetration_vector * margin);
EXPECT_FALSE(Gjk::is_collide(a, Collider{ mesh_sep })) << "Applying pen vector must separate";
// Moving the wrong way must still collide
Mesh mesh_wrong{ k_cube_vbo, k_empty_ebo };
mesh_wrong.set_origin(mesh_b.get_origin() - r.penetration_vector * margin);
EXPECT_TRUE(Gjk::is_collide(a, Collider{ mesh_wrong })) << "Opposite direction must still collide";
}
TEST(EpaComprehensive, RoundTrip_AlongY)
{
const auto a = make_cube({ 0, 0, 0 });
Mesh mesh_b{ k_cube_vbo, k_empty_ebo };
mesh_b.set_origin({ 0, 0.8f, 0 });
const auto b = Collider{ mesh_b };
const auto r = solve(a, b);
constexpr float margin = 1.f + 1e-3f;
Mesh mesh_sep{ k_cube_vbo, k_empty_ebo };
mesh_sep.set_origin(mesh_b.get_origin() + r.penetration_vector * margin);
EXPECT_FALSE(Gjk::is_collide(a, Collider{ mesh_sep }));
Mesh mesh_wrong{ k_cube_vbo, k_empty_ebo };
mesh_wrong.set_origin(mesh_b.get_origin() - r.penetration_vector * margin);
EXPECT_TRUE(Gjk::is_collide(a, Collider{ mesh_wrong }));
}
TEST(EpaComprehensive, RoundTrip_AlongZ)
{
const auto a = make_cube({ 0, 0, 0 });
Mesh mesh_b{ k_cube_vbo, k_empty_ebo };
mesh_b.set_origin({ 0, 0, 1.2f });
const auto b = Collider{ mesh_b };
const auto r = solve(a, b);
constexpr float margin = 1.f + 1e-3f;
Mesh mesh_sep{ k_cube_vbo, k_empty_ebo };
mesh_sep.set_origin(mesh_b.get_origin() + r.penetration_vector * margin);
EXPECT_FALSE(Gjk::is_collide(a, Collider{ mesh_sep }));
}
// ---------------------------------------------------------------------------
// Symmetry — swapping A and B preserves depth
// ---------------------------------------------------------------------------
TEST(EpaComprehensive, Symmetry_DepthIsIndependentOfOrder)
{
const auto a = make_cube({ 0, 0, 0 });
const auto b = make_cube({ 0.5f, 0, 0 });
const float depth_ab = solve(a, b).depth;
const float depth_ba = solve(b, a).depth;
EXPECT_NEAR(depth_ab, depth_ba, 1e-2f);
}
TEST(EpaComprehensive, Symmetry_NormalsAreOpposite)
{
const auto a = make_cube({ 0, 0, 0 });
const auto b = make_cube({ 0.5f, 0, 0 });
const Vec3 n_ab = solve(a, b).normal;
const Vec3 n_ba = solve(b, a).normal;
// The normals should be anti-parallel: n_ab · n_ba ≈ -1
EXPECT_NEAR(n_ab.dot(n_ba), -1.f, 1e-3f);
}
// ---------------------------------------------------------------------------
// Asymmetric sizes
// ---------------------------------------------------------------------------
TEST(EpaComprehensive, LargeVsSmall_DepthCorrect)
{
// Big (half-ext 2) at origin, small (half-ext 0.5) at (2.0, 0, 0)
// Minkowski diff closest face in X at distance 0.5
const auto r = solve(make_cube({ 0, 0, 0 }, { 2, 2, 2 }), make_cube({ 2.0f, 0, 0 }, { 0.5f, 0.5f, 0.5f }));
EXPECT_NEAR(r.depth, 0.5f, 1e-2f);
EXPECT_NEAR(std::abs(r.normal.x), 1.f, 1e-3f);
}
TEST(EpaComprehensive, LargeVsSmall_RoundTrip)
{
const auto a = make_cube({ 0, 0, 0 }, { 2, 2, 2 });
Mesh mesh_b{ k_cube_vbo, k_empty_ebo, { 0.5f, 0.5f, 0.5f } };
mesh_b.set_origin({ 2.0f, 0, 0 });
const auto b = Collider{ mesh_b };
const auto r = solve(a, b);
constexpr float margin = 1.f + 1e-3f;
Mesh mesh_sep{ k_cube_vbo, k_empty_ebo, { 0.5f, 0.5f, 0.5f } };
mesh_sep.set_origin(mesh_b.get_origin() + r.penetration_vector * margin);
EXPECT_FALSE(Gjk::is_collide(a, Collider{ mesh_sep }));
}
// ---------------------------------------------------------------------------
// Memory resource variants
// ---------------------------------------------------------------------------
TEST(EpaComprehensive, MonotonicBuffer_ConvergesCorrectly)
{
const auto a = make_cube({ 0, 0, 0 });
const auto b = make_cube({ 0.5f, 0, 0 });
const auto [hit, simplex] = Gjk::is_collide_with_simplex_info(a, b);
ASSERT_TRUE(hit);
constexpr std::size_t k_buf = 32768;
alignas(std::max_align_t) char buf[k_buf];
std::pmr::monotonic_buffer_resource mr{ buf, k_buf, std::pmr::null_memory_resource() };
const auto r = Epa::solve(a, b, simplex, k_default_params, mr);
ASSERT_TRUE(r.has_value());
EXPECT_NEAR(r->depth, 1.5f, 1e-2f);
}
TEST(EpaComprehensive, MonotonicBuffer_MultipleReleaseCycles)
{
// Verify mr.release() correctly resets the buffer across multiple calls
const auto a = make_cube({ 0, 0, 0 });
const auto b = make_cube({ 0.5f, 0, 0 });
const auto [hit, simplex] = Gjk::is_collide_with_simplex_info(a, b);
ASSERT_TRUE(hit);
constexpr std::size_t k_buf = 32768;
alignas(std::max_align_t) char buf[k_buf];
std::pmr::monotonic_buffer_resource mr{ buf, k_buf, std::pmr::null_memory_resource() };
float first_depth = 0.f;
for (int i = 0; i < 5; ++i)
{
mr.release();
const auto r = Epa::solve(a, b, simplex, k_default_params, mr);
ASSERT_TRUE(r.has_value()) << "solve must converge on iteration " << i;
if (i == 0)
first_depth = r->depth;
else
EXPECT_NEAR(r->depth, first_depth, 1e-6f) << "depth must be deterministic";
}
}
TEST(EpaComprehensive, DefaultResource_ConvergesCorrectly)
{
const auto a = make_cube({ 0, 0, 0 });
const auto b = make_cube({ 1.0f, 0, 0 });
const auto [hit, simplex] = Gjk::is_collide_with_simplex_info(a, b);
ASSERT_TRUE(hit);
const auto r = Epa::solve(a, b, simplex);
ASSERT_TRUE(r.has_value());
EXPECT_NEAR(r->depth, 1.0f, 1e-2f);
}
// ---------------------------------------------------------------------------
// Tolerance sensitivity
// ---------------------------------------------------------------------------
TEST(EpaComprehensive, TighterTolerance_MoreAccurateDepth)
{
const auto a = make_cube({ 0, 0, 0 });
const auto b = make_cube({ 1.0f, 0, 0 });
const auto [hit, simplex] = Gjk::is_collide_with_simplex_info(a, b);
ASSERT_TRUE(hit);
const Epa::Params loose{ .max_iterations = 64, .tolerance = 1e-2f };
const Epa::Params tight{ .max_iterations = 64, .tolerance = 1e-5f };
const auto r_loose = Epa::solve(a, b, simplex, loose);
const auto r_tight = Epa::solve(a, b, simplex, tight);
ASSERT_TRUE(r_loose.has_value());
ASSERT_TRUE(r_tight.has_value());
// Tighter tolerance must yield a result at least as accurate
EXPECT_LE(std::abs(r_tight->depth - 1.0f), std::abs(r_loose->depth - 1.0f) + 1e-4f);
}
// ---------------------------------------------------------------------------
// Bookkeeping fields
// ---------------------------------------------------------------------------
TEST(EpaComprehensive, Bookkeeping_IterationsInBounds)
{
const auto a = make_cube({ 0, 0, 0 });
const auto b = make_cube({ 0.5f, 0, 0 });
const auto r = solve(a, b);
EXPECT_GT(r.iterations, 0);
EXPECT_LE(r.iterations, k_default_params.max_iterations);
}
TEST(EpaComprehensive, Bookkeeping_FacesAndVerticesGrow)
{
const auto a = make_cube({ 0, 0, 0 });
const auto b = make_cube({ 0.5f, 0, 0 });
const auto r = solve(a, b);
// Started with a tetrahedron (4 faces, 4 vertices); EPA must have expanded it
EXPECT_GE(r.num_faces, 4);
EXPECT_GE(r.num_vertices, 4);
}
TEST(EpaComprehensive, Bookkeeping_MaxIterationsRespected)
{
const auto a = make_cube({ 0, 0, 0 });
const auto b = make_cube({ 0.5f, 0, 0 });
const auto [hit, simplex] = Gjk::is_collide_with_simplex_info(a, b);
ASSERT_TRUE(hit);
constexpr Epa::Params tight{ .max_iterations = 3, .tolerance = 1e-10f };
const auto r = Epa::solve(a, b, simplex, tight);
// Must return something (fallback best-face path) and respect the cap
if (r.has_value())
EXPECT_LE(r->iterations, tight.max_iterations);
}
// ---------------------------------------------------------------------------
// Determinism
// ---------------------------------------------------------------------------
TEST(EpaComprehensive, Deterministic_SameResultOnRepeatedCalls)
{
const auto a = make_cube({ 0, 0, 0 });
const auto b = make_cube({ 0.7f, 0, 0 });
const auto [hit, simplex] = Gjk::is_collide_with_simplex_info(a, b);
ASSERT_TRUE(hit);
const auto first = Epa::solve(a, b, simplex);
ASSERT_TRUE(first.has_value());
for (int i = 0; i < 5; ++i)
{
const auto r = Epa::solve(a, b, simplex);
ASSERT_TRUE(r.has_value());
EXPECT_NEAR(r->depth, first->depth, 1e-6f);
EXPECT_NEAR(r->normal.x, first->normal.x, 1e-6f);
EXPECT_NEAR(r->normal.y, first->normal.y, 1e-6f);
EXPECT_NEAR(r->normal.z, first->normal.z, 1e-6f);
}
}

96
tests/lua/color_tests.lua Normal file
View File

@@ -0,0 +1,96 @@
local function approx(a, b, eps) return math.abs(a - b) < (eps or 1e-4) end
function Color_Constructor_float()
local c = omath.Color.new(1, 0.5, 0.25, 1)
assert(approx(c.r, 1) and approx(c.g, 0.5) and approx(c.b, 0.25) and approx(c.a, 1))
end
function Color_Constructor_default()
local c = omath.Color.new()
assert(c ~= nil)
end
function Color_Constructor_clamping()
local c = omath.Color.new(2, -1, 0.5, 1)
assert(approx(c.r, 1) and approx(c.g, 0) and approx(c.b, 0.5))
end
function Color_from_rgba()
local c = omath.Color.from_rgba(255, 128, 0, 255)
assert(approx(c.r, 1) and approx(c.g, 128/255) and approx(c.b, 0) and approx(c.a, 1))
end
function Color_from_hsv_components()
local c = omath.Color.from_hsv(0, 1, 1)
assert(approx(c.r, 1) and approx(c.g, 0) and approx(c.b, 0))
end
function Color_from_hsv_struct()
local hsv = omath.Hsv.new()
hsv.hue = 0
hsv.saturation = 1
hsv.value = 1
local c = omath.Color.from_hsv(hsv)
assert(approx(c.r, 1) and approx(c.g, 0) and approx(c.b, 0))
end
function Color_red()
local c = omath.Color.red()
assert(approx(c.r, 1) and approx(c.g, 0) and approx(c.b, 0) and approx(c.a, 1))
end
function Color_green()
local c = omath.Color.green()
assert(approx(c.r, 0) and approx(c.g, 1) and approx(c.b, 0) and approx(c.a, 1))
end
function Color_blue()
local c = omath.Color.blue()
assert(approx(c.r, 0) and approx(c.g, 0) and approx(c.b, 1) and approx(c.a, 1))
end
function Color_to_hsv()
local hsv = omath.Color.red():to_hsv()
assert(approx(hsv.hue, 0) and approx(hsv.saturation, 1) and approx(hsv.value, 1))
end
function Color_set_hue()
local c = omath.Color.red()
c:set_hue(1/3)
assert(approx(c.g, 1, 1e-3))
end
function Color_set_saturation()
local c = omath.Color.red()
c:set_saturation(0)
assert(approx(c.r, c.g) and approx(c.g, c.b))
end
function Color_set_value()
local c = omath.Color.red()
c:set_value(0)
assert(approx(c.r, 0) and approx(c.g, 0) and approx(c.b, 0))
end
function Color_blend()
local c = omath.Color.red():blend(omath.Color.blue(), 0.5)
assert(approx(c.r, 0.5) and approx(c.b, 0.5))
end
function Color_blend_clamped_ratio()
local c = omath.Color.red():blend(omath.Color.blue(), 2.0)
assert(approx(c.r, 0) and approx(c.b, 1))
end
function Color_to_string()
local s = tostring(omath.Color.red())
assert(s == "[r:255, g:0, b:0, a:255]")
end
function Hsv_fields()
local hsv = omath.Hsv.new()
hsv.hue = 0.5
hsv.saturation = 0.8
hsv.value = 0.9
assert(approx(hsv.hue, 0.5) and approx(hsv.saturation, 0.8) and approx(hsv.value, 0.9))
end

View File

@@ -0,0 +1,197 @@
local function approx(a, b, eps) return math.abs(a - b) < (eps or 1e-4) end
local function make_camera()
local pos = omath.Vec3.new(0, 0, 0)
local pitch = omath.source.PitchAngle.from_degrees(0)
local yaw = omath.source.YawAngle.from_degrees(0)
local roll = omath.source.RollAngle.from_degrees(0)
local angles = omath.source.ViewAngles.new(pitch, yaw, roll)
local vp = omath.opengl.ViewPort.new(1920, 1080)
local fov = omath.source.FieldOfView.from_degrees(90)
return omath.source.Camera.new(pos, angles, vp, fov, 0.1, 1000)
end
-- PitchAngle
function Source_PitchAngle_from_degrees()
assert(omath.source.PitchAngle.from_degrees(45):as_degrees() == 45)
end
function Source_PitchAngle_clamping_max()
assert(omath.source.PitchAngle.from_degrees(100):as_degrees() == 89)
end
function Source_PitchAngle_clamping_min()
assert(omath.source.PitchAngle.from_degrees(-100):as_degrees() == -89)
end
function Source_PitchAngle_from_radians()
assert(approx(omath.source.PitchAngle.from_radians(math.pi / 4):as_degrees(), 45))
end
function Source_PitchAngle_as_radians()
assert(approx(omath.source.PitchAngle.from_degrees(0):as_radians(), 0))
end
function Source_PitchAngle_sin()
assert(approx(omath.source.PitchAngle.from_degrees(30):sin(), 0.5))
end
function Source_PitchAngle_cos()
assert(approx(omath.source.PitchAngle.from_degrees(60):cos(), 0.5))
end
function Source_PitchAngle_tan()
assert(approx(omath.source.PitchAngle.from_degrees(45):tan(), 1.0))
end
function Source_PitchAngle_addition()
local c = omath.source.PitchAngle.from_degrees(20) + omath.source.PitchAngle.from_degrees(15)
assert(c:as_degrees() == 35)
end
function Source_PitchAngle_addition_clamped()
local c = omath.source.PitchAngle.from_degrees(80) + omath.source.PitchAngle.from_degrees(20)
assert(c:as_degrees() == 89)
end
function Source_PitchAngle_subtraction()
local c = omath.source.PitchAngle.from_degrees(50) - omath.source.PitchAngle.from_degrees(20)
assert(c:as_degrees() == 30)
end
function Source_PitchAngle_unary_minus()
assert((-omath.source.PitchAngle.from_degrees(45)):as_degrees() == -45)
end
function Source_PitchAngle_equal_to()
local a = omath.source.PitchAngle.from_degrees(45)
assert(a == omath.source.PitchAngle.from_degrees(45))
assert(not (a == omath.source.PitchAngle.from_degrees(30)))
end
function Source_PitchAngle_to_string()
assert(tostring(omath.source.PitchAngle.from_degrees(45)) == "45deg")
end
-- YawAngle
function Source_YawAngle_from_degrees()
assert(omath.source.YawAngle.from_degrees(90):as_degrees() == 90)
end
function Source_YawAngle_normalization()
assert(approx(omath.source.YawAngle.from_degrees(200):as_degrees(), -160))
end
-- RollAngle
function Source_RollAngle_from_degrees()
assert(omath.source.RollAngle.from_degrees(45):as_degrees() == 45)
end
-- FieldOfView
function Source_FieldOfView_from_degrees()
assert(omath.source.FieldOfView.from_degrees(90):as_degrees() == 90)
end
function Source_FieldOfView_clamping()
assert(omath.source.FieldOfView.from_degrees(200):as_degrees() == 180)
end
-- ViewAngles
function Source_ViewAngles_new()
local angles = omath.source.ViewAngles.new(
omath.source.PitchAngle.from_degrees(30),
omath.source.YawAngle.from_degrees(90),
omath.source.RollAngle.from_degrees(0))
assert(angles.pitch:as_degrees() == 30)
assert(angles.yaw:as_degrees() == 90)
assert(angles.roll:as_degrees() == 0)
end
function Source_ViewAngles_field_mutation()
local angles = omath.source.ViewAngles.new(
omath.source.PitchAngle.from_degrees(0),
omath.source.YawAngle.from_degrees(0),
omath.source.RollAngle.from_degrees(0))
angles.pitch = omath.source.PitchAngle.from_degrees(45)
assert(angles.pitch:as_degrees() == 45)
end
-- Camera
function Source_Camera_constructor()
assert(make_camera() ~= nil)
end
function Source_Camera_get_set_origin()
local cam = make_camera()
cam:set_origin(omath.Vec3.new(1, 2, 3))
local o = cam:get_origin()
assert(approx(o.x, 1) and approx(o.y, 2) and approx(o.z, 3))
end
function Source_Camera_get_set_near_plane()
local cam = make_camera()
cam:set_near_plane(0.5)
assert(approx(cam:get_near_plane(), 0.5))
end
function Source_Camera_get_set_far_plane()
local cam = make_camera()
cam:set_far_plane(500)
assert(approx(cam:get_far_plane(), 500))
end
function Source_Camera_get_set_fov()
local cam = make_camera()
cam:set_field_of_view(omath.source.FieldOfView.from_degrees(60))
assert(approx(cam:get_field_of_view():as_degrees(), 60))
end
function Source_Camera_get_set_view_angles()
local cam = make_camera()
cam:set_view_angles(omath.source.ViewAngles.new(
omath.source.PitchAngle.from_degrees(30),
omath.source.YawAngle.from_degrees(90),
omath.source.RollAngle.from_degrees(0)))
assert(approx(cam:get_view_angles().pitch:as_degrees(), 30))
assert(approx(cam:get_view_angles().yaw:as_degrees(), 90))
end
function Source_Camera_look_at()
local cam = make_camera()
cam:look_at(omath.Vec3.new(10, 0, 0))
assert(cam:get_view_angles() ~= nil)
end
function Source_Camera_get_forward()
local fwd = make_camera():get_forward()
assert(approx(fwd:length(), 1.0))
end
function Source_Camera_get_right()
assert(approx(make_camera():get_right():length(), 1.0))
end
function Source_Camera_get_up()
assert(approx(make_camera():get_up():length(), 1.0))
end
function Source_Camera_world_to_screen_success()
local cam = make_camera()
cam:look_at(omath.Vec3.new(1, 0, 0))
local screen, err = cam:world_to_screen(omath.Vec3.new(5, 0, 0))
assert(screen ~= nil, "expected screen pos, got: " .. tostring(err))
end
function Source_Camera_world_to_screen_error()
local cam = make_camera()
cam:look_at(omath.Vec3.new(1, 0, 0))
local screen, err = cam:world_to_screen(omath.Vec3.new(-100, 0, 0))
assert(screen == nil and err ~= nil)
end
function Source_Camera_screen_to_world()
local cam = make_camera()
cam:look_at(omath.Vec3.new(1, 0, 0))
local world, err = cam:screen_to_world(omath.Vec3.new(960, 540, 1))
assert(world ~= nil, "expected world pos, got: " .. tostring(err))
end

View File

@@ -0,0 +1,82 @@
local function approx(a, b, eps) return math.abs(a - b) < (eps or 1e-5) end
function Triangle_Constructor_default()
local t = omath.Triangle.new()
assert(t.vertex1.x == 0 and t.vertex1.y == 0 and t.vertex1.z == 0)
assert(t.vertex2.x == 0 and t.vertex2.y == 0 and t.vertex2.z == 0)
assert(t.vertex3.x == 0 and t.vertex3.y == 0 and t.vertex3.z == 0)
end
function Triangle_Constructor_vertices()
local v1 = omath.Vec3.new(1, 0, 0)
local v2 = omath.Vec3.new(0, 1, 0)
local v3 = omath.Vec3.new(0, 0, 1)
local t = omath.Triangle.new(v1, v2, v3)
assert(t.vertex1.x == 1 and t.vertex1.y == 0 and t.vertex1.z == 0)
assert(t.vertex2.x == 0 and t.vertex2.y == 1 and t.vertex2.z == 0)
assert(t.vertex3.x == 0 and t.vertex3.y == 0 and t.vertex3.z == 1)
end
function Triangle_Vertex_mutation()
local t = omath.Triangle.new()
t.vertex1 = omath.Vec3.new(5, 6, 7)
assert(t.vertex1.x == 5 and t.vertex1.y == 6 and t.vertex1.z == 7)
end
-- Right triangle: v1=(0,3,0), v2=(0,0,0), v3=(4,0,0) — sides 3, 4, hypot 5
function Triangle_SideALength()
local t = omath.Triangle.new(omath.Vec3.new(0, 3, 0), omath.Vec3.new(0, 0, 0), omath.Vec3.new(4, 0, 0))
assert(approx(t:side_a_length(), 3.0))
end
function Triangle_SideBLength()
local t = omath.Triangle.new(omath.Vec3.new(0, 3, 0), omath.Vec3.new(0, 0, 0), omath.Vec3.new(4, 0, 0))
assert(approx(t:side_b_length(), 4.0))
end
function Triangle_Hypot()
local t = omath.Triangle.new(omath.Vec3.new(0, 3, 0), omath.Vec3.new(0, 0, 0), omath.Vec3.new(4, 0, 0))
assert(approx(t:hypot(), 5.0))
end
function Triangle_SideAVector()
local t = omath.Triangle.new(omath.Vec3.new(0, 3, 0), omath.Vec3.new(0, 0, 0), omath.Vec3.new(4, 0, 0))
local a = t:side_a_vector()
assert(approx(a.x, 0) and approx(a.y, 3) and approx(a.z, 0))
end
function Triangle_SideBVector()
local t = omath.Triangle.new(omath.Vec3.new(0, 3, 0), omath.Vec3.new(0, 0, 0), omath.Vec3.new(4, 0, 0))
local b = t:side_b_vector()
assert(approx(b.x, 4) and approx(b.y, 0) and approx(b.z, 0))
end
function Triangle_IsRectangular_true()
local t = omath.Triangle.new(omath.Vec3.new(0, 3, 0), omath.Vec3.new(0, 0, 0), omath.Vec3.new(4, 0, 0))
assert(t:is_rectangular() == true)
end
function Triangle_IsRectangular_false()
-- equilateral-ish triangle, not rectangular
local t = omath.Triangle.new(omath.Vec3.new(0, 1, 0), omath.Vec3.new(-1, 0, 0), omath.Vec3.new(1, 0, 0))
assert(t:is_rectangular() == false)
end
function Triangle_MidPoint()
local t = omath.Triangle.new(omath.Vec3.new(3, 0, 0), omath.Vec3.new(0, 3, 0), omath.Vec3.new(0, 0, 3))
local m = t:mid_point()
assert(approx(m.x, 1.0) and approx(m.y, 1.0) and approx(m.z, 1.0))
end
function Triangle_CalculateNormal()
-- flat triangle in XY plane — normal should be (0, 0, 1)
local t = omath.Triangle.new(omath.Vec3.new(0, 1, 0), omath.Vec3.new(0, 0, 0), omath.Vec3.new(1, 0, 0))
local n = t:calculate_normal()
assert(approx(n.x, 0) and approx(n.y, 0) and approx(n.z, 1))
end
function Triangle_ToString()
local t = omath.Triangle.new(omath.Vec3.new(1, 0, 0), omath.Vec3.new(0, 1, 0), omath.Vec3.new(0, 0, 1))
local s = tostring(t)
assert(s == "Triangle((1, 0, 0), (0, 1, 0), (0, 0, 1))")
end

View File

@@ -0,0 +1,51 @@
//
// Created by orange on 08.03.2026.
//
#include <gtest/gtest.h>
#include <lua.hpp>
#include <omath/lua/lua.hpp>
class LuaColor : 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 "/color_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(LuaColor, Constructor_float) { check("Color_Constructor_float"); }
TEST_F(LuaColor, Constructor_default) { check("Color_Constructor_default"); }
TEST_F(LuaColor, Constructor_clamping) { check("Color_Constructor_clamping"); }
TEST_F(LuaColor, from_rgba) { check("Color_from_rgba"); }
TEST_F(LuaColor, from_hsv_components) { check("Color_from_hsv_components"); }
TEST_F(LuaColor, from_hsv_struct) { check("Color_from_hsv_struct"); }
TEST_F(LuaColor, red) { check("Color_red"); }
TEST_F(LuaColor, green) { check("Color_green"); }
TEST_F(LuaColor, blue) { check("Color_blue"); }
TEST_F(LuaColor, to_hsv) { check("Color_to_hsv"); }
TEST_F(LuaColor, set_hue) { check("Color_set_hue"); }
TEST_F(LuaColor, set_saturation) { check("Color_set_saturation"); }
TEST_F(LuaColor, set_value) { check("Color_set_value"); }
TEST_F(LuaColor, blend) { check("Color_blend"); }
TEST_F(LuaColor, blend_clamped_ratio) { check("Color_blend_clamped_ratio"); }
TEST_F(LuaColor, to_string) { check("Color_to_string"); }
TEST_F(LuaColor, Hsv_fields) { check("Hsv_fields"); }

View File

@@ -0,0 +1,79 @@
//
// Created by orange on 07.03.2026.
//
#include <gtest/gtest.h>
#include <lua.hpp>
#include <omath/lua/lua.hpp>
class LuaSourceEngine : 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 "/source_engine_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);
}
}
};
// PitchAngle
TEST_F(LuaSourceEngine, PitchAngle_from_degrees) { check("Source_PitchAngle_from_degrees"); }
TEST_F(LuaSourceEngine, PitchAngle_clamping_max) { check("Source_PitchAngle_clamping_max"); }
TEST_F(LuaSourceEngine, PitchAngle_clamping_min) { check("Source_PitchAngle_clamping_min"); }
TEST_F(LuaSourceEngine, PitchAngle_from_radians) { check("Source_PitchAngle_from_radians"); }
TEST_F(LuaSourceEngine, PitchAngle_as_radians) { check("Source_PitchAngle_as_radians"); }
TEST_F(LuaSourceEngine, PitchAngle_sin) { check("Source_PitchAngle_sin"); }
TEST_F(LuaSourceEngine, PitchAngle_cos) { check("Source_PitchAngle_cos"); }
TEST_F(LuaSourceEngine, PitchAngle_tan) { check("Source_PitchAngle_tan"); }
TEST_F(LuaSourceEngine, PitchAngle_addition) { check("Source_PitchAngle_addition"); }
TEST_F(LuaSourceEngine, PitchAngle_addition_clamped) { check("Source_PitchAngle_addition_clamped"); }
TEST_F(LuaSourceEngine, PitchAngle_subtraction) { check("Source_PitchAngle_subtraction"); }
TEST_F(LuaSourceEngine, PitchAngle_unary_minus) { check("Source_PitchAngle_unary_minus"); }
TEST_F(LuaSourceEngine, PitchAngle_equal_to) { check("Source_PitchAngle_equal_to"); }
TEST_F(LuaSourceEngine, PitchAngle_to_string) { check("Source_PitchAngle_to_string"); }
// YawAngle
TEST_F(LuaSourceEngine, YawAngle_from_degrees) { check("Source_YawAngle_from_degrees"); }
TEST_F(LuaSourceEngine, YawAngle_normalization) { check("Source_YawAngle_normalization"); }
// RollAngle
TEST_F(LuaSourceEngine, RollAngle_from_degrees) { check("Source_RollAngle_from_degrees"); }
// FieldOfView
TEST_F(LuaSourceEngine, FieldOfView_from_degrees) { check("Source_FieldOfView_from_degrees"); }
TEST_F(LuaSourceEngine, FieldOfView_clamping) { check("Source_FieldOfView_clamping"); }
// ViewAngles
TEST_F(LuaSourceEngine, ViewAngles_new) { check("Source_ViewAngles_new"); }
TEST_F(LuaSourceEngine, ViewAngles_field_mutation) { check("Source_ViewAngles_field_mutation"); }
// Camera
TEST_F(LuaSourceEngine, Camera_constructor) { check("Source_Camera_constructor"); }
TEST_F(LuaSourceEngine, Camera_get_set_origin) { check("Source_Camera_get_set_origin"); }
TEST_F(LuaSourceEngine, Camera_get_set_near_plane) { check("Source_Camera_get_set_near_plane"); }
TEST_F(LuaSourceEngine, Camera_get_set_far_plane) { check("Source_Camera_get_set_far_plane"); }
TEST_F(LuaSourceEngine, Camera_get_set_fov) { check("Source_Camera_get_set_fov"); }
TEST_F(LuaSourceEngine, Camera_get_set_view_angles) { check("Source_Camera_get_set_view_angles"); }
TEST_F(LuaSourceEngine, Camera_look_at) { check("Source_Camera_look_at"); }
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_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"); }

View File

@@ -0,0 +1,47 @@
//
// Created by orange on 10.03.2026.
//
#include <gtest/gtest.h>
#include <lua.hpp>
#include <omath/lua/lua.hpp>
class LuaTriangle : 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 "/triangle_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(LuaTriangle, Constructor_default) { check("Triangle_Constructor_default"); }
TEST_F(LuaTriangle, Constructor_vertices) { check("Triangle_Constructor_vertices"); }
TEST_F(LuaTriangle, Vertex_mutation) { check("Triangle_Vertex_mutation"); }
TEST_F(LuaTriangle, SideALength) { check("Triangle_SideALength"); }
TEST_F(LuaTriangle, SideBLength) { check("Triangle_SideBLength"); }
TEST_F(LuaTriangle, Hypot) { check("Triangle_Hypot"); }
TEST_F(LuaTriangle, SideAVector) { check("Triangle_SideAVector"); }
TEST_F(LuaTriangle, SideBVector) { check("Triangle_SideBVector"); }
TEST_F(LuaTriangle, IsRectangular_true) { check("Triangle_IsRectangular_true"); }
TEST_F(LuaTriangle, IsRectangular_false) { check("Triangle_IsRectangular_false"); }
TEST_F(LuaTriangle, MidPoint) { check("Triangle_MidPoint"); }
TEST_F(LuaTriangle, CalculateNormal) { check("Triangle_CalculateNormal"); }
TEST_F(LuaTriangle, ToString) { check("Triangle_ToString"); }

View File

@@ -0,0 +1,56 @@
//
// Created by orange on 07.03.2026.
//
#include <gtest/gtest.h>
#include <lua.hpp>
#include <omath/lua/lua.hpp>
class LuaVec2 : 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 "/vec2_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(LuaVec2, Constructor_default) { check("Vec2_Constructor_default"); }
TEST_F(LuaVec2, Constructor_xy) { check("Vec2_Constructor_xy"); }
TEST_F(LuaVec2, Field_mutation) { check("Vec2_Field_mutation"); }
TEST_F(LuaVec2, Addition) { check("Vec2_Addition"); }
TEST_F(LuaVec2, Subtraction) { check("Vec2_Subtraction"); }
TEST_F(LuaVec2, UnaryMinus) { check("Vec2_UnaryMinus"); }
TEST_F(LuaVec2, Multiplication_scalar) { check("Vec2_Multiplication_scalar"); }
TEST_F(LuaVec2, Multiplication_scalar_reversed) { check("Vec2_Multiplication_scalar_reversed"); }
TEST_F(LuaVec2, Division_scalar) { check("Vec2_Division_scalar"); }
TEST_F(LuaVec2, EqualTo_true) { check("Vec2_EqualTo_true"); }
TEST_F(LuaVec2, EqualTo_false) { check("Vec2_EqualTo_false"); }
TEST_F(LuaVec2, LessThan) { check("Vec2_LessThan"); }
TEST_F(LuaVec2, LessThanOrEqual) { check("Vec2_LessThanOrEqual"); }
TEST_F(LuaVec2, ToString) { check("Vec2_ToString"); }
TEST_F(LuaVec2, Length) { check("Vec2_Length"); }
TEST_F(LuaVec2, LengthSqr) { check("Vec2_LengthSqr"); }
TEST_F(LuaVec2, Normalized) { check("Vec2_Normalized"); }
TEST_F(LuaVec2, Dot) { check("Vec2_Dot"); }
TEST_F(LuaVec2, DistanceTo) { check("Vec2_DistanceTo"); }
TEST_F(LuaVec2, DistanceToSqr) { check("Vec2_DistanceToSqr"); }
TEST_F(LuaVec2, Sum) { check("Vec2_Sum"); }
TEST_F(LuaVec2, Abs) { check("Vec2_Abs"); }

View File

@@ -0,0 +1,69 @@
//
// Created by orange on 07.03.2026.
//
#include <gtest/gtest.h>
#include <lua.hpp>
#include <omath/lua/lua.hpp>
class LuaVec3 : 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 "/vec3_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(LuaVec3, Constructor_default) { check("Vec3_Constructor_default"); }
TEST_F(LuaVec3, Constructor_xyz) { check("Vec3_Constructor_xyz"); }
TEST_F(LuaVec3, Field_mutation) { check("Vec3_Field_mutation"); }
TEST_F(LuaVec3, Addition) { check("Vec3_Addition"); }
TEST_F(LuaVec3, Subtraction) { check("Vec3_Subtraction"); }
TEST_F(LuaVec3, UnaryMinus) { check("Vec3_UnaryMinus"); }
TEST_F(LuaVec3, Multiplication_scalar) { check("Vec3_Multiplication_scalar"); }
TEST_F(LuaVec3, Multiplication_scalar_reversed) { check("Vec3_Multiplication_scalar_reversed"); }
TEST_F(LuaVec3, Multiplication_vec) { check("Vec3_Multiplication_vec"); }
TEST_F(LuaVec3, Division_scalar) { check("Vec3_Division_scalar"); }
TEST_F(LuaVec3, Division_vec) { check("Vec3_Division_vec"); }
TEST_F(LuaVec3, EqualTo_true) { check("Vec3_EqualTo_true"); }
TEST_F(LuaVec3, EqualTo_false) { check("Vec3_EqualTo_false"); }
TEST_F(LuaVec3, LessThan) { check("Vec3_LessThan"); }
TEST_F(LuaVec3, LessThanOrEqual) { check("Vec3_LessThanOrEqual"); }
TEST_F(LuaVec3, ToString) { check("Vec3_ToString"); }
TEST_F(LuaVec3, Length) { check("Vec3_Length"); }
TEST_F(LuaVec3, Length2d) { check("Vec3_Length2d"); }
TEST_F(LuaVec3, LengthSqr) { check("Vec3_LengthSqr"); }
TEST_F(LuaVec3, Normalized) { check("Vec3_Normalized"); }
TEST_F(LuaVec3, Dot_perpendicular) { check("Vec3_Dot_perpendicular"); }
TEST_F(LuaVec3, Dot_parallel) { check("Vec3_Dot_parallel"); }
TEST_F(LuaVec3, Cross) { check("Vec3_Cross"); }
TEST_F(LuaVec3, DistanceTo) { check("Vec3_DistanceTo"); }
TEST_F(LuaVec3, DistanceToSqr) { check("Vec3_DistanceToSqr"); }
TEST_F(LuaVec3, Sum) { check("Vec3_Sum"); }
TEST_F(LuaVec3, Sum2d) { check("Vec3_Sum2d"); }
TEST_F(LuaVec3, Abs) { check("Vec3_Abs"); }
TEST_F(LuaVec3, PointToSameDirection_true) { check("Vec3_PointToSameDirection_true"); }
TEST_F(LuaVec3, PointToSameDirection_false) { check("Vec3_PointToSameDirection_false"); }
TEST_F(LuaVec3, IsPerpendicular_true) { check("Vec3_IsPerpendicular_true"); }
TEST_F(LuaVec3, IsPerpendicular_false) { check("Vec3_IsPerpendicular_false"); }
TEST_F(LuaVec3, AngleBetween_90deg) { check("Vec3_AngleBetween_90deg"); }
TEST_F(LuaVec3, AngleBetween_zero_vector_error) { check("Vec3_AngleBetween_zero_vector_error"); }
TEST_F(LuaVec3, AsTable) { check("Vec3_AsTable"); }

View File

@@ -0,0 +1,57 @@
//
// Created by orange on 07.03.2026.
//
#include <gtest/gtest.h>
#include <lua.hpp>
#include <omath/lua/lua.hpp>
class LuaVec4 : 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 "/vec4_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(LuaVec4, Constructor_default) { check("Vec4_Constructor_default"); }
TEST_F(LuaVec4, Constructor_xyzw) { check("Vec4_Constructor_xyzw"); }
TEST_F(LuaVec4, Field_mutation) { check("Vec4_Field_mutation"); }
TEST_F(LuaVec4, Addition) { check("Vec4_Addition"); }
TEST_F(LuaVec4, Subtraction) { check("Vec4_Subtraction"); }
TEST_F(LuaVec4, UnaryMinus) { check("Vec4_UnaryMinus"); }
TEST_F(LuaVec4, Multiplication_scalar) { check("Vec4_Multiplication_scalar"); }
TEST_F(LuaVec4, Multiplication_scalar_reversed) { check("Vec4_Multiplication_scalar_reversed"); }
TEST_F(LuaVec4, Multiplication_vec) { check("Vec4_Multiplication_vec"); }
TEST_F(LuaVec4, Division_scalar) { check("Vec4_Division_scalar"); }
TEST_F(LuaVec4, Division_vec) { check("Vec4_Division_vec"); }
TEST_F(LuaVec4, EqualTo_true) { check("Vec4_EqualTo_true"); }
TEST_F(LuaVec4, EqualTo_false) { check("Vec4_EqualTo_false"); }
TEST_F(LuaVec4, LessThan) { check("Vec4_LessThan"); }
TEST_F(LuaVec4, LessThanOrEqual) { check("Vec4_LessThanOrEqual"); }
TEST_F(LuaVec4, ToString) { check("Vec4_ToString"); }
TEST_F(LuaVec4, Length) { check("Vec4_Length"); }
TEST_F(LuaVec4, LengthSqr) { check("Vec4_LengthSqr"); }
TEST_F(LuaVec4, Dot) { check("Vec4_Dot"); }
TEST_F(LuaVec4, Dot_perpendicular) { check("Vec4_Dot_perpendicular"); }
TEST_F(LuaVec4, Sum) { check("Vec4_Sum"); }
TEST_F(LuaVec4, Abs) { check("Vec4_Abs"); }
TEST_F(LuaVec4, Clamp) { check("Vec4_Clamp"); }

102
tests/lua/vec2_tests.lua Normal file
View File

@@ -0,0 +1,102 @@
local function approx(a, b) return math.abs(a - b) < 1e-5 end
function Vec2_Constructor_default()
local v = omath.Vec2.new()
assert(v.x == 0 and v.y == 0)
end
function Vec2_Constructor_xy()
local v = omath.Vec2.new(3, 4)
assert(v.x == 3 and v.y == 4)
end
function Vec2_Field_mutation()
local v = omath.Vec2.new(1, 2)
v.x = 9; v.y = 8
assert(v.x == 9 and v.y == 8)
end
function Vec2_Addition()
local c = omath.Vec2.new(1, 2) + omath.Vec2.new(3, 4)
assert(c.x == 4 and c.y == 6)
end
function Vec2_Subtraction()
local c = omath.Vec2.new(5, 7) - omath.Vec2.new(2, 3)
assert(c.x == 3 and c.y == 4)
end
function Vec2_UnaryMinus()
local b = -omath.Vec2.new(1, 2)
assert(b.x == -1 and b.y == -2)
end
function Vec2_Multiplication_scalar()
local b = omath.Vec2.new(2, 3) * 2.0
assert(b.x == 4 and b.y == 6)
end
function Vec2_Multiplication_scalar_reversed()
local b = 2.0 * omath.Vec2.new(2, 3)
assert(b.x == 4 and b.y == 6)
end
function Vec2_Division_scalar()
local b = omath.Vec2.new(4, 6) / 2.0
assert(b.x == 2 and b.y == 3)
end
function Vec2_EqualTo_true()
assert(omath.Vec2.new(1, 2) == omath.Vec2.new(1, 2))
end
function Vec2_EqualTo_false()
assert(not (omath.Vec2.new(1, 2) == omath.Vec2.new(9, 9)))
end
function Vec2_LessThan()
assert(omath.Vec2.new(1, 0) < omath.Vec2.new(3, 4))
end
function Vec2_LessThanOrEqual()
-- (3,4) and (4,3) both have length 5
assert(omath.Vec2.new(3, 4) <= omath.Vec2.new(4, 3))
end
function Vec2_ToString()
assert(tostring(omath.Vec2.new(1, 2)) == "Vec2(1, 2)")
end
function Vec2_Length()
assert(approx(omath.Vec2.new(3, 4):length(), 5.0))
end
function Vec2_LengthSqr()
assert(omath.Vec2.new(3, 4):length_sqr() == 25.0)
end
function Vec2_Normalized()
local n = omath.Vec2.new(3, 4):normalized()
assert(approx(n.x, 0.6) and approx(n.y, 0.8))
end
function Vec2_Dot()
assert(omath.Vec2.new(1, 2):dot(omath.Vec2.new(3, 4)) == 11.0)
end
function Vec2_DistanceTo()
assert(approx(omath.Vec2.new(0, 0):distance_to(omath.Vec2.new(3, 4)), 5.0))
end
function Vec2_DistanceToSqr()
assert(omath.Vec2.new(0, 0):distance_to_sqr(omath.Vec2.new(3, 4)) == 25.0)
end
function Vec2_Sum()
assert(omath.Vec2.new(3, 4):sum() == 7.0)
end
function Vec2_Abs()
local a = omath.Vec2.new(-3, -4):abs()
assert(a.x == 3 and a.y == 4)
end

163
tests/lua/vec3_tests.lua Normal file
View File

@@ -0,0 +1,163 @@
local function approx(a, b, eps) return math.abs(a - b) < (eps or 1e-5) end
function Vec3_Constructor_default()
local v = omath.Vec3.new()
assert(v.x == 0 and v.y == 0 and v.z == 0)
end
function Vec3_Constructor_xyz()
local v = omath.Vec3.new(1, 2, 3)
assert(v.x == 1 and v.y == 2 and v.z == 3)
end
function Vec3_Field_mutation()
local v = omath.Vec3.new(1, 2, 3)
v.x = 9; v.y = 8; v.z = 7
assert(v.x == 9 and v.y == 8 and v.z == 7)
end
function Vec3_Addition()
local c = omath.Vec3.new(1, 2, 3) + omath.Vec3.new(4, 5, 6)
assert(c.x == 5 and c.y == 7 and c.z == 9)
end
function Vec3_Subtraction()
local c = omath.Vec3.new(4, 5, 6) - omath.Vec3.new(1, 2, 3)
assert(c.x == 3 and c.y == 3 and c.z == 3)
end
function Vec3_UnaryMinus()
local b = -omath.Vec3.new(1, 2, 3)
assert(b.x == -1 and b.y == -2 and b.z == -3)
end
function Vec3_Multiplication_scalar()
local b = omath.Vec3.new(1, 2, 3) * 2.0
assert(b.x == 2 and b.y == 4 and b.z == 6)
end
function Vec3_Multiplication_scalar_reversed()
local b = 2.0 * omath.Vec3.new(1, 2, 3)
assert(b.x == 2 and b.y == 4 and b.z == 6)
end
function Vec3_Multiplication_vec()
local c = omath.Vec3.new(2, 3, 4) * omath.Vec3.new(2, 2, 2)
assert(c.x == 4 and c.y == 6 and c.z == 8)
end
function Vec3_Division_scalar()
local b = omath.Vec3.new(2, 4, 6) / 2.0
assert(b.x == 1 and b.y == 2 and b.z == 3)
end
function Vec3_Division_vec()
local c = omath.Vec3.new(4, 6, 8) / omath.Vec3.new(2, 2, 2)
assert(c.x == 2 and c.y == 3 and c.z == 4)
end
function Vec3_EqualTo_true()
assert(omath.Vec3.new(1, 2, 3) == omath.Vec3.new(1, 2, 3))
end
function Vec3_EqualTo_false()
assert(not (omath.Vec3.new(1, 2, 3) == omath.Vec3.new(9, 9, 9)))
end
function Vec3_LessThan()
assert(omath.Vec3.new(1, 0, 0) < omath.Vec3.new(3, 4, 0))
end
function Vec3_LessThanOrEqual()
-- (0,3,4) and (0,4,3) both have length 5
assert(omath.Vec3.new(0, 3, 4) <= omath.Vec3.new(0, 4, 3))
end
function Vec3_ToString()
assert(tostring(omath.Vec3.new(1, 2, 3)) == "Vec3(1, 2, 3)")
end
function Vec3_Length()
assert(approx(omath.Vec3.new(1, 2, 2):length(), 3.0))
end
function Vec3_Length2d()
assert(approx(omath.Vec3.new(3, 4, 99):length_2d(), 5.0))
end
function Vec3_LengthSqr()
assert(omath.Vec3.new(1, 2, 2):length_sqr() == 9.0)
end
function Vec3_Normalized()
local n = omath.Vec3.new(3, 0, 0):normalized()
assert(approx(n.x, 1.0) and approx(n.y, 0.0) and approx(n.z, 0.0))
end
function Vec3_Dot_perpendicular()
assert(omath.Vec3.new(1, 0, 0):dot(omath.Vec3.new(0, 1, 0)) == 0.0)
end
function Vec3_Dot_parallel()
local a = omath.Vec3.new(1, 2, 3)
assert(a:dot(a) == 14.0)
end
function Vec3_Cross()
local c = omath.Vec3.new(1, 0, 0):cross(omath.Vec3.new(0, 1, 0))
assert(approx(c.x, 0) and approx(c.y, 0) and approx(c.z, 1))
end
function Vec3_DistanceTo()
assert(approx(omath.Vec3.new(0, 0, 0):distance_to(omath.Vec3.new(1, 2, 2)), 3.0))
end
function Vec3_DistanceToSqr()
assert(omath.Vec3.new(0, 0, 0):distance_to_sqr(omath.Vec3.new(1, 2, 2)) == 9.0)
end
function Vec3_Sum()
assert(omath.Vec3.new(1, 2, 3):sum() == 6.0)
end
function Vec3_Sum2d()
assert(omath.Vec3.new(1, 2, 3):sum_2d() == 3.0)
end
function Vec3_Abs()
local a = omath.Vec3.new(-1, -2, -3):abs()
assert(a.x == 1 and a.y == 2 and a.z == 3)
end
function Vec3_PointToSameDirection_true()
assert(omath.Vec3.new(1, 1, 0):point_to_same_direction(omath.Vec3.new(2, 2, 0)) == true)
end
function Vec3_PointToSameDirection_false()
assert(omath.Vec3.new(1, 0, 0):point_to_same_direction(omath.Vec3.new(-1, 0, 0)) == false)
end
function Vec3_IsPerpendicular_true()
assert(omath.Vec3.new(1, 0, 0):is_perpendicular(omath.Vec3.new(0, 1, 0)) == true)
end
function Vec3_IsPerpendicular_false()
local a = omath.Vec3.new(1, 0, 0)
assert(a:is_perpendicular(a) == false)
end
function Vec3_AngleBetween_90deg()
local angle, err = omath.Vec3.new(1, 0, 0):angle_between(omath.Vec3.new(0, 1, 0))
assert(angle ~= nil, err)
assert(math.abs(angle - 90.0) < 1e-3)
end
function Vec3_AngleBetween_zero_vector_error()
local angle, err = omath.Vec3.new(0, 0, 0):angle_between(omath.Vec3.new(1, 0, 0))
assert(angle == nil and err ~= nil)
end
function Vec3_AsTable()
local t = omath.Vec3.new(1, 2, 3):as_table()
assert(t.x == 1 and t.y == 2 and t.z == 3)
end

110
tests/lua/vec4_tests.lua Normal file
View File

@@ -0,0 +1,110 @@
local function approx(a, b) return math.abs(a - b) < 1e-5 end
function Vec4_Constructor_default()
local v = omath.Vec4.new()
assert(v.x == 0 and v.y == 0 and v.z == 0 and v.w == 0)
end
function Vec4_Constructor_xyzw()
local v = omath.Vec4.new(1, 2, 3, 4)
assert(v.x == 1 and v.y == 2 and v.z == 3 and v.w == 4)
end
function Vec4_Field_mutation()
local v = omath.Vec4.new(1, 2, 3, 4)
v.w = 99
assert(v.w == 99)
end
function Vec4_Addition()
local c = omath.Vec4.new(1, 2, 3, 4) + omath.Vec4.new(4, 3, 2, 1)
assert(c.x == 5 and c.y == 5 and c.z == 5 and c.w == 5)
end
function Vec4_Subtraction()
local c = omath.Vec4.new(5, 5, 5, 5) - omath.Vec4.new(1, 2, 3, 4)
assert(c.x == 4 and c.y == 3 and c.z == 2 and c.w == 1)
end
function Vec4_UnaryMinus()
local b = -omath.Vec4.new(1, 2, 3, 4)
assert(b.x == -1 and b.y == -2 and b.z == -3 and b.w == -4)
end
function Vec4_Multiplication_scalar()
local b = omath.Vec4.new(1, 2, 3, 4) * 2.0
assert(b.x == 2 and b.y == 4 and b.z == 6 and b.w == 8)
end
function Vec4_Multiplication_scalar_reversed()
local b = 2.0 * omath.Vec4.new(1, 2, 3, 4)
assert(b.x == 2 and b.y == 4 and b.z == 6 and b.w == 8)
end
function Vec4_Multiplication_vec()
local c = omath.Vec4.new(2, 3, 4, 5) * omath.Vec4.new(2, 2, 2, 2)
assert(c.x == 4 and c.y == 6 and c.z == 8 and c.w == 10)
end
function Vec4_Division_scalar()
local b = omath.Vec4.new(2, 4, 6, 8) / 2.0
assert(b.x == 1 and b.y == 2 and b.z == 3 and b.w == 4)
end
function Vec4_Division_vec()
local c = omath.Vec4.new(4, 6, 8, 10) / omath.Vec4.new(2, 2, 2, 2)
assert(c.x == 2 and c.y == 3 and c.z == 4 and c.w == 5)
end
function Vec4_EqualTo_true()
assert(omath.Vec4.new(1, 2, 3, 4) == omath.Vec4.new(1, 2, 3, 4))
end
function Vec4_EqualTo_false()
assert(not (omath.Vec4.new(1, 2, 3, 4) == omath.Vec4.new(9, 9, 9, 9)))
end
function Vec4_LessThan()
assert(omath.Vec4.new(1, 0, 0, 0) < omath.Vec4.new(0, 0, 3, 4))
end
function Vec4_LessThanOrEqual()
-- (0,0,3,4) and (0,0,4,3) both have length 5
assert(omath.Vec4.new(0, 0, 3, 4) <= omath.Vec4.new(0, 0, 4, 3))
end
function Vec4_ToString()
assert(tostring(omath.Vec4.new(1, 2, 3, 4)) == "Vec4(1, 2, 3, 4)")
end
function Vec4_Length()
assert(approx(omath.Vec4.new(0, 0, 3, 4):length(), 5.0))
end
function Vec4_LengthSqr()
assert(omath.Vec4.new(0, 0, 3, 4):length_sqr() == 25.0)
end
function Vec4_Dot()
local a = omath.Vec4.new(1, 2, 3, 4)
assert(a:dot(a) == 30.0)
end
function Vec4_Dot_perpendicular()
assert(omath.Vec4.new(1, 0, 0, 0):dot(omath.Vec4.new(0, 1, 0, 0)) == 0.0)
end
function Vec4_Sum()
assert(omath.Vec4.new(1, 2, 3, 4):sum() == 10.0)
end
function Vec4_Abs()
local a = omath.Vec4.new(-1, -2, -3, -4):abs()
assert(a.x == 1 and a.y == 2 and a.z == 3 and a.w == 4)
end
function Vec4_Clamp()
local v = omath.Vec4.new(5, -3, 10, 99)
v:clamp(0, 7)
assert(v.x == 5 and v.y == 0 and v.z == 7)
end

View File

@@ -0,0 +1,29 @@
local a = omath.Vec2.new(1, 2)
local b = omath.Vec2.new(10, 20)
-- Operators
local c = a + b
local d = a - b
local e = a * 2.0
local f = -a
print("a + b = " .. tostring(c))
print("a - b = " .. tostring(d))
print("a * 2 = " .. tostring(e))
print("-a = " .. tostring(f))
print("a == Vec2(1,2): " .. tostring(a == omath.Vec2.new(1, 2)))
print("a < b: " .. tostring(a < b))
-- Field access + mutation
print("c.x = " .. c.x .. ", c.y = " .. c.y)
c.x = 99
print("c.x after mutation = " .. c.x)
-- Methods
print("a:length() = " .. a:length())
print("a:length_sqr() = " .. a:length_sqr())
print("a:normalized() = " .. tostring(a:normalized()))
print("a:dot(b) = " .. a:dot(b))
print("a:distance_to(b) = " .. a:distance_to(b))
print("a:distance_to_sqr(b) = " .. a:distance_to_sqr(b))
print("a:sum() = " .. a:sum())
print("a:abs() = " .. tostring(a:abs()))

View File

@@ -0,0 +1,55 @@
local a = omath.Vec3.new(1, 0, 0)
local b = omath.Vec3.new(0, 1, 0)
-- Operators
local c = a + b
local d = a - b
local e = a * 2.0
local f = -a
print("a + b = " .. tostring(c))
print("a - b = " .. tostring(d))
print("a * 2 = " .. tostring(e))
print("-a = " .. tostring(f))
print("a == Vec3(1,2,3): " .. tostring(a == omath.Vec3.new(1, 2, 3)))
print("a < b: " .. tostring(a < b))
-- Field access + mutation
print("c.x = " .. c.x .. ", c.y = " .. c.y .. ", c.z = " .. c.z)
c.x = 99
print("c.x after mutation = " .. c.x)
-- Methods
print("a:length() = " .. a:length())
print("a:length_2d() = " .. a:length_2d())
print("a:length_sqr() = " .. a:length_sqr())
print("a:normalized() = " .. tostring(a:normalized()))
print("a:dot(b) = " .. a:dot(b))
print("a:cross(b) = " .. tostring(a:cross(b)))
print("a:distance_to(b) = " .. a:distance_to(b))
print("a:distance_to_sqr(b) = " .. a:distance_to_sqr(b))
print("a:abs() = " .. tostring(a:abs()))
print("a:sum() = " .. a:sum())
print("a:sum_2d() = " .. a:sum_2d())
print("a:point_to_same_direction(b) = " .. tostring(a:point_to_same_direction(b)))
print("a:is_perpendicular(b) = " .. tostring(a:is_perpendicular(b)))
-- angle_between
local angle, err = a:angle_between(b)
if angle then
print("angle_between = " .. angle .. " degrees")
else
print("angle_between error: " .. err)
end
-- Zero vector edge case
local zero = omath.Vec3.new(0, 0, 0)
local ang2, err2 = zero:angle_between(a)
if ang2 then
print("zero angle = " .. ang2)
else
print("zero angle error: " .. err2)
end
-- as_table
local t = a:as_table()
print("as_table: x=" .. t.x .. " y=" .. t.y .. " z=" .. t.z)

View File

@@ -0,0 +1,31 @@
local a = omath.Vec4.new(1, 2, 3, 4)
local b = omath.Vec4.new(10, 20, 30, 40)
-- Operators
local c = a + b
local d = a - b
local e = a * 2.0
local f = -a
print("a + b = " .. tostring(c))
print("a - b = " .. tostring(d))
print("a * 2 = " .. tostring(e))
print("-a = " .. tostring(f))
print("a == Vec4(1,2,3,4): " .. tostring(a == omath.Vec4.new(1, 2, 3, 4)))
print("a < b: " .. tostring(a < b))
-- Field access + mutation
print("c.x=" .. c.x .. " c.y=" .. c.y .. " c.z=" .. c.z .. " c.w=" .. c.w)
c.w = 99
print("c.w after mutation = " .. c.w)
-- Methods
print("a:length() = " .. a:length())
print("a:length_sqr() = " .. a:length_sqr())
print("a:dot(b) = " .. a:dot(b))
print("a:sum() = " .. a:sum())
print("a:abs() = " .. tostring(a:abs()))
-- clamp
local clamped = omath.Vec4.new(5, -3, 10, 1)
clamped:clamp(0, 7)
print("clamp([5,-3,10,1], 0, 7).x=" .. clamped.x .. " .y=" .. clamped.y .. " .z=" .. clamped.z)

View File

@@ -17,7 +17,7 @@
], ],
"features": { "features": {
"avx2": { "avx2": {
"description": "Omath will use AVX2 to boost performance", "description": "omath will use AVX2 to boost performance",
"supports": "!arm" "supports": "!arm"
}, },
"benchmark": { "benchmark": {
@@ -35,7 +35,7 @@
] ]
}, },
"imgui": { "imgui": {
"description": "Omath will define method to convert omath types to imgui types", "description": "omath will define method to convert omath types to imgui types",
"dependencies": [ "dependencies": [
"imgui" "imgui"
] ]
@@ -45,6 +45,13 @@
"dependencies": [ "dependencies": [
"gtest" "gtest"
] ]
},
"lua": {
"description": "lua support for omath",
"dependencies": [
"lua",
"sol2"
]
} }
} }
} }