#include "linegeometry.hpp" #include #include namespace ZShell::internal { qreal snapLine(qreal logical, qint64 widthDev, qreal dpr) { const qreal half = qreal(widthDev) / 2.0; return (static_cast(std::llround(logical * dpr - half)) + half) / dpr; } void segmentControls( const QVector& 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& 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& 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 curveVertices( const QVector& points, qreal dpr, bool curveSmoothing) { QVector 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