#include "sparklinesceneitem.hpp" #include #include #include #include namespace ZShell::components { SparklineSceneItem::SparklineSceneItem(QQuickItem* parent) : QQuickItem(parent) { setClip(true); setFlag(QQuickItem::ItemHasContents, true); } void SparklineSceneItem::geometryChange( const QRectF& newGeometry, const QRectF& oldGeometry) { QQuickItem::geometryChange(newGeometry, oldGeometry); if (newGeometry.size() != oldGeometry.size()) invalidateGeometry(); } void SparklineSceneItem::invalidateGeometry() { m_geometryDirty = true; update(); } void SparklineSceneItem::applySmoothingRadius( internal::CircularBuffer* buffer) const { if (buffer) buffer->setSmoothingRadius(qRound(m_smoothing)); } void SparklineSceneItem::connectBuffer(internal::CircularBuffer* buffer) { if (!buffer) return; connect( buffer, &internal::CircularBuffer::valuesChanged, this, &SparklineSceneItem::invalidateGeometry); connect(buffer, &QObject::destroyed, this, [this, buffer]() { if (m_line1 == buffer) { m_line1 = nullptr; emit line1Changed(); } if (m_line2 == buffer) { m_line2 = nullptr; emit line2Changed(); } invalidateGeometry(); }); } qreal SparklineSceneItem::stepX() const { if (m_historyLength < 2 || width() <= 0.0) return 0.0; if (m_sliding) return width() / static_cast(m_historyLength - 1); const int len = qMax( 2, (m_line1 && m_line1->count() >= 2) ? m_line1->count() : (m_line2 ? m_line2->count() : 2)); return width() / static_cast(len - 1); } SparklineSceneItem::LineSample SparklineSceneItem::buildSample( internal::CircularBuffer* buffer) const { LineSample sample; if (!buffer || buffer->count() < 2 || m_maxValue <= 0.0 || width() <= 0.0) return sample; if (m_historyLength < 2) return sample; const int len = buffer->count(); const qreal step = stepX(); if (step <= 0.0) return sample; qreal startX = m_sliding ? (width() - (len - 1) * step + step) : 0.0; const qreal h = height(); const qreal strokePad = qCeil(m_lineWidth / 2); const qreal curvePad = 3.0; const qreal plotTop = strokePad + curvePad; const qreal plotBottom = h - strokePad; const qreal plotH = qMax(1.0, plotBottom - plotTop); QVector points; points.reserve(len); for (int i = 0; i < len; ++i) { // Already smoothed once, incrementally, by the buffer itself. const qreal value = qBound(0.0, buffer->smoothedAt(i), m_maxValue); const qreal x = startX + i * step; const qreal y = plotTop + (1.0 - (value / m_maxValue)) * plotH; points.append(QPointF(x, y)); } QPainterPath path; path.moveTo(points[0]); if (m_curveSmoothing && points.size() > 2) { const qreal tension = 1.0; const qreal k = tension / 6.0; const int n = points.size(); for (int i = 0; i < n - 1; ++i) { 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, n - 1)]; QPointF c1 = p1 + (p2 - p0) * k; QPointF c2 = p2 - (p3 - p1) * k; const qreal segMinY = qMin(p1.y(), p2.y()); const qreal segMaxY = qMax(p1.y(), p2.y()); c1.setY(qBound(segMinY, c1.y(), segMaxY)); c2.setY(qBound(segMinY, c2.y(), segMaxY)); path.cubicTo(c1, c2, p2); } } else { for (int i = 0; i < points.size() - 1; ++i) { const QPointF& p0 = points[i]; const QPointF& p1 = points[i + 1]; const qreal ctrlX = (p0.x() + p1.x()) * 0.5; path.cubicTo(QPointF(ctrlX, p0.y()), QPointF(ctrlX, p1.y()), p1); } } // Flatten the curve to a polyline once, here, on data change only. // This is CPU math only -- no QPainter draw calls, no image, no // texture upload happen as a result of this. const auto subpaths = path.toSubpathPolygons(); if (subpaths.isEmpty()) return sample; sample.polyline = subpaths.first(); sample.fillBottom = h; sample.hasData = sample.polyline.size() >= 2; return sample; } void SparklineSceneItem::writeFillGeometry( QSGGeometryNode* node, const LineSample& sample) { const int n = sample.polyline.size(); auto* geometry = node->geometry(); geometry->allocate(n * 2); geometry->setDrawingMode(QSGGeometry::DrawTriangleStrip); auto* vertices = geometry->vertexDataAsPoint2D(); for (int i = 0; i < n; ++i) { const QPointF& p = sample.polyline[i]; vertices[i * 2].set(p.x(), p.y()); vertices[i * 2 + 1].set(p.x(), sample.fillBottom); } node->markDirty(QSGNode::DirtyGeometry); } void SparklineSceneItem::writeStrokeGeometry( QSGGeometryNode* node, const LineSample& sample, qreal lineWidth) { const int n = sample.polyline.size(); auto* geometry = node->geometry(); geometry->allocate(n * 2); geometry->setDrawingMode(QSGGeometry::DrawTriangleStrip); auto* vertices = geometry->vertexDataAsPoint2D(); const qreal half = lineWidth * 0.5; for (int i = 0; i < n; ++i) { QPointF dir; if (i == 0) dir = sample.polyline[1] - sample.polyline[0]; else if (i == n - 1) dir = sample.polyline[n - 1] - sample.polyline[n - 2]; else dir = sample.polyline[i + 1] - sample.polyline[i - 1]; const qreal len = std::hypot(dir.x(), dir.y()); QPointF normal = len > 1e-6 ? QPointF(-dir.y() / len, dir.x() / len) : QPointF(0, 1); const QPointF& p = sample.polyline[i]; const QPointF offset = normal * half; vertices[i * 2].set(p.x() + offset.x(), p.y() + offset.y()); vertices[i * 2 + 1].set(p.x() - offset.x(), p.y() - offset.y()); } node->markDirty(QSGNode::DirtyGeometry); } void SparklineSceneItem::setMaterialColor( QSGGeometryNode* node, const QColor& color, qreal alpha) { auto* material = static_cast(node->material()); QColor c = color; c.setAlphaF(static_cast(alpha)); material->setColor(c); node->markDirty(QSGNode::DirtyMaterial); } QSGNode* SparklineSceneItem::updatePaintNode( QSGNode* node, UpdatePaintNodeData*) { bool samplesRebuilt = false; if (m_geometryDirty) { m_sample1 = buildSample(m_line1); m_sample2 = buildSample(m_line2); m_geometryDirty = false; samplesRebuilt = true; } const bool hasAny = m_sample1.hasData || m_sample2.hasData; if (!hasAny) { delete node; m_transformNode = nullptr; m_line1FillNode = m_line1StrokeNode = nullptr; m_line2FillNode = m_line2StrokeNode = nullptr; return nullptr; } auto* transformNode = static_cast(node); bool justCreated = false; if (!transformNode) { transformNode = new QSGTransformNode(); m_transformNode = transformNode; auto makeNode = [] { auto* n = new QSGGeometryNode(); n->setGeometry( new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 0)); n->setFlag(QSGNode::OwnsGeometry); n->setMaterial(new QSGFlatColorMaterial()); n->setFlag(QSGNode::OwnsMaterial); return n; }; m_line1FillNode = makeNode(); m_line1StrokeNode = makeNode(); m_line2FillNode = makeNode(); m_line2StrokeNode = makeNode(); transformNode->appendChildNode(m_line1FillNode); transformNode->appendChildNode(m_line1StrokeNode); transformNode->appendChildNode(m_line2FillNode); transformNode->appendChildNode(m_line2StrokeNode); justCreated = true; } QMatrix4x4 matrix; matrix.translate(-stepX() * m_slideProgress, 0); transformNode->setMatrix(matrix); const bool needsGeometryWrite = samplesRebuilt || justCreated; const bool needsColorWrite = m_colorsDirty || justCreated; auto writeLine = [&](QSGGeometryNode* fillNode, QSGGeometryNode* strokeNode, const LineSample& sample, const QColor& color, qreal fillAlpha) { if (needsGeometryWrite) { if (sample.hasData) { writeFillGeometry(fillNode, sample); writeStrokeGeometry(strokeNode, sample, m_lineWidth); } else { fillNode->geometry()->allocate(0); strokeNode->geometry()->allocate(0); fillNode->markDirty(QSGNode::DirtyGeometry); strokeNode->markDirty(QSGNode::DirtyGeometry); } } if (needsColorWrite) { setMaterialColor(fillNode, color, fillAlpha); setMaterialColor(strokeNode, color, 1.0); } }; writeLine( m_line1FillNode, m_line1StrokeNode, m_sample1, m_line1Color, m_line1FillAlpha); writeLine( m_line2FillNode, m_line2StrokeNode, m_sample2, m_line2Color, m_line2FillAlpha); m_colorsDirty = false; return transformNode; } internal::CircularBuffer* SparklineSceneItem::line1() const { return m_line1; } void SparklineSceneItem::setLine1(internal::CircularBuffer* buffer) { if (m_line1 == buffer) return; if (m_line1) disconnect(m_line1, nullptr, this, nullptr); m_line1 = buffer; connectBuffer(buffer); applySmoothingRadius(buffer); invalidateGeometry(); emit line1Changed(); } internal::CircularBuffer* SparklineSceneItem::line2() const { return m_line2; } void SparklineSceneItem::setLine2(internal::CircularBuffer* buffer) { if (m_line2 == buffer) return; if (m_line2) disconnect(m_line2, nullptr, this, nullptr); m_line2 = buffer; connectBuffer(buffer); applySmoothingRadius(buffer); invalidateGeometry(); emit line2Changed(); } QColor SparklineSceneItem::line1Color() const { return m_line1Color; } void SparklineSceneItem::setLine1Color(const QColor& color) { if (m_line1Color == color) return; m_line1Color = color; m_colorsDirty = true; update(); emit line1ColorChanged(); } QColor SparklineSceneItem::line2Color() const { return m_line2Color; } void SparklineSceneItem::setLine2Color(const QColor& color) { if (m_line2Color == color) return; m_line2Color = color; m_colorsDirty = true; update(); emit line2ColorChanged(); } qreal SparklineSceneItem::line1FillAlpha() const { return m_line1FillAlpha; } void SparklineSceneItem::setLine1FillAlpha(qreal alpha) { if (qFuzzyCompare(m_line1FillAlpha, alpha)) return; m_line1FillAlpha = alpha; m_colorsDirty = true; update(); emit line1FillAlphaChanged(); } qreal SparklineSceneItem::line2FillAlpha() const { return m_line2FillAlpha; } void SparklineSceneItem::setLine2FillAlpha(qreal alpha) { if (qFuzzyCompare(m_line2FillAlpha, alpha)) return; m_line2FillAlpha = alpha; m_colorsDirty = true; update(); emit line2FillAlphaChanged(); } qreal SparklineSceneItem::maxValue() const { return m_maxValue; } void SparklineSceneItem::setMaxValue(qreal value) { if (qFuzzyCompare(m_maxValue, value)) return; m_maxValue = value; invalidateGeometry(); emit maxValueChanged(); } qreal SparklineSceneItem::slideProgress() const { return m_slideProgress; } void SparklineSceneItem::setSlideProgress(qreal progress) { if (qFuzzyCompare(m_slideProgress, progress)) return; m_slideProgress = progress; update(); emit slideProgressChanged(); } int SparklineSceneItem::historyLength() const { return m_historyLength; } void SparklineSceneItem::setHistoryLength(int length) { if (m_historyLength == length) return; m_historyLength = length; invalidateGeometry(); emit historyLengthChanged(); } qreal SparklineSceneItem::lineWidth() const { return m_lineWidth; } void SparklineSceneItem::setLineWidth(qreal width) { if (qFuzzyCompare(m_lineWidth, width)) return; m_lineWidth = width; invalidateGeometry(); emit lineWidthChanged(); } bool SparklineSceneItem::sliding() const { return m_sliding; } void SparklineSceneItem::setSliding(bool sliding) { if (m_sliding == sliding) return; m_sliding = sliding; invalidateGeometry(); emit slidingChanged(); } qreal SparklineSceneItem::smoothing() const { return m_smoothing; } void SparklineSceneItem::setSmoothing(qreal smoothing) { smoothing = qMax(0.0, smoothing); if (qFuzzyCompare(m_smoothing, smoothing)) return; m_smoothing = smoothing; applySmoothingRadius(m_line1); applySmoothingRadius(m_line2); emit smoothingChanged(); } bool SparklineSceneItem::curveSmoothing() const { return m_curveSmoothing; } void SparklineSceneItem::setCurveSmoothing(bool enabled) { if (m_curveSmoothing == enabled) return; m_curveSmoothing = enabled; invalidateGeometry(); emit curveSmoothingChanged(); } } // namespace ZShell::components