| /**************************************************************************** |
| ** |
| ** Copyright (C) 2018 The Qt Company Ltd. |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the lottie-qt module of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:GPL$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/contact-us. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 3 or (at your option) any later version |
| ** approved by the KDE Free Qt Foundation. The licenses are as published by |
| ** the Free Software Foundation and appearing in the file LICENSE.GPL3 |
| ** included in the packaging of this file. Please review the following |
| ** information to ensure the GNU General Public License requirements will |
| ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| #include <trimpath_p.h> |
| #include <private/qpainterpath_p.h> |
| #include <private/qbezier_p.h> |
| #include <QtMath> |
| |
| QT_BEGIN_NAMESPACE |
| |
| /* |
| Returns the path trimmed to length fractions f1, f2, in range [0.0, 1.0]. |
| f1 and f2 are displaced, with wrapping, by the fractional part of offset, effective range <-1.0, 1.0> |
| */ |
| QPainterPath TrimPath::trimmed(qreal f1, qreal f2, qreal offset) const |
| { |
| QPainterPath res; |
| if (mPath.isEmpty() || !mPath.elementAt(0).isMoveTo()) |
| return res; |
| |
| f1 = qBound(qreal(0.0), f1, qreal(1.0)); |
| f2 = qBound(qreal(0.0), f2, qreal(1.0)); |
| if (qFuzzyCompare(f1, f2)) |
| return res; |
| if (f1 > f2) |
| qSwap(f1, f2); |
| if (qFuzzyCompare(f2 - f1, 1.0)) // Shortcut for no trimming |
| return mPath; |
| |
| qreal dummy; |
| offset = std::modf(offset, &dummy); // Use only the fractional part of offset, range <-1, 1> |
| |
| qreal of1 = f1 + offset; |
| qreal of2 = f2 + offset; |
| if (offset < 0.0) { |
| f1 = of1 < 0.0 ? of1 + 1.0 : of1; |
| f2 = of2 + 1.0 > 1.0 ? of2 : of2 + 1.0; |
| } else if (offset > 0.0) { |
| f1 = of1 - 1.0 < 0.0 ? of1 : of1 - 1.0; |
| f2 = of2 > 1.0 ? of2 - 1.0 : of2; |
| } |
| bool wrapping = (f1 > f2); |
| //qDebug() << "ADJ:" << f1 << f2 << wrapping << "(" << of1 << of2 << ")"; |
| |
| if (lensIsDirty()) |
| updateLens(); |
| qreal totLen = mLens.last(); |
| if (qFuzzyIsNull(totLen)) |
| return res; |
| |
| qreal l1 = f1 * totLen; |
| qreal l2 = f2 * totLen; |
| const int e1 = elementAtLength(l1); |
| const bool mustTrimE1 = !qFuzzyCompare(mLens.at(e1), l1); |
| const int e2 = elementAtLength(l2); |
| const bool mustTrimE2 = !qFuzzyCompare(mLens.at(e2), l2); |
| |
| //qDebug() << "Trim [" << f1 << f2 << "] e1:" << e1 << mustTrimE1 << "e2:" << e2 << mustTrimE2 << "wrapping:" << wrapping; |
| |
| if (e1 == e2 && !wrapping && mustTrimE1 && mustTrimE2) { |
| // Entire result is one element, clipped in both ends |
| appendTrimmedElement(&res, e1, true, l1, true, l2); |
| } else { |
| // Partial start element, or just its end point |
| if (mustTrimE1) |
| appendEndOfElement(&res, e1, l1); |
| else |
| res.moveTo(endPointOfElement(e1)); |
| |
| // Complete elements between start and end |
| if (wrapping) { |
| appendElementRange(&res, e1 + 1, mPath.elementCount() - 1); |
| res.moveTo(mPath.elementAt(0)); |
| appendElementRange(&res, 1, (mustTrimE2 ? e2 - 1 : e2)); |
| } else { |
| appendElementRange(&res, e1 + 1, (mustTrimE2 ? e2 - 1 : e2)); |
| } |
| |
| // Partial end element |
| if (mustTrimE2) |
| appendStartOfElement(&res, e2, l2); |
| } |
| return res; |
| } |
| |
| void TrimPath::updateLens() const |
| { |
| const int numElems = mPath.elementCount(); |
| mLens.resize(numElems); |
| if (!numElems) |
| return; |
| |
| QPointF runPt = mPath.elementAt(0); |
| qreal runLen = 0.0; |
| for (int i = 0; i < numElems; i++) { |
| QPainterPath::Element e = mPath.elementAt(i); |
| switch (e.type) { |
| case QPainterPath::LineToElement: |
| runLen += QLineF(runPt, e).length(); |
| runPt = e; |
| break; |
| case QPainterPath::CurveToElement: { |
| Q_ASSERT(i < numElems - 2); |
| QPainterPath::Element ee = mPath.elementAt(i + 2); |
| runLen += QBezier::fromPoints(runPt, e, mPath.elementAt(i + 1), ee).length(); |
| runPt = ee; |
| break; |
| } |
| case QPainterPath::MoveToElement: |
| runPt = e; |
| break; |
| case QPainterPath::CurveToDataElement: |
| break; |
| } |
| mLens[i] = runLen; |
| } |
| } |
| |
| int TrimPath::elementAtLength(qreal len) const |
| { |
| const auto it = std::lower_bound(mLens.constBegin(), mLens.constEnd(), len); |
| return (it == mLens.constEnd()) ? mLens.size() - 1 : int(it - mLens.constBegin()); |
| } |
| |
| QPointF TrimPath::endPointOfElement(int elemIdx) const |
| { |
| QPainterPath::Element e = mPath.elementAt(elemIdx); |
| if (e.isCurveTo()) |
| return mPath.elementAt(qMin(elemIdx + 2, mPath.elementCount() - 1)); |
| else |
| return e; |
| } |
| |
| void TrimPath::appendTrimmedElement(QPainterPath *to, int elemIdx, bool trimStart, qreal startLen, bool trimEnd, qreal endLen) const |
| { |
| Q_ASSERT(elemIdx > 0); |
| |
| if (lensIsDirty()) |
| updateLens(); |
| |
| qreal prevLen = mLens.at(elemIdx - 1); |
| qreal elemLen = mLens.at(elemIdx) - prevLen; |
| qreal len1 = startLen - prevLen; |
| qreal len2 = endLen - prevLen; |
| if (qFuzzyIsNull(elemLen)) |
| return; |
| |
| QPointF pp = mPath.elementAt(elemIdx - 1); |
| QPainterPath::Element e = mPath.elementAt(elemIdx); |
| if (e.isLineTo()) { |
| QLineF l(pp, e); |
| QPointF p1 = trimStart ? l.pointAt(len1 / elemLen) : pp; |
| QPointF p2 = trimEnd ? l.pointAt(len2 / elemLen) : e; |
| if (to->isEmpty()) |
| to->moveTo(p1); |
| to->lineTo(p2); |
| } else if (e.isCurveTo()) { |
| Q_ASSERT(elemIdx < mPath.elementCount() - 2); |
| |
| QBezier b = QBezier::fromPoints(pp, e, mPath.elementAt(elemIdx + 1), mPath.elementAt(elemIdx + 2)); |
| qreal t1 = trimStart ? b.tAtLength(len1) : 0.0; // or simply len1/elemLen to trim by t instead of len |
| qreal t2 = trimEnd ? b.tAtLength(len2) : 1.0; |
| QBezier c = b.getSubRange(t1, t2); |
| if (to->isEmpty()) |
| to->moveTo(c.pt1()); |
| to->cubicTo(c.pt2(), c.pt3(), c.pt4()); |
| } |
| else { |
| Q_UNREACHABLE(); |
| } |
| } |
| |
| void TrimPath::appendElementRange(QPainterPath *to, int first, int last) const |
| { |
| //# (in QPPP, could do direct vector copy, better performance) |
| if (first >= mPath.elementCount() || last >= mPath.elementCount()) |
| return; |
| |
| for (int i = first; i <= last; i++) { |
| QPainterPath::Element e = mPath.elementAt(i); |
| switch (e.type) { |
| case QPainterPath::MoveToElement: |
| to->moveTo(e); |
| break; |
| case QPainterPath::LineToElement: |
| to->lineTo(e); |
| break; |
| case QPainterPath::CurveToElement: |
| Q_ASSERT(i < mPath.elementCount() - 2); |
| to->cubicTo(e, mPath.elementAt(i + 1), mPath.elementAt(i + 2)); |
| i += 2; |
| break; |
| default: |
| // 'first' may point to CurveToData element, just skip it |
| break; |
| } |
| } |
| } |
| |
| QT_END_NAMESPACE |