Add QPainterPath::trimmed(), returns a slice of a path by length

Path trimming is used in vector graphics and design.

Change-Id: Id5f32b570182f0e8f790835b2fbeb28b3432e40d
Reviewed-by: Eskil Abrahamsen Blomfeldt <eskil.abrahamsen-blomfeldt@qt.io>
This commit is contained in:
Eirik Aavitsland 2025-02-13 11:32:22 +01:00
parent dc5c7f9ead
commit 91f502a7d6
5 changed files with 277 additions and 4 deletions

View File

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

View File

@ -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;

View File

@ -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)

View File

@ -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<QPainterPath>("p");
QTest::addColumn<bool>("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.));

View File

@ -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"