Implemented and fixed many issues

This commit is contained in:
Martino Ferrari
2026-08-21 23:24:48 +02:00
parent 14d5351a81
commit e03c60db25
52 changed files with 7726 additions and 769 deletions
+192 -61
View File
@@ -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);