148 lines
3.8 KiB
C++
148 lines
3.8 KiB
C++
#include "linegeometry.hpp"
|
|
|
|
#include <qglobal.h>
|
|
|
|
#include <algorithm>
|
|
|
|
namespace ZShell::internal {
|
|
|
|
qreal snapLine(qreal logical, qint64 widthDev, qreal dpr) {
|
|
const qreal half = qreal(widthDev) / 2.0;
|
|
return (static_cast<qreal>(std::llround(logical * dpr - half)) + half)
|
|
/ dpr;
|
|
}
|
|
|
|
void segmentControls(
|
|
const QVector<QPointF>& points,
|
|
int i,
|
|
bool curveSmoothing,
|
|
QPointF& c1,
|
|
QPointF& c2) {
|
|
const QPointF& p0 = points[qMax(i - 1, 0)];
|
|
const QPointF& p1 = points[i];
|
|
const QPointF& p2 = points[i + 1];
|
|
const QPointF& p3 = points[qMin(i + 2, points.size() - 1)];
|
|
|
|
if (curveSmoothing && points.size() > 2) {
|
|
const qreal k = 1.0 / 6.0;
|
|
c1 = p1 + (p2 - p0) * k;
|
|
c2 = p2 - (p3 - p1) * k;
|
|
|
|
const qreal minY = qMin(p1.y(), p2.y());
|
|
const qreal maxY = qMax(p1.y(), p2.y());
|
|
c1.setY(qBound(minY, c1.y(), maxY));
|
|
c2.setY(qBound(minY, c2.y(), maxY));
|
|
return;
|
|
}
|
|
|
|
const qreal cx = (p1.x() + p2.x()) * 0.5;
|
|
c1 = QPointF(cx, p1.y());
|
|
c2 = QPointF(cx, p2.y());
|
|
}
|
|
|
|
qreal cubicY(
|
|
const QPointF& p1,
|
|
const QPointF& c1,
|
|
const QPointF& c2,
|
|
const QPointF& p2,
|
|
qreal targetX) {
|
|
qreal lo = 0.0;
|
|
qreal hi = 1.0;
|
|
for (int it = 0; it < 16; ++it) {
|
|
const qreal t = (lo + hi) / 2.0;
|
|
const qreal u = 1.0 - t;
|
|
const qreal x = u * u * u * p1.x() + 3 * u * u * t * c1.x()
|
|
+ 3 * u * t * t * c2.x() + t * t * t * p2.x();
|
|
if (x < targetX)
|
|
lo = t;
|
|
else
|
|
hi = t;
|
|
}
|
|
|
|
const qreal t = (lo + hi) / 2.0;
|
|
const qreal u = 1.0 - t;
|
|
return u * u * u * p1.y() + 3 * u * u * t * c1.y()
|
|
+ 3 * u * t * t * c2.y() + t * t * t * p2.y();
|
|
}
|
|
|
|
namespace {
|
|
// Index of the segment whose right endpoint is at or past targetX, i.e. the
|
|
// segment that contains it. Clamps to the last segment when targetX is beyond the
|
|
// end. Assumes points ascend in x.
|
|
int segmentIndexAt(const QVector<QPointF>& points, qreal targetX) {
|
|
const int last = points.size() - 2;
|
|
for (int i = 0; i < points.size() - 1; ++i) {
|
|
if (targetX <= points[i + 1].x())
|
|
return i;
|
|
}
|
|
return qMax(0, last);
|
|
}
|
|
} // namespace
|
|
|
|
QPointF pointAtX(
|
|
const QVector<QPointF>& points,
|
|
qreal targetX,
|
|
bool curveSmoothing) {
|
|
const int j = segmentIndexAt(points, targetX);
|
|
QPointF c1;
|
|
QPointF c2;
|
|
segmentControls(points, j, curveSmoothing, c1, c2);
|
|
|
|
qreal lo = 0.0;
|
|
qreal hi = 1.0;
|
|
for (int it = 0; it < 24; ++it) {
|
|
const qreal t = (lo + hi) / 2.0;
|
|
const qreal u = 1.0 - t;
|
|
const qreal x = u * u * u * points[j].x()
|
|
+ 3 * u * u * t * c1.x() + 3 * u * t * t * c2.x()
|
|
+ t * t * t * points[j + 1].x();
|
|
if (x < targetX)
|
|
lo = t;
|
|
else
|
|
hi = t;
|
|
}
|
|
|
|
const qreal t = (lo + hi) / 2.0;
|
|
const qreal u = 1.0 - t;
|
|
const qreal y = u * u * u * points[j].y() + 3 * u * u * t * c1.y()
|
|
+ 3 * u * t * t * c2.y() + t * t * t * points[j + 1].y();
|
|
return QPointF(targetX, y);
|
|
}
|
|
|
|
QVector<QPointF> curveVertices(
|
|
const QVector<QPointF>& points,
|
|
qreal dpr,
|
|
bool curveSmoothing) {
|
|
QVector<QPointF> out;
|
|
if (points.size() < 2)
|
|
return out;
|
|
|
|
out.reserve(points.size());
|
|
out.append(points.first());
|
|
|
|
for (int i = 0; i < points.size() - 1; ++i) {
|
|
const QPointF& p1 = points[i];
|
|
const QPointF& p2 = points[i + 1];
|
|
QPointF c1;
|
|
QPointF c2;
|
|
segmentControls(points, i, curveSmoothing, c1, c2);
|
|
|
|
// One vertex per device-pixel column. A segment spanning several columns
|
|
// would otherwise interpolate its edge across all of them, which reads as
|
|
// a soft rather than crisply anti-aliased stroke. y stays continuous:
|
|
// snapping it per column would step the slope a pixel at a time and look
|
|
// like a staircase.
|
|
const qint64 firstCol = toDev(p1.x(), dpr) + 1;
|
|
const qint64 lastCol = toDev(p2.x(), dpr) - 1;
|
|
for (qint64 col = firstCol; col <= lastCol; ++col) {
|
|
const qreal x = fromDev(col, dpr);
|
|
out.append(QPointF(x, cubicY(p1, c1, c2, p2, x)));
|
|
}
|
|
|
|
out.append(p2);
|
|
}
|
|
|
|
return out;
|
|
}
|
|
} // namespace ZShell::internal
|