fix: graph jitter on load + format
This commit is contained in:
@@ -16,43 +16,21 @@ using internal::fromDev;
|
||||
using internal::snapLine;
|
||||
using internal::toDev;
|
||||
|
||||
// Device-pixel snapping, bezier sampling and Catmull-Rom controls come from
|
||||
// internal::linegeometry, shared with LineGraph so both graphs align and smooth
|
||||
// their lines identically.
|
||||
|
||||
namespace {
|
||||
// A QQuickPaintedItem only lands on the device grid when its logical size is a
|
||||
// whole number *and* that whole number times dpr is a whole number as well.
|
||||
// Qt derives the painter node from qRound(size) (qquickpainteditem.cpp), makes
|
||||
// the texture from that rounded size times dpr, scales the painter by
|
||||
// texture/size rather than by dpr, and draws the texture into a quad of the
|
||||
// rounded size (qsgdefaultpainternode.cpp). Any fraction therefore moves the
|
||||
// painter scale off dpr - a height of 75.3333 at dpr 1.5 becomes a node of 75
|
||||
// and a texture of qRound(112.5) = 113, a scale of 1.5067, so every y snapped
|
||||
// to the dpr 1.5 grid falls between texture rows and the whole texture is then
|
||||
// resampled to a 112.5 pixel tall quad. That is the fuzz. 74 -> 111 and
|
||||
// 76 -> 114 are both exact.
|
||||
//
|
||||
// `grow` picks the direction: shrink to stay inside the slot, grow to stay
|
||||
// outside it. The overhang on the right has to grow, or the texture's own edge
|
||||
// moves inside the parent's clip and slides left and right with the scroll.
|
||||
qreal snapPaintedExtent(qreal logical, qreal dpr, bool grow) {
|
||||
qreal whole = grow ? std::ceil(logical - 1e-6) : std::floor(logical + 1e-6);
|
||||
for (int guard = 0; guard < 16 && whole >= 1.0; ++guard) {
|
||||
const qreal device = whole * dpr;
|
||||
if (std::abs(device - std::round(device)) < 1e-6)
|
||||
return whole;
|
||||
if (std::abs(device - std::round(device)) < 1e-6) return whole;
|
||||
whole += grow ? 1.0 : -1.0;
|
||||
}
|
||||
// No whole extent within the guard window (an exotic dpr): a whole number is
|
||||
// still closer than a fractional one, which Qt rounds away anyway.
|
||||
return std::max(1.0, grow ? whole : std::floor(logical));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
SparklineContent::SparklineContent(QQuickItem* parent)
|
||||
: QQuickPaintedItem(parent) {
|
||||
m_sampleClock.start();
|
||||
setAntialiasing(true);
|
||||
}
|
||||
|
||||
@@ -75,13 +53,6 @@ qreal SparklineContent::stepX() const {
|
||||
2,
|
||||
(m_line1 && m_line1->count() >= 2) ? m_line1->count()
|
||||
: (m_line2 ? m_line2->count() : 2));
|
||||
// Exactly the slot divided by the history, in logical pixels. Rounding this
|
||||
// to a whole device pixel was tempting - it puts every sample on a pixel
|
||||
// column - but the rounding error accumulates over the whole window: history
|
||||
// - 1 steps then no longer span the slot, so the oldest value either leaves
|
||||
// the clip before its tick (rounding up) or sits at the edge until the buffer
|
||||
// evicts it, popping out at the tick (rounding down). crispness does not need
|
||||
// it: internal::curveVertices puts a vertex on every device column anyway.
|
||||
return m_sliding ? m_viewportW / static_cast<qreal>(m_historyLength - 1)
|
||||
: m_viewportW / static_cast<qreal>(len - 1);
|
||||
}
|
||||
@@ -99,23 +70,23 @@ void SparklineContent::setViewportSize(
|
||||
void SparklineContent::setSlideInterval(qreal intervalMs) {
|
||||
if (qFuzzyCompare(m_slideIntervalMs, intervalMs)) return;
|
||||
m_slideIntervalMs = intervalMs;
|
||||
m_sampleClock.restart();
|
||||
invalidate();
|
||||
}
|
||||
|
||||
qreal SparklineContent::slidePhase(qreal progress) const {
|
||||
if (m_slideIntervalMs <= 0.0) return qBound(0.0, progress, 1.0);
|
||||
// The sample clock, not the animation's timeline. The NumberAnimation runs on
|
||||
// the render-loop clock while the sample arrives from a QTimer of the same
|
||||
// duration, so it reaches 1 on the last frame before that timer fires and then
|
||||
// stops. For that frame the newest value is parked exactly on the parent's
|
||||
// clip edge - it shows up at the clipped edge instead of sliding in from
|
||||
// beyond it, and the following frame travels nothing. Holding is capped at one
|
||||
// step, so a stalled producer cannot scroll the curve into empty space.
|
||||
|
||||
if (!m_sampleClock.isValid()) return 1.0;
|
||||
|
||||
const qreal elapsed = static_cast<qreal>(m_sampleClock.elapsed());
|
||||
return qBound(0.0, elapsed / m_slideIntervalMs, 1.0);
|
||||
}
|
||||
|
||||
bool SparklineContent::slideActive() const {
|
||||
return m_slideIntervalMs > 0.0 && m_sampleClock.isValid() &&
|
||||
static_cast<qreal>(m_sampleClock.elapsed()) < m_slideIntervalMs;
|
||||
}
|
||||
|
||||
void SparklineContent::setSlideProgress(qreal progress) {
|
||||
setX(-stepX() * slidePhase(progress));
|
||||
}
|
||||
@@ -139,11 +110,6 @@ void SparklineContent::setLine2(internal::CircularBuffer* buffer) {
|
||||
void SparklineContent::onSampleArrived() {
|
||||
m_sampleClock.restart();
|
||||
invalidate();
|
||||
// The offset reset and the buffer shifting its values by one step are a
|
||||
// single change: applied here, both are pending before the next sync, so they
|
||||
// cannot land in different frames. Left to the animation to write, a frame
|
||||
// can paint the reindexed values with the previous cycle's full offset still
|
||||
// in place, which moves the whole graph one extra step left for that frame.
|
||||
setSlideProgress(0.0);
|
||||
}
|
||||
|
||||
@@ -223,16 +189,7 @@ void SparklineContent::invalidate() {
|
||||
|
||||
void SparklineContent::applyGeometry() {
|
||||
const qreal dpr = m_dpr > 0.0 ? m_dpr : 1.0;
|
||||
// The newest value is drawn one step past the slot's right edge, so the child
|
||||
// has to overhang the slot and the overhang is offset left as it slides. Two
|
||||
// steps, not one: the newest vertex sits at one step past the edge and its
|
||||
// stroke cap extends beyond that, so a one-step overhang puts the texture's
|
||||
// own right edge exactly on the clip boundary at the end of a cycle, where the
|
||||
// cut-off ink becomes visible for a frame.
|
||||
const qreal pad = m_sliding ? 2.0 * stepX() : 0.0;
|
||||
// Qt rounds the painted size anyway, so choose a whole value that keeps
|
||||
// device alignment. Width grows, to keep that overhang outside the clip;
|
||||
// height shrinks, to stay inside the slot.
|
||||
const qreal paintedW = snapPaintedExtent(m_viewportW + pad, dpr, true);
|
||||
m_paintedH = snapPaintedExtent(m_viewportH, dpr, false);
|
||||
setSize(QSizeF(paintedW, m_paintedH));
|
||||
@@ -241,13 +198,10 @@ void SparklineContent::applyGeometry() {
|
||||
SparklineContent::LineGeometry SparklineContent::buildGeometry(
|
||||
internal::CircularBuffer* buffer) const {
|
||||
LineGeometry geom;
|
||||
// Every input has to be a usable number: a NaN from QML (an animated maxValue
|
||||
// fed by an empty buffer, for instance) would otherwise reach llround and the
|
||||
// path stroker as garbage, which draws a shape that ignores the curve.
|
||||
if (!buffer || buffer->count() < 2 || !std::isfinite(m_maxValue) ||
|
||||
m_maxValue <= 0.0 || !std::isfinite(m_viewportW) ||
|
||||
m_viewportW <= 0.0 || !std::isfinite(m_viewportH) || m_viewportH <= 0.0 ||
|
||||
m_paintedH < 1.0)
|
||||
m_viewportW <= 0.0 || !std::isfinite(m_viewportH) ||
|
||||
m_viewportH <= 0.0 || m_paintedH < 1.0)
|
||||
return geom;
|
||||
if (m_historyLength < 2) return geom;
|
||||
|
||||
@@ -259,8 +213,6 @@ SparklineContent::LineGeometry SparklineContent::buildGeometry(
|
||||
const qint64 lineDev = std::isfinite(m_lineWidth)
|
||||
? (std::max)(qint64(1), toDev(m_lineWidth, dpr))
|
||||
: qint64(1);
|
||||
// The painted height, not the slot's: it is the number of texture rows that
|
||||
// actually exist, so the plot and the fill bottom match them exactly.
|
||||
const qint64 heightDev = toDev(m_paintedH, dpr);
|
||||
|
||||
qreal startX = 0.0;
|
||||
@@ -269,9 +221,6 @@ SparklineContent::LineGeometry SparklineContent::buildGeometry(
|
||||
else
|
||||
startX = 0.0;
|
||||
|
||||
// Plot bounds in device pixels: half a stroke plus the curve overshoot at
|
||||
// the top, a half stroke and one pixel of antialiasing feather at the
|
||||
// bottom, so neither edge of the stroke is cut by the item's own rect.
|
||||
const qint64 curvePadDev = toDev(m_curveSmoothing ? 3.0 : 0.0, dpr);
|
||||
const qint64 plotTopDev = lineDev / 2 + curvePadDev;
|
||||
const qint64 plotBottomDev =
|
||||
@@ -285,26 +234,18 @@ SparklineContent::LineGeometry SparklineContent::buildGeometry(
|
||||
for (int i = 0; i < len; ++i) {
|
||||
const qreal raw = buffer->smoothedAt(i);
|
||||
|
||||
// A non-finite sample poisons the whole path: llround of it is undefined,
|
||||
// and the stroke and fill then ignore everything after that vertex. Treat
|
||||
// it as no traffic instead.
|
||||
const qreal value =
|
||||
std::isfinite(raw) ? qBound<qreal>(0.0, raw, m_maxValue) : 0.0;
|
||||
|
||||
// The y of every sample goes on the grid the stroke width needs; x stays
|
||||
// exact so the window's span is exactly the slot's width.
|
||||
const qreal y = fromDev(
|
||||
plotTopDev + static_cast<qint64>(std::llround(
|
||||
(1.0 - value / m_maxValue) *
|
||||
static_cast<double>(plotHDev))),
|
||||
dpr);
|
||||
points.append(QPointF(startX + qreal(i) * step, snapLine(y, lineDev, dpr)));
|
||||
points.append(
|
||||
QPointF(startX + qreal(i) * step, snapLine(y, lineDev, dpr)));
|
||||
}
|
||||
|
||||
// One vertex per device-pixel column, shared with LineGraphItem through
|
||||
// internal::curveVertices: a segment spanning several columns would otherwise
|
||||
// interpolate its edge across all of them. y stays continuous, so a slope is
|
||||
// interpolated rather than stepped one pixel at a time.
|
||||
const QVector<QPointF> verts =
|
||||
internal::curveVertices(points, dpr, m_curveSmoothing);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user