// Copyright (C) 2026 Kiyotsugu Arai // SPDX-License-Identifier: LGPL-3.0-or-later // generic_math.hpp // Math-constant helpers that work for both standard and multiprecision floating-point (sangi::Float). // Generalizes the if constexpr branching pattern established in FFT (detail::fft_pi in fft.hpp). // // // Design policy (same as fft_pi): // - This header does not include Float.hpp (kept lightweight). // - The sangi::Float branch (T::pi(...)) is a dependent expression, so it is resolved at the // point of instantiation with T = Float (i.e. when the caller has already included Float.hpp). // - Function calls (sin/cos/sqrt, etc.) need no separate helper because Float.hpp injects // std::cos(Float) etc. into namespace std. This header only handles "constants" (π, etc.) // that cannot be resolved via ADL/std. #ifndef SANGI_GENERIC_MATH_HPP #define SANGI_GENERIC_MATH_HPP #include #include #include #include #include #include namespace sangi { // "Floating-point type" concept. // Accepts both built-in floating-point (float/double/long double) and multiprecision floating-point // (sangi::Float — a class with a defaultPrecision()). // std::floating_point passes only built-in types and rejects sangi::Float, so this is a relaxed // constraint that lets geometry types (AngleAxis/EulerAngles/Transform, etc.) use sangi::Float. // "Exact fields" such as Rational (which lack defaultPrecision()) are excluded — they have no // transcendental functions (sin/cos, etc.), so the internal geometry implementations would not work. // (This trait does not include Float.hpp; it decides by the presence of T::defaultPrecision().) template concept FloatingPointType = std::floating_point || requires { { T::defaultPrecision() } -> std::convertible_to; }; namespace detail { // Pi, π. constants::pi (a constexpr variable template) cannot be instantiated for the non-literal // type Float, so this is its Float-capable counterpart. template [[nodiscard]] inline T generic_pi() { if constexpr (std::is_floating_point_v) { return std::numbers::pi_v; } else if constexpr (requires { T::pi(); }) { // sangi::Float etc.: multiprecision π at the default precision return T::pi(); } else { // fallback for Complex etc. (same precision as constants::pi) return static_cast(3.141592653589793238462643383279502884L); } } // 2π template [[nodiscard]] inline T generic_two_pi() { return T(2) * generic_pi(); } // π/2 template [[nodiscard]] inline T generic_half_pi() { return generic_pi() / T(2); } // Napier's constant e template [[nodiscard]] inline T generic_e() { if constexpr (std::is_floating_point_v) { return std::numbers::e_v; } else if constexpr (requires { T::e(); }) { return T::e(); } else { return static_cast(2.718281828459045235360287471352662498L); } } // atan2(y, x). For multiprecision Float, `using std::atan2; atan2(y,x)` is ambiguous (C2668) // due to the double definition of the std-namespace bridge (std::atan2(Float,Float)) and // sangi::atan2(Float,Float). Here standard floating-point uses std::atan2, and // everything else resolves via ADL only (without a using) (Float → sangi::atan2, // Fixed → Fixed's atan2), avoiding the ambiguity. template [[nodiscard]] inline T generic_atan2(const T& y, const T& x) { if constexpr (std::is_floating_point_v) { return std::atan2(y, x); } else { return atan2(y, x); } } // abs(x). As with atan2, for multiprecision Float `using std::abs; abs(x)` is ambiguous (C2668) // between the std::abs(Float) bridge and sangi::abs(Float) // (std::abs has integer overloads etc., so ADL is not suppressed). // Standard floating-point uses std::abs; everything else resolves via ADL without a using. template [[nodiscard]] inline T generic_abs(const T& x) { if constexpr (std::is_floating_point_v) { return std::abs(x); } else { return abs(x); } } // One-argument transcendental functions. Because Float.hpp injects both the std::X(Float) bridge // into namespace std and sangi::X(Float) into namespace sangi, `using std::X; X(x)` // can be ambiguous (C2668) depending on context (depends on MSVC overload resolution and // is unstable). Here we resolve it reliably with the if constexpr branching established in FFT: // - standard floating-point (double, etc.) → std::X // - everything else (sangi::Float / Fixed, etc.) → bare ADL without a using // (for Float the std bridge is not a candidate, leaving only sangi::X → no ambiguity) #define SANGI_DEFINE_GENERIC_UNARY(NAME) \ template \ [[nodiscard]] inline T generic_##NAME(const T& x) { \ if constexpr (std::is_floating_point_v) { \ return std::NAME(x); \ } else { \ return NAME(x); \ } \ } SANGI_DEFINE_GENERIC_UNARY(sqrt) SANGI_DEFINE_GENERIC_UNARY(cbrt) SANGI_DEFINE_GENERIC_UNARY(sin) SANGI_DEFINE_GENERIC_UNARY(cos) SANGI_DEFINE_GENERIC_UNARY(tan) SANGI_DEFINE_GENERIC_UNARY(asin) SANGI_DEFINE_GENERIC_UNARY(acos) SANGI_DEFINE_GENERIC_UNARY(atan) SANGI_DEFINE_GENERIC_UNARY(exp) SANGI_DEFINE_GENERIC_UNARY(log) SANGI_DEFINE_GENERIC_UNARY(sinh) SANGI_DEFINE_GENERIC_UNARY(cosh) SANGI_DEFINE_GENERIC_UNARY(tanh) SANGI_DEFINE_GENERIC_UNARY(asinh) SANGI_DEFINE_GENERIC_UNARY(acosh) SANGI_DEFINE_GENERIC_UNARY(atanh) SANGI_DEFINE_GENERIC_UNARY(floor) SANGI_DEFINE_GENERIC_UNARY(ceil) SANGI_DEFINE_GENERIC_UNARY(round) SANGI_DEFINE_GENERIC_UNARY(trunc) SANGI_DEFINE_GENERIC_UNARY(exp2) SANGI_DEFINE_GENERIC_UNARY(expm1) SANGI_DEFINE_GENERIC_UNARY(log2) SANGI_DEFINE_GENERIC_UNARY(log10) SANGI_DEFINE_GENERIC_UNARY(log1p) SANGI_DEFINE_GENERIC_UNARY(erf) SANGI_DEFINE_GENERIC_UNARY(erfc) SANGI_DEFINE_GENERIC_UNARY(tgamma) SANGI_DEFINE_GENERIC_UNARY(lgamma) #undef SANGI_DEFINE_GENERIC_UNARY // Two-argument transcendental function (helper-ized for the same reason as atan2). #define SANGI_DEFINE_GENERIC_BINARY(NAME) \ template \ [[nodiscard]] inline T generic_##NAME(const T& a, const T& b) { \ if constexpr (std::is_floating_point_v) { \ return std::NAME(a, b); \ } else { \ return NAME(a, b); \ } \ } SANGI_DEFINE_GENERIC_BINARY(pow) SANGI_DEFINE_GENERIC_BINARY(hypot) SANGI_DEFINE_GENERIC_BINARY(fmod) #undef SANGI_DEFINE_GENERIC_BINARY // Integer power x^n. The general pow (exp(n·log x)) breaks for negative bases, so this is dedicated. // For Float, sangi::pow(Float,int) is resolved via ADL (no collision with the std bridge without a using). template [[nodiscard]] inline T generic_powi(const T& x, int n) { if constexpr (std::is_floating_point_v) { return std::pow(x, n); } else { return pow(x, n); } } // ---- conversion from type T to a built-in type ---- // Multiprecision Float has no static_cast conversion operators to double/int; use .toDouble()/ // .toInt64()/.toSizet(). These helpers let you write conversions that work whether T is double or Float. // // - to_double: ★loses precision (Float→double). Use only where double precision suffices, // such as deciding logs / thresholds / scaling exponents. Do not use for the actual numerics. // - to_int64 / to_sizet: for index computation. Float converts directly to an integer without going through double. template [[nodiscard]] inline double to_double(const T& x) { if constexpr (std::is_arithmetic_v) return static_cast(x); else return x.toDouble(); } template [[nodiscard]] inline std::int64_t to_int64(const T& x) { if constexpr (std::is_arithmetic_v) return static_cast(x); else return x.toInt64(); } template [[nodiscard]] inline std::size_t to_sizet(const T& x) { if constexpr (std::is_arithmetic_v) return static_cast(x); else return x.toSizet(); } } // namespace detail } // namespace sangi #endif // SANGI_GENERIC_MATH_HPP