Implemented client datasource
This commit is contained in:
+129
-54
@@ -39,8 +39,10 @@ struct DragPayload { int srcIdx; int sigIdx; };
|
||||
static const int kPlotSlotsMax = 8; /* mirrors App::kMaxPlotSlots */
|
||||
|
||||
/* Live windows at or below this width refresh via hub hi-res zoom fetches
|
||||
* instead of (in addition to) the decimated push stream. */
|
||||
static const double kLiveHiResMaxWin = 2.0; /* seconds */
|
||||
* instead of (in addition to) the decimated push stream. Wide windows
|
||||
* benefit too: LTTB on the server ring gives much better coverage than
|
||||
* the client's accumulated push-stream data. */
|
||||
static const double kLiveHiResMaxWin = 600.0; /* seconds (effectively always) */
|
||||
|
||||
/* ── Helpers ─────────────────────────────────────────────────────────────── */
|
||||
|
||||
@@ -198,18 +200,30 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
(zc.t1 - zc.t0) >= app.windowSec() * 0.9 &&
|
||||
(wallNow - zc.t1) < 3.0;
|
||||
|
||||
const bool useZoomData = !trigView && zc.valid &&
|
||||
const bool useZoomData = !trigView && !paused && zc.valid &&
|
||||
(liveHiRes ||
|
||||
(!live &&
|
||||
zc.t0 <= app.plotXMin(plotIdx) + 1e-9 &&
|
||||
zc.t1 >= app.plotXMax(plotIdx) - 1e-9));
|
||||
|
||||
/* History zoom cache (disk-backed, for ranges beyond in-memory ring) */
|
||||
/* History zoom cache (disk-backed, for ranges beyond in-memory ring).
|
||||
* Zoom responses carry the *requested* t0/t1 (not actual data bounds),
|
||||
* so a zoom cache can report coverage even when the ring had no data.
|
||||
* Prefer history over zoom when the in-memory ring cannot span the full
|
||||
* visible window — that is the typical history-browse case. */
|
||||
auto& hc = app.histZoomCache(plotIdx);
|
||||
const bool useHistData = !trigView && !live && !useZoomData &&
|
||||
hc.valid &&
|
||||
hc.t0 <= app.plotXMin(plotIdx) + 1e-9 &&
|
||||
hc.t1 >= app.plotXMax(plotIdx) - 1e-9;
|
||||
const bool haveHistCover = hc.valid &&
|
||||
hc.t0 <= app.plotXMin(plotIdx) + 1e-9 &&
|
||||
hc.t1 >= app.plotXMax(plotIdx) - 1e-9;
|
||||
bool useHistData = !trigView && !paused && !live && haveHistCover;
|
||||
if (useHistData) {
|
||||
/* Check that at least one signal has actual data points */
|
||||
bool anyData = false;
|
||||
for (const auto& hs : hc.signals) {
|
||||
if (!hs.t.empty()) { anyData = true; break; }
|
||||
}
|
||||
if (!anyData) { useHistData = false; }
|
||||
}
|
||||
|
||||
/* Fill tStore/vStore[si] from the frozen view (paused) or the live ring */
|
||||
auto readBase = [&](size_t si, const Signal& sig, const std::string& key) {
|
||||
@@ -369,8 +383,8 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
ImGui::PopStyleColor(2);
|
||||
}
|
||||
|
||||
/* Back / Fit (zoom history) — only meaningful when not live */
|
||||
if (!live && !trigView) {
|
||||
/* Back / Fit / Reset (zoom history) */
|
||||
if (!live || (trigView && app.trigZoomed(plotIdx))) {
|
||||
ImGui::SameLine();
|
||||
auto& hist = app.zoomHist(plotIdx);
|
||||
if (hist.empty()) { ImGui::BeginDisabled(); }
|
||||
@@ -380,16 +394,24 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
}
|
||||
if (hist.empty()) { ImGui::EndDisabled(); }
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton(ICON_FA_EXPAND " Fit##zf")) {
|
||||
double mn = 1e300, mx = -1e300;
|
||||
for (size_t si = 0; si < slots.size(); si++) {
|
||||
if (tStore[si].empty()) continue;
|
||||
mn = std::min(mn, tStore[si].front());
|
||||
mx = std::max(mx, tStore[si].back());
|
||||
if (trigView) {
|
||||
/* Reset to full capture window */
|
||||
if (ImGui::SmallButton(ICON_FA_EXPAND " Reset##zr")) {
|
||||
app.trigZoomed(plotIdx) = false;
|
||||
app.zoomHist(plotIdx).clear();
|
||||
}
|
||||
if (mx > mn) {
|
||||
app.pushZoomHist(plotIdx);
|
||||
app.setPlotX(plotIdx, mn, mx);
|
||||
} else {
|
||||
if (ImGui::SmallButton(ICON_FA_EXPAND " Fit##zf")) {
|
||||
double mn = 1e300, mx = -1e300;
|
||||
for (size_t si = 0; si < slots.size(); si++) {
|
||||
if (tStore[si].empty()) continue;
|
||||
mn = std::min(mn, tStore[si].front());
|
||||
mx = std::max(mx, tStore[si].back());
|
||||
}
|
||||
if (mx > mn) {
|
||||
app.pushZoomHist(plotIdx);
|
||||
app.setPlotX(plotIdx, mn, mx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -401,13 +423,23 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
ICON_FA_CLOCK_ROTATE_LEFT " HIST");
|
||||
}
|
||||
|
||||
/* Norm mode + cursors toggles */
|
||||
/* Norm/Dig/Mix mode — compact toggle buttons matching SmallButton height */
|
||||
ImGui::SameLine();
|
||||
{
|
||||
static const char* kVModes[] = {"Norm", "Dig", "Mix"};
|
||||
ImGui::SetNextItemWidth(58.f);
|
||||
char vmId[16]; snprintf(vmId, sizeof(vmId), "##vm%d", plotIdx);
|
||||
ImGui::Combo(vmId, &vMode, kVModes, 3);
|
||||
static const char* kVLabels[] = {"N", "D", "M"};
|
||||
static const char* kVTooltips[] = {"Normal", "Digital", "Mixed"};
|
||||
for (int vm = 0; vm < 3; vm++) {
|
||||
char vmId[16]; snprintf(vmId, sizeof(vmId), "%s##vm%d_%d", kVLabels[vm], plotIdx, vm);
|
||||
bool sel = (vMode == vm);
|
||||
if (sel) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.537f,0.706f,0.980f,0.4f));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.537f,0.706f,0.980f,1.f));
|
||||
}
|
||||
if (ImGui::SmallButton(vmId)) { vMode = vm; }
|
||||
if (sel) { ImGui::PopStyleColor(2); }
|
||||
if (ImGui::IsItemHovered()) { ImGui::SetTooltip("%s", kVTooltips[vm]); }
|
||||
if (vm < 2) { ImGui::SameLine(0.f, 1.f); }
|
||||
}
|
||||
}
|
||||
|
||||
/* ── VScale toolbar (shown when an active signal is selected) ───────── */
|
||||
@@ -500,10 +532,17 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
/* Y axis always ±4 div space */
|
||||
ImPlot::SetupAxisLimits(ImAxis_Y1, -4.05, 4.05, ImGuiCond_Always);
|
||||
|
||||
/* X axis: trig view → capture window; live → wall clock; else stored */
|
||||
/* X axis: trig view → capture window (zoomable); live → wall clock; else stored */
|
||||
double xMin, xMax;
|
||||
bool& trigZm = app.trigZoomed(plotIdx);
|
||||
if (trigView) {
|
||||
xMin = -cap->preSec; xMax = cap->postSec;
|
||||
if (trigZm) {
|
||||
xMin = app.plotXMin(plotIdx);
|
||||
xMax = app.plotXMax(plotIdx);
|
||||
} else {
|
||||
xMin = -cap->preSec;
|
||||
xMax = cap->postSec;
|
||||
}
|
||||
} else if (live && !paused) {
|
||||
if (liveHiRes) {
|
||||
/* Anchor to the latest fetched hi-res slice so the trace
|
||||
@@ -574,43 +613,54 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
/* Zoom-history debounce: coalesce rapid scroll steps into one entry */
|
||||
static double lastHistPush[kPlotSlotsMax] = {};
|
||||
|
||||
if (overPlot && !trigView) {
|
||||
if (overPlot) {
|
||||
ImGuiIO& io2 = ImGui::GetIO();
|
||||
const float wheel = io2.MouseWheel;
|
||||
const bool ctrl = io2.KeyCtrl;
|
||||
const bool shift = io2.KeyShift;
|
||||
const double now = ImGui::GetTime();
|
||||
|
||||
/* Helper: enter zoomed mode for trigger view (seed from capture window) */
|
||||
auto enterTrigZoom = [&]() {
|
||||
if (trigView && !trigZm) {
|
||||
app.setPlotX(plotIdx, -cap->preSec, cap->postSec);
|
||||
trigZm = true;
|
||||
}
|
||||
};
|
||||
|
||||
/* Helper: X-zoom the stored range by factor around center */
|
||||
auto xZoomStored = [&](double factor) {
|
||||
if (trigView) { enterTrigZoom(); }
|
||||
if (now - lastHistPush[plotIdx] > 0.6) {
|
||||
app.pushZoomHist(plotIdx);
|
||||
lastHistPush[plotIdx] = now;
|
||||
}
|
||||
double cx = (app.plotXMin(plotIdx) + app.plotXMax(plotIdx)) * 0.5;
|
||||
double half = (app.plotXMax(plotIdx) - app.plotXMin(plotIdx)) * 0.5 * factor;
|
||||
app.setPlotX(plotIdx, cx - half, cx + half);
|
||||
};
|
||||
|
||||
if (wheel != 0.f) {
|
||||
/* Zoom factor: scroll up = zoom in (×0.8), down = zoom out (×1.25) */
|
||||
const double zoomIn = 0.8;
|
||||
const double zoomOut = 1.25;
|
||||
double factor = (wheel > 0.f) ? zoomIn : zoomOut;
|
||||
|
||||
if (ctrl) {
|
||||
/* ── X zoom ─────────────────────────────────────────── */
|
||||
if (live) {
|
||||
if (!trigView && live) {
|
||||
app.setWindowSec(app.windowSec() * factor);
|
||||
} else {
|
||||
if (now - lastHistPush[plotIdx] > 0.6) {
|
||||
app.pushZoomHist(plotIdx);
|
||||
lastHistPush[plotIdx] = now;
|
||||
}
|
||||
double cx = (app.plotXMin(plotIdx) + app.plotXMax(plotIdx)) * 0.5;
|
||||
double half = (app.plotXMax(plotIdx) - app.plotXMin(plotIdx)) * 0.5 * factor;
|
||||
app.setPlotX(plotIdx, cx - half, cx + half);
|
||||
xZoomStored(factor);
|
||||
}
|
||||
} else if (shift) {
|
||||
/* ── Y offset of active signal ───────────────────────── */
|
||||
if (actSlot >= 0 && actSlot < (int)slots.size()) {
|
||||
auto& a = slots[actSlot];
|
||||
/* Switch to manual so position is retained */
|
||||
if (a.vs.mode != 2) {
|
||||
a.vs.divValue = std::max(a.vs.resolvedDiv, 1e-30);
|
||||
a.vs.offset = a.vs.resolvedOffset;
|
||||
a.vs.mode = 2;
|
||||
}
|
||||
/* Each scroll step moves by 0.5 screen divisions */
|
||||
a.vs.screenPos += (wheel > 0.f) ? 0.5 : -0.5;
|
||||
}
|
||||
} else {
|
||||
@@ -624,27 +674,21 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
}
|
||||
a.vs.divValue = std::max(a.vs.divValue * factor, 1e-30);
|
||||
} else {
|
||||
/* No active signal: plain scroll falls back to X zoom
|
||||
* so the wheel always does something useful. */
|
||||
if (live) {
|
||||
/* No active signal: plain scroll → X zoom */
|
||||
if (!trigView && live) {
|
||||
app.setWindowSec(app.windowSec() * factor);
|
||||
} else {
|
||||
if (now - lastHistPush[plotIdx] > 0.6) {
|
||||
app.pushZoomHist(plotIdx);
|
||||
lastHistPush[plotIdx] = now;
|
||||
}
|
||||
double cx = (app.plotXMin(plotIdx) + app.plotXMax(plotIdx)) * 0.5;
|
||||
double half = (app.plotXMax(plotIdx) - app.plotXMin(plotIdx)) * 0.5 * factor;
|
||||
app.setPlotX(plotIdx, cx - half, cx + half);
|
||||
xZoomStored(factor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Right-drag → X pan (non-live). Transition live→non-live on drag start. */
|
||||
/* Right-drag → X pan. Transition live→non-live on drag start;
|
||||
* in trigger view, enter trigger-zoom mode. */
|
||||
if (ImGui::IsMouseDragging(ImGuiMouseButton_Right)) {
|
||||
if (live) {
|
||||
/* Seed stored range from current live window */
|
||||
if (trigView) { enterTrigZoom(); }
|
||||
if (!trigView && live) {
|
||||
app.initPlotX(plotIdx, wallNow);
|
||||
live = false;
|
||||
lastHistPush[plotIdx] = now;
|
||||
@@ -662,8 +706,8 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Hi-res WS zoom requests ────────────────────────────────────── */
|
||||
if (!trigView) {
|
||||
/* ── Hi-res WS zoom requests (suppressed while paused) ──────────── */
|
||||
if (!trigView && !paused) {
|
||||
std::string csv;
|
||||
for (const auto& a : slots) {
|
||||
std::string k = app.slotKey(a);
|
||||
@@ -714,6 +758,28 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Clip data to visible X range before LTTB ─────────────────── *
|
||||
* The ring buffer may hold far more data than the visible window.
|
||||
* LTTB must operate only on the visible slice so it picks the best
|
||||
* 2400 points for what's actually on screen. */
|
||||
if (xMax > xMin) {
|
||||
for (size_t si = 0; si < slots.size(); si++) {
|
||||
auto& tv = tStore[si];
|
||||
auto& vv = vStore[si];
|
||||
if (tv.empty()) continue;
|
||||
auto lo = std::lower_bound(tv.begin(), tv.end(), xMin);
|
||||
auto hi = std::upper_bound(lo, tv.end(), xMax);
|
||||
if (lo != tv.begin()) --lo; /* keep one sample before window */
|
||||
if (hi != tv.end()) ++hi; /* keep one sample after window */
|
||||
size_t i0 = (size_t)(lo - tv.begin());
|
||||
size_t i1 = (size_t)(hi - tv.begin());
|
||||
if (i0 > 0 || i1 < tv.size()) {
|
||||
tv = std::vector<double>(tv.begin() + i0, tv.begin() + i1);
|
||||
vv = std::vector<double>(vv.begin() + i0, vv.begin() + i1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Plot each signal ──────────────────────────────────────────── */
|
||||
static const size_t kMaxPush = 2400;
|
||||
static thread_local std::vector<double> tDec, vDec, vNorm;
|
||||
@@ -762,8 +828,17 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
1.5f, ImPlotDragToolFlags_NoInputs);
|
||||
}
|
||||
|
||||
/* Cursors A/B — global: same position on every plot, drag anywhere */
|
||||
/* Cursors A/B — global: same position on every plot, drag anywhere.
|
||||
* Re-seed to visible range if both cursors are outside the view
|
||||
* (e.g. switching between live and trigger mode). */
|
||||
if (app.cursorsOn()) {
|
||||
bool aBeyond = (app.cursorA() < xMin || app.cursorA() > xMax);
|
||||
bool bBeyond = (app.cursorB() < xMin || app.cursorB() > xMax);
|
||||
if (aBeyond && bBeyond) {
|
||||
double range = xMax - xMin;
|
||||
app.cursorA() = xMin + range * 0.25;
|
||||
app.cursorB() = xMin + range * 0.75;
|
||||
}
|
||||
static const ImVec4 kCursA{0.980f,0.702f,0.529f,0.9f};
|
||||
static const ImVec4 kCursB{0.796f,0.651f,0.969f,0.9f};
|
||||
ImPlot::DragLineX(910, &app.cursorA(), kCursA, 1.2f);
|
||||
|
||||
Reference in New Issue
Block a user