From 49e6bc174125d6c8dd9d5fabdf83752af11e1790 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 30 Jun 2026 00:15:21 +0000 Subject: [PATCH] Add monotone cubic Hermite plot style to TimeValueLayer Adds a new PlotCubicHermite plot style (appended to the PlotStyle enum as value 7) that draws a smooth curve through the layer's breakpoints using monotone cubic Hermite interpolation with Fritsch-Carlson tangents (Fritsch & Carlson, "Monotone Piecewise Cubic Interpolation", SIAM J. Numer. Anal. 17(2), 1980). Unlike PlotCurve, which uses Bezier control points and can overshoot the data on steep transitions or narrow peaks, this style is guaranteed to stay within the range of the surrounding breakpoints: tangents are driven to zero at local extrema, so plateaus stay flat and monotone runs never overshoot. With only two breakpoints the curve degenerates to a symmetric smoothstep. Implementation (layer/TimeValueLayer.{h,cpp} only): - Two file-local helpers, computeFritschCarlsonTangents() and cubicHermite(). Coincident frames are guarded against division by zero. - A dedicated rendering branch in paint() that samples the curve at 1px (O(paint width), same cost as PlotCurve) and draws it with drawPath and antialiasing. A single point falls back to a small rectangular marker. - Plot Type range extended (max 6 -> 7) and a "Cubic Hermite" label. - The style is treated as scrollable, like the other curve styles. Tangents are computed over all model events, not just the visible window, so the curve does not change shape while scrolling; for the same reason the branch runs before the visible-window emptiness check, so a curve passing between two off-screen breakpoints is still drawn. Interpolation is done in value space and each sample is mapped through the vertical CoordinateScale, so the curve is correct on a logarithmic vertical scale. Save/load needs no change: toXml already serialises m_plotStyle as an integer and setProperties casts it back, so plotStyle="7" round-trips, and appending the value keeps the existing styles 0-6 unchanged. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_016ANtJmJppqkSVEk6AVtKrn --- layer/TimeValueLayer.cpp | 146 ++++++++++++++++++++++++++++++++++++++- layer/TimeValueLayer.h | 3 +- 2 files changed, 145 insertions(+), 4 deletions(-) diff --git a/layer/TimeValueLayer.cpp b/layer/TimeValueLayer.cpp index 32e1058..1ee5020 100644 --- a/layer/TimeValueLayer.cpp +++ b/layer/TimeValueLayer.cpp @@ -49,6 +49,9 @@ #include #include +#include +#include +#include //#define DEBUG_TIME_VALUE_LAYER 1 @@ -215,9 +218,9 @@ TimeValueLayer::getPropertyRangeAndValue(const PropertyName &name, val = m_colourMap; } else if (name == "Plot Type") { - + if (min) *min = 0; - if (max) *max = 6; + if (max) *max = 7; if (deflt) *deflt = int(PlotConnectedPoints); val = int(m_plotStyle); @@ -284,6 +287,7 @@ TimeValueLayer::getPropertyValueLabel(const PropertyName &name, case 4: return tr("Curve"); case 5: return tr("Segmentation"); case 6: return tr("Discrete Curves"); + case 7: return tr("Cubic Hermite"); } } else if (name == "Vertical Scale") { switch (value) { @@ -393,7 +397,8 @@ TimeValueLayer::isLayerScrollable(const LayerGeometryProvider *v) const if (m_plotStyle == PlotLines || m_plotStyle == PlotCurve || - m_plotStyle == PlotDiscreteCurves) return true; + m_plotStyle == PlotDiscreteCurves || + m_plotStyle == PlotCubicHermite) return true; QPoint discard; return !v->shouldIlluminateLocalFeatures(this, discard); @@ -877,6 +882,54 @@ TimeValueLayer::getDefaultColourHint(bool darkbg, bool &impose) (QString(darkbg ? "Bright Green" : "Green")); } +namespace { + +// Monotone cubic tangents after Fritsch & Carlson, "Monotone Piecewise Cubic +// Interpolation" (SIAM J. Numer. Anal. 17(2), 1980). Driving the tangent to +// zero at local extrema prevents overshoot and preserves monotonicity. The +// n == 2 case returns zero tangents on purpose, so the Hermite degenerates to +// a symmetric smoothstep (ease-in-out) rather than a straight line. +std::vector +computeFritschCarlsonTangents(const std::vector> &points) +{ + size_t n = points.size(); + if (n < 2) return std::vector(n, 0.0); + if (n == 2) return { 0.0, 0.0 }; // smoothstep + + std::vector deltas(n - 1, 0.0); + for (size_t i = 0; i + 1 < n; ++i) { + double t0 = points[i].first, v0 = points[i].second; + double t1 = points[i+1].first, v1 = points[i+1].second; + deltas[i] = (t1 > t0) ? (v1 - v0) / (t1 - t0) : 0.0; + } + + std::vector tangents(n, 0.0); + tangents[0] = deltas[0]; + for (size_t i = 1; i + 1 < n; ++i) { + double dL = deltas[i-1], dR = deltas[i]; + tangents[i] = (dL * dR <= 0.0) ? 0.0 : 2.0 / (1.0/dL + 1.0/dR); + } + tangents[n-1] = deltas[n-2]; + return tangents; +} + +// Cubic Hermite basis evaluated at t. t is expressed in frames; since s and +// the h*m products are invariant under a linear rescaling of the time axis, +// using frames instead of seconds yields the identical curve shape. The +// h == 0 guard handles coincident frames. +double cubicHermite(double t, + double t0, double v0, double m0, + double t1, double v1, double m1) +{ + double h = t1 - t0; + if (h == 0.0) return v0; + double s = (t - t0) / h, s2 = s*s, s3 = s2*s; + return (2*s3 - 3*s2 + 1)*v0 + (s3 - 2*s2 + s)*h*m0 + + (-2*s3 + 3*s2)*v1 + (s3 - s2)*h*m1; +} + +} // namespace + void TimeValueLayer::paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) const { @@ -916,6 +969,93 @@ TimeValueLayer::paint(LayerGeometryProvider *v, QPainter &paint, QRect rect) con << model->getEndFrame() << ")" << endl; #endif + // PlotCubicHermite is rendered in a dedicated branch that bypasses the + // per-point loop. It draws a monotone cubic Hermite curve through the + // breakpoints using Fritsch-Carlson tangents, so the interpolation never + // overshoots the data. Tangents are computed over *all* model events, not + // just the visible window, so the curve does not change shape while + // scrolling; for the same reason this branch must run before the + // visible-window emptiness check above, otherwise a curve passing between + // two off-screen breakpoints would not be drawn. Interpolation happens in + // value space and each sample is mapped through yCoordScale, so the result + // is correct on a logarithmic vertical scale too. The derivative display + // (m_derivative) is intentionally not applied to this style. + if (m_plotStyle == PlotCubicHermite) { + + EventVector allEvents = model->getAllEvents(); + if (allEvents.empty()) return; + + std::vector> pts; + pts.reserve(allEvents.size()); + for (const auto &e : allEvents) { + pts.push_back({ double(e.getFrame()), double(e.getValue()) }); + } + size_t n = pts.size(); + + int w = v->getXForFrame(frame0 + model->getResolution()) - + v->getXForFrame(frame0); + if (w < 1) w = 1; + + paint.save(); + + // Zero / origin reference line, as in the other non-segmentation styles + int originY = yCoordScale.getCoordForValueRounded(v, 0.f); + if (originY > 0 && originY < v->getPaintHeight()) { + paint.save(); + paint.setPen(getPartialShades(v)[1]); + paint.drawLine(x0, originY, x1, originY); + paint.restore(); + } + + if (n == 1) { + + // Single point: small rectangle marker, like the other styles + int x = v->getXForFrame(sv_frame_t(llround(pts[0].first))); + int y = yCoordScale.getCoordForValueRounded(v, pts[0].second); + QColor brushColour(getBaseQColor()); + brushColour.setAlpha(80); + paint.setPen(v->scalePen(QPen(getBaseQColor()))); + paint.setBrush(brushColour); + paint.drawRect(x, y - 1, w, 2); + + } else { + + std::vector tangents = computeFritschCarlsonTangents(pts); + + int gx0 = std::max + (x0, v->getXForFrame(sv_frame_t(llround(pts.front().first)))); + int gx1 = std::min + (x1, v->getXForFrame(sv_frame_t(llround(pts.back().first)))); + + QPainterPath curve; + bool started = false; + size_t seg = 0; + + for (int px = gx0; px <= gx1; ++px) { + double tf = double(v->getFrameForX(px)); + // Advance to the segment [seg, seg+1] that contains tf + while (seg + 2 < n && tf > pts[seg + 1].first) ++seg; + double value = cubicHermite + (tf, + pts[seg].first, pts[seg].second, tangents[seg], + pts[seg+1].first, pts[seg+1].second, tangents[seg+1]); + double y = yCoordScale.getCoordForValue(v, value); + if (!started) { curve.moveTo(px, y); started = true; } + else { curve.lineTo(px, y); } + } + + paint.setPen(v->scalePen(QPen(getBaseQColor()))); + paint.setBrush(Qt::NoBrush); + paint.setRenderHint(QPainter::Antialiasing, true); + paint.drawPath(curve); + } + + paint.restore(); + // looks like save/restore doesn't deal with this: + paint.setRenderHint(QPainter::Antialiasing, false); + return; + } + if (points.empty()) return; paint.setPen(getBaseQColor()); diff --git a/layer/TimeValueLayer.h b/layer/TimeValueLayer.h index f8175c7..4431f47 100644 --- a/layer/TimeValueLayer.h +++ b/layer/TimeValueLayer.h @@ -99,7 +99,8 @@ class TimeValueLayer : public SingleColourLayer, PlotLines, PlotCurve, PlotSegmentation, - PlotDiscreteCurves + PlotDiscreteCurves, + PlotCubicHermite }; void setPlotStyle(PlotStyle style);