Skip to content

angle.hpp

Angle — a heading on SE(2). The one type that owns angle wrapping, so the "degrees into cos/sin" and "359° vs -1°" bug classes are impossible by construction.

This header declares 1 type (11 members).

Extracted from include/shulib/math/angle.hpp — this page is that header's documentation, reformatted, so it cannot disagree with the code. Prose about how to think about the API lives in the user guide; worked recipes live in the cookbook; this page is the complete, mechanical list of what exists.

Contents

class Angle

class Angle

A heading on SE(2), and the one type that owns angle wrapping — so the "degrees into cos/sin" and "359° vs -1°" bug classes cannot be written. Stored in RADIANS, always normalized to (-π, π], CCW-positive to match the field frame. The interval is half-open at the bottom: an Angle NEVER holds -π, it holds +π instead, which is what makes the exact-antipodal case in errorTo() resolve deterministically. Degrees appear only at this API boundary; nothing downstream computes in them. Construction goes exclusively through radians() / degrees(), which reject non-finite input, so a NaN heading is unrepresentable rather than merely unlikely. A default-constructed Angle is the zero heading (field +X). The conventions above are LOCKED (Freeze F3).

class, declared at include/shulib/math/angle.hpp:35.

Angle::kPi

static constexpr double kPi = 3.14159265358979323846

Pi, exact to double precision. Public so tests/callers share one constant.

field, declared at include/shulib/math/angle.hpp:38.

Angle::Angle

Angle() = default

The zero heading.

function, declared at include/shulib/math/angle.hpp:41.

Angle::radians

[[nodiscard]] static Angle radians(double r)

Construct from radians; input must be finite. Result is wrapped to (-π, π].

function, declared at include/shulib/math/angle.hpp:44.

Angle::degrees

[[nodiscard]] static Angle degrees(double d)

Construct from degrees; input must be finite. Result is wrapped to (-π, π].

function, declared at include/shulib/math/angle.hpp:50.

Angle::radians (overload 2)

[[nodiscard]] double radians() const noexcept

The heading in radians, already normalized to (-π, π] — never a raw accumulated value, so it is safe to hand straight to cos/sin.

function, declared at include/shulib/math/angle.hpp:57.

Angle::degrees (overload 2)

[[nodiscard]] double degrees() const noexcept

The same heading in degrees, therefore always in (-180, 180]. For humans and telemetry; no library computation reads this.

function, declared at include/shulib/math/angle.hpp:60.

Angle::errorTo

[[nodiscard]] double errorTo(Angle target) const noexcept

Shortest signed rotation FROM this heading TO target, in (-π, π] radians. The exact-antipodal case resolves to +π (never -π) — see Freeze F3.

function, declared at include/shulib/math/angle.hpp:64.

Angle::operator+

[[nodiscard]] Angle operator+(Angle o) const noexcept

This heading rotated FORWARD (CCW) by o, re-wrapped. There is no separate rotation type, so the right operand is read as a rotation: 170° + 20° is -170°, not 190°.

function, declared at include/shulib/math/angle.hpp:70.

Angle::operator-

[[nodiscard]] Angle operator-(Angle o) const noexcept

This heading rotated BACKWARD by o, re-wrapped. It yields an Angle, not an error term — for the signed rotation a controller should act on use o.errorTo(*this), which is numerically identical but returns a plain double that cannot be mistaken for a heading.

function, declared at include/shulib/math/angle.hpp:74.

Angle::operator- (overload 2)

[[nodiscard]] Angle operator-() const noexcept

The mirrored heading — the same rotation taken clockwise. Watch the boundary: -π is not representable, so negating +π gives back +π.

function, declared at include/shulib/math/angle.hpp:77.

Angle::approxEqual

[[nodiscard]] bool approxEqual(Angle o, double tolRad = 1e-9) const noexcept

True if the shortest error to o is within tolRad. Never compares raw doubles with ==, and is correct across the wrap boundary.

function, declared at include/shulib/math/angle.hpp:81.

Design commentary, from the header

The header opens with the reasoning behind these shapes. It is reproduced here in full because a reference that only lists signatures teaches nobody why.

The header’s own reasoning — 15 lines
 Angle — a heading on SE(2). The one type that owns angle wrapping, so the
 "degrees into cos/sin" and "359° vs -1°" bug classes are impossible by
 construction.

 Conventions (LOCKED — master plan §7 / Freeze F3):
   * Internal unit: RADIANS. Degrees appear only at this API boundary.
   * Normalization interval: (-π, π]  — half-open at -π; +π is the canonical
     boundary value, so an angle NEVER stores -π (it stores +π instead).
   * errorTo() returns the shortest signed rotation, also in (-π, π]; the
     exact-antipodal case (|Δ| == π) resolves deterministically to +π, never -π.
   * CCW-positive, matching the field frame.

 All construction goes through radians()/degrees(), which reject non-finite
 input — a NaN can never enter the type.

Last updated 2026-08-15 — published from the main release branch, commit fa545ff. This site tracks releases, so between them it can lag the repository — that lag is deliberate, and it is what keeps the development log off the public site. For the current state of the code, see the repository.