File indexing completed on 2024-05-12 04:44:33
0001 // SPDX-FileCopyrightText: Lukas Sommer <sommerluk@gmail.com> 0002 // SPDX-License-Identifier: BSD-2-Clause OR MIT 0003 0004 #ifndef HELPERMATH_H 0005 #define HELPERMATH_H 0006 0007 #include <cmath> 0008 #include <limits> 0009 #include <optional> 0010 #include <qgenericmatrix.h> 0011 #include <qglobal.h> 0012 #include <qmetatype.h> 0013 #include <stdlib.h> 0014 #include <type_traits> 0015 0016 /** @internal 0017 * 0018 * @file 0019 * 0020 * Mathematical helper functions. */ 0021 0022 namespace PerceptualColor 0023 { 0024 0025 /** @internal 0026 * 0027 * @brief A vector with 4 elements (double precision). 0028 * 0029 * This type is declared as type to Qt’s type system via 0030 * <tt>Q_DECLARE_METATYPE</tt>. Depending on your use case (for 0031 * example if you want to use for <em>queued</em> signal-slot connections), 0032 * you might consider calling <tt>qRegisterMetaType()</tt> for 0033 * this type, once you have a QApplication object. */ 0034 using Quartet = QGenericMatrix<1, 4, double>; 0035 0036 /** @internal 0037 * 0038 * @brief A 3×3 matrix (double precision). 0039 * 0040 * This type is declared as type to Qt’s type system via 0041 * <tt>Q_DECLARE_METATYPE</tt>. Depending on your use case (for 0042 * example if you want to use for <em>queued</em> signal-slot connections), 0043 * you might consider calling <tt>qRegisterMetaType()</tt> for 0044 * this type, once you have a QApplication object. */ 0045 using SquareMatrix3 = QGenericMatrix<3, 3, double>; 0046 0047 /** @internal 0048 * 0049 * @brief A vector with 3 elements (double precision). 0050 * 0051 * This type is declared as type to Qt’s type system via 0052 * <tt>Q_DECLARE_METATYPE</tt>. Depending on your use case (for 0053 * example if you want to use for <em>queued</em> signal-slot connections), 0054 * you might consider calling <tt>qRegisterMetaType()</tt> for 0055 * this type, once you have a QApplication object. 0056 * 0057 * @sa @ref createTrio() */ 0058 using Trio = QGenericMatrix<1, 3, double>; 0059 0060 /** @internal 0061 * 0062 * @brief Convenience constructor for QGenericMatrix. 0063 * 0064 * @tparam N columns 0065 * @tparam M rows 0066 * @tparam T typename 0067 * @param args Initialization values. The number of arguments must be 0068 * exactly N × M. 0069 * 0070 * @returns The corresponding QGenericMatrix. */ 0071 template<int N, int M, typename T, typename... Args> 0072 [[nodiscard]] constexpr QGenericMatrix<N, M, T> createMatrix(Args... args) 0073 { 0074 // Too few arguments leave values uninitialized, too many arguments 0075 // result in compiler warnings. 0076 static_assert(sizeof...(args) == N * M, "Invalid number of arguments."); 0077 const T valueArray[] = {args...}; 0078 return QGenericMatrix<N, M, T>(valueArray); 0079 } 0080 0081 SquareMatrix3 createSquareMatrix3(double r0c0, double r0c1, double r0c2, double r1c0, double r1c1, double r1c2, double r2c0, double r2c1, double r2c2); 0082 0083 Trio createTrio(double first, double second, double third); 0084 0085 int decimalPlaces(const int rangeMax, const int significantFigures); 0086 0087 std::optional<SquareMatrix3> inverseMatrix(const SquareMatrix3 &matrix); 0088 0089 /** @internal 0090 * 0091 * @brief Template function to test if a value is within a certain range 0092 * @param low the lower limit 0093 * @param x the value that will be tested 0094 * @param high the higher limit 0095 * @returns @snippet this Helper isInRange */ 0096 template<typename T> 0097 [[nodiscard]] constexpr bool isInRange(const T &low, const T &x, const T &high) 0098 { 0099 return ( 0100 // The Doxygen comments contain @private because apparently 0101 // the tag @internal is not enough to hide it in the API documentation. 0102 // The snippet marker [] is hidden within HTML comments to avoid 0103 // that is shows up literally in the private documentation, and this 0104 // independent from the HIDE_IN_BODY_DOCS parameter in Doxyfile. 0105 //! @private @internal <!-- [Helper isInRange] --> 0106 (low <= x) && (x <= high) 0107 //! @private @internal <!-- [Helper isInRange] --> 0108 ); 0109 } 0110 0111 /** @internal 0112 * 0113 * @brief Test if an integer is odd. 0114 * 0115 * @param number The number to test. Must be an integer type 0116 * 0117 * @returns <tt>true</tt> if the number is odd, <tt>false</tt> otherwise. */ 0118 template<typename T> 0119 [[nodiscard]] constexpr bool isOdd(const T &number) 0120 { 0121 static_assert(std::is_integral_v<T>, // 0122 "Template isOdd() only works with integer types."); 0123 constexpr T two = 2; 0124 return static_cast<bool>(number % two); 0125 } 0126 0127 /** @internal 0128 * 0129 * @brief Test if two floating point values are nearly equal. 0130 * 0131 * Comparison is done in a relative way, where the 0132 * exactness is stronger the smaller the numbers are. 0133 * <a href="https://embeddeduse.com/2019/08/26/qt-compare-two-floats/"> 0134 * This is the reasonable behaviour for floating point comparisons.</a> 0135 * Unlike <tt>qFuzzyCompare</tt> and <tt>qFuzzyIsNull</tt> this function 0136 * works for both cases: numbers near to 0 and numbers far from 0. 0137 * 0138 * @tparam T Must be a floating point type. 0139 * @param a one of the values to compare 0140 * @param b one of the values to compare 0141 * @param epsilon indicator for desired precision assuming that the values 0142 * to compare are close to 1. Values lower the the compiler-epsilon 0143 * from type T will be replaced by the compiler-epsilon from type T. 0144 * If epsilon is infinity or near to the maximum value of type T, 0145 * the result of this function might be wrong. 0146 * @returns <tt>true</tt> if the values are nearly equal. <tt>false</tt> 0147 * otherwise. 0148 * 0149 * @sa @ref isNearlyEqual(A a, B b) provides a default epsilon. */ 0150 template<typename T> 0151 [[nodiscard]] constexpr bool isNearlyEqual(T a, T b, T epsilon) 0152 { 0153 static_assert( // 0154 std::is_floating_point<T>::value, // 0155 "Template isNearlyEqual(T a, T b, T epsilon) only works with floating point types"); 0156 0157 // Implementation based on https://stackoverflow.com/a/32334103 0158 const auto actualEpsilon = // 0159 qMax(std::numeric_limits<T>::epsilon(), epsilon); 0160 0161 if ((a == b) && (!std::isnan(epsilon))) { 0162 // Not explicitly checking if a or b are NaN, because if any of those 0163 // is NaN, the code above will return “false” anyway. 0164 return true; 0165 } 0166 0167 const auto norm = qMin<T>( // 0168 qAbs(a) + qAbs(b), // 0169 std::numeric_limits<T>::max()); 0170 return std::abs(a - b) < std::max(actualEpsilon, actualEpsilon * norm); 0171 } 0172 0173 /** @internal 0174 * 0175 * @brief Test if two floating point values are nearly equal, using 0176 * a default epsilon. 0177 * 0178 * Calls @ref isNearlyEqual(T a, T b, T epsilon) with a default epsilon 0179 * who’s value depends on the type with <em>less</em> precision among A and B. 0180 * 0181 * @tparam A Must be a floating point type. 0182 * @tparam B Must be a floating point type. 0183 * @param a one of the values to compare 0184 * @param b one of the values to compare 0185 * @returns <tt>true</tt> if the values are nearly equal. <tt>false</tt> 0186 * otherwise. */ 0187 template<typename A, typename B> 0188 [[nodiscard]] constexpr bool isNearlyEqual(A a, B b) 0189 { 0190 static_assert( // 0191 std::is_floating_point<A>::value, // 0192 "Template isNearlyEqual(A a, B b) only works with floating point types"); 0193 static_assert( // 0194 std::is_floating_point<B>::value, // 0195 "Template isNearlyEqual(A a, B b) only works with floating point types"); 0196 0197 // Define a factor to multiply with. Our epsilon has to be bigger than 0198 // std::numeric_limits<>::epsilon(), which represents the smallest 0199 // representable difference for the value 1.0. Doing various consecutive 0200 // floating point operations will increase the error, therefore we need 0201 // a factor with which we multiply std::numeric_limits<>::epsilon(). 0202 // The choice is somewhat arbitrary. Qt’s qFuzzyCompare uses this: 0203 // 0204 // float: 0205 // std::numeric_limits<>::epsilon() is around 1.2e-07. 0206 // Qt uses 1e-5. 0207 // Factor is around 100 0208 // 0209 // double: 0210 // std::numeric_limits<>::epsilon() is around 2.2e-16. 0211 // Qt uses 1e-12. 0212 // Factor is around 5000 0213 // 0214 // long double: 0215 // std::numeric_limits<>::epsilon() might vary depending on implementation, 0216 // as “long double” might have different sizes on different implementations. 0217 // Qt does not support “long double” in qFuzzyCompare. 0218 constexpr auto factor = 100; 0219 0220 // Use the type with less precision to get epsilon, but use the type 0221 // with more precision to the the actual comparison. 0222 if constexpr (sizeof(A) > sizeof(B)) { 0223 return PerceptualColor::isNearlyEqual<A>( // 0224 a, // 0225 static_cast<A>(b), // 0226 static_cast<A>(std::numeric_limits<B>::epsilon() * factor)); 0227 } else { 0228 return PerceptualColor::isNearlyEqual<B>( // 0229 static_cast<B>(a), // 0230 b, // 0231 static_cast<B>(std::numeric_limits<A>::epsilon() * factor)); 0232 } 0233 } 0234 0235 /** @internal 0236 * 0237 * @brief Normalizes an angle. 0238 * 0239 * | Value | Normalized Value | 0240 * | :-------------: | :--------------: | 0241 * | <tt> 0° </tt> | <tt> 0° </tt> | 0242 * | <tt>359.9°</tt> | <tt>359.9°</tt> | 0243 * | <tt>360° </tt> | <tt> 0° </tt> | 0244 * | <tt>361.2°</tt> | <tt> 1.2°</tt> | 0245 * | <tt>720° </tt> | <tt> 0° </tt> | 0246 * | <tt> −1° </tt> | <tt>359° </tt> | 0247 * | <tt> −1.3°</tt> | <tt>358.7°</tt> | 0248 * 0249 * @param value an angle (coordinates in degree) 0250 * @returns the value, normalized to the range 0° ≤ value < 360° */ 0251 template<typename T> 0252 T normalizedAngle360(T value) 0253 { 0254 static_assert( // 0255 std::is_floating_point<T>::value, // 0256 "Template normalizeAngle360() only works with floating point types"); 0257 constexpr T min = 0; 0258 constexpr T max = 360; 0259 qreal temp = fmod(value, max); 0260 if (temp < min) { 0261 temp += max; 0262 } 0263 return temp; 0264 } 0265 0266 /** @internal 0267 * 0268 * @brief Normalizes polar coordinates. 0269 * 0270 * @param radius Reference to the radius. It will get normalized to value ≥ 0. 0271 * If it was < 0 (but not if it was 0 with a negative sign) its sign 0272 * is changed and angleDegree is turned by 180°. 0273 * @param angleDegree Reference to the angle (measured in degree). It will get 0274 * normalized to 0° ≤ value < 360° (see @ref normalizedAngle360() for 0275 * details) 0276 * 0277 * @note When the radius is 0, one could set by convention the (meaningless) 0278 * angle also 0. However, note that this function does <em>not</em> do that! */ 0279 template<typename T> 0280 void normalizePolar360(T &radius, T &angleDegree) 0281 { 0282 if (radius < 0) { 0283 radius *= (-1); 0284 angleDegree = normalizedAngle360(angleDegree + 180); 0285 } else { 0286 angleDegree = normalizedAngle360(angleDegree); 0287 } 0288 } 0289 0290 /** @internal 0291 * 0292 * @brief Round floating point numbers to a certain number of digits 0293 * 0294 * @tparam T a floating point type 0295 * @param value the value that will be rounded 0296 * @param precision the number of decimal places to which rounding takes place 0297 * @returns the rounded value */ 0298 template<typename T> 0299 [[nodiscard]] constexpr T roundToDigits(T value, int precision) 0300 { 0301 static_assert( // 0302 std::is_floating_point<T>::value, // 0303 "Template roundToDigits() only works with floating point types"); 0304 const T multiplier = std::pow( 0305 // Make sure that pow returns a T: 0306 static_cast<T>(10), 0307 precision); 0308 return std::round(value * multiplier) / multiplier; 0309 } 0310 0311 } // namespace PerceptualColor 0312 0313 Q_DECLARE_METATYPE(PerceptualColor::Quartet) 0314 Q_DECLARE_METATYPE(PerceptualColor::SquareMatrix3) 0315 Q_DECLARE_METATYPE(PerceptualColor::Trio) 0316 0317 #endif // HELPERMATH_H