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