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
9QT_BEGIN_NAMESPACE
10
11/*
12Returns the path trimmed to length fractions f1, f2, in range [0.0, 1.0].
13f1 and f2 are displaced, with wrapping, by the fractional part of offset, effective range <-1.0, 1.0>
14*/
15QPainterPath 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
86void 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
119int 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
125QPointF 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
134void 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
173void 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
200QT_END_NAMESPACE
201

source code of qtlottie/src/bodymovin/trimpath.cpp