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

+15 -3
View File
@@ -178,6 +178,14 @@ class RowSet {
void set_time_window(double seconds);
double time_window() const noexcept { return window_s_; }
/// The newest x any row's live traces hold, which is what the link group follows -- and, since
/// it is reported in the display epoch currently in force, also what the frame tick subtracts
/// from that epoch to park the newest sample at x = 0. It must be read **after** `bind()` and
/// `acquire()`: it asks the bound series, so before them it answers for the previous frame's
/// window, and a frame's production at 1 MS/s over a 50 ms view is a third of the screen.
/// False when no row holds a streaming trace.
bool live_edge(double& newest) const;
void set_y_auto(int row, bool on);
void set_y_range(int row, double lo, double hi);
@@ -191,7 +199,13 @@ class RowSet {
void place_crossings();
CursorMode cursor_mode() const noexcept { return cursor_mode_; }
/// The two vertical cursors' stream times; shared by every row.
/// The two vertical cursors' positions, in the **display** x the axis reads -- not stream
/// seconds, which they coincided with only while `display_epoch()` was always 0. A live view
/// is newest-relative now, so a cursor left at -1.5 s keeps meaning "1.5 s ago" and the data
/// flows under it, which is what a bench scope's roll mode does and is the behaviour that was
/// asked for. They are read back out of the plots and written to them unchanged, so there is
/// nothing to convert; anything that needs the stream's own clock goes through
/// `ScopeModel::stream_at_x0()`. Shared by every row.
double cursor_a_x() const noexcept { return cursor_a_x_; }
double cursor_b_x() const noexcept { return cursor_b_x_; }
/// One row's ruler values, or 0 for a row that does not exist.
@@ -399,8 +413,6 @@ class RowSet {
/// at the end of the gesture (`Controller::end_drag()`), and a value read out of the plot
/// mid-drag has already been through that act.
void place_cursors(Row& r, plot::CursorPlacement placement = plot::CursorPlacement::Snapped);
/// The newest x any row's live traces hold, which is what the group follows.
bool live_edge(double& newest) const;
std::unique_ptr<plot::Figure> figure_;
/// The one x link group every visible row belongs to. Groups are numbered from 1.