Implemented and fixed many issues
This commit is contained in:
@@ -825,11 +825,17 @@ void App::onTriggerState(const std::string& json) {
|
||||
trigger_.trigTime = msg.trigTime;
|
||||
trigger_.hasTrigTime = true;
|
||||
}
|
||||
if (msg.hasWindow) {
|
||||
trigger_.firedPreS = msg.preSec;
|
||||
trigger_.firedPostS = msg.postSec;
|
||||
trigger_.hasFiredWin = true;
|
||||
}
|
||||
/* Double-buffer semantics: the last recorded capture stays on display
|
||||
* (even while re-armed/collecting) and is only replaced when a new
|
||||
* capture frame has been fully received and parsed (handleBinary v2). */
|
||||
if (msg.state == "idle") {
|
||||
trigger_.hasTrigTime = false;
|
||||
trigger_.hasFiredWin = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+11
-2
@@ -61,6 +61,11 @@ struct TriggerState {
|
||||
bool stopped = false;
|
||||
bool hasTrigTime = false;
|
||||
double trigTime = 0.0;
|
||||
/* Window the hub latched at fire time. Not the same as windowSec/prePercent
|
||||
* above, which are editable and may have moved on since the trigger fired. */
|
||||
bool hasFiredWin = false;
|
||||
double firedPreS = 0.0;
|
||||
double firedPostS = 0.0;
|
||||
};
|
||||
|
||||
/** Per-signal vertical scale state (oscilloscope style). */
|
||||
@@ -183,9 +188,12 @@ public:
|
||||
plotXMax_[i] = tMax;
|
||||
}
|
||||
|
||||
/** @brief Per-plot vertical normalisation: 0=normal 1=digital 2=mixed. */
|
||||
/** @brief Per-plot vertical normalisation: 0=normal 1=digital 2=mixed 3=unified. */
|
||||
int& plotVMode(int i) { return plotVMode_[i]; }
|
||||
|
||||
/** @brief The one scale every trace shares in unified mode (vMode 3). */
|
||||
VScale& plotUnifiedVS(int i) { return plotUniVS_[i]; }
|
||||
|
||||
/* ---- Cursors A/B (global: shared & synchronised across all plots) ---- */
|
||||
bool& cursorsOn() { return cursorsOn_; }
|
||||
double& cursorA() { return cursorA_; }
|
||||
@@ -302,7 +310,8 @@ private:
|
||||
double windowSec_ = 10.0; /* live scroll window width */
|
||||
double plotXMin_[kMaxPlotSlots] = {}; /* stored X min for non-live mode */
|
||||
double plotXMax_[kMaxPlotSlots] = {}; /* stored X max for non-live mode */
|
||||
int plotVMode_[kMaxPlotSlots] = {}; /* 0=normal 1=digital 2=mixed */
|
||||
int plotVMode_[kMaxPlotSlots] = {}; /* 0=normal 1=digital 2=mixed 3=unified */
|
||||
VScale plotUniVS_[kMaxPlotSlots]; /* shared scale used by vMode 3 */
|
||||
|
||||
/* Cursors (global) */
|
||||
bool cursorsOn_ = false;
|
||||
|
||||
+192
-61
@@ -85,6 +85,49 @@ static double normalizeY(double raw, const VScale& vs) {
|
||||
return (raw - vs.resolvedOffset) / vs.resolvedDiv + vs.screenPos;
|
||||
}
|
||||
|
||||
/** Resolve the one scale every trace shares in unified mode.
|
||||
*
|
||||
* Same rules as the per-signal version, applied to the union of the plot:
|
||||
* range takes the union of the declared ranges, auto fits the union of the
|
||||
* data. Signals whose slot is empty contribute nothing. */
|
||||
static void resolveUnifiedVScale(VScale& vs,
|
||||
const std::vector<PlotAssignment>& slots,
|
||||
const std::vector<Source>& sources,
|
||||
const std::vector<std::vector<double> >& vStore) {
|
||||
if (vs.mode == 2) { /* manual */
|
||||
vs.resolvedDiv = std::max(vs.divValue, 1e-30);
|
||||
vs.resolvedOffset = vs.offset;
|
||||
return;
|
||||
}
|
||||
double mn = 1e300, mx = -1e300;
|
||||
if (vs.mode == 1) { /* range: union of every declared range */
|
||||
for (const auto& a : slots) {
|
||||
if (a.sourceIdx < 0 || a.sourceIdx >= (int)sources.size()) continue;
|
||||
const auto& m = sources[a.sourceIdx].signals[a.signalIdx].meta;
|
||||
if (!(m.rangeMax > m.rangeMin)) continue;
|
||||
if (m.rangeMin < mn) mn = m.rangeMin;
|
||||
if (m.rangeMax > mx) mx = m.rangeMax;
|
||||
}
|
||||
if (mx > mn) {
|
||||
vs.resolvedDiv = std::max((mx - mn) / 8.0, 1e-30);
|
||||
vs.resolvedOffset = (mn + mx) / 2.0;
|
||||
return;
|
||||
}
|
||||
mn = 1e300; mx = -1e300; /* no usable range: fall through to auto */
|
||||
}
|
||||
for (const auto& vv : vStore) {
|
||||
for (double v : vv) {
|
||||
if (!std::isfinite(v)) continue;
|
||||
if (v < mn) mn = v;
|
||||
if (v > mx) mx = v;
|
||||
}
|
||||
}
|
||||
if (!std::isfinite(mn) || mn > mx) { mn = -1.0; mx = 1.0; }
|
||||
if (mn == mx) { mn -= 1.0; mx += 1.0; }
|
||||
vs.resolvedDiv = std::max((mx - mn) / 6.0, 1e-30);
|
||||
vs.resolvedOffset = (mx + mn) / 2.0;
|
||||
}
|
||||
|
||||
/** Min/max of a vector (returns false if empty/non-finite). */
|
||||
static bool dataMinMax(const std::vector<double>& v, double& mn, double& mx) {
|
||||
mn = 1e300; mx = -1e300;
|
||||
@@ -149,9 +192,36 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
const double wallNow = std::chrono::duration<double>(
|
||||
std::chrono::system_clock::now().time_since_epoch()).count();
|
||||
|
||||
/* Trigger view: render the hub capture relative to the trigger instant */
|
||||
/* Trigger view: render the hub capture relative to the trigger instant.
|
||||
*
|
||||
* Two ways to end up in trigger-relative time. Either a v2 capture frame
|
||||
* has arrived (trigView), or a trigger has fired and its window is still
|
||||
* filling (trigFill). In the second case the hub sends nothing until the
|
||||
* whole window has been produced — several seconds for a long window at a
|
||||
* high rate — so the trace is drawn from this client's own rings on the
|
||||
* final axis, growing left to right. Filling wins over the previous
|
||||
* capture: once a new trigger fires, the stale waveform is history. */
|
||||
const CaptureFrame* cap = app.capture();
|
||||
const bool trigView = (cap != nullptr) && app.showTrigBar();
|
||||
const TriggerState& trg = app.trigger();
|
||||
/* Prefer the window the hub latched at fire time; the local config is only
|
||||
* a fallback for hubs that do not report it, and may have been edited
|
||||
* since the trigger fired. */
|
||||
const double fillPreS = trg.hasFiredWin ? trg.firedPreS
|
||||
: trg.windowSec * trg.prePercent * 0.01;
|
||||
const double fillPostS = trg.hasFiredWin ? trg.firedPostS
|
||||
: trg.windowSec - fillPreS;
|
||||
|
||||
const bool trigFill = app.showTrigBar() && !paused &&
|
||||
trg.status == "collecting" && trg.hasTrigTime;
|
||||
const bool trigView = (cap != nullptr) && app.showTrigBar() && !trigFill;
|
||||
const bool trigRel = trigView || trigFill;
|
||||
|
||||
/* Window edges of whatever is on screen. A capture latches its own
|
||||
* pre/post at fire time, so later edits in the trigger bar must not move
|
||||
* the axis of a finished capture. */
|
||||
const double trigT = trigView ? cap->trigTime : trg.trigTime;
|
||||
const double trigPreS = trigView ? cap->preSec : fillPreS;
|
||||
const double trigPostS = trigView ? cap->postSec : fillPostS;
|
||||
|
||||
/* Hi-res zoom cache for this plot */
|
||||
auto& zc = app.zoomCache(plotIdx);
|
||||
@@ -194,13 +264,13 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
* from decimated pushes) undersamples the visible range. Periodically
|
||||
* fetch a fresh ~2400-pt slice from the hub raw ring and anchor the X
|
||||
* axis to the fetched slice (scope-style refresh at the fetch rate). */
|
||||
const bool liveHiRes = !trigView && live && !paused &&
|
||||
const bool liveHiRes = !trigRel && live && !paused &&
|
||||
app.windowSec() <= kLiveHiResMaxWin &&
|
||||
zc.valid &&
|
||||
(zc.t1 - zc.t0) >= app.windowSec() * 0.9 &&
|
||||
(wallNow - zc.t1) < 3.0;
|
||||
|
||||
const bool useZoomData = !trigView && !paused && zc.valid &&
|
||||
const bool useZoomData = !trigRel && !paused && zc.valid &&
|
||||
(liveHiRes ||
|
||||
(!live &&
|
||||
zc.t0 <= app.plotXMin(plotIdx) + 1e-9 &&
|
||||
@@ -215,7 +285,7 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
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;
|
||||
bool useHistData = !trigRel && !paused && !live && haveHistCover;
|
||||
if (useHistData) {
|
||||
/* Check that at least one signal has actual data points */
|
||||
bool anyData = false;
|
||||
@@ -231,7 +301,12 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
* copied tens of MB per signal per frame. A 10% margin keeps a sample on
|
||||
* each side so the later fine clip still has its boundary points. */
|
||||
double visT0, visT1;
|
||||
if (live) { visT1 = wallNow; visT0 = wallNow - app.windowSec(); }
|
||||
if (trigFill) {
|
||||
/* Absolute bounds of the trigger window: the ring is indexed on the
|
||||
* hub clock, the axis on trigger-relative time. */
|
||||
visT0 = trigT - trigPreS; visT1 = trigT + trigPostS;
|
||||
}
|
||||
else if (live) { visT1 = wallNow; visT0 = wallNow - app.windowSec(); }
|
||||
else { visT1 = app.plotXMax(plotIdx); visT0 = app.plotXMin(plotIdx); }
|
||||
{
|
||||
double margin = (visT1 - visT0) * 0.1;
|
||||
@@ -272,6 +347,15 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else if (trigFill) {
|
||||
/* Live ring, clipped to the (absolute) window and shifted onto the
|
||||
* trigger-relative axis. visT0/visT1 already carry a margin, so
|
||||
* clip here rather than reusing readBase. */
|
||||
(void) sig.buf.readRange(trigT - trigPreS, trigT + trigPostS,
|
||||
tStore[si], vStore[si]);
|
||||
for (size_t i = 0; i < tStore[si].size(); i++) {
|
||||
tStore[si][i] -= trigT;
|
||||
}
|
||||
} else if (useZoomData) {
|
||||
bool found = false;
|
||||
for (const auto& zs : zc.signals) {
|
||||
@@ -302,6 +386,11 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
resolveVScale(a, sig, vStore[si]);
|
||||
}
|
||||
|
||||
VScale& uniVS = app.plotUnifiedVS(plotIdx);
|
||||
if (vMode == 3) {
|
||||
resolveUnifiedVScale(uniVS, slots, sources, vStore);
|
||||
}
|
||||
|
||||
/* clamp active slot */
|
||||
if (actSlot >= (int)slots.size()) actSlot = -1;
|
||||
|
||||
@@ -326,9 +415,11 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
ImVec4(0.067f,0.067f,0.106f,1.f));
|
||||
|
||||
char badge[80];
|
||||
/* show vscale info: resolved div value */
|
||||
/* Show the div value actually in force: the plot's shared one in
|
||||
* unified mode, this signal's otherwise. */
|
||||
char dvbuf[16];
|
||||
fmtVal(dvbuf, sizeof(dvbuf), a.vs.resolvedDiv);
|
||||
fmtVal(dvbuf, sizeof(dvbuf),
|
||||
(vMode == 3) ? uniVS.resolvedDiv : a.vs.resolvedDiv);
|
||||
snprintf(badge, sizeof(badge), "%s %s/div##b%d",
|
||||
sig.meta.name.c_str(), dvbuf, i);
|
||||
|
||||
@@ -398,7 +489,7 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
}
|
||||
|
||||
/* Back / Fit / Reset (zoom history) */
|
||||
if (!live || (trigView && app.trigZoomed(plotIdx))) {
|
||||
if (!live || (trigRel && app.trigZoomed(plotIdx))) {
|
||||
ImGui::SameLine();
|
||||
auto& hist = app.zoomHist(plotIdx);
|
||||
if (hist.empty()) { ImGui::BeginDisabled(); }
|
||||
@@ -408,7 +499,7 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
}
|
||||
if (hist.empty()) { ImGui::EndDisabled(); }
|
||||
ImGui::SameLine();
|
||||
if (trigView) {
|
||||
if (trigRel) {
|
||||
/* Reset to full capture window */
|
||||
if (ImGui::SmallButton(ICON_FA_EXPAND " Reset##zr")) {
|
||||
app.trigZoomed(plotIdx) = false;
|
||||
@@ -440,10 +531,15 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
/* Norm/Dig/Mix mode — compact toggle buttons matching SmallButton height */
|
||||
ImGui::SameLine();
|
||||
{
|
||||
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);
|
||||
static const char* kVLabels[] = {"N", "U", "D", "M"};
|
||||
static const char* kVTooltips[] = {
|
||||
"Normal: one vertical scale per signal",
|
||||
"Unified: one vertical scale shared by every signal",
|
||||
"Digital", "Mixed" };
|
||||
static const int kVModes[] = {0, 3, 1, 2};
|
||||
for (int i = 0; i < 4; i++) {
|
||||
const int vm = kVModes[i];
|
||||
char vmId[16]; snprintf(vmId, sizeof(vmId), "%s##vm%d_%d", kVLabels[i], plotIdx, vm);
|
||||
bool sel = (vMode == vm);
|
||||
if (sel) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.537f,0.706f,0.980f,0.4f));
|
||||
@@ -451,28 +547,41 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
}
|
||||
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); }
|
||||
if (ImGui::IsItemHovered()) { ImGui::SetTooltip("%s", kVTooltips[i]); }
|
||||
if (i < 3) { ImGui::SameLine(0.f, 1.f); }
|
||||
}
|
||||
}
|
||||
|
||||
/* ── VScale toolbar (shown when an active signal is selected) ───────── */
|
||||
/* ── VScale toolbar ──────────────────────────────────────────────────── *
|
||||
* Normal mode edits the active signal's scale; unified mode edits the one
|
||||
* scale the whole plot shares, so it needs no selection. */
|
||||
VScale *toolVS = static_cast<VScale *>(0);
|
||||
if (vMode == 0 && actSlot >= 0 && actSlot < (int)slots.size()) {
|
||||
auto& a = slots[actSlot];
|
||||
toolVS = &slots[actSlot].vs;
|
||||
} else if (vMode == 3) {
|
||||
toolVS = &uniVS;
|
||||
}
|
||||
if (toolVS != static_cast<VScale *>(0)) {
|
||||
VScale& tvs = *toolVS;
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.f,2.f));
|
||||
|
||||
if (vMode == 3) {
|
||||
ImGui::TextDisabled("all signals");
|
||||
ImGui::SameLine(0.f,10.f);
|
||||
}
|
||||
|
||||
/* mode buttons */
|
||||
static const char* kModeLabels[] = {"Auto","Range","Manual"};
|
||||
for (int m = 0; m < 3; m++) {
|
||||
bool sel = (a.vs.mode == m);
|
||||
bool sel = (tvs.mode == m);
|
||||
if (sel) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button,
|
||||
ImVec4(0.537f,0.706f,0.980f,0.3f));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text,
|
||||
ImVec4(0.537f,0.706f,0.980f,1.f));
|
||||
}
|
||||
if (ImGui::SmallButton(kModeLabels[m])) { a.vs.mode = m; }
|
||||
if (ImGui::SmallButton(kModeLabels[m])) { tvs.mode = m; }
|
||||
if (sel) ImGui::PopStyleColor(2);
|
||||
if (m < 2) ImGui::SameLine(0.f,2.f);
|
||||
}
|
||||
@@ -480,23 +589,23 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
|
||||
/* resolved info */
|
||||
char rbuf[24], obuf[24];
|
||||
fmtVal(rbuf, sizeof(rbuf), a.vs.resolvedDiv);
|
||||
fmtVal(obuf, sizeof(obuf), a.vs.resolvedOffset);
|
||||
fmtVal(rbuf, sizeof(rbuf), tvs.resolvedDiv);
|
||||
fmtVal(obuf, sizeof(obuf), tvs.resolvedOffset);
|
||||
|
||||
if (a.vs.mode == 2) { /* manual: editable */
|
||||
if (tvs.mode == 2) { /* manual: editable */
|
||||
ImGui::SetNextItemWidth(70.f);
|
||||
ImGui::InputDouble("V/div##vd", &a.vs.divValue, 0,0,"%.4g");
|
||||
ImGui::InputDouble("V/div##vd", &tvs.divValue, 0,0,"%.4g");
|
||||
ImGui::SameLine(0.f,4.f);
|
||||
ImGui::SetNextItemWidth(80.f);
|
||||
ImGui::InputDouble("Offset##vo", &a.vs.offset, 0,0,"%.4g");
|
||||
ImGui::InputDouble("Offset##vo", &tvs.offset, 0,0,"%.4g");
|
||||
} else {
|
||||
ImGui::TextDisabled("%s/div @%s", rbuf, obuf);
|
||||
}
|
||||
ImGui::SameLine(0.f,10.f);
|
||||
ImGui::SetNextItemWidth(50.f);
|
||||
float sp = (float)a.vs.screenPos;
|
||||
float sp = (float)tvs.screenPos;
|
||||
if (ImGui::InputFloat("Pos(div)##vp", &sp, 0,0,"%.1f")) {
|
||||
a.vs.screenPos = sp;
|
||||
tvs.screenPos = sp;
|
||||
}
|
||||
|
||||
ImGui::PopStyleVar();
|
||||
@@ -539,7 +648,7 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
if (ImPlot::BeginPlot(plotId, ImVec2(-1.f,-1.f), plotFlags)) {
|
||||
|
||||
/* Both axes locked so ImPlot never overrides our explicit limits. */
|
||||
ImPlot::SetupAxes(trigView ? "t - trig (s)" : "Time (s)", nullptr,
|
||||
ImPlot::SetupAxes(trigRel ? "t - trig (s)" : "Time (s)", nullptr,
|
||||
ImPlotAxisFlags_Lock,
|
||||
ImPlotAxisFlags_Lock);
|
||||
|
||||
@@ -549,13 +658,17 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
/* X axis: trig view → capture window (zoomable); live → wall clock; else stored */
|
||||
double xMin, xMax;
|
||||
bool& trigZm = app.trigZoomed(plotIdx);
|
||||
if (trigView) {
|
||||
if (trigRel) {
|
||||
if (trigZm) {
|
||||
xMin = app.plotXMin(plotIdx);
|
||||
xMax = app.plotXMax(plotIdx);
|
||||
} else {
|
||||
xMin = -cap->preSec;
|
||||
xMax = cap->postSec;
|
||||
/* Full window from the start, even while filling: a trace that
|
||||
* grows into a fixed axis reads as progress; an axis that
|
||||
* grows with the data makes the whole trace shift every
|
||||
* frame and the time base meaningless. */
|
||||
xMin = -trigPreS;
|
||||
xMax = trigPostS;
|
||||
}
|
||||
} else if (live && !paused) {
|
||||
if (liveHiRes) {
|
||||
@@ -568,7 +681,7 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
} else {
|
||||
xMin = app.plotXMin(plotIdx); xMax = app.plotXMax(plotIdx);
|
||||
}
|
||||
if (trigView || (live && !paused) || !live) {
|
||||
if (trigRel || (live && !paused) || !live) {
|
||||
if (xMax > xMin) {
|
||||
ImPlot::SetupAxisLimits(ImAxis_X1, xMin, xMax, ImGuiCond_Always);
|
||||
}
|
||||
@@ -579,8 +692,16 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
static char yTickBufs[9][20];
|
||||
static const char* yTickLabels[9];
|
||||
|
||||
const VScale *axisVS = static_cast<const VScale *>(0);
|
||||
if (vMode == 0 && actSlot >= 0 && actSlot < (int)slots.size()) {
|
||||
const auto& av = slots[actSlot].vs;
|
||||
axisVS = &slots[actSlot].vs;
|
||||
} else if (vMode == 3) {
|
||||
/* Unified: the shared scale labels the axis for every trace at
|
||||
* once, so no signal has to be selected first. */
|
||||
axisVS = &uniVS;
|
||||
}
|
||||
if (axisVS != static_cast<const VScale *>(0)) {
|
||||
const VScale& av = *axisVS;
|
||||
for (int d = 0; d < 9; d++) {
|
||||
double divPos = yTickVals[d];
|
||||
double rawVal = av.resolvedOffset + (divPos - av.screenPos) * av.resolvedDiv;
|
||||
@@ -636,15 +757,15 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
|
||||
/* Helper: enter zoomed mode for trigger view (seed from capture window) */
|
||||
auto enterTrigZoom = [&]() {
|
||||
if (trigView && !trigZm) {
|
||||
app.setPlotX(plotIdx, -cap->preSec, cap->postSec);
|
||||
if (trigRel && !trigZm) {
|
||||
app.setPlotX(plotIdx, -trigPreS, trigPostS);
|
||||
trigZm = true;
|
||||
}
|
||||
};
|
||||
|
||||
/* Helper: X-zoom the stored range by factor around center */
|
||||
auto xZoomStored = [&](double factor) {
|
||||
if (trigView) { enterTrigZoom(); }
|
||||
if (trigRel) { enterTrigZoom(); }
|
||||
if (now - lastHistPush[plotIdx] > 0.6) {
|
||||
app.pushZoomHist(plotIdx);
|
||||
lastHistPush[plotIdx] = now;
|
||||
@@ -659,37 +780,45 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
const double zoomOut = 1.25;
|
||||
double factor = (wheel > 0.f) ? zoomIn : zoomOut;
|
||||
|
||||
/* Scroll adjusts the scale the axis is labelled with: the
|
||||
* active signal's in normal mode, the plot's shared one in
|
||||
* unified mode (where there is nothing to select). */
|
||||
VScale *wheelVS = static_cast<VScale *>(0);
|
||||
if (vMode == 3) {
|
||||
wheelVS = &uniVS;
|
||||
} else if (actSlot >= 0 && actSlot < (int)slots.size()) {
|
||||
wheelVS = &slots[actSlot].vs;
|
||||
}
|
||||
/* Seed manual from the resolved values so the gesture sticks. */
|
||||
auto latchManual = [](VScale& v) {
|
||||
if (v.mode != 2) {
|
||||
v.divValue = std::max(v.resolvedDiv, 1e-30);
|
||||
v.offset = v.resolvedOffset;
|
||||
v.mode = 2;
|
||||
}
|
||||
};
|
||||
|
||||
if (ctrl) {
|
||||
/* ── X zoom ─────────────────────────────────────────── */
|
||||
if (!trigView && live) {
|
||||
if (!trigRel && live) {
|
||||
app.setWindowSec(app.windowSec() * factor);
|
||||
} else {
|
||||
xZoomStored(factor);
|
||||
}
|
||||
} else if (shift) {
|
||||
/* ── Y offset of active signal ───────────────────────── */
|
||||
if (actSlot >= 0 && actSlot < (int)slots.size()) {
|
||||
auto& a = slots[actSlot];
|
||||
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;
|
||||
}
|
||||
a.vs.screenPos += (wheel > 0.f) ? 0.5 : -0.5;
|
||||
/* ── Y pan ───────────────────────────────────────────── */
|
||||
if (wheelVS != static_cast<VScale *>(0)) {
|
||||
latchManual(*wheelVS);
|
||||
wheelVS->screenPos += (wheel > 0.f) ? 0.5 : -0.5;
|
||||
}
|
||||
} else {
|
||||
/* ── Y zoom of active signal ─────────────────────────── */
|
||||
if (actSlot >= 0 && actSlot < (int)slots.size()) {
|
||||
auto& a = slots[actSlot];
|
||||
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;
|
||||
}
|
||||
a.vs.divValue = std::max(a.vs.divValue * factor, 1e-30);
|
||||
/* ── Y zoom ──────────────────────────────────────────── */
|
||||
if (wheelVS != static_cast<VScale *>(0)) {
|
||||
latchManual(*wheelVS);
|
||||
wheelVS->divValue = std::max(wheelVS->divValue * factor, 1e-30);
|
||||
} else {
|
||||
/* No active signal: plain scroll → X zoom */
|
||||
if (!trigView && live) {
|
||||
if (!trigRel && live) {
|
||||
app.setWindowSec(app.windowSec() * factor);
|
||||
} else {
|
||||
xZoomStored(factor);
|
||||
@@ -701,8 +830,8 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
/* Right-drag → X pan. Transition live→non-live on drag start;
|
||||
* in trigger view, enter trigger-zoom mode. */
|
||||
if (ImGui::IsMouseDragging(ImGuiMouseButton_Right)) {
|
||||
if (trigView) { enterTrigZoom(); }
|
||||
if (!trigView && live) {
|
||||
if (trigRel) { enterTrigZoom(); }
|
||||
if (!trigRel && live) {
|
||||
app.initPlotX(plotIdx, wallNow);
|
||||
live = false;
|
||||
lastHistPush[plotIdx] = now;
|
||||
@@ -721,7 +850,7 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
}
|
||||
|
||||
/* ── Hi-res WS zoom requests (suppressed while paused) ──────────── */
|
||||
if (!trigView && !paused) {
|
||||
if (!trigRel && !paused) {
|
||||
std::string csv;
|
||||
for (const auto& a : slots) {
|
||||
std::string k = app.slotKey(a);
|
||||
@@ -821,9 +950,11 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
} else if (vMode == 2) { /* mixed */
|
||||
bandNormalize(vDec, vNorm, myKi, nTraces, a.vs.digitalInMixed);
|
||||
} else {
|
||||
/* unified shares one scale, normal gives each trace its own */
|
||||
const VScale& nvs = (vMode == 3) ? uniVS : a.vs;
|
||||
vNorm.resize(nOut);
|
||||
for (size_t k = 0; k < nOut; k++) {
|
||||
vNorm[k] = normalizeY(vDec[k], a.vs);
|
||||
vNorm[k] = normalizeY(vDec[k], nvs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -836,7 +967,7 @@ void RenderPlotPanel(App& app, int plotIdx, bool& paused) {
|
||||
}
|
||||
|
||||
/* Trigger instant marker (capture view: t = 0) */
|
||||
if (trigView) {
|
||||
if (trigRel) {
|
||||
double t0m = 0.0;
|
||||
ImPlot::DragLineX(900, &t0m, ImVec4(1.f,1.f,0.f,0.8f),
|
||||
1.5f, ImPlotDragToolFlags_NoInputs);
|
||||
|
||||
@@ -458,6 +458,12 @@ bool ParseTriggerState(const std::string& json, TriggerStateMsg& out) {
|
||||
double tt = 0.0;
|
||||
out.hasTrigTime = jsonGetDouble(json.c_str(), "trigTime", tt);
|
||||
out.trigTime = tt;
|
||||
|
||||
double pre = 0.0, post = 0.0;
|
||||
out.hasWindow = jsonGetDouble(json.c_str(), "preSec", pre) &&
|
||||
jsonGetDouble(json.c_str(), "postSec", post);
|
||||
out.preSec = pre;
|
||||
out.postSec = post;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -109,6 +109,11 @@ struct TriggerStateMsg {
|
||||
bool stopped = false;
|
||||
bool hasTrigTime = false;
|
||||
double trigTime = 0.0;
|
||||
/* Window latched at fire time, sent alongside trigTime. Older hubs omit
|
||||
* it, hence hasWindow — fall back to the local trigger config then. */
|
||||
bool hasWindow = false;
|
||||
double preSec = 0.0;
|
||||
double postSec = 0.0;
|
||||
};
|
||||
|
||||
/*---------------------------------------------------------------------------*/
|
||||
|
||||
Reference in New Issue
Block a user