Adds documentation for the `omath::ViewAngles` struct, clarifying its purpose, common usage patterns, and the definition of the types of pitch, yaw and roll. Also, adds short explanations of how to use ViewAngles and what tradeoffs exist between using raw float types and strongly typed Angle<> types.
6.0 KiB
omath::Angle — templated angle with normalize/clamper + trig
Header:
omath/trigonometry/angle.hppNamespace:omathTemplate:Angle<Type = float, min = 0, max = 360, flags = AngleFlags::Normalized>Requires:std::is_arithmetic_v<Type>Formatters:std::formatterforchar,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]usingangles::wrap_angle.AngleFlags::Clamped: values are clamped to[min, max]usingstd::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 inomath::angles. - Arithmetic honors policy:
operator+=/-=and the binary+/-apply wrap or clamp in[min,max], mirroring construction behavior. atan(): returnsstd::atan(as_radians())(the arctangent of the radian value). This is mathematically unusual for an angle type and is rarely useful; prefertan()/atan2in client code when solving geometry problems.cot()/tan()singularities: Near multiples wheresin() ≈ 0orcos() ≈ 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.,-180vs180in signed ranges are distinct endpoints). - No implicit numeric conversion: There’s no
operator Type(). Useas_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:
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 < maxat 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