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.
This commit is contained in:
2025-09-08 20:15:56 +03:00
parent b964661030
commit 14fa810ecb
3 changed files with 194 additions and 5 deletions

View File

@@ -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_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_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_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) 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}") message(STATUS "[${PROJECT_NAME}]: Building on ${CMAKE_HOST_SYSTEM_NAME}")

View File

@@ -123,13 +123,13 @@ namespace omath
} }
[[nodiscard]] [[nodiscard]]
constexpr Angle& operator+(const Angle& other) noexcept constexpr Angle operator+(const Angle& other) noexcept
{ {
if constexpr (flags == AngleFlags::Normalized) 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) 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 else
static_assert(false); static_assert(false);
@@ -138,7 +138,7 @@ namespace omath
} }
[[nodiscard]] [[nodiscard]]
constexpr Angle& operator-(const Angle& other) noexcept constexpr Angle operator-(const Angle& other) noexcept
{ {
return operator+(-other); return operator+(-other);
} }

View File

@@ -1,3 +1,192 @@
// //
// Created by Orange on 11/30/2024. // Created by Orange on 11/30/2024.
// //
#include <cmath>
#include <gtest/gtest.h>
#include <numbers>
#include <omath/angle.hpp>
using namespace omath;
namespace
{
// Handy aliases (defaults: Type=float, [0,360], Normalized)
using Deg = Angle<float, float(0), float(360), AngleFlags::Normalized>;
using Pitch = Angle<float, float(-90), float(90), AngleFlags::Clamped>;
using Turn = Angle<float, float(-180), float(180), AngleFlags::Normalized>;
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<float>);
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<float>, 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);
}