feat: graphs now properly sized + port static graphs to c++

This commit is contained in:
2026-11-08 01:28:27 +01:00
parent a7767d2a80
commit d69a6bcafd
17 changed files with 1592 additions and 1257 deletions
+1
View File
@@ -7,6 +7,7 @@ qml_module(ZShell-internal
cachingimagemanager.hpp cachingimagemanager.cpp
circularindicatormanager.hpp circularindicatormanager.cpp
circularbuffer.hpp circularbuffer.cpp
linegeometry.hpp linegeometry.cpp
arcgauge.hpp arcgauge.cpp
lidwatcher.hpp lidwatcher.cpp
visualizerbars.hpp visualizerbars.cpp
+147
View File
@@ -0,0 +1,147 @@
#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
+64
View File
@@ -0,0 +1,64 @@
#pragma once
#include <qpoint.h>
#include <qvector.h>
#include <cmath>
#include <cstdint>
namespace ZShell::internal {
// Shared curve construction for SparklineContent (QPainter) and LineGraphItem
// (QCanvasPainter). Input and output coordinates are logical (item-local);
// device-pixel quantization happens inside so a stroke lands on whole pixel
// columns and its edges are not interpolated across several columns. Keeping the
// math in one place means both graphs align and smooth their lines identically,
// whatever backend they paint through.
inline qint64 toDev(qreal logical, qreal dpr) {
return std::llround(logical * dpr);
}
inline qreal fromDev(qint64 device, qreal dpr) {
return qreal(device) / dpr;
}
// Snap a line center so a whole-pixel-wide stroke sits on pixel boundaries when
// the device width is even and on pixel centers when it is odd.
qreal snapLine(qreal logical, qint64 widthDev, qreal dpr);
// Catmull-Rom control points for the segment points[i] -> points[i + 1], with the
// control y clamped to the segment's own y span so a curve never overshoots past
// its endpoints. When curveSmoothing is off (or there are too few points) the
// tangents are flat.
void segmentControls(
const QVector<QPointF>& points,
int i,
bool curveSmoothing,
QPointF& c1,
QPointF& c2);
// y of the cubic bezier p1-c1-c2-p2 at targetX, found by bisection on t.
qreal cubicY(
const QPointF& p1,
const QPointF& c1,
const QPointF& c2,
const QPointF& p2,
qreal targetX);
// The curve point at targetX on the polyline, interpolating across the segment
// that contains it. Assumes points ascend in x.
QPointF pointAtX(
const QVector<QPointF>& points,
qreal targetX,
bool curveSmoothing);
// A dense polyline through points, adding one vertex per device-pixel column
// inside each segment so an edge never interpolates across several columns. The
// result starts at points.first() and ends at points.last(). points must ascend in
// x and hold at least two entries. Backends consume it as a moveTo/lineTo chain.
QVector<QPointF> curveVertices(
const QVector<QPointF>& points,
qreal dpr,
bool curveSmoothing);
} // namespace ZShell::internal