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
|
||||
caption must share one anchor. `x_origin_set` is read back out of the plot; it must be 0 in
|
||||
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
|
||||
`RowSet` calls, because half of what a fit now does lives there (see the time-scale invariant
|
||||
below). It first puts the y scale and the window where the data is *not* (a fixed
|
||||
`[-100, 100]`, a 50 ms window), so the assertions have something to be wrong about: **`fit x`
|
||||
must widen the window to the data's span and leave `y` exactly as it was**, and **`fit all`
|
||||
must then move `y` to the data's own range**. A `y` that changed on the `fit x` line is the two
|
||||
paths aliased. **`event_span` must equal `window` on every line**: a fit states a new time
|
||||
scale, so the trigger's event span, the buffer partition and the control have to move with it —
|
||||
a fit that moved the view alone left `event_span` on the *old* window, and the next decade step
|
||||
or unit change then applied the fitted value for real, so one action changed the time scale
|
||||
twice.
|
||||
`RowSet` calls, because the notice and the control are the handler's half. A fit is the **undo
|
||||
of an x zoom**, so the probe performs one: it moves the y scale off the data (a fixed
|
||||
`[-100, 100]`), sets a 2 s window through the control's own path, then zooms row 0 to a quarter
|
||||
of it the way a gesture does — on the *member* axis — and fits. Four assertions.
|
||||
**`window` and `event_span` must be unchanged on every line** and equal to the typed 2 s: a fit
|
||||
is a plot interaction and may not restate the time scale (see the invariant below), and they are
|
||||
the only two readings that can see it try. The `zoomed` line's `span` must be a quarter of the
|
||||
window, or the fits have nothing to undo and the lines below mean nothing. **`fit x` must put
|
||||
`span` back to the whole window and leave `y` exactly as it was**, and **`fit all` must do the
|
||||
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=[..]`
|
||||
(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
|
||||
@@ -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
|
||||
on a mere change of window. That is also why the window is typed into a popover rather
|
||||
than shown in the bar.
|
||||
- **A plot interaction may not quietly redefine the time scale.** The *time scale* is more than
|
||||
the axis: `RowSet::window_s_` also feeds `ScopeModel::set_event_span()` (the trigger's
|
||||
pre-trigger share is a share of *this* window), `ScopeModel::set_target_span()` (which may
|
||||
re-plan the partition, move the decimation and drop history), the window control's readout and
|
||||
the session file. A **zoom or a pan therefore does not touch it at all** — the controller
|
||||
writes the *member* axis and `clamp_to_data()` caps the span at `window_s_`, so a gesture is
|
||||
view-only by construction and needs no code to keep it that way. The **two fits do** state a
|
||||
new scale, and they must state the whole of it: `on_fit_clicked`/`on_fit_x_clicked` take the
|
||||
span `RowSet::fit_x()`/`fit_all()` report and hand it to **`apply_window_scale()`**. A fit that
|
||||
moved `window_s_` alone — which is what both of them used to do — left the event span on the
|
||||
*old* window, and the next decade step or unit change then fed the fitted value in for real:
|
||||
one action, two changes of time scale, and `window == event_span` true at neither point.
|
||||
- **`apply_window_scale()` is the only place the time scale changes, and `apply_time_window()`
|
||||
is the only place the *view* changes with it.** The first clamps to `window_limit()` and moves
|
||||
`set_target_span()`, `set_event_span()`, the control and the session save together; the second
|
||||
calls it and then `RowSet::set_time_window()`. The split is not cosmetic:
|
||||
`set_time_window()` unpins and re-arms the live follow, which is right for a typed window (a
|
||||
statement about what to watch from now on) and wrong for a fit of a held event or a paused
|
||||
view, which has nothing bound to a live edge — so the fits take the scale half only, and
|
||||
`fit_x()` writes the link group directly while `pinned_`. The partition is moved **before** the
|
||||
axis, so the axis is framed over the rings that will exist. Paths that change the span for
|
||||
their own reasons (an event pin) still call `sync_window_display()` alone, which only restates
|
||||
the control and never moves the plot — calling either `apply_` there would clamp a legitimately
|
||||
short capture up to the requested window.
|
||||
- **No plot interaction changes the time scale; only the window control does.** The *time scale*
|
||||
is more than the axis: `RowSet::window_s_` also feeds `ScopeModel::set_event_span()` (the
|
||||
trigger's pre-trigger share is a share of *this* window), `ScopeModel::set_target_span()`
|
||||
(which may re-plan the partition, move the decimation and **drop the history**), the window
|
||||
control's readout and the session file. A **zoom or a pan** does not touch it by construction —
|
||||
the controller writes the *member* axis and `clamp_to_data()` caps the span at `window_s_`, so
|
||||
nothing has to be written to keep it that way. **Neither do the two fits**: a fit is the
|
||||
*undo of a zoom*, so `RowSet::fit_x()` restores the window that is already set and reads
|
||||
`window_s_` without ever writing it. Both fits used to frame the *data* instead and move
|
||||
`window_s_` alone, which was wrong twice over — a gesture-triggered re-plan of the rings, and
|
||||
an event span left 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).
|
||||
- **`apply_time_window()` is the only place the time scale changes**, and the window control is
|
||||
its only caller. It clamps to `window_limit()` and moves `set_target_span()`,
|
||||
`set_event_span()`, `RowSet::set_time_window()`, the control and the session save together —
|
||||
the partition **before** the axis, so the axis is framed over the rings that will exist. A fit
|
||||
must not come here for two reasons: it is not entitled to the scale at all, and
|
||||
`set_time_window()` unpins and re-arms the live follow, which would throw a held event or a
|
||||
paused view onto a live edge it has nothing bound to. Paths that change the span for their own
|
||||
reasons (an event pin) call `sync_window_display()` alone, which only restates the control and
|
||||
never moves the plot — calling `apply_time_window()` there would clamp a legitimately short
|
||||
capture up to the requested window.
|
||||
- **`fit_x()` restores the *whole of what the view may show*, which is not always `window_s_`.**
|
||||
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
|
||||
through the `App` pointers (`trigger_toggle`, `pause_button`, …), not via
|
||||
`find_labelled_button()`. The **pane switches are toolbar buttons** too (`cursors_toggle`,
|
||||
|
||||
Reference in new issue
Block a user