diff --git a/src/gui/painting/qpainterpath.cpp b/src/gui/painting/qpainterpath.cpp index 21a0d39855c..daea8d7d0f0 100644 --- a/src/gui/painting/qpainterpath.cpp +++ b/src/gui/painting/qpainterpath.cpp @@ -2821,6 +2821,44 @@ QPolygonF QPainterPath::toFillPolygon(const QTransform &matrix) const return polygon; } +/*! + Returns true if caching is enabled; otherwise returns false. + + \since 6.10 + \sa setCachingEnabled() +*/ +bool QPainterPath::isCachingEnabled() const +{ + Q_D(QPainterPath); + return d && d->cacheEnabled; +} + +/*! + Enables or disables length caching according to the value of \a enabled. + + Enabling caching speeds up repeated calls to the member functions involving path length + and percentage values, such as length(), percentAtLength(), pointAtPercent() etc., at the cost + of some extra memory usage for storage of intermediate calculations. By default it is disabled. + + Disabling caching will release any allocated cache memory. + + \since 6.10 + \sa isCachingEnabled(), length(), percentAtLength(), pointAtPercent() +*/ +void QPainterPath::setCachingEnabled(bool enabled) +{ + ensureData(); + if (d_func()->cacheEnabled == enabled) + return; + detach(); + QPainterPathPrivate *d = d_func(); + d->cacheEnabled = enabled; + if (!enabled) { + d->m_runLengths.clear(); + d->m_runLengths.squeeze(); + } +} + //derivative of the equation static inline qreal slopeAt(qreal t, qreal a, qreal b, qreal c, qreal d) { @@ -2835,6 +2873,11 @@ qreal QPainterPath::length() const Q_D(QPainterPath); if (isEmpty()) return 0; + if (d->cacheEnabled) { + if (d->dirtyRunLengths) + d->computeRunLengths(); + return d->m_runLengths.last(); + } qreal len = 0; for (int i=1; ielements.size(); ++i) { @@ -2883,6 +2926,32 @@ qreal QPainterPath::percentAtLength(qreal len) const if (len > totalLength) return 1; + if (d->cacheEnabled) { + const int ei = qMax(d->elementAtT(len / totalLength), 1); // Skip initial MoveTo + qreal res = 0; + const QPainterPath::Element &e = d->elements[ei]; + switch (e.type) { + case QPainterPath::LineToElement: + res = len / totalLength; + break; + case CurveToElement: + { + QBezier b = QBezier::fromPoints(d->elements.at(ei-1), + e, + d->elements.at(ei+1), + d->elements.at(ei+2)); + qreal prevLen = d->m_runLengths[ei - 1]; + qreal blen = d->m_runLengths[ei] - prevLen; + qreal elemRes = b.tAtLength(len - prevLen); + res = (elemRes * blen + prevLen) / totalLength; + break; + } + default: + Q_UNREACHABLE(); + } + return res; + } + qreal curLen = 0; for (int i=1; ielements.size(); ++i) { const Element &e = d->elements.at(i); @@ -2927,7 +2996,8 @@ qreal QPainterPath::percentAtLength(qreal len) const return 0; } -static inline QBezier bezierAtT(const QPainterPath &path, qreal t, qreal *startingLength, qreal *bezierLength) +static inline QBezier uncached_bezierAtT(const QPainterPath &path, qreal t, qreal *startingLength, + qreal *bezierLength) { *startingLength = 0; if (t > 1) @@ -2981,6 +3051,35 @@ static inline QBezier bezierAtT(const QPainterPath &path, qreal t, qreal *starti return QBezier(); } +QBezier QPainterPathPrivate::bezierAtT(const QPainterPath &path, qreal t, qreal *startingLength, + qreal *bezierLength) const +{ + Q_ASSERT(t >= 0 && t <= 1); + QPainterPathPrivate *d = path.d_func(); + if (!path.isEmpty() && d->cacheEnabled) { + const int ei = qMax(d->elementAtT(t), 1); // Avoid the initial MoveTo element + const qreal prevRunLength = d->m_runLengths[ei - 1]; + *startingLength = prevRunLength; + *bezierLength = d->m_runLengths[ei] - prevRunLength; + const QPointF prev = d->elements[ei - 1]; + const QPainterPath::Element &e = d->elements[ei]; + switch (e.type) { + case QPainterPath::LineToElement: + { + QPointF delta = (e - prev) / 3; + return QBezier::fromPoints(prev, prev + delta, prev + 2 * delta, e); + } + case QPainterPath::CurveToElement: + return QBezier::fromPoints(prev, e, elements[ei + 1], elements[ei + 2]); + break; + default: + Q_UNREACHABLE(); + } + } + + return uncached_bezierAtT(path, t, startingLength, bezierLength); +} + /*! Returns the point at at the percentage \a t of the current path. The argument \a t has to be between 0 and 1. @@ -3006,7 +3105,7 @@ QPointF QPainterPath::pointAtPercent(qreal t) const qreal totalLength = length(); qreal curLen = 0; qreal bezierLen = 0; - QBezier b = bezierAtT(*this, t, &curLen, &bezierLen); + QBezier b = d_ptr->bezierAtT(*this, t, &curLen, &bezierLen); qreal realT = (totalLength * t - curLen) / bezierLen; return b.pointAt(qBound(qreal(0), realT, qreal(1))); @@ -3034,7 +3133,7 @@ qreal QPainterPath::angleAtPercent(qreal t) const qreal totalLength = length(); qreal curLen = 0; qreal bezierLen = 0; - QBezier bez = bezierAtT(*this, t, &curLen, &bezierLen); + QBezier bez = d_ptr->bezierAtT(*this, t, &curLen, &bezierLen); qreal realT = (totalLength * t - curLen) / bezierLen; qreal m1 = slopeAt(realT, bez.x1, bez.x2, bez.x3, bez.x4); @@ -3063,7 +3162,7 @@ qreal QPainterPath::slopeAtPercent(qreal t) const qreal totalLength = length(); qreal curLen = 0; qreal bezierLen = 0; - QBezier bez = bezierAtT(*this, t, &curLen, &bezierLen); + QBezier bez = d_ptr->bezierAtT(*this, t, &curLen, &bezierLen); qreal realT = (totalLength * t - curLen) / bezierLen; qreal m1 = slopeAt(realT, bez.x1, bez.x2, bez.x3, bez.x4); @@ -3283,9 +3382,10 @@ bool QPainterPath::contains(const QPainterPath &p) const void QPainterPath::setDirty(bool dirty) { + d_func()->pathConverter.reset(); d_func()->dirtyBounds = dirty; d_func()->dirtyControlBounds = dirty; - d_func()->pathConverter.reset(); + d_func()->dirtyRunLengths = dirty; d_func()->convex = false; } @@ -3358,6 +3458,43 @@ void QPainterPath::computeControlPointRect() const d->controlBounds = QRectF(minx, miny, maxx - minx, maxy - miny); } +void QPainterPathPrivate::computeRunLengths() +{ + Q_ASSERT(!elements.isEmpty()); + + m_runLengths.clear(); + const int numElems = elements.size(); + m_runLengths.reserve(numElems); + + QPointF runPt = elements[0]; + qreal runLen = 0.0; + for (int i = 0; i < numElems; i++) { + QPainterPath::Element e = elements[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 = elements[i + 2]; + runLen += QBezier::fromPoints(runPt, e, elements[i + 1], ee).length(); + runPt = ee; + break; + } + case QPainterPath::MoveToElement: + runPt = e; + break; + case QPainterPath::CurveToDataElement: + break; + } + m_runLengths.append(runLen); + } + Q_ASSERT(m_runLengths.size() == elements.size()); + + dirtyRunLengths = false; +} + #ifndef QT_NO_DEBUG_STREAM QDebug operator<<(QDebug s, const QPainterPath &p) { diff --git a/src/gui/painting/qpainterpath.h b/src/gui/painting/qpainterpath.h index 4caf1aa5c01..59faf4a53fd 100644 --- a/src/gui/painting/qpainterpath.h +++ b/src/gui/painting/qpainterpath.h @@ -134,8 +134,10 @@ public: QPainterPath::Element elementAt(int i) const; void setElementPositionAt(int i, qreal x, qreal y); + bool isCachingEnabled() const; + void setCachingEnabled(bool enabled); qreal length() const; - qreal percentAtLength(qreal t) const; + qreal percentAtLength(qreal len) const; QPointF pointAtPercent(qreal t) const; qreal angleAtPercent(qreal t) const; qreal slopeAtPercent(qreal t) const; @@ -174,6 +176,7 @@ private: friend class QPainterPathStroker; friend class QPainterPathStrokerPrivate; + friend class QPainterPathPrivate; friend class QTransform; friend class QVectorPath; friend Q_GUI_EXPORT const QVectorPath &qtVectorPathForPath(const QPainterPath &); diff --git a/src/gui/painting/qpainterpath_p.h b/src/gui/painting/qpainterpath_p.h index eb6878ce712..6de7db4ed3b 100644 --- a/src/gui/painting/qpainterpath_p.h +++ b/src/gui/painting/qpainterpath_p.h @@ -24,6 +24,7 @@ #include #include +#include #include @@ -111,7 +112,8 @@ public: dirtyBounds(false), dirtyControlBounds(false), convex(false), - hasWindingFill(false) + hasWindingFill(false), + cacheEnabled(false) { } @@ -124,21 +126,25 @@ public: dirtyBounds(false), dirtyControlBounds(false), convex(false), - hasWindingFill(false) + hasWindingFill(false), + cacheEnabled(false) { } QPainterPathPrivate(const QPainterPathPrivate &other) noexcept : QSharedData(other), elements(other.elements), + m_runLengths(other.m_runLengths), bounds(other.bounds), controlBounds(other.controlBounds), cStart(other.cStart), require_moveTo(false), dirtyBounds(other.dirtyBounds), dirtyControlBounds(other.dirtyControlBounds), + dirtyRunLengths(other.dirtyRunLengths), convex(other.convex), - hasWindingFill(other.hasWindingFill) + hasWindingFill(other.hasWindingFill), + cacheEnabled(other.cacheEnabled) { } @@ -149,6 +155,10 @@ public: inline void close(); inline void maybeMoveTo(); inline void clear(); + void computeRunLengths(); + int elementAtT(qreal t); + QBezier bezierAtT(const QPainterPath &path, qreal t, qreal *startingLength, + qreal *bezierLength) const; const QVectorPath &vectorPath() { if (!pathConverter) @@ -159,6 +169,7 @@ public: private: QList elements; std::unique_ptr pathConverter; + QList m_runLengths; QRectF bounds; QRectF controlBounds; @@ -167,8 +178,10 @@ private: bool require_moveTo : 1; bool dirtyBounds : 1; bool dirtyControlBounds : 1; + bool dirtyRunLengths : 1; bool convex : 1; bool hasWindingFill : 1; + bool cacheEnabled : 1; }; class QPainterPathStrokerPrivate @@ -257,6 +270,7 @@ inline void QPainterPathPrivate::clear() Q_ASSERT(ref.loadRelaxed() == 1); elements.clear(); + m_runLengths.clear(); cStart = 0; bounds = {}; @@ -265,12 +279,23 @@ inline void QPainterPathPrivate::clear() require_moveTo = false; dirtyBounds = false; dirtyControlBounds = false; + dirtyRunLengths = false; convex = false; pathConverter.reset(); } -#define KAPPA qreal(0.5522847498) +inline int QPainterPathPrivate::elementAtT(qreal t) +{ + Q_ASSERT(cacheEnabled); + if (dirtyRunLengths) + computeRunLengths(); + qreal len = t * m_runLengths.constLast(); + const auto it = std::lower_bound(m_runLengths.constBegin(), m_runLengths.constEnd(), len); + return (it == m_runLengths.constEnd()) ? m_runLengths.size() - 1 : int(it - m_runLengths.constBegin()); +} + +#define KAPPA qreal(0.5522847498) QT_END_NAMESPACE diff --git a/tests/auto/gui/painting/qpainterpath/tst_qpainterpath.cpp b/tests/auto/gui/painting/qpainterpath/tst_qpainterpath.cpp index cd343445490..71c689114cb 100644 --- a/tests/auto/gui/painting/qpainterpath/tst_qpainterpath.cpp +++ b/tests/auto/gui/painting/qpainterpath/tst_qpainterpath.cpp @@ -1145,10 +1145,16 @@ void tst_QPainterPath::pointAtPercent() QFETCH(qreal, percent); QFETCH(QPointF, point); + QVERIFY(!path.isCachingEnabled()); QPointF result = path.pointAtPercent(percent); - QVERIFY(pathFuzzyCompare(point.x() , result.x())); QVERIFY(pathFuzzyCompare(point.y() , result.y())); + + path.setCachingEnabled(true); + QVERIFY(path.isCachingEnabled()); + result = path.pointAtPercent(percent); + QVERIFY2(pathFuzzyCompare(point.x() , result.x()), "caching"); + QVERIFY2(pathFuzzyCompare(point.y() , result.y()), "caching"); } void tst_QPainterPath::lengths_data() @@ -1190,11 +1196,20 @@ void tst_QPainterPath::lengths() QFETCH(qreal, lenAt50); QFETCH(qreal, lenAt75); + QVERIFY(!path.isCachingEnabled()); QVERIFY(pathFuzzyCompare(path.length() / 1000, length / 1000)); QVERIFY(pathFuzzyCompare(path.percentAtLength(lenAt25), qreal(0.25))); QVERIFY(pathFuzzyCompare(path.percentAtLength(lenAt50), qreal(0.50))); QVERIFY(pathFuzzyCompare(path.percentAtLength(lenAt75), qreal(0.75))); QVERIFY(pathFuzzyCompare(path.percentAtLength(length), qreal(1))); + + path.setCachingEnabled(true); + QVERIFY(path.isCachingEnabled()); + QVERIFY2(pathFuzzyCompare(path.length() / 1000, length / 1000), "caching"); + QVERIFY2(pathFuzzyCompare(path.percentAtLength(lenAt25), qreal(0.25)), "caching"); + QVERIFY2(pathFuzzyCompare(path.percentAtLength(lenAt50), qreal(0.50)), "caching"); + QVERIFY2(pathFuzzyCompare(path.percentAtLength(lenAt75), qreal(0.75)), "caching"); + QVERIFY2(pathFuzzyCompare(path.percentAtLength(length), qreal(1)), "caching"); } void tst_QPainterPath::setElementPositionAt() @@ -1226,6 +1241,11 @@ void tst_QPainterPath::angleAtPercent() path.moveTo(line.p1()); path.lineTo(line.p2()); + QVERIFY(!path.isCachingEnabled()); + QCOMPARE(path.angleAtPercent(0.5), line.angle()); + + path.setCachingEnabled(true); + QVERIFY(path.isCachingEnabled()); QCOMPARE(path.angleAtPercent(0.5), line.angle()); } } diff --git a/tests/benchmarks/gui/painting/qpainterpath/tst_bench_qpainterpath.cpp b/tests/benchmarks/gui/painting/qpainterpath/tst_bench_qpainterpath.cpp index ab861d63585..a4854e3fdb2 100644 --- a/tests/benchmarks/gui/painting/qpainterpath/tst_bench_qpainterpath.cpp +++ b/tests/benchmarks/gui/painting/qpainterpath/tst_bench_qpainterpath.cpp @@ -15,8 +15,12 @@ public: private slots: void initTestCase_data(); + void general_data(); + void length_data() { general_data(); } void length(); + void percentAtLength_data() { general_data(); } void percentAtLength(); + void pointAtPercent_data() { general_data(); } void pointAtPercent(); }; @@ -62,11 +66,19 @@ void tst_QPainterPath::initTestCase_data() QTest::newRow("2k_text") << p; } +void tst_QPainterPath::general_data() +{ + QTest::addColumn("caching"); + + QTest::newRow("Uncached") << false; + QTest::newRow("Cached") << true; +} + void tst_QPainterPath::length() { QFETCH_GLOBAL(QPainterPath, path); - - //const qreal len = path.length() * 0.72; + QFETCH(bool, caching); + path.setCachingEnabled(caching); QBENCHMARK { path.length(); @@ -76,6 +88,8 @@ void tst_QPainterPath::length() void tst_QPainterPath::percentAtLength() { QFETCH_GLOBAL(QPainterPath, path); + QFETCH(bool, caching); + path.setCachingEnabled(caching); const qreal len = path.length() * 0.72; @@ -87,6 +101,8 @@ void tst_QPainterPath::percentAtLength() void tst_QPainterPath::pointAtPercent() { QFETCH_GLOBAL(QPainterPath, path); + QFETCH(bool, caching); + path.setCachingEnabled(caching); const qreal t = 0.72;