fix: only a window change sets the time scale; a fit states the whole of it
The time scale is more than the axis: it is also the trigger's event span, the buffer partition and the control's readout. A zoom or a pan was already clean -- the controller writes the member axis and `clamp_to_data()` caps the span -- but both fits moved `window_s_` alone and only restated the control. The event span then stayed on the previous window until the next decade step or unit change applied the fitted value for real, so one action changed the time scale twice. `apply_window_scale()` is split out of `apply_time_window()` for the fits to use: it moves the partition, the event span, the control and the session, and leaves the view alone, because `set_time_window()` re-arms the live follow and would throw a held event or a paused view back onto the stream. The fits report the span they framed, `fit_all()` delegates its x half to `fit_x()` so the two cannot drift, and the probe drives the menu handlers rather than `RowSet` so it sees the half that lives there. `[fit-probe]` gained `event_span=`, which must equal `window=` on every line. Also moves the plotter pin to 03c41ad (three build warnings, no behaviour). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
1f6738031a
commit
9888fd9977
7 files changed
+128
-63
No files matched your search
+48
-18
@@ -1865,11 +1865,19 @@ void sync_window_display(App& app, double seconds) {
|
||||
app.updating_panels = false;
|
||||
}
|
||||
|
||||
/// The one place the display window changes. 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) {
|
||||
/// Everything the time scale consists of *except* the view: the buffer partition, the trigger's
|
||||
/// event span, the control's readout and the session. Split out of `apply_time_window()` because
|
||||
/// a **fit** frames the axis itself -- and when the view is pinned it must keep the pin, which
|
||||
/// `RowSet::set_time_window()` would clear, throwing a frozen picture onto the live stream -- but
|
||||
/// 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
|
||||
/// never touch it (they move the axis, and `clamp_to_data()` caps them at `window_s_`); the two
|
||||
/// fits do set it, and so must come through here. A fit that moved `window_s_` alone -- which is
|
||||
/// what both of them used to do -- left the trigger's event span on the *old* window, and the
|
||||
/// next decade step or unit change then fed the fitted value in for real, so the time scale
|
||||
/// changed twice for one action and the `window == event_span` invariant held at neither point.
|
||||
void apply_window_scale(App& app, double seconds) {
|
||||
if (!(seconds > 0.0)) return;
|
||||
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
|
||||
@@ -1882,12 +1890,25 @@ void apply_time_window(App& app, double seconds) {
|
||||
format_rate(app.model->partition().stored_rate()) + ", history cleared";
|
||||
app.notice_until_us = g_get_monotonic_time() + 4 * G_TIME_SPAN_SECOND;
|
||||
}
|
||||
app.rows->set_time_window(clamped);
|
||||
app.model->set_event_span(clamped);
|
||||
sync_window_display(app, clamped);
|
||||
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
|
||||
/// reporting an error: the entry is also the readout, so leaving junk in it would leave the
|
||||
/// control disagreeing with the plot.
|
||||
@@ -4047,12 +4068,15 @@ void on_signals_toggled(GtkToggleButton* button, gpointer user) {
|
||||
}
|
||||
|
||||
/// Fit: auto-scale every row's y axis and re-frame the x window on the data the
|
||||
/// ring holds (the plot's own `f` semantics), then tell the toolbar what span that
|
||||
/// is.
|
||||
/// ring holds (the plot's own `f` semantics), then apply that span as the time scale.
|
||||
///
|
||||
/// `apply_window_scale()` and not `sync_window_display()`: a fitted x span *is* a new window, so
|
||||
/// the trigger's event span and the buffer partition move with it. Restating the control alone
|
||||
/// left those on the previous window.
|
||||
void on_fit_clicked(GtkButton*, gpointer user) {
|
||||
App* app = static_cast<App*>(user);
|
||||
app->rows->fit_all();
|
||||
sync_window_display(*app, app->rows->time_window());
|
||||
double span = 0.0;
|
||||
if (app->rows->fit_all(span)) apply_window_scale(*app, span);
|
||||
app->notice = "fitted to the buffered data (" + format_window(app->rows->time_window()) + ")";
|
||||
app->notice_until_us = g_get_monotonic_time() + 3 * G_TIME_SPAN_SECOND;
|
||||
}
|
||||
@@ -4062,8 +4086,8 @@ void on_fit_clicked(GtkButton*, gpointer user) {
|
||||
/// were placed by hand.
|
||||
void on_fit_x_clicked(GtkButton*, gpointer user) {
|
||||
App* app = static_cast<App*>(user);
|
||||
app->rows->fit_x();
|
||||
sync_window_display(*app, app->rows->time_window());
|
||||
double span = 0.0;
|
||||
if (app->rows->fit_x(span)) apply_window_scale(*app, span);
|
||||
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;
|
||||
}
|
||||
@@ -7182,9 +7206,14 @@ void fit_probe_report(App& a, const char* what) {
|
||||
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 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:
|
||||
// it is the trigger's, written only by `apply_window_scale()`, so a fit that moved `window`
|
||||
// alone shows up here as the two disagreeing.
|
||||
std::fprintf(stderr,
|
||||
"[fit-probe] %-10s window=%.6f x=[%.6f..%.6f] span=%.6f y=[%.6f..%.6f]\n",
|
||||
what, a.rows->time_window(), x.min, x.max, x.span(), y.min, y.max);
|
||||
"[fit-probe] %-10s window=%.6f event_span=%.6f x=[%.6f..%.6f] span=%.6f "
|
||||
"y=[%.6f..%.6f]\n",
|
||||
what, a.rows->time_window(), a.model->event_span(), x.min, x.max, x.span(), y.min,
|
||||
y.max);
|
||||
std::fflush(stderr);
|
||||
}
|
||||
|
||||
@@ -7275,15 +7304,16 @@ gboolean fit_probe_step(gpointer user) {
|
||||
a->dbg_fit_step = 1;
|
||||
break;
|
||||
case 1:
|
||||
a->rows->fit_x();
|
||||
sync_window_display(*a, a->rows->time_window());
|
||||
// The handlers themselves, not `RowSet::fit_x()` alone: the whole point of the
|
||||
// `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);
|
||||
a->dbg_fit_step = 2;
|
||||
break;
|
||||
case 2:
|
||||
fit_probe_report(*a, "fit x");
|
||||
a->rows->fit_all();
|
||||
sync_window_display(*a, a->rows->time_window());
|
||||
on_fit_clicked(nullptr, a);
|
||||
g_timeout_add(400, fit_probe_step, a);
|
||||
a->dbg_fit_step = 3;
|
||||
break;
|
||||
|
||||
Reference in new issue
Block a user