fix: a fit undoes an x zoom instead of redefining the time window
The previous commit took "fit x sets the window properly" to mean a fit should frame the data and propagate that as the new time scale. It should not: a fit is a plot interaction, and the window is also the trigger's event span and how the samples are stored, so a fit that framed the data could re-plan the rings and drop the history on a menu click. What a fit means here is the undo of a zoom -- a 2 s window zoomed into reads 2 s again. `fit_x()` therefore reads `window_s_` and never writes it. Live it takes the window-control path with the window already set, which re-arms the follow and parks the newest span at the live edge. Pinned -- a held event, a filling capture or a paused view -- it restores the pin's full range, which is the snapshot's own span and may be narrower than the requested window, and it writes the per-row member axes as well as the link group: `hold_pinned_window()` prefers a member that differs from `pinned_stated_`, so writing the group alone is reverted to the zoomed range one frame later. `apply_window_scale()` is folded back into `apply_time_window()`, which the window control is again the only caller of. The probe now zooms in before fitting, since otherwise there is nothing to undo, and reports `pinned=`. `window` and `event_span` must be unchanged on every line. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
9888fd9977
commit
3d7e214295
7 files changed
+193
-150
No files matched your search
@@ -449,18 +449,28 @@ Two lines of the self-check output are assertions in disguise:
|
|||||||
equal `origin + first_rel` as a *tick label* plus its caption, i.e. the labels and the
|
equal `origin + first_rel` as a *tick label* plus its caption, i.e. the labels and the
|
||||||
caption must share one anchor. `x_origin_set` is read back out of the plot; it must be 0 in
|
caption must share one anchor. `x_origin_set` is read back out of the plot; it must be 0 in
|
||||||
the absolute form.
|
the absolute form.
|
||||||
- `[fit-probe] <action> window=.. event_span=.. x=[..] span=.. y=[..]` (stderr,
|
- `[fit-probe] <action> pinned=D window=.. event_span=.. x=[..] span=.. y=[..]` (stderr,
|
||||||
`UDPSCOPE_PROBE_FIT_MS`) — the two fits, through the **menu handlers** rather than the bare
|
`UDPSCOPE_PROBE_FIT_MS`) — the two fits, through the **menu handlers** rather than the bare
|
||||||
`RowSet` calls, because half of what a fit now does lives there (see the time-scale invariant
|
`RowSet` calls, because the notice and the control are the handler's half. A fit is the **undo
|
||||||
below). It first puts the y scale and the window where the data is *not* (a fixed
|
of an x zoom**, so the probe performs one: it moves the y scale off the data (a fixed
|
||||||
`[-100, 100]`, a 50 ms window), so the assertions have something to be wrong about: **`fit x`
|
`[-100, 100]`), sets a 2 s window through the control's own path, then zooms row 0 to a quarter
|
||||||
must widen the window to the data's span and leave `y` exactly as it was**, and **`fit all`
|
of it the way a gesture does — on the *member* axis — and fits. Four assertions.
|
||||||
must then move `y` to the data's own range**. A `y` that changed on the `fit x` line is the two
|
**`window` and `event_span` must be unchanged on every line** and equal to the typed 2 s: a fit
|
||||||
paths aliased. **`event_span` must equal `window` on every line**: a fit states a new time
|
is a plot interaction and may not restate the time scale (see the invariant below), and they are
|
||||||
scale, so the trigger's event span, the buffer partition and the control have to move with it —
|
the only two readings that can see it try. The `zoomed` line's `span` must be a quarter of the
|
||||||
a fit that moved the view alone left `event_span` on the *old* window, and the next decade step
|
window, or the fits have nothing to undo and the lines below mean nothing. **`fit x` must put
|
||||||
or unit change then applied the fitted value for real, so one action changed the time scale
|
`span` back to the whole window and leave `y` exactly as it was**, and **`fit all` must do the
|
||||||
twice.
|
same and move `y` to the data's own range** — a `y` that changed on the `fit x` line is the two
|
||||||
|
paths aliased. `x`'s *position* advances between lines on a live view and is not an assertion:
|
||||||
|
the follow is re-armed on the newest data, which is what the whole window means there.
|
||||||
|
Run it **inside a pause** as well (`UDPSCOPE_PROBE_PAUSE_MS=2000 UDPSCOPE_PROBE_FIT_MS=2200`),
|
||||||
|
which is the other branch and the one that can silently revert: `pinned=1`, and `fit x` must
|
||||||
|
restore the *pin's* full range — the snapshot's own stamps, legitimately narrower than the
|
||||||
|
requested window (`span=1.179900` against `window=2.000000` is correct, not a finding) — and
|
||||||
|
must still read `pinned=1` afterwards. The pin is restored by writing the **members** as well as
|
||||||
|
the group, because `hold_pinned_window()` prefers a member that has moved since `pinned_stated_`
|
||||||
|
— that is how it tells a gesture from its own echo — so a fit that wrote the group alone reads
|
||||||
|
back as the zoomed range one frame later.
|
||||||
- `[layout-probe] <action> rows=N buttons=B done=D done_box=[x,y,WxH] rows_y=[..] close=[..] add=[..] traces=[..]`
|
- `[layout-probe] <action> rows=N buttons=B done=D done_box=[x,y,WxH] rows_y=[..] close=[..] add=[..] traces=[..]`
|
||||||
(stderr, `UDPSCOPE_PROBE_LAYOUT_MS`) — **`buttons` must be `2N+1`** while the editor is
|
(stderr, `UDPSCOPE_PROBE_LAYOUT_MS`) — **`buttons` must be `2N+1`** while the editor is
|
||||||
up (one `×` per row, one `+` per boundary) and **0 on the `editor off` line**: the
|
up (one `×` per row, one `+` per boundary) and **0 on the `editor off` line**: the
|
||||||
@@ -1361,30 +1371,37 @@ far the largest file.
|
|||||||
that grew from `1 s` to `123.456 ms` would widen the bar and relayout everything under it
|
that grew from `1 s` to `123.456 ms` would widen the bar and relayout everything under it
|
||||||
on a mere change of window. That is also why the window is typed into a popover rather
|
on a mere change of window. That is also why the window is typed into a popover rather
|
||||||
than shown in the bar.
|
than shown in the bar.
|
||||||
- **A plot interaction may not quietly redefine the time scale.** The *time scale* is more than
|
- **No plot interaction changes the time scale; only the window control does.** The *time scale*
|
||||||
the axis: `RowSet::window_s_` also feeds `ScopeModel::set_event_span()` (the trigger's
|
is more than the axis: `RowSet::window_s_` also feeds `ScopeModel::set_event_span()` (the
|
||||||
pre-trigger share is a share of *this* window), `ScopeModel::set_target_span()` (which may
|
trigger's pre-trigger share is a share of *this* window), `ScopeModel::set_target_span()`
|
||||||
re-plan the partition, move the decimation and drop history), the window control's readout and
|
(which may re-plan the partition, move the decimation and **drop the history**), the window
|
||||||
the session file. A **zoom or a pan therefore does not touch it at all** — the controller
|
control's readout and the session file. A **zoom or a pan** does not touch it by construction —
|
||||||
writes the *member* axis and `clamp_to_data()` caps the span at `window_s_`, so a gesture is
|
the controller writes the *member* axis and `clamp_to_data()` caps the span at `window_s_`, so
|
||||||
view-only by construction and needs no code to keep it that way. The **two fits do** state a
|
nothing has to be written to keep it that way. **Neither do the two fits**: a fit is the
|
||||||
new scale, and they must state the whole of it: `on_fit_clicked`/`on_fit_x_clicked` take the
|
*undo of a zoom*, so `RowSet::fit_x()` restores the window that is already set and reads
|
||||||
span `RowSet::fit_x()`/`fit_all()` report and hand it to **`apply_window_scale()`**. A fit that
|
`window_s_` without ever writing it. Both fits used to frame the *data* instead and move
|
||||||
moved `window_s_` alone — which is what both of them used to do — left the event span on the
|
`window_s_` alone, which was wrong twice over — a gesture-triggered re-plan of the rings, and
|
||||||
*old* window, and the next decade step or unit change then fed the fitted value in for real:
|
an event span left on the *old* window until the next decade step or unit change applied the
|
||||||
one action, two changes of time scale, and `window == event_span` true at neither point.
|
fitted value for real (one action, two changes of time scale).
|
||||||
- **`apply_window_scale()` is the only place the time scale changes, and `apply_time_window()`
|
- **`apply_time_window()` is the only place the time scale changes**, and the window control is
|
||||||
is the only place the *view* changes with it.** The first clamps to `window_limit()` and moves
|
its only caller. It clamps to `window_limit()` and moves `set_target_span()`,
|
||||||
`set_target_span()`, `set_event_span()`, the control and the session save together; the second
|
`set_event_span()`, `RowSet::set_time_window()`, the control and the session save together —
|
||||||
calls it and then `RowSet::set_time_window()`. The split is not cosmetic:
|
the partition **before** the axis, so the axis is framed over the rings that will exist. A fit
|
||||||
`set_time_window()` unpins and re-arms the live follow, which is right for a typed window (a
|
must not come here for two reasons: it is not entitled to the scale at all, and
|
||||||
statement about what to watch from now on) and wrong for a fit of a held event or a paused
|
`set_time_window()` unpins and re-arms the live follow, which would throw a held event or a
|
||||||
view, which has nothing bound to a live edge — so the fits take the scale half only, and
|
paused view onto a live edge it has nothing bound to. Paths that change the span for their own
|
||||||
`fit_x()` writes the link group directly while `pinned_`. The partition is moved **before** the
|
reasons (an event pin) call `sync_window_display()` alone, which only restates the control and
|
||||||
axis, so the axis is framed over the rings that will exist. Paths that change the span for
|
never moves the plot — calling `apply_time_window()` there would clamp a legitimately short
|
||||||
their own reasons (an event pin) still call `sync_window_display()` alone, which only restates
|
capture up to the requested window.
|
||||||
the control and never moves the plot — calling either `apply_` there would clamp a legitimately
|
- **`fit_x()` restores the *whole of what the view may show*, which is not always `window_s_`.**
|
||||||
short capture up to the requested window.
|
Live, that is the newest `window_s_` at the live edge (`set_time_window(window_s_)`, the same
|
||||||
|
path a resume takes). Pinned — a held event, a filling capture or a paused view — it is the
|
||||||
|
**pin's** full range, which is the snapshot's own stamps and may be legitimately *narrower*
|
||||||
|
than the requested window. It must write the per-row **member** axes as well as the link group:
|
||||||
|
`hold_pinned_window()` prefers a member that differs from `pinned_stated_`, which is how it
|
||||||
|
tells a real gesture from its own echo, so writing the group alone is reverted to the zoomed
|
||||||
|
range on the next frame. `fit_all()` is the y fit plus this, shared rather than duplicated so
|
||||||
|
the two cannot drift apart.
|
||||||
- **The header bar's buttons are icon-only**, so headless probes must reach them
|
- **The header bar's buttons are icon-only**, so headless probes must reach them
|
||||||
through the `App` pointers (`trigger_toggle`, `pause_button`, …), not via
|
through the `App` pointers (`trigger_toggle`, `pause_button`, …), not via
|
||||||
`find_labelled_button()`. The **pane switches are toolbar buttons** too (`cursors_toggle`,
|
`find_labelled_button()`. The **pane switches are toolbar buttons** too (`cursors_toggle`,
|
||||||
|
|||||||
@@ -983,9 +983,9 @@ that changes nothing still says why. Each plot's y axis has a right-click menu
|
|||||||
with auto scale, a fixed range, **Fit X axis**, **Fit all**, and one
|
with auto scale, a fixed range, **Fit X axis**, **Fit all**, and one
|
||||||
**Remove '<signal>' from plot N** entry per occupied trace slot (a plot with nothing on
|
**Remove '<signal>' from plot N** entry per occupied trace slot (a plot with nothing on
|
||||||
it says so instead). The two fits are the same pair the overflow menu and the `f` key
|
it says so instead). The two fits are the same pair the overflow menu and the `f` key
|
||||||
offer, split by what they touch: **Fit X axis** frames the window on the data the ring
|
offer, split by what they touch: **Fit X axis** puts the x view back on the whole time
|
||||||
holds and leaves every y scale exactly as the user set it, **Fit all** does that and
|
window and leaves every y scale exactly as the user set it, **Fit all** does that and
|
||||||
auto-scales the y axes too.
|
auto-scales the y axes too. Neither changes the time window itself — see below.
|
||||||
|
|
||||||
**The plot does not twitch.** Two independent causes were fixed, both of which
|
**The plot does not twitch.** Two independent causes were fixed, both of which
|
||||||
showed up as the x axis moving a few pixels every frame — including while paused,
|
showed up as the x axis moving a few pixels every frame — including while paused,
|
||||||
@@ -1276,18 +1276,24 @@ logged with both rates and the new anti-alias corner. A window change that leave
|
|||||||
limitations below); only the shared clock is decimated.
|
limitations below); only the shared clock is decimated.
|
||||||
|
|
||||||
**Fit to data** (overflow menu, the `f` key, or **Fit all** in a plot's right-click menu)
|
**Fit to data** (overflow menu, the `f` key, or **Fit all** in a plot's right-click menu)
|
||||||
auto-scales every row's y axis and re-frames the x window on the data the ring holds,
|
auto-scales every row's y axis to the data it holds and puts the x view back on the whole time
|
||||||
propagated to every row. **Fit X axis** is the same re-framing without the y scaling, for
|
window, propagated to every row. **Fit X axis** is that x half alone, for the common case where
|
||||||
the common case where the window should be the data's but the levels were placed by hand.
|
a zoom should be undone but the levels were placed by hand. The status line acknowledges either
|
||||||
The status line acknowledges either (`x fitted to the buffered data (1.25 s)`).
|
(`x back to the 2 s window`).
|
||||||
|
|
||||||
Either fit **states a new time window**, and states the whole of it: the span it framed goes
|
**Nothing you do on the plot changes the time window** — only the window control does. A fit is
|
||||||
through the same door a typed window does, so the trigger's event span, the buffer partition and
|
the *undo of a zoom*: a 2 s window zoomed into reads 2 s again. That matters because the window
|
||||||
the window control all land on the fitted value and the control reads it back. A **zoom or a pan
|
is not just the axis. It is also the trigger's event span, how the samples are stored (a window
|
||||||
never does** — a gesture moves the view inside the window and leaves the window alone, which is
|
long enough to move the decimation re-plans the rings and **drops the history**), the control's
|
||||||
what keeps a wheel from silently re-planning the buffer and dropping the history. The one thing a
|
readout and what the session file records — so a wheel notch or a Fit that redefined it would
|
||||||
fit does *not* borrow from the typed path is the live re-follow: fitting a held event or a paused
|
quietly throw the buffer away. A zoom and a pan are view-only by construction; the fits read the
|
||||||
view keeps it pinned rather than throwing it back onto the stream.
|
window and never write it.
|
||||||
|
|
||||||
|
Which 2 s depends on the state, and the fit restores the whole of whatever the view is allowed to
|
||||||
|
show: live, the newest window at the live edge; and for a held event or a paused view, the pin's
|
||||||
|
full range — the snapshot's own span, which is legitimately *narrower* than the requested window
|
||||||
|
when the ring had not filled it. A fit of a frozen view keeps it frozen rather than throwing it
|
||||||
|
back onto the stream.
|
||||||
|
|
||||||
**Shared x axis.** The rows are the rows of **one `plot::Figure`**, drawn by one
|
**Shared x axis.** The rows are the rows of **one `plot::Figure`**, drawn by one
|
||||||
`plot::gtk4::PlotArea` in figure mode, and they all join a single x link group.
|
`plot::gtk4::PlotArea` in figure mode, and they all join a single x link group.
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ unit, width-locked so a change of window cannot widen the header bar.
|
|||||||
value. Stamping at the end of a group would shift every trace half a stored sample late.
|
value. Stamping at the end of a group would shift every trace half a stored sample late.
|
||||||
• Moving the window across a decimation boundary rebuilds the buffer and drops the history.
|
• Moving the window across a decimation boundary rebuilds the buffer and drops the history.
|
||||||
Walking it inside one decade is free.
|
Walking it inside one decade is free.
|
||||||
|
• <b>This control is the only thing that changes the window.</b> A zoom, a pan or a fit moves the
|
||||||
|
view <i>inside</i> it and leaves it alone — which is the point of the previous bullet: a wheel
|
||||||
|
notch that redefined the window could re-plan the buffer and throw your samples away.
|
||||||
• The trigger sees the <i>stored</i> (filtered) signal, not the wire signal, because an edge
|
• The trigger sees the <i>stored</i> (filtered) signal, not the wire signal, because an edge
|
||||||
found on a sample that is not in the buffer could not be cut out of it.
|
found on a sample that is not in the buffer could not be cut out of it.
|
||||||
|
|
||||||
|
|||||||
@@ -19,10 +19,11 @@ The full table is in the shortcut overview (<tt>Ctrl+/</tt>).
|
|||||||
• A <i>pinned</i> view is navigable within its own data but cannot leave it. While an event is
|
• A <i>pinned</i> view is navigable within its own data but cannot leave it. While an event is
|
||||||
held, a capture is filling, or the view is paused, a zoom <i>in</i> works normally — "not
|
held, a capture is filling, or the view is paused, a zoom <i>in</i> works normally — "not
|
||||||
outside the event" is not "not readable" — but a pan or a zoom out is clamped to the data.
|
outside the event" is not "not readable" — but a pan or a zoom out is clamped to the data.
|
||||||
• A gesture never changes the <i>time window</i>, only the view inside it. The window is what the
|
• Nothing you do on the plot changes the <i>time window</i> — only the header control does. A
|
||||||
header control states, and it also sets the trigger's event span and how the buffer is stored —
|
zoom or a pan moves the view inside the window; <b>f</b> (and Fit X axis) is the <i>undo</i> of a
|
||||||
so a wheel that redefined it would re-plan the buffer and drop your history. The two fits
|
zoom, so a 2 s window zoomed into reads 2 s again. The window is more than the axis: it is also
|
||||||
<i>do</i> set it, deliberately, and the control then reads the fitted value back.
|
the trigger's event span and how the samples are stored, so a wheel notch that redefined it
|
||||||
|
would re-plan the buffer and drop your history.
|
||||||
• A live view is clamped to the window you asked for, not to what the ring happens to hold.
|
• A live view is clamped to the window you asked for, not to what the ring happens to hold.
|
||||||
That is why a buffer that has only just started filling leaves its left-hand side empty
|
That is why a buffer that has only just started filling leaves its left-hand side empty
|
||||||
instead of squeezing the window down to the first few samples.
|
instead of squeezing the window down to the first few samples.
|
||||||
|
|||||||
+24
-23
@@ -657,24 +657,27 @@ void RowSet::unpin_pause_window() {
|
|||||||
set_time_window(window_s_);
|
set_time_window(window_s_);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RowSet::fit_x(double& span) {
|
void RowSet::fit_x() {
|
||||||
Row& first = row(0);
|
if (!figure_) return;
|
||||||
if (!first.plot || !figure_) return false;
|
if (!pinned_) {
|
||||||
first.plot->auto_fit_x();
|
// Live: the same path the window control and a resume take, with the window it already has.
|
||||||
const plot::Range fitted = first.plot->x_range(first.axis_x);
|
// That re-arms the follow and parks the newest `window_s_` at the live edge, which is the
|
||||||
const double fitted_span = fitted.span();
|
// whole of what a live view may show.
|
||||||
if (!std::isfinite(fitted_span) || !(fitted_span > 0.0)) return false;
|
set_time_window(window_s_);
|
||||||
if (pinned_) {
|
return;
|
||||||
// Fitting a captured or paused view: the window is the snapshot's own span, and
|
|
||||||
// `set_time_window()` would re-arm the follow and re-frame on a live edge that a frozen
|
|
||||||
// view has nothing bound to.
|
|
||||||
window_s_ = fitted_span;
|
|
||||||
figure_->set_x_link_range(x_group_, fitted);
|
|
||||||
} else {
|
|
||||||
set_time_window(fitted_span);
|
|
||||||
}
|
}
|
||||||
span = window_s_;
|
// Pinned (a held event or a paused view): the pin's full range, and the pin stays. The
|
||||||
return true;
|
// *members* have to be written as well as the group, because `hold_pinned_window()` prefers a
|
||||||
|
// member that has moved since our own last statement -- that is how it tells a gesture from its
|
||||||
|
// own echo -- so writing the group alone would be reverted to the zoomed-in member next frame.
|
||||||
|
const plot::Range pin{-pinned_pre_, pinned_post_};
|
||||||
|
if (!(pin.span() > 0.0) || !std::isfinite(pin.min) || !std::isfinite(pin.max)) return;
|
||||||
|
for (int i = 0; i < rows_; ++i) {
|
||||||
|
Row& r = row(i);
|
||||||
|
if (r.plot) r.plot->set_x_range(r.axis_x, pin);
|
||||||
|
}
|
||||||
|
pinned_stated_ = pin;
|
||||||
|
figure_->set_x_link_range(x_group_, pin);
|
||||||
}
|
}
|
||||||
|
|
||||||
void RowSet::sync_cursors(int drag_row) {
|
void RowSet::sync_cursors(int drag_row) {
|
||||||
@@ -788,16 +791,14 @@ void RowSet::pin_event_window(double pre_s, double post_s) {
|
|||||||
figure_->set_x_link_range(x_group_, {-pre_s, post_s});
|
figure_->set_x_link_range(x_group_, {-pre_s, post_s});
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RowSet::fit_all(double& span) {
|
void RowSet::fit_all() {
|
||||||
for (int i = 0; i < rows_; ++i) {
|
for (int i = 0; i < rows_; ++i) {
|
||||||
Row& r = row(i);
|
Row& r = row(i);
|
||||||
if (r.plot) r.plot->auto_fit_y(r.axis_y);
|
if (r.plot) r.plot->auto_fit_y(r.axis_y);
|
||||||
}
|
}
|
||||||
// The x half is `fit_x()`'s exactly -- row 0's data span, stated through the link group so
|
// The x half is `fit_x()`'s exactly -- the zoom undone, the time window untouched -- and
|
||||||
// every row lands on it -- and sharing it is what keeps the two fits from drifting apart.
|
// sharing it is what keeps the two fits from drifting apart.
|
||||||
// The y fit above has already happened either way: a row 0 that holds nothing still gets
|
fit_x();
|
||||||
// every row's y scaled, it just has no span to report.
|
|
||||||
return fit_x(span);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void RowSet::note_y_gutter(double need_px) {
|
void RowSet::note_y_gutter(double need_px) {
|
||||||
|
|||||||
+21
-17
@@ -298,17 +298,24 @@ class RowSet {
|
|||||||
void set_data_pin(double lo, double hi);
|
void set_data_pin(double lo, double hi);
|
||||||
void unpin_pause_window();
|
void unpin_pause_window();
|
||||||
|
|
||||||
/// Frames the x window on the data row 0 holds, leaving every y axis alone. The x
|
/// Undoes an x zoom: puts the view back on the whole time window, leaving every y axis alone.
|
||||||
/// counterpart of `fit_all()`, for the one thing the y axes cannot say: how wide the
|
/// The x counterpart of `fit_all()`.
|
||||||
/// window should be.
|
|
||||||
///
|
///
|
||||||
/// Reports the span it framed in `span` and `true` when it framed one at all (row 0 may hold
|
/// **It does not change the time window** -- `window_s_` is read, never written. A fit is a
|
||||||
/// no data, and a degenerate range is refused). **The caller owes the rest of the time
|
/// plot interaction, and the window is also the trigger's event span, the buffer partition and
|
||||||
/// scale**: this moves the view and `window_s_`, and the trigger's event span, the buffer
|
/// the control's readout, so a fit that *framed the data* instead would redefine all of that
|
||||||
/// partition and the window control are the caller's to move with it -- see
|
/// from a gesture (and re-plan the rings, dropping the history). What a user asks for here is
|
||||||
/// `apply_window_scale()` in `ui.cpp`. Leaving them behind is what made a fit and the control
|
/// the zoom undone: a 2 s window zoomed into reads 2 s again.
|
||||||
/// disagree about what the window was.
|
///
|
||||||
bool fit_x(double& span);
|
/// Which 2 s depends on the state, and both cases restore the *whole* of what the view is
|
||||||
|
/// allowed to show: live, the newest window at the live edge; pinned (a held event or a paused
|
||||||
|
/// view), the pin's full range, with the pin kept -- `set_time_window()` would clear it and
|
||||||
|
/// re-frame on a live edge that a frozen view has nothing bound to.
|
||||||
|
void fit_x();
|
||||||
|
|
||||||
|
/// Whether a window is pinned: a held event, a filling capture or a paused view. The condition
|
||||||
|
/// `fit_x()` branches on, which is why the fit probe reports it.
|
||||||
|
bool window_pinned() const noexcept { return pinned_; }
|
||||||
|
|
||||||
/// Adopts the instruments a gesture is moving, on `drag_row` (the row whose controller holds
|
/// Adopts the instruments a gesture is moving, on `drag_row` (the row whose controller holds
|
||||||
/// a cursor), and nothing otherwise.
|
/// a cursor), and nothing otherwise.
|
||||||
@@ -343,13 +350,10 @@ class RowSet {
|
|||||||
/// 0, so nothing outside the pre+post window is visible.
|
/// 0, so nothing outside the pre+post window is visible.
|
||||||
void pin_event_window(double pre_s, double post_s);
|
void pin_event_window(double pre_s, double post_s);
|
||||||
|
|
||||||
/// Fits every row's y axis to the data it holds, and re-frames the shared x window
|
/// Fits every row's y axis to the data it holds, and puts x back on the whole time window --
|
||||||
/// on what row 0 retains (the same thing the plot's own `f` key does).
|
/// `fit_x()`'s x half exactly, so the two cannot drift apart. Like it, **this does not change
|
||||||
///
|
/// the time window**: the y axes are fitted to their data, x is merely un-zoomed.
|
||||||
/// Reports the framed x span exactly as `fit_x()` does, and leaves the caller the same debt:
|
void fit_all();
|
||||||
/// the y fit is this object's business alone, but the x span is the *time scale*, and the
|
|
||||||
/// trigger's event span, the buffer partition and the window control have to move with it.
|
|
||||||
bool fit_all(double& span);
|
|
||||||
|
|
||||||
/// Reports the left gutter the rows currently need, and reserves at least the width
|
/// Reports the left gutter the rows currently need, and reserves at least the width
|
||||||
/// of the widest *plausible* y label.
|
/// of the widest *plausible* y label.
|
||||||
|
|||||||
+68
-57
@@ -1865,19 +1865,20 @@ void sync_window_display(App& app, double seconds) {
|
|||||||
app.updating_panels = false;
|
app.updating_panels = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Everything the time scale consists of *except* the view: the buffer partition, the trigger's
|
/// The one place the time scale changes. Clamps to what the buffer can hold, moves the plot and
|
||||||
/// event span, the control's readout and the session. Split out of `apply_time_window()` because
|
/// the trigger's event span together -- its pre-trigger share is a share of *this* window -- and
|
||||||
/// a **fit** frames the axis itself -- and when the view is pinned it must keep the pin, which
|
/// then restates the result in the control, so a clamped request reads back as the window that
|
||||||
/// `RowSet::set_time_window()` would clear, throwing a frozen picture onto the live stream -- but
|
/// was actually taken rather than the one that was typed.
|
||||||
/// a fit still has to move all of this.
|
|
||||||
///
|
///
|
||||||
/// That is the rule: a plot interaction may not quietly redefine the window. A zoom and a pan
|
/// **Only the window control comes here.** The time scale is more than the axis: it is also the
|
||||||
/// never touch it (they move the axis, and `clamp_to_data()` caps them at `window_s_`); the two
|
/// trigger's event span, the buffer partition (so a change of decimation drops the history) and
|
||||||
/// fits do set it, and so must come through here. A fit that moved `window_s_` alone -- which is
|
/// the session, so a *plot interaction* may not state one. A zoom and a pan never touch it by
|
||||||
/// what both of them used to do -- left the trigger's event span on the *old* window, and the
|
/// construction -- they move the row's axis, and `clamp_to_data()` caps the span at `window_s_` --
|
||||||
/// next decade step or unit change then fed the fitted value in for real, so the time scale
|
/// and the two fits do not either: `RowSet::fit_x()` undoes a zoom by restoring the window that
|
||||||
/// changed twice for one action and the `window == event_span` invariant held at neither point.
|
/// is already set. Both fits used to frame the data instead and move `window_s_` alone, which
|
||||||
void apply_window_scale(App& app, double seconds) {
|
/// left the event span on the *old* window until the next decade step or unit change applied the
|
||||||
|
/// fitted value for real: one action, two changes of time scale.
|
||||||
|
void apply_time_window(App& app, double seconds) {
|
||||||
if (!(seconds > 0.0)) return;
|
if (!(seconds > 0.0)) return;
|
||||||
const double clamped = std::min(std::max(seconds, kWindowMinS), window_limit(app));
|
const double clamped = std::min(std::max(seconds, kWindowMinS), window_limit(app));
|
||||||
// The buffer has to cover the window, so this may re-plan the partition and drop the
|
// The buffer has to cover the window, so this may re-plan the partition and drop the
|
||||||
@@ -1891,24 +1892,12 @@ void apply_window_scale(App& app, double seconds) {
|
|||||||
app.notice_until_us = g_get_monotonic_time() + 4 * G_TIME_SPAN_SECOND;
|
app.notice_until_us = g_get_monotonic_time() + 4 * G_TIME_SPAN_SECOND;
|
||||||
}
|
}
|
||||||
app.model->set_event_span(clamped);
|
app.model->set_event_span(clamped);
|
||||||
|
// The partition before the axis, so the axis is framed over the rings that will exist.
|
||||||
|
app.rows->set_time_window(clamped);
|
||||||
sync_window_display(app, clamped);
|
sync_window_display(app, clamped);
|
||||||
schedule_layout_save(app);
|
schedule_layout_save(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The one place the display window changes *on the control's behalf*. Clamps to what the buffer
|
|
||||||
/// can hold, moves the plot and the trigger's event span together -- its pre-trigger share is a
|
|
||||||
/// share of *this* window -- and then restates the result in the control, so a clamped request
|
|
||||||
/// reads back as the window that was actually taken rather than the one that was typed.
|
|
||||||
void apply_time_window(App& app, double seconds) {
|
|
||||||
if (!(seconds > 0.0)) return;
|
|
||||||
const double clamped = std::min(std::max(seconds, kWindowMinS), window_limit(app));
|
|
||||||
// The partition first, so the axis below is framed over the rings that will exist.
|
|
||||||
apply_window_scale(app, clamped);
|
|
||||||
// And the view, which unpins and re-arms the live follow -- right for a typed window, which
|
|
||||||
// is a statement about what to watch from now on, and the reason a fit does not come here.
|
|
||||||
app.rows->set_time_window(clamped);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Enter in the entry. A number that will not parse restores the current window instead of
|
/// Enter in the entry. A number that will not parse restores the current window instead of
|
||||||
/// reporting an error: the entry is also the readout, so leaving junk in it would leave the
|
/// reporting an error: the entry is also the readout, so leaving junk in it would leave the
|
||||||
/// control disagreeing with the plot.
|
/// control disagreeing with the plot.
|
||||||
@@ -4067,28 +4056,25 @@ void on_signals_toggled(GtkToggleButton* button, gpointer user) {
|
|||||||
gtk_toggle_button_get_active(button));
|
gtk_toggle_button_get_active(button));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fit: auto-scale every row's y axis and re-frame the x window on the data the
|
/// Fit: auto-scale every row's y axis to the data it holds, and put x back on the whole time
|
||||||
/// ring holds (the plot's own `f` semantics), then apply that span as the time scale.
|
/// window.
|
||||||
///
|
///
|
||||||
/// `apply_window_scale()` and not `sync_window_display()`: a fitted x span *is* a new window, so
|
/// No `apply_time_window()` here, deliberately: a fit is a plot interaction and may not restate
|
||||||
/// the trigger's event span and the buffer partition move with it. Restating the control alone
|
/// the time scale (see that function). The y axes are fitted to the data; x is only un-zoomed.
|
||||||
/// left those on the previous window.
|
|
||||||
void on_fit_clicked(GtkButton*, gpointer user) {
|
void on_fit_clicked(GtkButton*, gpointer user) {
|
||||||
App* app = static_cast<App*>(user);
|
App* app = static_cast<App*>(user);
|
||||||
double span = 0.0;
|
app->rows->fit_all();
|
||||||
if (app->rows->fit_all(span)) apply_window_scale(*app, span);
|
app->notice = "y fitted to the data, x back to the " +
|
||||||
app->notice = "fitted to the buffered data (" + format_window(app->rows->time_window()) + ")";
|
format_window(app->rows->time_window()) + " window";
|
||||||
app->notice_until_us = g_get_monotonic_time() + 3 * G_TIME_SPAN_SECOND;
|
app->notice_until_us = g_get_monotonic_time() + 3 * G_TIME_SPAN_SECOND;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fit the x window only: the y axes keep the scales the user set. The counterpart of the
|
/// Reset the x zoom only: the y axes keep the scales the user set. The counterpart of the other
|
||||||
/// other fit, for the common case where the interesting range is the data's but the levels
|
/// fit, for the common case where a zoom should be undone but the levels were placed by hand.
|
||||||
/// were placed by hand.
|
|
||||||
void on_fit_x_clicked(GtkButton*, gpointer user) {
|
void on_fit_x_clicked(GtkButton*, gpointer user) {
|
||||||
App* app = static_cast<App*>(user);
|
App* app = static_cast<App*>(user);
|
||||||
double span = 0.0;
|
app->rows->fit_x();
|
||||||
if (app->rows->fit_x(span)) apply_window_scale(*app, span);
|
app->notice = "x back to the " + format_window(app->rows->time_window()) + " window";
|
||||||
app->notice = "x fitted to the buffered data (" + format_window(app->rows->time_window()) + ")";
|
|
||||||
app->notice_until_us = g_get_monotonic_time() + 3 * G_TIME_SPAN_SECOND;
|
app->notice_until_us = g_get_monotonic_time() + 3 * G_TIME_SPAN_SECOND;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7200,20 +7186,21 @@ void cursors_probe_report(App& a, const char* what) {
|
|||||||
gboolean cursors_probe_step(gpointer user);
|
gboolean cursors_probe_step(gpointer user);
|
||||||
|
|
||||||
/// The two fits the right-click menu offers, driven through the handlers the menu wires
|
/// The two fits the right-click menu offers, driven through the handlers the menu wires
|
||||||
/// (a null button: neither handler looks at it). `fit x` must narrow the window to the
|
/// (a null button: neither handler looks at it). Both must put x back on the whole time window
|
||||||
/// data's own span and leave the y ranges alone; `fit all` must then move the y ranges too.
|
/// and **leave the window itself alone**; `fit x` must leave the y ranges alone too, and
|
||||||
|
/// `fit all` must move them to the data's.
|
||||||
void fit_probe_report(App& a, const char* what) {
|
void fit_probe_report(App& a, const char* what) {
|
||||||
const RowSet::Row& r0 = a.rows->row(0);
|
const RowSet::Row& r0 = a.rows->row(0);
|
||||||
const plot::Range x = r0.plot != nullptr ? r0.plot->x_range(r0.axis_x) : plot::Range{};
|
const plot::Range x = r0.plot != nullptr ? r0.plot->x_range(r0.axis_x) : plot::Range{};
|
||||||
const plot::Range y = r0.plot != nullptr ? r0.plot->y_range(r0.axis_y) : plot::Range{};
|
const plot::Range y = r0.plot != nullptr ? r0.plot->y_range(r0.axis_y) : plot::Range{};
|
||||||
// `event_span` is the assertion that a fit moved the whole time scale and not just the view:
|
// `window` and `event_span` are the assertion that a fit did *not* restate the time scale:
|
||||||
// it is the trigger's, written only by `apply_window_scale()`, so a fit that moved `window`
|
// both are written only by `apply_time_window()`, so they must hold the typed value on every
|
||||||
// alone shows up here as the two disagreeing.
|
// line, with `span` the reading that moves -- down on the zoom, back up on the fit.
|
||||||
std::fprintf(stderr,
|
std::fprintf(stderr,
|
||||||
"[fit-probe] %-10s window=%.6f event_span=%.6f x=[%.6f..%.6f] span=%.6f "
|
"[fit-probe] %-10s pinned=%d window=%.6f event_span=%.6f x=[%.6f..%.6f] span=%.6f "
|
||||||
"y=[%.6f..%.6f]\n",
|
"y=[%.6f..%.6f]\n",
|
||||||
what, a.rows->time_window(), a.model->event_span(), x.min, x.max, x.span(), y.min,
|
what, a.rows->window_pinned() ? 1 : 0, a.rows->time_window(), a.model->event_span(),
|
||||||
y.max);
|
x.min, x.max, x.span(), y.min, y.max);
|
||||||
std::fflush(stderr);
|
std::fflush(stderr);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7290,33 +7277,57 @@ gboolean timelabels_probe_step(gpointer user) {
|
|||||||
return G_SOURCE_REMOVE;
|
return G_SOURCE_REMOVE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Zooms row 0 in to a quarter of what it shows, the way a gesture does -- on the *member* axis,
|
||||||
|
/// which `Figure::layout()` adopts into the group. A fit is the undo of this, so the probe has to
|
||||||
|
/// perform it or it has nothing to undo.
|
||||||
|
void fit_probe_zoom_in(App& a) {
|
||||||
|
RowSet::Row& r0 = a.rows->row(0);
|
||||||
|
if (r0.plot == nullptr) return;
|
||||||
|
const plot::Range x = r0.plot->x_range(r0.axis_x);
|
||||||
|
const double mid = x.min + 0.5 * x.span();
|
||||||
|
const double half = 0.125 * x.span();
|
||||||
|
r0.plot->set_x_range(r0.axis_x, {mid - half, mid + half});
|
||||||
|
}
|
||||||
|
|
||||||
gboolean fit_probe_step(gpointer user) {
|
gboolean fit_probe_step(gpointer user) {
|
||||||
App* a = static_cast<App*>(user);
|
App* a = static_cast<App*>(user);
|
||||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||||
switch (a->dbg_fit_step) {
|
switch (a->dbg_fit_step) {
|
||||||
case 0:
|
case 0:
|
||||||
// Move the y scale off the data's range first, so "fit x leaves y alone" is a
|
// Move the y scale off the data's range first, so "fit x leaves y alone" is a statement
|
||||||
// statement with something to be wrong about.
|
// with something to be wrong about -- and set the window through the control's own path,
|
||||||
|
// since that is the only thing allowed to, and its value is what both fits must preserve.
|
||||||
a->rows->set_y_range(0, -100.0, 100.0);
|
a->rows->set_y_range(0, -100.0, 100.0);
|
||||||
a->rows->set_time_window(0.05);
|
// Not while pinned: `apply_time_window()` unpins, and the pinned branch of the fit is the
|
||||||
|
// other half of what this probe is for. Timed inside a pause (`UDPSCOPE_PROBE_PAUSE_MS`)
|
||||||
|
// it takes the window the pause froze and reports `pinned=1`.
|
||||||
|
if (!a->rows->window_pinned()) apply_time_window(*a, 2.0);
|
||||||
fit_probe_report(*a, "start");
|
fit_probe_report(*a, "start");
|
||||||
g_timeout_add(400, fit_probe_step, a);
|
g_timeout_add(400, fit_probe_step, a);
|
||||||
a->dbg_fit_step = 1;
|
a->dbg_fit_step = 1;
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
// The handlers themselves, not `RowSet::fit_x()` alone: the whole point of the
|
fit_probe_zoom_in(*a);
|
||||||
// `event_span=` reading below is that a fit moves the *time scale* and not just the view,
|
|
||||||
// and that half lives in the handler.
|
|
||||||
on_fit_x_clicked(nullptr, a);
|
|
||||||
g_timeout_add(400, fit_probe_step, a);
|
g_timeout_add(400, fit_probe_step, a);
|
||||||
a->dbg_fit_step = 2;
|
a->dbg_fit_step = 2;
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
fit_probe_report(*a, "fit x");
|
// A quarter of the window: what the fits have to undo, and the reading that proves the
|
||||||
on_fit_clicked(nullptr, a);
|
// `span=` on the fit lines is the fit's work rather than a window nothing ever left.
|
||||||
|
fit_probe_report(*a, "zoomed");
|
||||||
|
// The handlers themselves, not `RowSet::fit_x()` alone: the window the fit must *not*
|
||||||
|
// move is the app's, and the notice and the control are the handler's half.
|
||||||
|
on_fit_x_clicked(nullptr, a);
|
||||||
g_timeout_add(400, fit_probe_step, a);
|
g_timeout_add(400, fit_probe_step, a);
|
||||||
a->dbg_fit_step = 3;
|
a->dbg_fit_step = 3;
|
||||||
break;
|
break;
|
||||||
|
case 3:
|
||||||
|
fit_probe_report(*a, "fit x");
|
||||||
|
fit_probe_zoom_in(*a);
|
||||||
|
on_fit_clicked(nullptr, a);
|
||||||
|
g_timeout_add(400, fit_probe_step, a);
|
||||||
|
a->dbg_fit_step = 4;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
fit_probe_report(*a, "fit all");
|
fit_probe_report(*a, "fit all");
|
||||||
break;
|
break;
|
||||||
|
|||||||
Reference in new issue
Block a user