improved measuraments ,cursors, zooming and user interactions

This commit is contained in:
Martino Ferrari committed 2026-10-03 16:05:45 +02:00
1 parent 0eceaf537b
commit 02620ca946
12 files changed
+1488 -171

No files matched your search

+101 -8
View File
@@ -106,6 +106,11 @@ class RowSet {
/// it keeps its position whatever the cursor toggle says.
plot::u32 trigger_marker = 0;
/// The caption naming the window's start while the x labels are relative: an annotation
/// inside the axes, so its text can change every frame without moving the layout (an axis
/// *title* would, and it would be a different width each time).
plot::u32 time_origin_note = 0;
/// The measurements' readout marks: a pool created with the plot (a `Plot` cannot add or
/// drop a cursor after the fact) and parked unless a measurement is using the slot.
std::array<plot::u32, kMeasureMarks> measure_h{};
@@ -208,6 +213,19 @@ class RowSet {
/// end up with its instruments parked while every other row showed them.
bool cursors_visible() const noexcept { return cursors_on_; }
/// Whether the x labels are currently written relative to the window's start, and what that
/// start was. The mode is decided by the span (see `update_time_labels()`), which is why it
/// is state rather than a per-frame expression: the switch has hysteresis, so a span
/// hovering on the boundary cannot make the labels flicker between two forms.
bool time_labels_relative() const noexcept { return relative_x_; }
/// The value the x labels are relative to (meaningless when `time_labels_relative()` is
/// false). The host prints it beside the axis so the reader can add it to a tick.
double time_labels_origin() const noexcept { return x_origin_; }
/// Chooses the x label form for the window the rows are showing and applies it: relative
/// with the window's own start as the origin when the span is sub-second, absolute
/// otherwise. Called once per frame from the tick, and returns true when the form changed.
bool update_time_labels();
/// One measurement's readout mark on a row: a *level* (drawn as a horizontal ruler) or a
/// *time* (a vertical marker). Which one a measurement needs is the measurement's own
/// business; the row is the one that plots its signal.
@@ -227,18 +245,75 @@ class RowSet {
/// measurement is drawn", as opposed to merely computed.
int measure_marks_placed() const noexcept { return measure_marks_placed_; }
/// Whether every instrument this app *computed* is out of the pointer's reach: the trigger
/// marker and the measurement marks. They are readouts -- where the scope put the edge, what a
/// measurement measured -- and a user must not be able to drag them; the report in the dump is
/// how that is checked, since a drag is not something a probe can perform.
bool readouts_locked() const noexcept;
/// How many times `clamp_to_data()` re-framed the view. A clamp that fires every frame is a
/// *loop*, not a clamp -- and `span > window_s_` can stay true after clamping to exactly
/// `window_s_`, because the group's span is the range's span through f64 and lands a ULP over.
/// Reported in the dump so a flickering view has a number rather than a description.
int clamp_fires() const noexcept { return clamp_fires_; }
/// The window the rows are actually on: the *group's* range, which is what the gestures and the
/// clamps write. A member's own range only catches up at the next layout, so a probe that reads
/// a member to judge a clamp sees the previous frame's window.
plot::Range view_range() const noexcept {
return figure_ ? figure_->x_link_range(x_group_) : plot::Range{0.0, 0.0};
}
/// Re-states the pinned window, so a gesture cannot move it.
///
/// In trigger mode the group follows nothing: `clamp_to_data()` is live-only by design, and the
/// library's own pan bound lives in the *following* path (a `Live` group's offset is clamped
/// against what its members retain), so with the follow off nothing pulled a drag back -- a pan
/// or a zoom-out walked the frozen capture off its axis and left an empty plot over data the
/// scope never had. The view is kept inside the event instead: the span is capped at the event's
/// and the position clamped into it, so a **zoom in is allowed** (less of the same event is
/// still the event) while leaving it is not. Doing it by re-stating the pin -- this function's
/// first version -- made the wheel a no-op, i.e. an acquired event could not be examined at all.
/// Does nothing when no window is pinned.
void hold_pinned_window();
/// Pins the axes where they are for the pause's frozen view: the snapshot is not a streaming
/// series, so live follow has nothing to derive a window from, and the frame the pause froze
/// has to stay the frame on screen. `unpin_pause_window()` restores following and re-frames on
/// the newest data, which is what a resume should show.
/// `lo`/`hi` are the *snapshot's* own stamps -- the data the frozen view actually holds, which
/// may be narrower than the window that was requested when the pause began.
void pin_pause_window(double lo, double hi);
/// The window the bound **event data** spans: the union of the event series the rows hold. What
/// a `Held` view may be navigated *within* -- the snapshot's stamps, never the recorded
/// `pre_s`/`post_s` split, which is the *request* and can exceed what was captured. False when
/// no row holds an event.
bool event_data_window(double& lo, double& hi) const;
/// Sets the pin without touching what is on screen: the bound a gesture is clamped into. The
/// `Held` tick re-states it every frame (the snapshot does not move, so it is idempotent) while
/// a user's zoom -- which is a narrower window *inside* the pin -- survives.
void set_data_pin(double lo, double hi);
void unpin_pause_window();
/// Frames the x window on the data row 0 holds, leaving every y axis alone. The x
/// counterpart of `fit_all()`, for the one thing the y axes cannot say: how wide the
/// window should be.
void fit_x();
/// Adopts the instruments a gesture moved. The vertical pair is one time for the whole
/// figure, so a drag on any row's marker moves every row's; a ruler belongs to its own
/// row and is only read back. Either way the plot is where a drag writes, so this is the
/// one place the app can see it -- and the pane reads what is stored here, which is why a
/// ruler that was not read back showed a stale value while its marker on the plot moved.
/// Called once a frame from the tick.
void sync_cursors();
/// Adopts the instruments a gesture is moving, on `drag_row` (the row whose controller holds
/// a cursor), and nothing otherwise.
///
/// Reading the plots is only meaningful *while a drag runs*: `cursor(id).x` reports the free
/// pointer position then, and the snapped one otherwise -- which is what this very code wrote
/// a frame earlier (or the snap of it). Adopting outside a drag therefore adopts our own echo
/// back and a row's stale copy of the shared time, which is what made a dragged cursor spring
/// to the position it had before the gesture. The vertical pair is one time for the whole
/// figure, so a drag on any row moves every row's; a ruler belongs to its own row alone.
/// Called once a frame from the tick, with `drag_row` < 0 when no cursor is held.
void sync_cursors(int drag_row);
/// Puts both vertical cursors at a quarter and three quarters of the x window, and
/// the two rulers at a third and two thirds of each row's y range. Also switches the
@@ -309,7 +384,13 @@ class RowSet {
static void clear_row(Row& r);
void install(Row& r, int row_index, plot::Plot& p);
void place_cursors(Row& r);
/// Writes a row's instruments from the stored values. `placement` is passed through to the
/// plot: a *programmatic* placement (reset, a mode change) wants the default `Snapped`, so
/// the marker lands on a sample; echoing a value a *drag* just produced wants `Free`, because
/// snapping it again is what made a dragged cursor spring back -- the library snaps a cursor
/// 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;
@@ -325,11 +406,23 @@ class RowSet {
double pinned_pre_ = 0.0;
double pinned_post_ = 0.0;
bool pinned_ = false;
int clamp_fires_ = 0;
/// The window the pinned clamp last stated. A row's own axis that differs from this is a
/// gesture to be honoured; one that matches it is just the clamp's own answer arriving back.
plot::Range pinned_stated_{0.0, 0.0};
double y_gutter_px_ = 0.0;
bool y_gutter_reserved_ = false;
std::array<Row, static_cast<std::size_t>(kMaxRows)> storage_{};
CursorMode cursor_mode_ = CursorMode::Vertical;
bool cursors_on_ = false;
bool relative_x_ = false;
double x_origin_ = 0.0;
/// What was last *written* to each row's own vertical cursors. A row is only evidence of a
/// gesture when its value differs from its own last write: the pair's value is shared, so a
/// row that has not been re-written yet still holds the previous shared value, and adopting
/// *that* would undo the row the user actually dragged -- which is what made a drag spring
/// back, one row's stale echo at a time.
int measure_marks_placed_ = 0;
/// The one stream time each vertical cursor is at, whatever row's copy was last moved.
double cursor_a_x_ = 0.0;