scope windows: mirror a pin as a range, and restore the tick rebuild

**The blank scope window in trigger mode.** A held event pins the main window to
`[-pre, +post]`, and the tick mirrors that pin onto every scope window. It passed
`pinned_range().min/max` to `pin_event_window()`, which takes **spans** (`pre, post`): `min` is
signed, so `pre` came out negative and the group range became `[+pre, +post]` — an inverted x
window. The plotter then had nothing to draw: no trace, no x-axis labels, an empty plot that
looked like a broken window. `RowSet::pin_display_range(x0, x1)` states the pin as the range a
mirroring view actually has, and the tick uses it; live mode was never affected, which is why the
defect only showed under the trigger.

Also restores the scope-window `rebuild_if_dirty()` in the tick, which a reproduction experiment
had left commented out (`REPRO-TEMP`) and a later commit carried: a window opened empty and filled
by a drop kept the figure it was built with.

Probe: `[scope-probe] pin_mirror=1 main=[..] scope=[..] x=[..]` pins the main window, reads the
scope's pin and plot x range a frame later, and reads `0` for an inverted or mismatched pin.
Docs: probe readings and the trigger/pinning invariant (range vs spans).
This commit is contained in:
Martino Ferrari committed 2026-11-06 10:39:10 +01:00
1 parent 2279e2140a
commit ecdbf4f0e1
6 files changed
+75 -9

No files matched your search

+12 -6
View File
@@ -4633,17 +4633,23 @@ gboolean tick(gpointer user) {
const double window_s = app->rows->time_window();
for (const std::unique_ptr<App::ScopeWindow>& scope : app->scopes) {
if (pinned) {
scope->rows->pin_event_window(pin.min, pin.max);
// The pin as a *range*: `pin_event_window()` takes spans (pre, post), and handing it
// `pin.min` -- a signed time -- inverted the window, so a scope in Held or Paused mode
// drew no trace and no x labels at all.
scope->rows->pin_display_range(pin.min, pin.max);
} else if (std::fabs(scope->rows->time_window() - window_s) > 1.0e-12) {
scope->rows->set_time_window(window_s);
}
// An arrangement change (a drop, a *Plot on* button) rebuilt the row set: point the area at
// the new figure here, in the tick -- the change arrives from inside this very widget (a drop
// signal), and a synchronous rebuild would reset the drag controller under it.
// REPRO-TEMP: rebuild disabled
// if (scope->rows->rebuild_if_dirty() && scope->area != nullptr) {
// scope->area->set_figure(&scope->rows->figure());
// }
// signal), and a synchronous rebuild would reset the drag controller under it. The traces
// themselves are `SlotSeries` rebinds, so a slot that gains or loses a source draws either
// way; what *does* need this is the figure's own state: a row's legend, its cell structure,
// and the trace objects `install()` creates only for the slots that hold a signal at rebuild
// time -- a window opened empty and filled by a drop has none until this runs.
if (scope->rows->rebuild_if_dirty() && scope->area != nullptr) {
scope->area->set_figure(&scope->rows->figure());
}
if (scope->detached) sync_scope_attach(*app, *scope);
// **A window that cannot paint says so.** A blank plot is indistinguishable from a
// context that failed or an area that never got a frame, and the report ("nothing is