From e99ca0bc2b7bf634cc90ec15f7cc8299f749762b Mon Sep 17 00:00:00 2001 From: Orange Date: Thu, 19 Mar 2026 19:19:42 +0300 Subject: [PATCH 1/3] update --- include/omath/algorithm/targeting.hpp | 50 +++++++++ include/omath/projection/camera.hpp | 5 + tests/general/unit_test_targeting.cpp | 154 ++++++++++++++++++++++++++ 3 files changed, 209 insertions(+) create mode 100644 include/omath/algorithm/targeting.hpp create mode 100644 tests/general/unit_test_targeting.cpp diff --git a/include/omath/algorithm/targeting.hpp b/include/omath/algorithm/targeting.hpp new file mode 100644 index 00000000..3c06ea01 --- /dev/null +++ b/include/omath/algorithm/targeting.hpp @@ -0,0 +1,50 @@ +// +// Created by Vladislav on 19.03.2026. +// + +#pragma once +#include "omath/linear_algebra/vector3.hpp" +#include +#include +#include + +namespace omath::algorithm +{ + template + requires std::is_invocable_r_v, std::iter_reference_t> + [[nodiscard]] + IteratorType get_closest_target_by_fov(const IteratorType& begin, const IteratorType& end, const CameraType& camera, + auto get_position, + const std::optional>& filter_func = std::nullopt) + { + auto best_target = end; + const auto& camera_angles = camera.get_view_angles(); + const Vector2 camera_angles_vec = {camera_angles.pitch.as_degrees(), camera_angles.yaw.as_degrees()}; + + for (auto current = begin; current != end; current = std::next(current)) + { + if (filter_func && !filter_func.value()(*current)) + continue; + + if (best_target == end) + { + best_target = current; + continue; + } + const auto current_target_angles = camera.calc_look_at_angles(get_position(*current)); + const auto best_target_angles = camera.calc_look_at_angles(get_position(*best_target)); + + const Vector2 current_angles_vec = {current_target_angles.pitch.as_degrees(), + current_target_angles.yaw.as_degrees()}; + const Vector2 best_angles_vec = {best_target_angles.pitch.as_degrees(), + best_target_angles.yaw.as_degrees()}; + + const auto current_target_distance = camera_angles_vec.distance_to(current_angles_vec); + const auto best_target_distance = camera_angles_vec.distance_to(best_angles_vec); + if (current_target_distance < best_target_distance) + best_target = current; + } + return best_target; + } + +} // namespace omath::algorithm \ No newline at end of file diff --git a/include/omath/projection/camera.hpp b/include/omath/projection/camera.hpp index c2d82f63..1678d4bf 100644 --- a/include/omath/projection/camera.hpp +++ b/include/omath/projection/camera.hpp @@ -82,6 +82,11 @@ namespace omath::projection m_view_projection_matrix = std::nullopt; m_view_matrix = std::nullopt; } + [[nodiscard]] + ViewAnglesType calc_look_at_angles(const Vector3& look_to) const + { + return TraitClass::calc_look_at_angle(m_origin, look_to); + } [[nodiscard]] Vector3 get_forward() const noexcept diff --git a/tests/general/unit_test_targeting.cpp b/tests/general/unit_test_targeting.cpp new file mode 100644 index 00000000..83862909 --- /dev/null +++ b/tests/general/unit_test_targeting.cpp @@ -0,0 +1,154 @@ +// +// Created by claude on 19.03.2026. +// +#include +#include +#include +#include + +namespace +{ + using Camera = omath::source_engine::Camera; + using ViewAngles = omath::source_engine::ViewAngles; + using Targets = std::vector>; + using Iter = Targets::const_iterator; + using FilterSig = bool(const omath::Vector3&); + + constexpr auto k_fov = omath::Angle::from_degrees(90.f); + + Camera make_camera(const omath::Vector3& origin, float pitch_deg, float yaw_deg) + { + ViewAngles angles{ + omath::source_engine::PitchAngle::from_degrees(pitch_deg), + omath::source_engine::YawAngle::from_degrees(yaw_deg), + omath::source_engine::RollAngle::from_degrees(0.f), + }; + return Camera{origin, angles, {1920.f, 1080.f}, k_fov, 0.01f, 1000.f}; + } + + auto get_pos = [](const omath::Vector3& v) -> const omath::Vector3& { return v; }; + + Iter find_closest(const Iter begin, const Iter end, const Camera& camera) + { + return omath::algorithm::get_closest_target_by_fov( + begin, end, camera, get_pos); + } +} + +TEST(unit_test_targeting, returns_end_for_empty_range) +{ + const auto camera = make_camera({0, 0, 0}, 0.f, 0.f); + Targets targets; + + EXPECT_EQ(find_closest(targets.cbegin(), targets.cend(), camera), targets.cend()); +} + +TEST(unit_test_targeting, single_target_returns_that_target) +{ + const auto camera = make_camera({0, 0, 0}, 0.f, 0.f); + Targets targets = {{100.f, 0.f, 0.f}}; + + EXPECT_EQ(find_closest(targets.cbegin(), targets.cend(), camera), targets.cbegin()); +} + +TEST(unit_test_targeting, picks_closest_to_crosshair) +{ + // Camera looking forward along +X (yaw=0, pitch=0 in source engine) + const auto camera = make_camera({0, 0, 0}, 0.f, 0.f); + + Targets targets = { + {100.f, 50.f, 0.f}, // off to the side + {100.f, 1.f, 0.f}, // nearly on crosshair + {100.f, -30.f, 0.f}, // off to the other side + }; + + const auto result = find_closest(targets.cbegin(), targets.cend(), camera); + + ASSERT_NE(result, targets.cend()); + EXPECT_EQ(result, targets.cbegin() + 1); +} + +TEST(unit_test_targeting, picks_closest_with_vertical_offset) +{ + const auto camera = make_camera({0, 0, 0}, 0.f, 0.f); + + Targets targets = { + {100.f, 0.f, 50.f}, // high above + {100.f, 0.f, 2.f}, // slightly above + {100.f, 0.f, 30.f}, // moderately above + }; + + const auto result = find_closest(targets.cbegin(), targets.cend(), camera); + + ASSERT_NE(result, targets.cend()); + EXPECT_EQ(result, targets.cbegin() + 1); +} + +TEST(unit_test_targeting, respects_camera_direction) +{ + // Camera looking along +Y (yaw=90) + const auto camera = make_camera({0, 0, 0}, 0.f, 90.f); + + Targets targets = { + {100.f, 0.f, 0.f}, // to the side relative to camera facing +Y + {0.f, 100.f, 0.f}, // directly in front + }; + + const auto result = find_closest(targets.cbegin(), targets.cend(), camera); + + ASSERT_NE(result, targets.cend()); + EXPECT_EQ(result, targets.cbegin() + 1); +} + +TEST(unit_test_targeting, equidistant_targets_returns_first) +{ + const auto camera = make_camera({0, 0, 0}, 0.f, 0.f); + + // Two targets symmetric about the forward axis — same angular distance + Targets targets = { + {100.f, 10.f, 0.f}, + {100.f, -10.f, 0.f}, + }; + + const auto result = find_closest(targets.cbegin(), targets.cend(), camera); + + ASSERT_NE(result, targets.cend()); + // First target should be selected (strict < means first wins on tie) + EXPECT_EQ(result, targets.cbegin()); +} + +TEST(unit_test_targeting, camera_pitch_affects_selection) +{ + // Camera looking upward (pitch < 0) + const auto camera = make_camera({0, 0, 0}, -40.f, 0.f); + + Targets targets = { + {100.f, 0.f, 0.f}, // on the horizon + {100.f, 0.f, 40.f}, // above, closer to where camera is looking + }; + + const auto result = find_closest(targets.cbegin(), targets.cend(), camera); + + ASSERT_NE(result, targets.cend()); + EXPECT_EQ(result, targets.cbegin() + 1); +} + +TEST(unit_test_targeting, many_targets_picks_best) +{ + const auto camera = make_camera({0, 0, 0}, 0.f, 0.f); + + Targets targets = { + {100.f, 80.f, 80.f}, + {100.f, 60.f, 60.f}, + {100.f, 40.f, 40.f}, + {100.f, 20.f, 20.f}, + {100.f, 0.5f, 0.5f}, // closest to crosshair + {100.f, 10.f, 10.f}, + {100.f, 30.f, 30.f}, + }; + + const auto result = find_closest(targets.cbegin(), targets.cend(), camera); + + ASSERT_NE(result, targets.cend()); + EXPECT_EQ(result, targets.cbegin() + 4); +} From ef422f0a86d6200e39a8df92e0d6e54c401326a7 Mon Sep 17 00:00:00 2001 From: Orange Date: Thu, 19 Mar 2026 19:23:39 +0300 Subject: [PATCH 2/3] added overload --- include/omath/algorithm/targeting.hpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/include/omath/algorithm/targeting.hpp b/include/omath/algorithm/targeting.hpp index 3c06ea01..dee152e9 100644 --- a/include/omath/algorithm/targeting.hpp +++ b/include/omath/algorithm/targeting.hpp @@ -7,6 +7,7 @@ #include #include #include +#include namespace omath::algorithm { @@ -47,4 +48,16 @@ namespace omath::algorithm return best_target; } + template + requires std::is_invocable_r_v, + std::ranges::range_reference_t> + [[nodiscard]] + auto get_closest_target_by_fov(const RangeType& range, const CameraType& camera, + auto get_position, + const std::optional>& filter_func = std::nullopt) + { + return get_closest_target_by_fov( + std::ranges::begin(range), std::ranges::end(range), camera, get_position, filter_func); + } + } // namespace omath::algorithm \ No newline at end of file From d737aee1c536676557a4b4c9829d38f2bd79592a Mon Sep 17 00:00:00 2001 From: Orange Date: Thu, 19 Mar 2026 19:29:01 +0300 Subject: [PATCH 3/3] added by distance targeting --- include/omath/algorithm/targeting.hpp | 40 ++++++++++ tests/general/unit_test_targeting.cpp | 106 ++++++++++++++++++++++++++ 2 files changed, 146 insertions(+) diff --git a/include/omath/algorithm/targeting.hpp b/include/omath/algorithm/targeting.hpp index dee152e9..6d4dbea6 100644 --- a/include/omath/algorithm/targeting.hpp +++ b/include/omath/algorithm/targeting.hpp @@ -60,4 +60,44 @@ namespace omath::algorithm std::ranges::begin(range), std::ranges::end(range), camera, get_position, filter_func); } + // ── By world-space distance ─────────────────────────────────────────────── + + template + requires std::is_invocable_r_v, std::iter_reference_t> + [[nodiscard]] + IteratorType get_closest_target_by_distance(const IteratorType& begin, const IteratorType& end, + const Vector3& origin, auto get_position, + const std::optional>& filter_func = std::nullopt) + { + auto best_target = end; + + for (auto current = begin; current != end; current = std::next(current)) + { + if (filter_func && !filter_func.value()(*current)) + continue; + + if (best_target == end) + { + best_target = current; + continue; + } + + if (origin.distance_to(get_position(*current)) < origin.distance_to(get_position(*best_target))) + best_target = current; + } + return best_target; + } + + template + requires std::is_invocable_r_v, + std::ranges::range_reference_t> + [[nodiscard]] + auto get_closest_target_by_distance(const RangeType& range, const Vector3& origin, + auto get_position, + const std::optional>& filter_func = std::nullopt) + { + return get_closest_target_by_distance( + std::ranges::begin(range), std::ranges::end(range), origin, get_position, filter_func); + } + } // namespace omath::algorithm \ No newline at end of file diff --git a/tests/general/unit_test_targeting.cpp b/tests/general/unit_test_targeting.cpp index 83862909..f1697260 100644 --- a/tests/general/unit_test_targeting.cpp +++ b/tests/general/unit_test_targeting.cpp @@ -33,6 +33,12 @@ namespace return omath::algorithm::get_closest_target_by_fov( begin, end, camera, get_pos); } + + Iter find_nearest(const Iter begin, const Iter end, const omath::Vector3& origin) + { + return omath::algorithm::get_closest_target_by_distance( + begin, end, origin, get_pos); + } } TEST(unit_test_targeting, returns_end_for_empty_range) @@ -152,3 +158,103 @@ TEST(unit_test_targeting, many_targets_picks_best) ASSERT_NE(result, targets.cend()); EXPECT_EQ(result, targets.cbegin() + 4); } + +// ── get_closest_target_by_distance tests ──────────────────────────────────── + +TEST(unit_test_targeting, distance_returns_end_for_empty_range) +{ + Targets targets; + + EXPECT_EQ(find_nearest(targets.cbegin(), targets.cend(), {0, 0, 0}), targets.cend()); +} + +TEST(unit_test_targeting, distance_single_target) +{ + Targets targets = {{50.f, 0.f, 0.f}}; + + EXPECT_EQ(find_nearest(targets.cbegin(), targets.cend(), {0, 0, 0}), targets.cbegin()); +} + +TEST(unit_test_targeting, distance_picks_nearest) +{ + const omath::Vector3 origin{0.f, 0.f, 0.f}; + + Targets targets = { + {100.f, 0.f, 0.f}, // distance = 100 + {10.f, 0.f, 0.f}, // distance = 10 (closest) + {50.f, 0.f, 0.f}, // distance = 50 + }; + + const auto result = find_nearest(targets.cbegin(), targets.cend(), origin); + + ASSERT_NE(result, targets.cend()); + EXPECT_EQ(result, targets.cbegin() + 1); +} + +TEST(unit_test_targeting, distance_considers_all_axes) +{ + const omath::Vector3 origin{0.f, 0.f, 0.f}; + + Targets targets = { + {30.f, 30.f, 30.f}, // distance = sqrt(2700) ~ 51.96 + {50.f, 0.f, 0.f}, // distance = 50 + {0.f, 0.f, 10.f}, // distance = 10 (closest) + }; + + const auto result = find_nearest(targets.cbegin(), targets.cend(), origin); + + ASSERT_NE(result, targets.cend()); + EXPECT_EQ(result, targets.cbegin() + 2); +} + +TEST(unit_test_targeting, distance_from_nonzero_origin) +{ + const omath::Vector3 origin{100.f, 100.f, 100.f}; + + Targets targets = { + {0.f, 0.f, 0.f}, // distance = sqrt(30000) ~ 173 + {105.f, 100.f, 100.f}, // distance = 5 (closest) + {200.f, 200.f, 200.f}, // distance = sqrt(30000) ~ 173 + }; + + const auto result = find_nearest(targets.cbegin(), targets.cend(), origin); + + ASSERT_NE(result, targets.cend()); + EXPECT_EQ(result, targets.cbegin() + 1); +} + +TEST(unit_test_targeting, distance_equidistant_returns_first) +{ + const omath::Vector3 origin{0.f, 0.f, 0.f}; + + // Both targets at distance 100, symmetric + Targets targets = { + {100.f, 0.f, 0.f}, + {-100.f, 0.f, 0.f}, + }; + + const auto result = find_nearest(targets.cbegin(), targets.cend(), origin); + + ASSERT_NE(result, targets.cend()); + EXPECT_EQ(result, targets.cbegin()); +} + +TEST(unit_test_targeting, distance_many_targets) +{ + const omath::Vector3 origin{0.f, 0.f, 0.f}; + + Targets targets = { + {500.f, 0.f, 0.f}, + {200.f, 200.f, 0.f}, + {100.f, 100.f, 100.f}, + {50.f, 50.f, 50.f}, + {1.f, 1.f, 1.f}, // distance = sqrt(3) ~ 1.73 (closest) + {10.f, 10.f, 10.f}, + {80.f, 0.f, 0.f}, + }; + + const auto result = find_nearest(targets.cbegin(), targets.cend(), origin); + + ASSERT_NE(result, targets.cend()); + EXPECT_EQ(result, targets.cbegin() + 4); +}