Skip to content

omath::Angle — templated angle with normalize/clamper + trig

Header: omath/trigonometry/angle.hpp Namespace: omath Template: Angle<Type = float, min = 0, max = 360, flags = AngleFlags::Normalized> Requires: std::is_arithmetic_v<Type> Formatters: std::formatter for char, wchar_t, char8_t"{}deg"


Overview

Angle is a tiny value-type that stores an angle in degrees and automatically normalizes or clamps it into a compile-time range. It exposes conversions to/from radians, common trig (sin/cos/tan/cot), arithmetic with wrap/clamp semantics, and lightweight formatting.

Two behaviors via AngleFlags:

  • AngleFlags::Normalized (default): values are wrapped into [min, max] using angles::wrap_angle.
  • AngleFlags::Clamped: values are clamped to [min, max] using std::clamp.

API

namespace omath {

enum class AngleFlags { Normalized = 0, Clamped = 1 };

template<class Type = float, Type min = Type(0), Type max = Type(360),
         AngleFlags flags = AngleFlags::Normalized>
requires std::is_arithmetic_v<Type>
class Angle {
public:
  // Construction
  static constexpr Angle from_degrees(const Type& deg) noexcept;
  static constexpr Angle from_radians(const Type& rad) noexcept;
  constexpr Angle() noexcept;                  // 0 deg, adjusted by flags/range

  // Accessors / conversions (degrees stored internally)
  constexpr const Type& operator*() const noexcept;   // raw degrees reference
  constexpr Type        as_degrees()  const noexcept;
  constexpr Type        as_radians()  const noexcept;

  // Trig (computed from radians)
  Type sin() const noexcept;
  Type cos() const noexcept;
  Type tan() const noexcept;
  Type atan() const noexcept;  // atan(as_radians())  (rarely used)
  Type cot() const noexcept;   // cos()/sin()  (watch sin≈0)

  // Arithmetic (wraps or clamps per flags and [min,max])
  constexpr Angle& operator+=(const Angle&) noexcept;
  constexpr Angle& operator-=(const Angle&) noexcept;
  constexpr Angle  operator+(const Angle&) noexcept;
  constexpr Angle  operator-(const Angle&) noexcept;
  constexpr Angle  operator-()        const noexcept;

  // Comparison (partial ordering)
  constexpr std::partial_ordering operator<=>(const Angle&) const noexcept = default;
};

} // namespace omath

Formatting

std::format("{}", Angle<float>::from_degrees(45)); // "45deg"

Formatters exist for char, wchar_t, and char8_t.


Usage examples

Defaults (0–360, normalized)

using Deg = omath::Angle<>;                     // float, [0,360], Normalized

auto a = Deg::from_degrees(370);                // -> 10deg
auto b = Deg::from_radians(omath::angles::pi);  // -> 180deg

a += Deg::from_degrees(355);                    // 10 + 355 -> 365 -> wraps -> 5deg

float s = a.sin();                              // sin(5°)

Clamped range

using Fov = omath::Angle<float, 1.f, 179.f, omath::AngleFlags::Clamped>;
auto fov = Fov::from_degrees(200.f);            // -> 179deg (clamped)

Signed, normalized range

using SignedDeg = omath::Angle<float, -180.f, 180.f, omath::AngleFlags::Normalized>;

auto x = SignedDeg::from_degrees(190.f);        // -> -170deg
auto y = SignedDeg::from_degrees(-200.f);       // -> 160deg
auto z = x + y;                                 // -170 + 160 = -10deg (wrapped if needed)

Get/set raw degrees

auto yaw = SignedDeg::from_degrees(-45.f);
float deg = *yaw;                                // same as yaw.as_degrees()

Semantics & notes

  • Storage & units: Internally stores degrees (Type m_angle). as_radians()/from_radians() use the project helpers in omath::angles.
  • Arithmetic honors policy: operator+=/-= and the binary +/- apply wrap or clamp in [min,max], mirroring construction behavior.
  • atan(): returns std::atan(as_radians()) (the arctangent of the radian value). This is mathematically unusual for an angle type and is rarely useful; prefer tan()/atan2 in client code when solving geometry problems.
  • cot() / tan() singularities: Near multiples where sin() ≈ 0 or cos() ≈ 0, results blow up. Guard in your usage if inputs can approach these points.
  • Comparison: operator<=> is defaulted. With normalization, distinct representatives can compare as expected (e.g., -180 vs 180 in signed ranges are distinct endpoints).
  • No implicit numeric conversion: There’s no operator Type(). Use as_degrees()/as_radians() (or *angle) explicitly—this intentional friction avoids unit mistakes.

Customization patterns

  • Radians workflow: Keep angles in degrees internally but wrap helper creators:

cpp inline Deg degf(float d) { return Deg::from_degrees(d); } inline Deg radf(float r) { return Deg::from_radians(r); } * Compile-time policy: Pick ranges/flags at the type level to enforce invariants (e.g., YawDeg = Angle<float,-180,180,Normalized>; FovDeg = Angle<float,1,179,Clamped>).


Pitfalls & gotchas

  • Ensure min < max at compile time for meaningful wrap/clamp behavior.
  • For normalized signed ranges, decide whether your wrap_angle(min,max) treats endpoints half-open (e.g., [-180,180)) to avoid duplicate representations; the formatter will print the stored value verbatim.
  • If you need sum of many angles, accumulating in radians then converting back can improve numeric stability at extreme values.

Minimal tests

using A = omath::Angle<>;
REQUIRE(A::from_degrees(360).as_degrees() == 0.f);
REQUIRE(A::from_degrees(-1).as_degrees()  == 359.f);

using S = omath::Angle<float,-180.f,180.f, omath::AngleFlags::Normalized>;
REQUIRE(S::from_degrees( 181).as_degrees() == -179.f);
REQUIRE(S::from_degrees(-181).as_degrees() ==  179.f);

using C = omath::Angle<float, 10.f, 20.f, omath::AngleFlags::Clamped>;
REQUIRE(C::from_degrees(5).as_degrees()    == 10.f);
REQUIRE(C::from_degrees(25).as_degrees()   == 20.f);

Last updated: 1 Nov 2025