Files
zach 08d42e1f8a
C++ / fmt (pull_request) Successful in 5s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 7s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 27s
Rust / build (pull_request) Successful in 50s
Python / verify (pull_request) Successful in 1m40s
Rust / fmt (pull_request) Successful in 28s
Rust / clippy (pull_request) Successful in 45s
chore: formatter & clangd lsp option
2026-10-18 15:23:40 +02:00

450 lines
12 KiB
C++

#include "sparklinesceneitem.hpp"
#include <qmath.h>
#include <qpainterpath.h>
#include <qsgflatcolormaterial.h>
#include <qsggeometry.h>
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<qreal>(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<qreal>(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<qreal>(1.0, plotBottom - plotTop);
QVector<QPointF> points;
points.reserve(len);
for (int i = 0; i < len; ++i) {
const qreal value =
qBound<qreal>(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 = static_cast<int>(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);
}
}
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 = static_cast<int>(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(
static_cast<float>(p.x()), static_cast<float>(p.y()));
vertices[i * 2 + 1].set(
static_cast<float>(p.x()), static_cast<float>(sample.fillBottom));
}
node->markDirty(QSGNode::DirtyGeometry);
}
void SparklineSceneItem::writeStrokeGeometry(
QSGGeometryNode* node, const LineSample& sample, qreal lineWidth) {
const int n = static_cast<int>(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(
static_cast<float>(p.x() + offset.x()),
static_cast<float>(p.y() + offset.y()));
vertices[i * 2 + 1].set(
static_cast<float>(p.x() - offset.x()),
static_cast<float>(p.y() - offset.y()));
}
node->markDirty(QSGNode::DirtyGeometry);
}
void SparklineSceneItem::setMaterialColor(
QSGGeometryNode* node, const QColor& color, qreal alpha) {
auto* material = static_cast<QSGFlatColorMaterial*>(node->material());
QColor c = color;
c.setAlphaF(static_cast<float>(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<QSGTransformNode*>(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(static_cast<float>(-stepX() * m_slideProgress), 0.0f);
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<qreal>(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