| 1 | // Copyright (C) 2018 The Qt Company Ltd. | 
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only | 
| 3 |  | 
| 4 | #include <trimpath_p.h> | 
| 5 | #include <private/qpainterpath_p.h> | 
| 6 | #include <private/qbezier_p.h> | 
| 7 | #include <QtMath> | 
| 8 |  | 
| 9 | QT_BEGIN_NAMESPACE | 
| 10 |  | 
| 11 | /* | 
| 12 | Returns the path trimmed to length fractions f1, f2, in range [0.0, 1.0]. | 
| 13 | f1 and f2 are displaced, with wrapping, by the fractional part of offset, effective range <-1.0, 1.0> | 
| 14 | */ | 
| 15 | QPainterPath TrimPath::trimmed(qreal f1, qreal f2, qreal offset) const | 
| 16 | { | 
| 17 |     QPainterPath res; | 
| 18 |     if (mPath.isEmpty() || !mPath.elementAt(i: 0).isMoveTo()) | 
| 19 |         return res; | 
| 20 |  | 
| 21 |     f1 = qBound(min: qreal(0.0), val: f1, max: qreal(1.0)); | 
| 22 |     f2 = qBound(min: qreal(0.0), val: f2, max: qreal(1.0)); | 
| 23 |     if (qFuzzyCompare(p1: f1, p2: f2)) | 
| 24 |         return res; | 
| 25 |     if (f1 > f2) | 
| 26 |         qSwap(value1&: f1, value2&: f2); | 
| 27 |     if (qFuzzyCompare(p1: f2 - f1, p2: qreal(1.0))) // Shortcut for no trimming | 
| 28 |         return mPath; | 
| 29 |  | 
| 30 |     qreal dummy; | 
| 31 |     offset = std::modf(x: offset, iptr: &dummy);    // Use only the fractional part of offset, range <-1, 1> | 
| 32 |  | 
| 33 |     qreal of1 = f1 + offset; | 
| 34 |     qreal of2 = f2 + offset; | 
| 35 |     if (offset < 0.0) { | 
| 36 |         f1 = of1 < 0.0 ? of1 + 1.0 : of1; | 
| 37 |         f2 = of2 + 1.0 > 1.0 ? of2 : of2 + 1.0; | 
| 38 |     } else if (offset > 0.0) { | 
| 39 |         f1 = of1 - 1.0 < 0.0 ? of1 : of1 - 1.0; | 
| 40 |         f2 = of2 > 1.0 ? of2 - 1.0 : of2; | 
| 41 |     } | 
| 42 |     bool wrapping = (f1 > f2); | 
| 43 |     //qDebug() << "ADJ:" << f1 << f2 << wrapping << "(" << of1 << of2 << ")"; | 
| 44 |  | 
| 45 |     if (lensIsDirty()) | 
| 46 |         updateLens(); | 
| 47 |     qreal totLen = mLens.last(); | 
| 48 |     if (qFuzzyIsNull(d: totLen)) | 
| 49 |         return res; | 
| 50 |  | 
| 51 |     qreal l1 = f1 * totLen; | 
| 52 |     qreal l2 = f2 * totLen; | 
| 53 |     const int e1 = elementAtLength(len: l1); | 
| 54 |     const bool mustTrimE1 = !qFuzzyCompare(p1: mLens.at(i: e1), p2: l1); | 
| 55 |     const int e2 = elementAtLength(len: l2); | 
| 56 |     const bool mustTrimE2 = !qFuzzyCompare(p1: mLens.at(i: e2), p2: l2); | 
| 57 |  | 
| 58 |     //qDebug() << "Trim [" << f1 << f2 << "] e1:" << e1 << mustTrimE1 << "e2:" << e2 << mustTrimE2 << "wrapping:" << wrapping; | 
| 59 |  | 
| 60 |     if (e1 == e2 && !wrapping && mustTrimE1 && mustTrimE2) { | 
| 61 |         // Entire result is one element, clipped in both ends | 
| 62 |         appendTrimmedElement(to: &res, elemIdx: e1, trimStart: true, startLen: l1, trimEnd: true, endLen: l2); | 
| 63 |     } else { | 
| 64 |         // Partial start element, or just its end point | 
| 65 |         if (mustTrimE1) | 
| 66 |             appendEndOfElement(to: &res, elemIdx: e1, len: l1); | 
| 67 |         else | 
| 68 |             res.moveTo(p: endPointOfElement(elemIdx: e1)); | 
| 69 |  | 
| 70 |         // Complete elements between start and end | 
| 71 |         if (wrapping) { | 
| 72 |             appendElementRange(to: &res, first: e1 + 1, last: mPath.elementCount() - 1); | 
| 73 |             res.moveTo(p: mPath.elementAt(i: 0)); | 
| 74 |             appendElementRange(to: &res, first: 1, last: (mustTrimE2 ? e2 - 1 : e2)); | 
| 75 |         } else { | 
| 76 |             appendElementRange(to: &res, first: e1 + 1, last: (mustTrimE2 ? e2 - 1 : e2)); | 
| 77 |         } | 
| 78 |  | 
| 79 |         // Partial end element | 
| 80 |         if (mustTrimE2) | 
| 81 |             appendStartOfElement(to: &res, elemIdx: e2, len: l2); | 
| 82 |     } | 
| 83 |     return res; | 
| 84 | } | 
| 85 |  | 
| 86 | void TrimPath::updateLens() const | 
| 87 | { | 
| 88 |     const int numElems = mPath.elementCount(); | 
| 89 |     mLens.resize(size: numElems); | 
| 90 |     if (!numElems) | 
| 91 |         return; | 
| 92 |  | 
| 93 |     QPointF runPt = mPath.elementAt(i: 0); | 
| 94 |     qreal runLen = 0.0; | 
| 95 |     for (int i = 0; i < numElems; i++) { | 
| 96 |         QPainterPath::Element e = mPath.elementAt(i); | 
| 97 |         switch (e.type) { | 
| 98 |         case QPainterPath::LineToElement: | 
| 99 |             runLen += QLineF(runPt, e).length(); | 
| 100 |             runPt = e; | 
| 101 |             break; | 
| 102 |         case QPainterPath::CurveToElement: { | 
| 103 |             Q_ASSERT(i < numElems - 2); | 
| 104 |             QPainterPath::Element ee = mPath.elementAt(i: i + 2); | 
| 105 |             runLen += QBezier::fromPoints(p1: runPt, p2: e, p3: mPath.elementAt(i: i + 1), p4: ee).length(); | 
| 106 |             runPt = ee; | 
| 107 |             break; | 
| 108 |         } | 
| 109 |         case QPainterPath::MoveToElement: | 
| 110 |             runPt = e; | 
| 111 |             break; | 
| 112 |         case QPainterPath::CurveToDataElement: | 
| 113 |             break; | 
| 114 |         } | 
| 115 |         mLens[i] = runLen; | 
| 116 |     } | 
| 117 | } | 
| 118 |  | 
| 119 | int TrimPath::elementAtLength(qreal len) const | 
| 120 | { | 
| 121 |     const auto it = std::lower_bound(first: mLens.constBegin(), last: mLens.constEnd(), val: len); | 
| 122 |     return (it == mLens.constEnd()) ? mLens.size() - 1 : int(it - mLens.constBegin()); | 
| 123 | } | 
| 124 |  | 
| 125 | QPointF TrimPath::endPointOfElement(int elemIdx) const | 
| 126 | { | 
| 127 |     QPainterPath::Element e = mPath.elementAt(i: elemIdx); | 
| 128 |     if (e.isCurveTo()) | 
| 129 |         return mPath.elementAt(i: qMin(a: elemIdx + 2, b: mPath.elementCount() - 1)); | 
| 130 |     else | 
| 131 |         return e; | 
| 132 | } | 
| 133 |  | 
| 134 | void TrimPath::appendTrimmedElement(QPainterPath *to, int elemIdx, bool trimStart, qreal startLen, bool trimEnd, qreal endLen) const | 
| 135 | { | 
| 136 |     Q_ASSERT(elemIdx > 0); | 
| 137 |  | 
| 138 |     if (lensIsDirty()) | 
| 139 |         updateLens(); | 
| 140 |  | 
| 141 |     qreal prevLen = mLens.at(i: elemIdx - 1); | 
| 142 |     qreal elemLen = mLens.at(i: elemIdx) - prevLen; | 
| 143 |     qreal len1 = startLen - prevLen; | 
| 144 |     qreal len2 = endLen - prevLen; | 
| 145 |     if (qFuzzyIsNull(d: elemLen)) | 
| 146 |         return; | 
| 147 |  | 
| 148 |     QPointF pp = mPath.elementAt(i: elemIdx - 1); | 
| 149 |     QPainterPath::Element e = mPath.elementAt(i: elemIdx); | 
| 150 |     if (e.isLineTo()) { | 
| 151 |         QLineF l(pp, e); | 
| 152 |         QPointF p1 = trimStart ? l.pointAt(t: len1 / elemLen) : pp; | 
| 153 |         QPointF p2 = trimEnd ? l.pointAt(t: len2 / elemLen) : e; | 
| 154 |         if (to->isEmpty()) | 
| 155 |             to->moveTo(p: p1); | 
| 156 |         to->lineTo(p: p2); | 
| 157 |     } else if (e.isCurveTo()) { | 
| 158 |         Q_ASSERT(elemIdx < mPath.elementCount() - 2); | 
| 159 |  | 
| 160 |         QBezier b = QBezier::fromPoints(p1: pp, p2: e, p3: mPath.elementAt(i: elemIdx + 1), p4: mPath.elementAt(i: elemIdx + 2)); | 
| 161 |         qreal t1 = trimStart ? b.tAtLength(len: len1) : 0.0;  // or simply len1/elemLen to trim by t instead of len | 
| 162 |         qreal t2 = trimEnd ? b.tAtLength(len: len2) : 1.0; | 
| 163 |         QBezier c = b.getSubRange(t0: t1, t1: t2); | 
| 164 |         if (to->isEmpty()) | 
| 165 |             to->moveTo(p: c.pt1()); | 
| 166 |         to->cubicTo(ctrlPt1: c.pt2(), ctrlPt2: c.pt3(), endPt: c.pt4()); | 
| 167 |     } | 
| 168 |     else { | 
| 169 |         Q_UNREACHABLE(); | 
| 170 |     } | 
| 171 | } | 
| 172 |  | 
| 173 | void TrimPath::appendElementRange(QPainterPath *to, int first, int last) const | 
| 174 | { | 
| 175 |     //# (in QPPP, could do direct vector copy, better performance) | 
| 176 |     if (first >= mPath.elementCount() || last >= mPath.elementCount()) | 
| 177 |         return; | 
| 178 |  | 
| 179 |     for (int i = first; i <= last; i++) { | 
| 180 |         QPainterPath::Element e = mPath.elementAt(i); | 
| 181 |         switch (e.type) { | 
| 182 |         case QPainterPath::MoveToElement: | 
| 183 |             to->moveTo(p: e); | 
| 184 |             break; | 
| 185 |         case QPainterPath::LineToElement: | 
| 186 |             to->lineTo(p: e); | 
| 187 |             break; | 
| 188 |         case QPainterPath::CurveToElement: | 
| 189 |             Q_ASSERT(i < mPath.elementCount() - 2); | 
| 190 |             to->cubicTo(ctrlPt1: e, ctrlPt2: mPath.elementAt(i: i + 1), endPt: mPath.elementAt(i: i + 2)); | 
| 191 |             i += 2; | 
| 192 |             break; | 
| 193 |         default: | 
| 194 |             // 'first' may point to CurveToData element, just skip it | 
| 195 |             break; | 
| 196 |         } | 
| 197 |     } | 
| 198 | } | 
| 199 |  | 
| 200 | QT_END_NAMESPACE | 
| 201 |  |