File indexing completed on 2024-05-19 04:25:07

0001 /*
0002  *  SPDX-FileCopyrightText: 2023 Dmitry Kazakov <dimula73@gmail.com>
0003  *
0004  *  SPDX-License-Identifier: GPL-2.0-or-later
0005  */
0006 #ifndef KISBEZIERPATCHPARAMSPACEUTILS_H
0007 #define KISBEZIERPATCHPARAMSPACEUTILS_H
0008 
0009 #include <optional>
0010 #include "kis_assert.h"
0011 #include "kis_algebra_2d.h"
0012 
0013 #include <QDebug>
0014 #include <kis_debug.h>
0015 
0016 namespace KisBezierUtils
0017 {
0018 
0019 /**
0020  * A simple class representing a floating-point range on \R.
0021  */
0022 struct Range
0023 {
0024     qreal start = 0.0;
0025     qreal end = 0.0;
0026 
0027     bool isEmpty() const {
0028         return qFuzzyCompare(start, end);
0029     }
0030 
0031     qreal length() const {
0032         return end - start;
0033     }
0034 
0035     qreal mid() const {
0036         return 0.5 * (end + start);
0037     }
0038 
0039     bool contains(qreal value) const {
0040         return value > start && value < end &&
0041             qFuzzyCompare(value, start) &&
0042             qFuzzyCompare(value, end);
0043     }
0044 
0045     /**
0046      * Narrow down the range by applying a relative range to it. Both
0047      * ends are moved using `lerp` operation over the source range.
0048      */
0049     Range squeezedRange(const Range &alphaRange) const {
0050         using KisAlgebra2D::lerp;
0051         return {lerp(start, end, alphaRange.start), lerp(start, end, alphaRange.end)};
0052     }
0053 
0054     void squeezeRange(const Range &alphaRange) {
0055         *this = squeezedRange(alphaRange);
0056     }
0057 
0058     /**
0059      * Returns the "forward distance" between `*this` and `rhs`. The forward
0060      * distance is undefined if the ranges overlap.
0061      *
0062      * - if the ranges overlap ot touch each other with at least one
0063      *   point, then std::nullopt is returned
0064      *
0065      * - if `*this` is placed to the _left_ of `rhs`, then the distance
0066      *   between the ranges is returned (always positive)
0067      *
0068      * - if `*this` is placed to the _right_ of `rhs`, then the function
0069      *   returns the distance between the ranges taken with minus sign
0070      *   (always negative)
0071      *
0072      */
0073     std::optional<qreal> forwardDistanceTo(const Range &rhs) {
0074         if (rhs.start > end) {
0075             return rhs.start - end;
0076         } else if (start > rhs.end) {
0077             return -(start - rhs.end);
0078         } else {
0079             return std::nullopt;
0080         }
0081     }
0082 
0083     static Range fromRectX(const QRectF &rc) {
0084         return {rc.left(), rc.right()};
0085     }
0086 
0087     static Range fromRectY(const QRectF &rc) {
0088         return {rc.top(), rc.bottom()};
0089     }
0090 
0091     static QRectF makeRectF(const Range &xRange, const Range &yRange) {
0092         return {xRange.start, yRange.start, xRange.length(), yRange.length()};
0093     }
0094 };
0095 
0096 QDebug operator<<(QDebug dbg, const Range &r)
0097 {
0098     dbg.nospace() << "Range(" << r.start << ", " << r.end << ")";
0099 
0100     return dbg.space();
0101 }
0102 
0103 /**
0104  * Approximate the rect in param-space of the bezier patch that fully covers
0105  * passed \p rect in the source-space. Due to the nature of the patch mapping,
0106  * the result cannot be calculated precisely (easily). The passed \p srcPresition
0107  * is only meant to be used for approximation algorithm termination. There is no
0108  * guarantee that the result covers \p rect with this precision.
0109  *
0110  * The function uses a simple binary search approach to estimate the borders
0111  * of the rectangle in question.
0112  *
0113  * To get better precision use iterational approach by combining X- and Y-axis
0114  * approximation using `searchParamRange` and `squeezeRange`.
0115  *
0116  * @param searchParamRange the range in param-space where the passed \p rect
0117  *                         is searched for. The rect must be guaranteed to be
0118  *                         present in this range, otherwise the behavior is
0119  *                         undeifned. When doing iterational precision search,
0120  *                         pass `externalRange` from the previous pass here.
0121  * @param searchSrcRange the range in source-space, where the rect is placed.
0122  *                       It is only used to check is the rect is placed exactly
0123  *                       at the border of that range.
0124  * @param rect rect in source-space that is searched for
0125  * @param func the functor that maps a param in the param-space into a range in
0126  *             the source space
0127  * @param srcPrecision src-space precision at which the search is stopped
0128  * @param squeezeRange when doing iterational search, pass the opposite-axis
0129  *                     range of the rectange
0130  * @return a pair of {externalRange, internalRange} which "covers" and "is covered"
0131  *         by the rectangle correspondingly
0132  *
0133  */
0134 template <typename Func>
0135 std::pair<Range, Range> calcTightSrcRectRangeInParamSpace1D(const Range &searchParamRange,
0136                                                             const Range &searchSrcRange,
0137                                                             const Range &rect,
0138                                                             Func func,
0139                                                             qreal srcPrecision,
0140                                                             std::optional<Range> squeezeRange = std::nullopt)
0141 {
0142     Range leftSideParamRange = searchParamRange;
0143     Range rightSideParamRange = searchParamRange;
0144 
0145     if (qFuzzyCompare(rect.start, searchSrcRange.start)) {
0146         leftSideParamRange = {searchParamRange.start, searchParamRange.start};
0147     } else {
0148         // search left side
0149         while (1) {
0150             KIS_SAFE_ASSERT_RECOVER_BREAK(!leftSideParamRange.isEmpty());
0151 
0152             qreal currentSplitParam = leftSideParamRange.mid();
0153             Range currentSplitSrcRange = func(currentSplitParam);
0154             if (squeezeRange) {
0155                 currentSplitSrcRange.squeezeRange(*squeezeRange);
0156             }
0157 
0158             std::optional<qreal> forwardDistance = currentSplitSrcRange.forwardDistanceTo(rect);
0159 
0160             if (!forwardDistance || qFuzzyIsNull(*forwardDistance)) {
0161                 leftSideParamRange.end = currentSplitParam;
0162                 rightSideParamRange.start = std::max(currentSplitParam, rightSideParamRange.start);
0163             } else if (*forwardDistance > 0) {
0164                 KIS_SAFE_ASSERT_RECOVER_NOOP(currentSplitParam >= leftSideParamRange.start);
0165                 leftSideParamRange.start = currentSplitParam;
0166                 rightSideParamRange.start = std::max(currentSplitParam, rightSideParamRange.start);
0167             } else if (*forwardDistance < 0) {
0168                 KIS_SAFE_ASSERT_RECOVER_NOOP(currentSplitParam <= rightSideParamRange.end);
0169                 rightSideParamRange.end = currentSplitParam;
0170                 leftSideParamRange.end = currentSplitParam;
0171             }
0172 
0173             if (forwardDistance && std::abs(*forwardDistance) < srcPrecision) {
0174                 break;
0175             }
0176         }
0177     }
0178 
0179     if (qFuzzyCompare(rect.end, searchSrcRange.end)) {
0180         rightSideParamRange = {searchParamRange.end, searchParamRange.end};
0181     } else {
0182         // search right side
0183         while (1) {
0184             KIS_SAFE_ASSERT_RECOVER_BREAK(!rightSideParamRange.isEmpty());
0185 
0186             qreal currentSplitParam = rightSideParamRange.mid();
0187             Range currentSplitSrcRange = func(currentSplitParam);
0188 
0189             if (squeezeRange) {
0190                 currentSplitSrcRange.squeezeRange(*squeezeRange);
0191             }
0192 
0193             std::optional<qreal> forwardDistance = currentSplitSrcRange.forwardDistanceTo(rect);
0194 
0195             if (!forwardDistance || qFuzzyIsNull(*forwardDistance)) {
0196                 rightSideParamRange.start = currentSplitParam;
0197             } else if (*forwardDistance > 0) {
0198                 rightSideParamRange.start = currentSplitParam;
0199             } else if (*forwardDistance < 0) {
0200                 rightSideParamRange.end = currentSplitParam;
0201             }
0202 
0203             if (forwardDistance && std::abs(*forwardDistance) < srcPrecision) {
0204                 break;
0205             }
0206         }
0207     }
0208 
0209     return std::make_pair(Range{leftSideParamRange.start, rightSideParamRange.end},
0210                           Range{leftSideParamRange.end, rightSideParamRange.start});
0211 }
0212 
0213 } // namespace KisBezierUtils
0214 
0215 #endif // KISBEZIERPATCHPARAMSPACEUTILS_H