Files
omath/docs/trigonometry/angle.md
2026-07-19 23:44:50 +03:00

168 lines
5.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# `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_floating_point_v<Type>`
> Formatters: `std::formatter` for `char` and `wchar_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
```cpp
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_floating_point_v<Type>
class Angle {
public:
// Construction
static constexpr Angle from_degrees(const Type& deg) noexcept;
static constexpr Angle from_radians(const Type& rad) noexcept;
static constexpr Angle from_asin(const Type& value) noexcept;
static constexpr Angle from_acos(const Type& value) noexcept;
static constexpr Angle from_atan(const Type& value) noexcept;
static constexpr Angle from_atan2(const Type& y, const Type& x) 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 cot() const noexcept; // cos()/sin() (watch sin≈0)
// Arithmetic (wraps or clamps per flags and configured range)
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
```cpp
std::format("{}", Angle<float>::from_degrees(45)); // "45deg"
```
Formatters exist for `char` and `wchar_t`.
---
## Usage examples
### Defaults (0360, normalized)
```cpp
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
```cpp
using Fov = omath::Angle<float, 1.f, 179.f, omath::AngleFlags::Clamped>;
auto fov = Fov::from_degrees(200.f); // -> 179deg (clamped)
```
### Signed, normalized range
```cpp
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
```cpp
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**, mirroring construction behavior.
* **`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. Normalization canonicalizes the maximum endpoint to the minimum endpoint.
* **No implicit numeric conversion:** Theres **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
```cpp
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*