#pragma once #include "source.h" #include "signal_buffer.h" #include #include #include #include #include #include "qcustomplot.h" /** * A single plot panel backed by QCustomPlot. * * Rendering pipeline per trace each timer tick: * 1. select ring buffer or archive depending on window coverage * 2. read_window_decimated → kRawPts (uniform stride) * 3. lttb(kRawPts → kDisplayPts) — visually optimal 3000-point line * * Zoom behaviour: * - Live mode: rolling window updated each tick UNLESS userZoomed_ is set. * - Zoomed (userZoomed_): axis stays at user range; re-queries buffer with * that range → zooming in gives higher resolution from the ring buffer. * - "Reset Zoom" context menu clears userZoomed_ and returns to live mode. * * x-range adapts to actual available data so the plot never shows empty space * on the left when the signal has just started. */ class PlotWidget : public QWidget { Q_OBJECT public: explicit PlotWidget(int plotId, QWidget* parent = nullptr); int plotId() const { return plotId_; } // ── Signal management ───────────────────────────────────────────── void addSignal(std::shared_ptr src, int sigIdx); void removeSignal(std::shared_ptr src, int sigIdx); bool hasSignal(const std::shared_ptr& src, int sigIdx) const; /** For saving/restoring traces across layout changes. */ struct SignalId { std::shared_ptr src; int sigIdx; }; std::vector getSignals() const; // ── View control (called from MainWindow timer) ─────────────────── void refresh(double viewTMax, double windowSec, bool paused); /** Force the visible range (e.g. when a trigger fires). */ void setViewRange(double tMin, double tMax); /** Return to live rolling window. */ void resetZoom(); /** Apply a specific x range (for zoom-back; suppresses history emission). */ void applyZoomRange(double lo, double hi); /** Fit Y axis to currently visible data. */ void fitYAxis(); /** Current x axis range. */ QPair currentXRange() const; /** Sync X range from another plot (cross-plot zoom). */ void syncXRange(double lo, double hi); /** Push current cursor state to display (called from MainWindow). */ void refreshCursors(double tA, double tB, bool aActive, bool bActive); signals: void signalDropped(int srcIdx, int sigIdx); void xRangeChanged(double lo, double hi); // emitted on user zoom/pan void cursorAPlaced(double t); // emitted on Ctrl+Left click void cursorBPlaced(double t); // emitted on Ctrl+Right click protected: void dragEnterEvent(QDragEnterEvent* e) override; void dropEvent(QDropEvent* e) override; bool eventFilter(QObject* obj, QEvent* ev) override; private slots: void onLegendDoubleClick(QCPLegend* legend, QCPAbstractLegendItem* item); void onContextMenuRequest(QPoint pos); private: struct Trace { std::shared_ptr src; int sigIdx; QCPGraph* graphMin = nullptr; QCPGraph* graphMax = nullptr; QCPGraph* graphLine = nullptr; size_t lastHead = 0; double lastTMin = 0.0; double lastTMax = 0.0; QColor color; // persistent user color (invalid = use palette) qreal lineWidth = 1.5; }; void applyDarkTheme(); void rebuildTrace(Trace& tr); void updateCursorItems(); // update QCPItemLine positions + visibility int plotId_; QCustomPlot* qcp_ = nullptr; std::vector traces_; // Zoom / sync state bool userZoomed_ = false; bool zoomGuard_ = false; bool syncGuard_ = false; // prevent re-entrant xRangeChanged during syncXRange // A/B cursor items (owned by qcp_, positions set via refreshCursors) QCPItemLine* cursorA_ = nullptr; QCPItemLine* cursorB_ = nullptr; double cursorATime_ = 0.0; double cursorBTime_ = 0.0; bool cursorAActive_ = false; bool cursorBActive_ = false; // Rendering pipeline scratch buffers // kFullResPts: full-res read budget (light path). Must be >= 4*kMaxBuckets so // the light-path threshold never exceeds what read_window can return. static constexpr size_t kFullResPts = 65'536; // kRawPts: stride-decimated read budget (heavy path, >kModerateThreshold samples) static constexpr size_t kRawPts = 8'192; // kMaxBuckets: number of min-max buckets (moderate path + LTTB output) static constexpr size_t kMaxBuckets = 4'096; std::vector tmpT_, tmpV_; // full-res / stride-decimated intermediary std::vector tmpMinV_, tmpMaxV_; // min-max decimation intermediary std::vector outT_, outV_; // LTTB output (resized as needed) };