134 lines
4.9 KiB
C++
134 lines
4.9 KiB
C++
#pragma once
|
|
|
|
#include "source.h"
|
|
#include "signal_buffer.h"
|
|
|
|
#include <QColor>
|
|
#include <QPair>
|
|
#include <QWidget>
|
|
#include <memory>
|
|
#include <vector>
|
|
|
|
#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<Source> src, int sigIdx);
|
|
void removeSignal(std::shared_ptr<Source> src, int sigIdx);
|
|
bool hasSignal(const std::shared_ptr<Source>& src, int sigIdx) const;
|
|
|
|
/** For saving/restoring traces across layout changes. */
|
|
struct SignalId { std::shared_ptr<Source> src; int sigIdx; };
|
|
std::vector<SignalId> 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<double,double> 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<Source> 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<Trace> 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<double> tmpT_, tmpV_; // full-res / stride-decimated intermediary
|
|
std::vector<double> tmpMinV_, tmpMaxV_; // min-max decimation intermediary
|
|
std::vector<double> outT_, outV_; // LTTB output (resized as needed)
|
|
};
|