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