From 14fa810ecb3c09c6146b77f8865d86ca64d1225b Mon Sep 17 00:00:00 2001 From: Orange Date: Mon, 8 Sep 2025 20:15:56 +0300 Subject: [PATCH] Implements angle class with normalization Adds an angle class with support for different normalization and clamping strategies. Includes trigonometric functions and arithmetic operators. Introduces unit tests to verify correct functionality. Disables unity builds to address a compilation issue. --- CMakeLists.txt | 2 +- include/omath/angle.hpp | 8 +- tests/general/unit_test_angle.cpp | 189 ++++++++++++++++++++++++++++++ 3 files changed, 194 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f2ebbed8..ad862534 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,7 +13,7 @@ option(OMATH_IMGUI_INTEGRATION "Omath will define method to convert omath types option(OMATH_BUILD_EXAMPLES "Build example projects with you can learn & play" OFF) option(OMATH_STATIC_MSVC_RUNTIME_LIBRARY "Force Omath to link static runtime" OFF) option(OMATH_SUPRESS_SAFETY_CHECKS "Supress some safety checks in release build to improve general performance" ON) -option(OMATH_USE_UNITY_BUILD "Will enable unity build to speed up compilation" ON) +option(OMATH_USE_UNITY_BUILD "Will enable unity build to speed up compilation" OFF) option(OMATH_ENABLE_LEGACY "Will enable legacy classes that MUST be used ONLY for backward compatibility" OFF) message(STATUS "[${PROJECT_NAME}]: Building on ${CMAKE_HOST_SYSTEM_NAME}") diff --git a/include/omath/angle.hpp b/include/omath/angle.hpp index a13b27fa..ee539219 100644 --- a/include/omath/angle.hpp +++ b/include/omath/angle.hpp @@ -123,13 +123,13 @@ namespace omath } [[nodiscard]] - constexpr Angle& operator+(const Angle& other) noexcept + constexpr Angle operator+(const Angle& other) noexcept { if constexpr (flags == AngleFlags::Normalized) - return {angles::wrap_angle(m_angle + other.m_angle, min, max)}; + return Angle{angles::wrap_angle(m_angle + other.m_angle, min, max)}; else if constexpr (flags == AngleFlags::Clamped) - return {std::clamp(m_angle + other.m_angle, min, max)}; + return Angle{std::clamp(m_angle + other.m_angle, min, max)}; else static_assert(false); @@ -138,7 +138,7 @@ namespace omath } [[nodiscard]] - constexpr Angle& operator-(const Angle& other) noexcept + constexpr Angle operator-(const Angle& other) noexcept { return operator+(-other); } diff --git a/tests/general/unit_test_angle.cpp b/tests/general/unit_test_angle.cpp index 66bc0d12..3fafd24e 100644 --- a/tests/general/unit_test_angle.cpp +++ b/tests/general/unit_test_angle.cpp @@ -1,3 +1,192 @@ // // Created by Orange on 11/30/2024. // + +#include +#include +#include +#include + +using namespace omath; + +namespace +{ + + // Handy aliases (defaults: Type=float, [0,360], Normalized) + using Deg = Angle; + using Pitch = Angle; + using Turn = Angle; + + constexpr float kEps = 1e-5f; + +} // namespace + +// ---------- Construction / factories ---------- + +TEST(UnitTestAngle, DefaultConstructor_IsZeroDegrees) +{ + Deg a; // default ctor + EXPECT_FLOAT_EQ(*a, 0.0f); + EXPECT_FLOAT_EQ(a.as_degrees(), 0.0f); +} + +TEST(UnitTestAngle, FromDegrees_Normalized_WrapsAboveMax) +{ + const Deg a = Deg::from_degrees(370.0f); + EXPECT_FLOAT_EQ(a.as_degrees(), 10.0f); +} + +TEST(UnitTestAngle, FromDegrees_Normalized_WrapsBelowMin) +{ + const Deg a = Deg::from_degrees(-10.0f); + EXPECT_FLOAT_EQ(a.as_degrees(), 350.0f); +} + +TEST(UnitTestAngle, FromDegrees_Clamped_ClampsToRange) +{ + const Pitch hi = Pitch::from_degrees(100.0f); + const Pitch lo = Pitch::from_degrees(-120.0f); + + EXPECT_FLOAT_EQ(hi.as_degrees(), 90.0f); + EXPECT_FLOAT_EQ(lo.as_degrees(), -90.0f); +} + +TEST(UnitTestAngle, FromRadians_And_AsRadians) +{ + const Deg a = Deg::from_radians(std::numbers::pi_v); + EXPECT_FLOAT_EQ(a.as_degrees(), 180.0f); + + const Deg b = Deg::from_degrees(180.0f); + EXPECT_NEAR(b.as_radians(), std::numbers::pi_v, 1e-6f); +} + +// ---------- Unary minus & deref ---------- + +TEST(UnitTestAngle, UnaryMinus_Normalized) +{ + const Deg a = Deg::from_degrees(30.0f); + const Deg b = -a; // wraps to 330 in [0,360) + EXPECT_FLOAT_EQ(b.as_degrees(), 330.0f); +} + +TEST(UnitTestAngle, DereferenceReturnsDegrees) +{ + const Deg a = Deg::from_degrees(42.0f); + EXPECT_FLOAT_EQ(*a, 42.0f); +} + +// ---------- Trigonometric helpers ---------- + +TEST(UnitTestAngle, SinCosTanCot_BasicCases) +{ + const Deg a0 = Deg::from_degrees(0.0f); + EXPECT_NEAR(a0.sin(), 0.0f, kEps); + EXPECT_NEAR(a0.cos(), 1.0f, kEps); + // cot(0) -> cos/sin -> div by 0: allow inf or nan + const float cot0 = a0.cot(); + EXPECT_TRUE(std::isinf(cot0) || std::isnan(cot0)); + + const Deg a45 = Deg::from_degrees(45.0f); + EXPECT_NEAR(a45.tan(), 1.0f, 1e-4f); + EXPECT_NEAR(a45.cot(), 1.0f, 1e-4f); + + const Deg a90 = Deg::from_degrees(90.0f); + EXPECT_NEAR(a90.sin(), 1.0f, 1e-4f); + EXPECT_NEAR(a90.cos(), 0.0f, 1e-4f); +} + +TEST(UnitTestAngle, Atan_IsAtanOfRadians) +{ + // atan(as_radians). For 0° -> atan(0)=0. + const Deg a0 = Deg::from_degrees(0.0f); + EXPECT_NEAR(a0.atan(), 0.0f, kEps); + + const Deg a45 = Deg::from_degrees(45.0f); + // atan(pi/4) ≈ 0.665773... + EXPECT_NEAR(a45.atan(), 0.66577375f, 1e-6f); +} + +// ---------- Compound arithmetic ---------- + +TEST(UnitTestAngle, PlusEquals_Normalized_Wraps) +{ + Deg a = Deg::from_degrees(350.0f); + a += Deg::from_degrees(20.0f); // 370 -> 10 + EXPECT_FLOAT_EQ(a.as_degrees(), 10.0f); +} + +TEST(UnitTestAngle, MinusEquals_Normalized_Wraps) +{ + Deg a = Deg::from_degrees(10.0f); + a -= Deg::from_degrees(30.0f); // -20 -> 340 + EXPECT_FLOAT_EQ(a.as_degrees(), 340.0f); +} + +TEST(UnitTestAngle, PlusEquals_Clamped_Clamps) +{ + Pitch p = Pitch::from_degrees(80.0f); + p += Pitch::from_degrees(30.0f); // 110 -> clamp to 90 + EXPECT_FLOAT_EQ(p.as_degrees(), 90.0f); +} + +TEST(UnitTestAngle, MinusEquals_Clamped_Clamps) +{ + Pitch p = Pitch::from_degrees(-70.0f); + p -= Pitch::from_degrees(40.0f); // -110 -> clamp to -90 + EXPECT_FLOAT_EQ(p.as_degrees(), -90.0f); +} + +// ---------- Alternative ranges ---------- + +TEST(UnitTestAngle, NormalizedRange_Neg180To180) +{ + const Turn a = Turn::from_degrees(190.0f); // -> -170 + const Turn b = Turn::from_degrees(-190.0f); // -> 170 + + EXPECT_FLOAT_EQ(a.as_degrees(), -170.0f); + EXPECT_FLOAT_EQ(b.as_degrees(), 170.0f); +} + +// ---------- Comparisons (via <=>) ---------- + +TEST(UnitTestAngle, Comparisons_WorkWithPartialOrdering) +{ + const Deg a = Deg::from_degrees(10.0f); + const Deg b = Deg::from_degrees(20.0f); + const Deg c = Deg::from_degrees(10.0f); + + EXPECT_TRUE(a < b); + EXPECT_TRUE(b > a); + EXPECT_TRUE(a <= c); + EXPECT_TRUE(c >= a); +} + +// ---------- std::format formatter ---------- + +TEST(UnitTestAngle, Formatter_PrintsDegreesWithSuffix) +{ + const Deg a = Deg::from_degrees(15.0f); + EXPECT_EQ(std::format("{}", a), "15deg"); + + const Deg b = Deg::from_degrees(10.5f); + EXPECT_EQ(std::format("{}", b), "10.5deg"); + + const Turn t = Turn::from_degrees(-170.0f); + EXPECT_EQ(std::format("{}", t), "-170deg"); +} + +TEST(UnitTestAngle, BinaryPlus_ReturnsWrappedSum) +{ + Angle<> a = Deg::from_degrees(350.0f); + const Deg b = Deg::from_degrees(20.0f); + const Deg c = a + b; // expect 10° + EXPECT_FLOAT_EQ(c.as_degrees(), 10.0f); +} + +TEST(UnitTestAngle, BinaryMinus_ReturnsWrappedDiff) +{ + Angle<> a = Deg::from_degrees(10.0f); + const Deg b = Deg::from_degrees(30.0f); + const Deg c = a - b; // expect 340° + EXPECT_FLOAT_EQ(c.as_degrees(), 340.0f); +}