mirror of
https://github.com/orange-cpp/omath.git
synced 2026-02-13 07:03:25 +00:00
Documents view angle struct and related API
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.
This commit is contained in:
165
docs/trigonometry/angle.md
Normal file
165
docs/trigonometry/angle.md
Normal file
@@ -0,0 +1,165 @@
|
||||
# `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
|
||||
|
||||
```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_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
|
||||
|
||||
```cpp
|
||||
std::format("{}", Angle<float>::from_degrees(45)); // "45deg"
|
||||
```
|
||||
|
||||
Formatters exist for `char`, `wchar_t`, and `char8_t`.
|
||||
|
||||
---
|
||||
|
||||
## Usage examples
|
||||
|
||||
### Defaults (0–360, 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** 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
|
||||
|
||||
```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*
|
||||
107
docs/trigonometry/angles.md
Normal file
107
docs/trigonometry/angles.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# `omath::angles` — angle conversions, FOV helpers, and wrapping
|
||||
|
||||
> Header: `omath/trigonometry/angles.hpp`
|
||||
> Namespace: `omath::angles`
|
||||
> All functions are `[[nodiscard]]` and `noexcept` where applicable.
|
||||
|
||||
A small set of constexpr-friendly utilities for converting between degrees/radians, converting horizontal/vertical field of view, and wrapping angles into a closed interval.
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
```cpp
|
||||
// Degrees ↔ Radians (Type must be floating-point)
|
||||
template<class Type>
|
||||
requires std::is_floating_point_v<Type>
|
||||
constexpr Type radians_to_degrees(const Type& radians) noexcept;
|
||||
|
||||
template<class Type>
|
||||
requires std::is_floating_point_v<Type>
|
||||
constexpr Type degrees_to_radians(const Type& degrees) noexcept;
|
||||
|
||||
// FOV conversion (inputs/outputs in degrees, aspect = width/height)
|
||||
template<class Type>
|
||||
requires std::is_floating_point_v<Type>
|
||||
Type horizontal_fov_to_vertical(const Type& horizontal_fov, const Type& aspect) noexcept;
|
||||
|
||||
template<class Type>
|
||||
requires std::is_floating_point_v<Type>
|
||||
Type vertical_fov_to_horizontal(const Type& vertical_fov, const Type& aspect) noexcept;
|
||||
|
||||
// Wrap angle into [min, max] (any arithmetic type)
|
||||
template<class Type>
|
||||
requires std::is_arithmetic_v<Type>
|
||||
Type wrap_angle(const Type& angle, const Type& min, const Type& max) noexcept;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### Degrees ↔ Radians
|
||||
|
||||
```cpp
|
||||
float rad = omath::angles::degrees_to_radians(180.0f); // π
|
||||
double deg = omath::angles::radians_to_degrees(std::numbers::pi); // 180
|
||||
```
|
||||
|
||||
### Horizontal ↔ Vertical FOV
|
||||
|
||||
* `aspect` = **width / height**.
|
||||
* Inputs/outputs are **degrees**.
|
||||
|
||||
```cpp
|
||||
float hdeg = 90.0f;
|
||||
float aspect = 16.0f / 9.0f;
|
||||
|
||||
float vdeg = omath::angles::horizontal_fov_to_vertical(hdeg, aspect); // ~58.0°
|
||||
float hdeg2 = omath::angles::vertical_fov_to_horizontal(vdeg, aspect); // ≈ 90.0°
|
||||
```
|
||||
|
||||
Formulas (in radians):
|
||||
|
||||
* `v = 2 * atan( tan(h/2) / aspect )`
|
||||
* `h = 2 * atan( tan(v/2) * aspect )`
|
||||
|
||||
### Wrapping angles (or any periodic value)
|
||||
|
||||
Wrap any numeric `angle` into `[min, max]`:
|
||||
|
||||
```cpp
|
||||
// Wrap degrees into [0, 360]
|
||||
float a = omath::angles::wrap_angle( 370.0f, 0.0f, 360.0f); // 10
|
||||
float b = omath::angles::wrap_angle( -15.0f, 0.0f, 360.0f); // 345
|
||||
// Signed range [-180,180]
|
||||
float c = omath::angles::wrap_angle( 200.0f, -180.0f, 180.0f); // -160
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes & edge cases
|
||||
|
||||
* **Type requirements**
|
||||
|
||||
* Converters & FOV helpers require **floating-point** `Type`.
|
||||
* `wrap_angle` accepts any arithmetic `Type` (floats or integers).
|
||||
* **Aspect ratio** must be **positive** and finite. For `aspect == 0` the FOV helpers are undefined.
|
||||
* **Units**: FOV functions accept/return **degrees** but compute internally in radians.
|
||||
* **Wrapping interval**: Behavior assumes `max > min`. The result lies in the **closed interval** `[min, max]` with modulo arithmetic; if you need half-open behavior (e.g., `[min,max)`), adjust your range or post-process endpoint cases.
|
||||
* **constexpr**: Converters are `constexpr`; FOV helpers are runtime constexpr-compatible except for `std::atan/std::tan` constraints on some standard libraries.
|
||||
|
||||
---
|
||||
|
||||
## Quick tests
|
||||
|
||||
```cpp
|
||||
using namespace omath::angles;
|
||||
|
||||
static_assert(degrees_to_radians(180.0) == std::numbers::pi);
|
||||
static_assert(radians_to_degrees(std::numbers::pi_v<float>) == 180.0f);
|
||||
|
||||
float v = horizontal_fov_to_vertical(90.0f, 16.0f/9.0f);
|
||||
float h = vertical_fov_to_horizontal(v, 16.0f/9.0f);
|
||||
assert(std::abs(h - 90.0f) < 1e-5f);
|
||||
|
||||
assert(wrap_angle(360.0f, 0.0f, 360.0f) == 0.0f || wrap_angle(360.0f, 0.0f, 360.0f) == 360.0f);
|
||||
```
|
||||
87
docs/trigonometry/view_angles.md
Normal file
87
docs/trigonometry/view_angles.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# `omath::ViewAngles` — tiny POD for pitch/yaw/roll
|
||||
|
||||
> Header: your project’s `view_angles.hpp`
|
||||
> Namespace: `omath`
|
||||
> Kind: **aggregate struct** (POD), no methods, no allocation
|
||||
|
||||
A minimal container for Euler angles. You choose the types for each component (e.g., raw `float` or the strong `omath::Angle<>` type), and plug it into systems like `projection::Camera`.
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
```cpp
|
||||
namespace omath {
|
||||
template<class PitchType, class YawType, class RollType>
|
||||
struct ViewAngles {
|
||||
PitchType pitch;
|
||||
YawType yaw;
|
||||
RollType roll;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
* Aggregate: supports brace-init, aggregate copying, and `constexpr` usage when the component types do.
|
||||
* Semantics (units/handedness/ranges) are **entirely defined by your chosen types**.
|
||||
|
||||
---
|
||||
|
||||
## Common aliases
|
||||
|
||||
```cpp
|
||||
// Simple, raw degrees as floats (be careful with wrapping!)
|
||||
using ViewAnglesF = omath::ViewAngles<float, float, float>;
|
||||
|
||||
// Safer, policy-based angles (recommended)
|
||||
using PitchDeg = omath::Angle<float, -89.f, 89.f, omath::AngleFlags::Clamped>;
|
||||
using YawDeg = omath::Angle<float, -180.f, 180.f, omath::AngleFlags::Normalized>;
|
||||
using RollDeg = omath::Angle<float, -180.f, 180.f, omath::AngleFlags::Normalized>;
|
||||
using ViewAnglesDeg = omath::ViewAngles<PitchDeg, YawDeg, RollDeg>;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic construction
|
||||
|
||||
```cpp
|
||||
omath::ViewAngles<float,float,float> a{ 10.f, 45.f, 0.f }; // pitch, yaw, roll in degrees
|
||||
```
|
||||
|
||||
### With `omath::Angle<>` (automatic wrap/clamper)
|
||||
|
||||
```cpp
|
||||
ViewAnglesDeg v{
|
||||
PitchDeg::from_degrees( 95.f), // -> 89deg (clamped)
|
||||
YawDeg::from_degrees (-190.f), // -> 170deg (wrapped)
|
||||
RollDeg::from_degrees ( 30.f)
|
||||
};
|
||||
```
|
||||
|
||||
### Using with `projection::Camera`
|
||||
|
||||
```cpp
|
||||
using Mat4 = omath::Mat<4,4,float, omath::MatStoreType::COLUMN_MAJOR>;
|
||||
using Cam = omath::projection::Camera<Mat4, ViewAnglesDeg, MyCameraTrait>;
|
||||
|
||||
omath::projection::ViewPort vp{1920,1080};
|
||||
auto fov = omath::angles::degrees_to_radians(70.f); // or your Angle type
|
||||
|
||||
Cam cam(/*position*/ {0,1.7f,-3},
|
||||
/*angles*/ ViewAnglesDeg{ PitchDeg::from_degrees(0),
|
||||
YawDeg::from_degrees(0),
|
||||
RollDeg::from_degrees(0) },
|
||||
/*viewport*/ vp,
|
||||
/*fov*/ omath::Angle<float,0.f,180.f,omath::AngleFlags::Clamped>::from_degrees(70.f),
|
||||
/*near*/ 0.1f,
|
||||
/*far*/ 1000.f);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes & tips
|
||||
|
||||
* **Ranges/units**: pick types that encode your policy (e.g., signed yaw in `[-180,180]`, pitch clamped to avoid gimbal flips).
|
||||
* **Handedness & order**: this struct doesn’t impose rotation order. Your math/trait layer (e.g., `MyCameraTrait`) must define how `(pitch, yaw, roll)` map to a view matrix (common orders: ZYX or XYZ).
|
||||
* **Zero-cost**: with plain `float`s this is as cheap as three scalars; with `Angle<>` you gain safety at the cost of tiny wrap/clamp logic on construction/arithmetic.
|
||||
Reference in New Issue
Block a user