feat: the x axis is relative to now, and an export carries both clocks

Three asked-for changes, one commit because they are one coordinate system.

The live axis is a fixed [-window, 0] with the newest sample at 0, so x reads
as an age and the labels stand still while the trace rolls under them. That
makes the display clock distinct from the stream clock for the first time, so
`ScopeModel::stream_at_x0()` becomes the single conversion between them
(`display_x = stream_t - stream_at_x0()` in all four modes) and a snapshot
carries its own origin in `EventSnapshot::x_offset`. The pause view is now
rebased like an event's, reversing an earlier deliberate choice: a frozen
absolute-stamped picture would sit a whole uptime right of a newest-relative
axis. That also fixed a latent bug, the export adding `trigger_time()` to a
*paused* view's stamps.

The status bar says how long you have been paused -- wall time, since the
producer never stops, through `format_elapsed()` so the field cannot widen.

The export writes two time columns: `time`, the axis as displayed, and
`abs_seconds`, the stream's own clock for the same sample. f64 seconds and
not the wire's integer, because `udps_frame_t::values` is a `const double*`
and the integer never exists in this process.

Cursors and measurement marks move in opposite directions under the rolling
axis, deliberately: a cursor is a position on screen and stays put, a mark
names a sample and travels with it. `App::measure_window` is stream-clocked
for a sharper reason -- a display-coordinate live window never moves, so the
"the window moved" recompute would never fire again.

Moves the deps/plotter pin to 58473db, which stops a time label's mantissa
reaching its own radix: a one-second tick differenced off a live axis read
`1e+3ms`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Opus 4.6 committed 2026-10-05 21:49:12 +02:00
1 parent 2f4530c128
commit b313cac46e
18 files changed
+501 -145

No files matched your search

+10 -4
View File
@@ -515,8 +515,9 @@ void ScopeModel::reset_live_history_locked() {
}
}
void ScopeModel::publish_pause_view(double lo_s, double hi_s) {
void ScopeModel::publish_pause_view(double lo_s, double hi_s, double x_offset) {
auto snap = std::make_shared<EventSnapshot>();
snap->x_offset = x_offset;
std::lock_guard<std::mutex> lock(mutex_);
BufferStore& src = live();
u64 first = 0;
@@ -525,10 +526,14 @@ void ScopeModel::publish_pause_view(double lo_s, double hi_s) {
const u64 n = last - first;
snap->stamps.resize(static_cast<std::size_t>(n));
if (n > 0) {
// The stamps go in *as the ring holds them*: the stream's own seconds, which is the
// coordinate the axis was already reading. Nothing is rebased, so a pause does not move the
// dates under the reader the way a trigger event's edge-relative axis deliberately does.
// The stamps come out of the ring on the stream's own clock and are rebased onto
// `x_offset`, which is the coordinate the axis was *already* reading: a live view is
// newest-relative, so leaving them absolute would throw the frozen picture a whole uptime
// to the right of the axis at the instant the user pressed pause. Nothing is lost by the
// shift -- `x_offset` is exactly what makes the stream's time recoverable, which is what
// `stream_at_x0()` reads it for.
src.time().copy_from(first, n, snap->stamps.data());
for (f64& t : snap->stamps) t -= x_offset;
}
snap->values.resize(channels_.size());
snap->y.resize(channels_.size(), plot::Range{0.0, 0.0});
@@ -551,6 +556,7 @@ void ScopeModel::publish_pause_view(double lo_s, double hi_s) {
void ScopeModel::publish_event(double pre_s, double post_s, double edge) {
auto snap = std::make_shared<EventSnapshot>();
snap->x_offset = edge; // the stream time x = 0 means, for everything that has to cross back
BufferStore& src = live();
plot::data::RingBuffer<f64>& tsrc = src.time();