diff --git a/docs/claude/invariants-trigger-and-pinning.md b/docs/claude/invariants-trigger-and-pinning.md index 1e7afca..25ec0ae 100644 --- a/docs/claude/invariants-trigger-and-pinning.md +++ b/docs/claude/invariants-trigger-and-pinning.md @@ -90,6 +90,17 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear window, which is a re-open with a new extent and is a new event, stated in full. `[boxzoom-probe]`'s `zoomed=` is the guard, and it is a reading to take in **both** modes: live and file must both read `1`. +- **`set_data_pin()` takes a *range*; `pin_event_window()` and `pinned_window_is()` take the pre/post + *split*.** The three are used in one breath by the file view's statement and by the held event's, and + the two forms are not interchangeable: for data over `[lo, hi]` it is `set_data_pin(lo, hi)` against + `pin_event_window(-lo, hi)`. Handing the split to `set_data_pin()` stores the pin as `[lo, -lo]`-ish, + so the next frame's `pinned_window_is()` reads false, the pin is stated in full again, and the view + alternates between the file's span and the corrupted pin at the frame rate -- the flicker in which no + interaction survives, because every second frame rewrites the group's range. It is invisible on a file + whose stamps are all positive (the corrupted pin is then *wider* than the data and + `hold_pinned_window()` never clamps) and on one that ends at 0 (the pin inverts, and the clamp returns + early), which is why it reached a user on a recording spanning `[-4..16]` and nothing before it. + `[drop-probe]`'s `repins=` is the reading: **2 for a whole run**, not one per frame. - **A pinned view is navigable *within its data*, and that is the bound -- not the request.** Two things had to agree for this, and both were wrong in the same direction: the **pause** pinned on the window it asked for rather than on the snapshot it got (a cut diff --git a/docs/claude/probe-readings.md b/docs/claude/probe-readings.md index 23a3562..01224ec 100644 --- a/docs/claude/probe-readings.md +++ b/docs/claude/probe-readings.md @@ -997,7 +997,7 @@ configuration that carries settings, is the defect this exists for — a saved h ## `[drop-probe]` ``` -[drop-probe] signals=4 channels=4 rows=4 slots0=[sine|||] +[drop-probe] signals=4 channels=4 rows=4 slots0=[sine|||] view=[-3.999690..15.999670] repins=2 [drop-probe] dropped signal 0 -> 1 slots=[sine|sine||] [drop-probe] dropped signal 1 -> 1 slots=[sine|sine|square|] ``` @@ -1007,6 +1007,8 @@ configuration that carries settings, is the defect this exists for — a saved h | `rows` | the column count, capped at `RowSet::kMaxRows` — one row per trace | rows sized four signals to one row: the only row fills and *every* drop is refused ("plot 1 already holds four signals"), which reads as a scope that cannot plot anything | | `dropped … -> 1` | 1, and the row gains that trace | a drop that cannot find a free slot, or a signal with no channel to bind | | `channels` | one per signal | a file whose columns did not become channels, so nothing can be plotted at all | +| `view` | the file's own span, or whatever the user last zoomed to — never a *sub-range of it the user did not ask for* | a pin that no longer holds the data: `view=[3.999690..15.999670]` against a file spanning `[-4..16]` is the pin written as a pre/post split where a range belongs | +| `repins` | **2**, for the whole run, however long | the file view stating its pin per frame instead of refreshing its numbers. Each statement writes the group's range, so the axis alternates between the file's span and the pin's and no gesture survives: `repins=116` at three seconds is one per frame, and it reads on screen as a flicker in which nothing can be clicked | ## `[events]` diff --git a/docs/claude/verification.md b/docs/claude/verification.md index 9545f1b..4b3935b 100644 --- a/docs/claude/verification.md +++ b/docs/claude/verification.md @@ -377,12 +377,25 @@ as distinct from the fault injection below, which exercises ingest: the plan, this file arrived as its **last 0.9 s** and nothing in the log said so. - **A loaded file can be plotted.** `UDPSCOPE_PROBE_DROP_MS` drives the drop handler the way a drag from the signal panel does, on a file opened with `--open`: it must read - `[drop-probe] signals=4 channels=4 rows=4 slots0=[sine|||]` — **`rows` equal to the column count**, + `[drop-probe] signals=4 channels=4 rows=4 slots0=[sine|||] view=[-3.999690..15.999670] repins=2` — + **`rows` equal to the column count**, one trace per row — and each drop must return `1` and add its trace (`dropped signal 1 -> 1 slots=[sine|sine|square|]`). `rows=1 slots0=[sine|square|ramp|noise]` with every drop returning `0` is the defect this exists for: rows sized four signals to one row fill the only row, so the app reads as one that cannot plot anything. A file-mode run must also write **no session** for the default `127.0.0.1:8000` (`App::source_port` is 0), or it overwrites that source's saved layout. +- **A file view states its pin once.** `repins` on the line above must read **2** whenever the probe + is scheduled — at one second or at seven — and `view` must be the file's whole span. A file's extent + does not change while it is open, so the tick refreshes the pin's *numbers* and writes the group's + range only when the view first becomes an event. Reading it against the decimated file above + (`/tmp/long.npy`, stamps `[-4..16]`) is the case that matters: with the pin written as a pre/post + split where a range belonged it read `view=[3.999690..15.999670] repins=116` at three seconds — + the axis alternating between the file's span and a corrupted pin every frame, which on screen is a + flicker in which no pan, zoom or click survives to the next frame. A file whose stamps are all + positive, or which ends at 0 (an export's own window), hides it: the corrupted pin is then wider + than the data, or empty, and the clamp never fires. `UDPSCOPE_PROBE_PAN_MS` on the same run is the + other half — `allowed=1` and a `view` that is still the zoomed range when the drop probe reads it + later is interaction actually taking. - **The chooser's Waveforms page**: `UDPSCOPE_PROBE_WAVEFORMS=1` prints `[sources] waveforms: N recent (ok|missing) … page=waveforms`, and `UDPSCOPE_PROBE_WAVE=` sets the field the file dialog's callback sets and prints `[sources] picked waveform ` — then the diff --git a/src/ui.cpp b/src/ui.cpp index a7b39b0..3eebabc 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -3392,11 +3392,19 @@ gboolean tick(gpointer user) { // made it a no-op. The held-event branch above has this shape for this reason: `set_data_pin()` // for the numbers, `pin_event_window()` when the view first becomes an event. A re-open into the // same row set with a different extent *is* a new event and re-states it. - const auto pin_view = [lo, hi](RowSet& rows) { + const auto pin_view = [lo, hi, app](RowSet& rows) { + // `set_data_pin()` takes the **range**, `pinned_window_is()`/`pin_event_window()` the + // pre/post split: passing the split to the former wrote the pin as `[lo, -lo]`-ish, which + // on a recording whose stamps start *before* zero is a window that no longer holds the + // data. The next frame's `pinned_window_is()` then read false, re-stated the pin, and the + // view alternated between the file's span and the corrupted one every frame -- a flicker + // with no interaction surviving it. A file whose first stamp was positive hid this: the + // corrupted pin was wider than the data and the clamp never fired. if (rows.pinned_window_is(-lo, hi)) { - rows.set_data_pin(-lo, hi); + rows.set_data_pin(lo, hi); } else { rows.pin_event_window(-lo, hi); + ++app->dbg_file_repins; } rows.set_trigger_marker(0.0, true); }; diff --git a/src/ui_app.hpp b/src/ui_app.hpp index 4b6395d..5a0db88 100644 --- a/src/ui_app.hpp +++ b/src/ui_app.hpp @@ -644,6 +644,11 @@ struct App { /// afterwards is the glitch. int dbg_notrace_frames = 0; int dbg_notrace_last = -1; + /// Times the file view has *stated* its pin rather than only refreshing its numbers. A file's + /// extent does not change while it is open, so this must read **1** for a whole run: stating the + /// pin writes the group's range, which discards whatever gesture the user had left on the axis, + /// so a count that grows with the frames is both the flicker and the reason no interaction took. + int dbg_file_repins = 0; /// The widest *minimum* width the side column or either pane has ever demanded. The side /// column is a fixed 320 px by design, so a figure far above that is a label whose text drove /// its own minimum -- the "layout must not depend on content" rule broken, and the only known diff --git a/src/ui_probe.cpp b/src/ui_probe.cpp index f547c00..4ef4053 100644 --- a/src/ui_probe.cpp +++ b/src/ui_probe.cpp @@ -3863,10 +3863,17 @@ gboolean noise_probe_denoise(gpointer user) { gboolean drop_probe(gpointer user) { App* a = static_cast(user); if (a->window == nullptr || a->model->signals().empty()) return G_SOURCE_REMOVE; - std::fprintf(stderr, "[drop-probe] signals=%zu channels=%zu rows=%d slots0=[%s|%s|%s|%s]\n", + // `repins` beside the view: a file's extent is fixed, so the pin is stated once and only its + // numbers are refreshed afterwards. A count that tracks the frame number is the pin being written + // every frame, which flickers the view and swallows every gesture. + const plot::Range view = a->rows->view_range(); + std::fprintf(stderr, + "[drop-probe] signals=%zu channels=%zu rows=%d slots0=[%s|%s|%s|%s] " + "view=[%.6f..%.6f] repins=%d\n", a->model->signals().size(), a->model->channels().size(), a->rows->row_count(), a->rows->row(0).name[0].c_str(), a->rows->row(0).name[1].c_str(), - a->rows->row(0).name[2].c_str(), a->rows->row(0).name[3].c_str()); + a->rows->row(0).name[2].c_str(), a->rows->row(0).name[3].c_str(), view.min, view.max, + a->dbg_file_repins); for (int signal = 0; signal < 2 && static_cast(signal) < a->model->signals().size(); ++signal) { GValue value = G_VALUE_INIT;