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: 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 | |