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:
parent
dc5c7f9ead
commit
91f502a7d6
@ -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
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
|
@ -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.));
|
||||
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user