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

+13
View File
@@ -1509,6 +1509,19 @@ void RowSet::pin_event_window(double pre_s, double post_s) {
figure_->set_x_link_range(x_group_, {-pre_s, post_s});
}
void RowSet::pin_display_range(double x0, double x1) {
if (!(x1 > x0)) return;
window_s_ = std::max(1.0e-3, x1 - x0);
pinned_ = true;
pinned_pre_ = -x0;
pinned_post_ = x1;
if (!figure_) return;
// The same two statements `pin_event_window()` makes, from the range instead of the spans: the
// group stops following the live edge and is placed on the event's own window.
figure_->set_x_link_follow(x_group_, plot::XFollow::Off);
figure_->set_x_link_range(x_group_, {x0, x1});
}
void RowSet::fit_all() {
for (int i = 0; i < rows_; ++i) {
Row& r = row(i);
+7
View File
@@ -570,6 +570,13 @@ class RowSet {
/// 0, so nothing outside the pre+post window is visible.
void pin_event_window(double pre_s, double post_s);
/// The same pin, stated as the **range** a second view mirrors: the caller has the display
/// range (`x0 < x1`) and not the spans -- a scope window takes `pinned_range()` from the main
/// window and hands it on. `pin_event_window()` takes *spans*, and passing it a range's signed
/// `min` turned the pin inside out (a scope in Held mode pinned to `[+pre, +post]`: no trace
/// and no x labels). A degenerate or inverted range pins nothing.
void pin_display_range(double x0, double x1);
/// Fits every row's y axis to the data it holds, and puts x back on the whole time window --
/// `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.
+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
+28 -3
View File
@@ -1915,11 +1915,13 @@ gboolean scope_probe_step(gpointer user) {
det.rows != nullptr && det.rows->row_count() > 0
? det.rows->row(0).signal_name[0].c_str()
: "");
}
// The scope window carries its own right-click menu (its gesture is on the GL area, wired
// beside the drop target): one click gesture whose button is 3, per window.
int right_clicks = 0;
if (det.widget != nullptr) {
GListModel* controllers = gtk_widget_observe_controllers(det.widget);
GtkWidget* det_widget = a->scopes.empty() ? nullptr : a->scopes.back()->widget;
if (det_widget != nullptr) {
GListModel* controllers = gtk_widget_observe_controllers(det_widget);
const guint n = g_list_model_get_n_items(G_LIST_MODEL(controllers));
for (guint i = 0; i < n; ++i) {
GObject* c = G_OBJECT(g_list_model_get_item(controllers, i));
@@ -1933,8 +1935,31 @@ gboolean scope_probe_step(gpointer user) {
}
std::fprintf(stderr, "[scope-probe] rclick=%d\n", right_clicks);
std::fflush(stderr);
}
// **The pin mirror**: a held event pins the main window, and every scope must be pinned to
// the *same range*. `pin_event_window()` takes spans (pre, post) and passing it a range's
// signed min turned the pin inside out (a scope in Held drew no trace and no x labels).
a->rows->pin_event_window(1.6, 0.4);
a->dbg_scope_step = 4;
g_timeout_add(350, scope_probe_step, a);
return G_SOURCE_REMOVE;
}
case 4: {
const plot::Range main_pin = a->rows->pinned_range();
const plot::Range scope_pin = a->scopes[0]->rows->pinned_range();
const plot::Range scope_x = a->scopes[0]->rows->row(0).plot != nullptr
? a->scopes[0]->rows->row(0).plot->x_range(0)
: plot::Range{};
const bool pinned = a->scopes[0]->rows->window_pinned() == a->rows->window_pinned();
const bool same = std::fabs(scope_pin.min - main_pin.min) < 1e-9 &&
std::fabs(scope_pin.max - main_pin.max) < 1e-9 &&
scope_x.max > scope_x.min;
std::fprintf(stderr,
"[scope-probe] pin_mirror=%d main=[%.3f..%.3f] scope=[%.3f..%.3f] "
"x=[%.3f..%.3f]\n",
pinned && same ? 1 : 0, main_pin.min, main_pin.max, scope_pin.min,
scope_pin.max, scope_x.min, scope_x.max);
std::fflush(stderr);
a->rows->set_time_window(2.0); // unpin: leave the window as the run found it
return G_SOURCE_REMOVE;
}
default: