diff --git a/src/gui/painting/qpainterpath.cpp b/src/gui/painting/qpainterpath.cpp index daea8d7d0f0..4e1726c3832 100644 --- a/src/gui/painting/qpainterpath.cpp +++ b/src/gui/painting/qpainterpath.cpp @@ -2843,7 +2843,7 @@ bool QPainterPath::isCachingEnabled() const Disabling caching will release any allocated cache memory. \since 6.10 - \sa isCachingEnabled(), length(), percentAtLength(), pointAtPercent() + \sa isCachingEnabled(), length(), percentAtLength(), pointAtPercent(), trimmed() */ void QPainterPath::setCachingEnabled(bool enabled) { @@ -3188,6 +3188,173 @@ qreal QPainterPath::slopeAtPercent(qreal t) const return slope; } +/*! + \since 6.10 + + Returns the section of the path between the length fractions \a f1 and \a f2. The effective range + of the fractions are from 0, denoting the start point of the path, to 1, denoting its end point. + The fractions are linear with respect to path length, in contrast to the percentage \e t values. + + The value of \a offset will be added to the fraction values. If that causes an over- or underflow + of the [0, 1] range, the values will be wrapped around, as will the resulting path. The effective + range of the offset is between -1 and 1. + + Repeated calls to this function can be optimized by {enabling caching}{setCachingEnabled()}. + + \sa length(), percentAtLength(), setCachingEnabled() +*/ + +QPainterPath QPainterPath::trimmed(qreal f1, qreal f2, qreal offset) const +{ + if (isEmpty()) + return *this; + + f1 = qBound(qreal(0), f1, qreal(1)); + f2 = qBound(qreal(0), f2, qreal(1)); + if (f1 > f2) + qSwap(f1, f2); + if (qFuzzyCompare(f2 - f1, qreal(1))) // Shortcut for no trimming + return *this; + + QPainterPath res; + if (qFuzzyCompare(f1, f2)) + return res; + res.setFillRule(fillRule()); + + // We need length caching enabled for the calulations. + QPainterPath copy(*this); // At most meta data will be copied; path element list unchanged + copy.setCachingEnabled(true); // noop if caching already enabled on *this + QPainterPathPrivate *d = copy.d_func(); + + if (offset) { + qreal dummy; + offset = modf(offset, &dummy); // Use only the fractional part of offset, range <-1, 1> + + qreal of1 = f1 + offset; + qreal of2 = f2 + offset; + if (offset < 0) { + f1 = of1 < 0 ? of1 + 1 : of1; + f2 = of2 + 1 > 1 ? of2 : of2 + 1; + } else if (offset > 0) { + f1 = of1 - 1 < 0 ? of1 : of1 - 1; + f2 = of2 > 1 ? of2 - 1 : of2; + } + } + const bool wrapping = (f1 > f2); + //qDebug() << "ADJ:" << f1 << f2 << wrapping << "(" << of1 << of2 << ")"; + + if (d->dirtyRunLengths) + d->computeRunLengths(); + const qreal totalLength = d->m_runLengths.last(); + if (qFuzzyIsNull(totalLength)) + return res; + + const qreal l1 = f1 * totalLength; + const qreal l2 = f2 * totalLength; + const int e1 = d->elementAtLength(l1); + const bool mustTrimE1 = !qFuzzyCompare(d->m_runLengths.at(e1), l1); + const int e2 = d->elementAtLength(l2); + const bool mustTrimE2 = !qFuzzyCompare(d->m_runLengths.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 + d->appendSliceOfElement(&res, e1, l1, l2); + } else { + // Add partial start element (or just its end point, being the start of the next) + if (mustTrimE1) + d->appendEndOfElement(&res, e1, l1); + else + res.moveTo(d->endPointOfElement(e1)); + + // Add whole elements between start and end + int firstWholeElement = e1 + 1; + int lastWholeElement = (mustTrimE2 ? e2 - 1 : e2); + if (!wrapping) { + d->appendElementRange(&res, firstWholeElement, lastWholeElement); + } else { + int lastIndex = d->elements.size() - 1; + d->appendElementRange(&res, firstWholeElement, lastIndex); + bool isClosed = (QPointF(d->elements.at(0)) == QPointF(d->elements.at(lastIndex))); + // If closed we can skip the initial moveto + d->appendElementRange(&res, (isClosed ? 1 : 0), lastWholeElement); + } + + // Partial end element + if (mustTrimE2) + d->appendStartOfElement(&res, e2, l2); + } + + return res; +} + +void QPainterPathPrivate::appendTrimmedElement(QPainterPath *to, int elemIdx, int trimFlags, + qreal startLen, qreal endLen) +{ + Q_ASSERT(cacheEnabled); + Q_ASSERT(!dirtyRunLengths); + + if (elemIdx <= 0 || elemIdx >= elements.size()) + return; + + const qreal prevLen = m_runLengths.at(elemIdx - 1); + const qreal elemLen = m_runLengths.at(elemIdx) - prevLen; + const qreal len1 = startLen - prevLen; + const qreal len2 = endLen - prevLen; + if (qFuzzyIsNull(elemLen)) + return; + + const QPointF pp = elements.at(elemIdx - 1); + const QPainterPath::Element e = elements.at(elemIdx); + if (e.isLineTo()) { + QLineF l(pp, e); + QPointF p1 = (trimFlags & TrimStart) ? l.pointAt(len1 / elemLen) : pp; + QPointF p2 = (trimFlags & TrimEnd) ? l.pointAt(len2 / elemLen) : e; + if (to->isEmpty()) + to->moveTo(p1); + to->lineTo(p2); + } else if (e.isCurveTo()) { + Q_ASSERT(elemIdx < elements.size() - 2); + QBezier b = QBezier::fromPoints(pp, e, elements.at(elemIdx + 1), elements.at(elemIdx + 2)); + qreal t1 = (trimFlags & TrimStart) ? b.tAtLength(len1) : 0.0; // or simply len1/elemLen to trim by t instead of len + qreal t2 = (trimFlags & 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 QPainterPathPrivate::appendElementRange(QPainterPath *to, int first, int last) +{ + if (first < 0 || first >= elements.size() || last < 0 || last >= elements.size()) + return; + + // (Could optimize by direct copy of elements, but must ensure correct state flags) + for (int i = first; i <= last; i++) { + const QPainterPath::Element &e = elements.at(i); + switch (e.type) { + case QPainterPath::MoveToElement: + to->moveTo(e); + break; + case QPainterPath::LineToElement: + to->lineTo(e); + break; + case QPainterPath::CurveToElement: + Q_ASSERT(i < elements.size() - 2); + to->cubicTo(e, elements.at(i + 1), elements.at(i + 2)); + i += 2; + break; + default: + // 'first' may point to CurveToData element, just skip it + break; + } + } +} + + /*! \since 4.4 diff --git a/src/gui/painting/qpainterpath.h b/src/gui/painting/qpainterpath.h index 59faf4a53fd..2d502936dfd 100644 --- a/src/gui/painting/qpainterpath.h +++ b/src/gui/painting/qpainterpath.h @@ -141,6 +141,7 @@ public: QPointF pointAtPercent(qreal t) const; qreal angleAtPercent(qreal t) const; qreal slopeAtPercent(qreal t) const; + [[nodiscard]] QPainterPath trimmed(qreal f1, qreal f2, qreal offset = 0) const; bool intersects(const QPainterPath &p) const; bool contains(const QPainterPath &p) const; diff --git a/src/gui/painting/qpainterpath_p.h b/src/gui/painting/qpainterpath_p.h index 6de7db4ed3b..de8aedb5be4 100644 --- a/src/gui/painting/qpainterpath_p.h +++ b/src/gui/painting/qpainterpath_p.h @@ -155,10 +155,30 @@ public: inline void close(); inline void maybeMoveTo(); inline void clear(); + QPointF endPointOfElement(int elemIdx) const; void computeRunLengths(); + int elementAtLength(qreal len); int elementAtT(qreal t); QBezier bezierAtT(const QPainterPath &path, qreal t, qreal *startingLength, qreal *bezierLength) const; + enum TrimFlags { + TrimStart = 0x01, + TrimEnd = 0x02 + }; + void appendTrimmedElement(QPainterPath *to, int elemIdx, int trimFlags, qreal startLen, qreal endLen); + void appendStartOfElement(QPainterPath *to, int elemIdx, qreal len) + { + appendTrimmedElement(to, elemIdx, TrimEnd, 0, len); + } + void appendEndOfElement(QPainterPath *to, int elemIdx, qreal len) + { + appendTrimmedElement(to, elemIdx, TrimStart, len, 0); + } + void appendSliceOfElement(QPainterPath *to, int elemIdx, qreal fromLen, qreal toLen) + { + appendTrimmedElement(to, elemIdx, TrimStart | TrimEnd, fromLen, toLen); + } + void appendElementRange(QPainterPath *to, int first, int last); const QVectorPath &vectorPath() { if (!pathConverter) @@ -285,14 +305,30 @@ inline void QPainterPathPrivate::clear() pathConverter.reset(); } +inline QPointF QPainterPathPrivate::endPointOfElement(int elemIdx) const +{ + const QPainterPath::Element &e = elements.at(elemIdx); + if (e.isCurveTo()) + return elements.at(elemIdx + 2); + else + return e; +} + +inline int QPainterPathPrivate::elementAtLength(qreal len) +{ + Q_ASSERT(cacheEnabled); + Q_ASSERT(!dirtyRunLengths); + 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()); +} + 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()); + return elementAtLength(len); } #define KAPPA qreal(0.5522847498) diff --git a/tests/auto/gui/painting/qpainterpath/tst_qpainterpath.cpp b/tests/auto/gui/painting/qpainterpath/tst_qpainterpath.cpp index 71c689114cb..bd9e00fc376 100644 --- a/tests/auto/gui/painting/qpainterpath/tst_qpainterpath.cpp +++ b/tests/auto/gui/painting/qpainterpath/tst_qpainterpath.cpp @@ -57,6 +57,8 @@ private slots: void pointAtPercent(); void lengths_data(); void lengths(); + void trimmed_data(); + void trimmed(); void angleAtPercent(); void arcWinding_data(); @@ -1212,6 +1214,57 @@ void tst_QPainterPath::lengths() QVERIFY2(pathFuzzyCompare(path.percentAtLength(length), qreal(1)), "caching"); } +void tst_QPainterPath::trimmed_data() +{ + QTest::addColumn("p"); + QTest::addColumn("caching"); + + QPainterPath p; + p.addEllipse(50, 50, 200, 100); + QTest::newRow("ellipse") << p << false; + QTest::newRow("ellipse, caching") << p << true; + + p.clear(); + p.addRect(-50, -100, 200, 150); + QTest::newRow("rect") << p << false; + QTest::newRow("rect, caching") << p << true; +} + +void tst_QPainterPath::trimmed() +{ + QFETCH(QPainterPath, p); + QFETCH(bool, caching); + p.setCachingEnabled(caching); + + { + QPainterPath tp = p.trimmed(0, 0.5); + QCOMPARE(tp.pointAtPercent(0), p.pointAtPercent(0)); + QCOMPARE(tp.pointAtPercent(1), p.pointAtPercent(0.5)); + QCOMPARE(tp.length(), p.length() / 2); + } + + { + QPainterPath tp = p.trimmed(0, 0.5, -0.25); + QCOMPARE(tp.pointAtPercent(0), p.pointAtPercent(0.75)); + QCOMPARE(tp.pointAtPercent(1), p.pointAtPercent(0.25)); + QCOMPARE(tp.length(), p.length() / 2); + } + + { + QPainterPath tp = p.trimmed(0.5, 1); + QCOMPARE(tp.pointAtPercent(0), p.pointAtPercent(0.5)); + QCOMPARE(tp.pointAtPercent(1), p.pointAtPercent(1)); + QCOMPARE(tp.length(), p.length() / 2); + } + + { + QPainterPath tp = p.trimmed(0.5, 1, 0.25); + QCOMPARE(tp.pointAtPercent(0), p.pointAtPercent(0.75)); + QCOMPARE(tp.pointAtPercent(1), p.pointAtPercent(0.25)); + QCOMPARE(tp.length(), p.length() / 2); + } +} + void tst_QPainterPath::setElementPositionAt() { QPainterPath path(QPointF(42., 42.)); diff --git a/tests/benchmarks/gui/painting/qpainterpath/tst_bench_qpainterpath.cpp b/tests/benchmarks/gui/painting/qpainterpath/tst_bench_qpainterpath.cpp index a4854e3fdb2..a97c8d92a43 100644 --- a/tests/benchmarks/gui/painting/qpainterpath/tst_bench_qpainterpath.cpp +++ b/tests/benchmarks/gui/painting/qpainterpath/tst_bench_qpainterpath.cpp @@ -22,7 +22,8 @@ private slots: void percentAtLength(); void pointAtPercent_data() { general_data(); } void pointAtPercent(); - + void trimmed_data() { general_data(); } + void trimmed(); }; tst_QPainterPath::tst_QPainterPath() @@ -111,5 +112,20 @@ void tst_QPainterPath::pointAtPercent() } } +void tst_QPainterPath::trimmed() +{ + QFETCH_GLOBAL(QPainterPath, path); + QFETCH(bool, caching); + path.setCachingEnabled(caching); + + const qreal fromF = 0.47; + const qreal toF = 0.77; + + QBENCHMARK { + (void)path.trimmed(fromF, toF); + (void)path.trimmed(fromF, toF, 0.4); + } +} + QTEST_MAIN(tst_QPainterPath) #include "tst_bench_qpainterpath.moc"