fix(input,examples): a linked figure's wheel anchor uses the group's live edge, and every demo verdict reaches its exit status
Review fix pass 1 on the task 7 work; four findings, all closed. 1. The Qt demo's region sat across the middle of the window, which is exactly where its own scripted input check pans: after the check's box zoom the press opened DragMode::MoveRegion, the x range never moved, and the check printed 'input check pan FAIL' -- under an exit 0, because the verdict was printed and not returned. The region now rides the left sixth (as the GLFW demo's does), both checks pass, and the verdict is wired to the exit status. The same audit found two more printed-only verdicts -- the GTK demo's scripted-resize layout check and its screenshot write (and Qt's) -- now wired too. What stays printed-only is the screenshot line of the GLFW host and the ImGui widget, where the dump happens inside the adapter's frame loop. 2. A follow-ing link group was invisible to the input layer: a member's own axis must be XFollow::Off (link_x() refuses a Live member), so Controller's x_zoom_anchor() extrapolated a coordinate from the pointer instead of using the live edge and a wheel over a row's axis strip walked the window back by 19% of its width (behind 0.000000 -> 0.285809 s). The group now publishes its own edge to every member (Axis::live_owner + Plot::set_x_live_owner, written by Figure::push_link, cleared when the group stops following or the row unlinks), and x_zoom_anchor() reads it. Public-header addition, called out. Measured after: behind 0.012992 s and the window's right edge bit-identical to the edge the group published at the gesture. 3. The multiplot summary printed x_link_count() -- a number of groups -- under 'rows in it'. It now reports the members and the group count separately, and every row line carries its own group id. 4. Host::controller_at() promised to clamp a pixel in a row gap to the nearest row and only clamped below the last one, so a gap pixel went to row 0. It now selects by distance, and tests/test_glfw_host.cpp pins it with a 40 px gap -- the first coverage of the host's figure routing, which needs no window. New cases: tests/test_controller.cpp x2 (the anchor and the hint's lifecycle), tests/test_glfw_host.cpp x3 (routing, the gap, the single-plot fallback): 229 cases, 0 failed, ASan clean. All five demos exit 0, the three self-checking modes exit 0, and the amended assertions were confirmed failing against the pre-fix behaviour.
This commit is contained in:
1 parent
a70c381a1e
commit
daff60e3f0
14 files changed
+441
-55
No files matched your search
+27
-1
@@ -278,6 +278,12 @@ struct Demo {
|
||||
/// and the teardown (scheduled as an idle source).
|
||||
bool exit_started = false;
|
||||
bool torn_down = false;
|
||||
/// The exit path's verdicts, carried out of the run so `main` can return them: whether the
|
||||
/// scripted resize produced a recomputed layout, and whether the screenshot the headless
|
||||
/// mode exists for actually reached the disk. Both are properties a caller checks with the
|
||||
/// exit status, so neither may live only in stdout.
|
||||
bool layout_recomputed = false;
|
||||
bool screenshot_written = true; ///< false only when a dump was asked for and failed
|
||||
|
||||
bool cursors_follow = true;
|
||||
Meter meter;
|
||||
@@ -364,6 +370,7 @@ gboolean on_render_after(GtkGLArea*, GdkGLContext*, gpointer user_data) {
|
||||
const plot::Rect viewport = demo.area->device_viewport();
|
||||
const bool written = dump_area_ppm(demo.dump_path, static_cast<int>(viewport.w),
|
||||
static_cast<int>(viewport.h));
|
||||
demo.screenshot_written = written;
|
||||
std::printf("[gtk_demo] screenshot %s (%dx%d GL px): %s\n", demo.dump_path,
|
||||
static_cast<int>(viewport.w), static_cast<int>(viewport.h),
|
||||
written ? "written" : "FAILED");
|
||||
@@ -446,6 +453,12 @@ gboolean on_exit_timer(gpointer user_data) {
|
||||
after.x_title_height)) <= 0.5f;
|
||||
const bool metric_strip = drift < std::fabs(after.axes.x - scaled_strip);
|
||||
const bool recomputed = after.axes.w != before.axes.w && tiles && metric_strip;
|
||||
// The verdict is carried out of the run, not only printed: a headless caller reads the
|
||||
// exit status, and a check whose only output is a line of stdout is a check that passes
|
||||
// whatever it finds. A run that cannot resize at all (a window manager or an X server
|
||||
// that clamps the window to the screen) fails here, which is the honest answer -- the
|
||||
// layout this run is supposed to prove is the layout it never got.
|
||||
demo.layout_recomputed = recomputed;
|
||||
std::printf("\n[gtk_demo] ---- exit frame ----\n");
|
||||
std::printf("[gtk_demo] resize logical %dx%d -> %dx%d, device viewport %ux%u, scale %g\n",
|
||||
width_before, height_before, gtk_widget_get_width(demo.area->widget()),
|
||||
@@ -663,5 +676,18 @@ int main(int argc, char** argv) {
|
||||
" resources went with it)");
|
||||
|
||||
g_object_unref(app);
|
||||
return app_result;
|
||||
if (app_result != 0) return app_result;
|
||||
if (!demo.screenshot_written) {
|
||||
std::fprintf(stderr, "plot_gtk_demo: the screenshot could not be written\n");
|
||||
return 1;
|
||||
}
|
||||
if (demo.automated && !demo.layout_recomputed) {
|
||||
std::fprintf(stderr,
|
||||
"plot_gtk_demo: the scripted resize did not produce a recomputed layout (see the"
|
||||
" 'exit frame' lines above; the window must be able to reach %dx%d on this"
|
||||
" screen)\n",
|
||||
kExitWidth, kExitHeight);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
+57
-26
@@ -266,17 +266,15 @@ f64 newest_x(const Series& series) {
|
||||
/// is what "new data keeps arriving at the right edge" means for a window the user has
|
||||
/// moved away from the edge.
|
||||
///
|
||||
/// One measurement is deliberately *not* checked, and the reason is worth stating because
|
||||
/// the demo is where it shows: the zoom above rolls over the axis strip, outside `axes`,
|
||||
/// where a single `Live` axis anchors at its live edge and keeps the window ending at the
|
||||
/// newest sample. A row of a following link group holds `XFollow::Off` -- the group owns
|
||||
/// the follow state, and `link_x()` refuses a `Live` member -- so `Controller`'s
|
||||
/// `x_zoom_anchor()` cannot see that the window is live and falls back to the data
|
||||
/// coordinate the pointer's x extrapolates to (the middle of the axes for a strip-centre
|
||||
/// pointer). The zoom still multiplies the width and keeps following, but it also walks
|
||||
/// the window back from the edge. Measured here rather than assumed: `behind` after the
|
||||
/// zoom is what it is, and the checks below tolerate it while pinning everything the
|
||||
/// library does guarantee.
|
||||
/// The zoom above rolls over the axis strip, outside `axes`, where the anchor is the live
|
||||
/// edge rather than the coordinate under the pointer. That is the one thing a *link group*
|
||||
/// used to lose: a member's axis is `XFollow::Off` by construction (the group owns the
|
||||
/// follow state, and `link_x()` refuses a `Live` member), so `Controller::x_zoom_anchor()`
|
||||
/// could not see that the window was live and fell back to the extent the pointer's x
|
||||
/// extrapolates to -- the middle of the axes -- which walked the window back off the edge
|
||||
/// by ~19 % of its width. A following group now publishes its own edge to every member
|
||||
/// (`Plot::set_x_live_owner`), the controller reads it, and `behind` after the zoom must
|
||||
/// still be ~0 -- which the check below asserts, so the fix cannot silently regress.
|
||||
///
|
||||
/// The verdict is carried out of the run in `failed`, so `main()` exits non-zero on a
|
||||
/// failed check -- a check whose result only stdout knows is not a check -- and also when
|
||||
@@ -293,6 +291,12 @@ struct SyncScript {
|
||||
f64 span[4]{};
|
||||
bool shared[4]{};
|
||||
bool live[4]{};
|
||||
/// The live edge the group had published to its members when the zoom was made: the
|
||||
/// coordinate the window's right edge must still be at afterwards, to the bit. Reading
|
||||
/// it here rather than comparing against the newest sample is what makes the check
|
||||
/// exact -- the edge moves while the frame runs, so "the window ends at the newest
|
||||
/// sample" is true only to within the one frame the measurement takes.
|
||||
f64 anchor_edge = 0.0;
|
||||
|
||||
static SyncScript from_env() {
|
||||
SyncScript s;
|
||||
@@ -314,10 +318,22 @@ struct SyncScript {
|
||||
if (series[0]->size() < 65536u || figure.plot(0).text_backend() == nullptr) return;
|
||||
record(0, figure, series);
|
||||
break;
|
||||
case 1:
|
||||
case 1: {
|
||||
// The edge the zoom must anchor on. A following group publishes it to every member
|
||||
// (`Plot::set_x_live_owner`), which is what `Controller::x_zoom_anchor()` now reads
|
||||
// for a gesture outside `axes` -- without it the group's live window is invisible to
|
||||
// the input layer and the anchor is the extent the pointer's x extrapolates to.
|
||||
f64 edge = 0.0;
|
||||
if (!figure.plot(kRows - 1u).x_live_owner(0u, edge)) {
|
||||
std::fprintf(stderr, "[multiplot] sync: no live edge was published to the rows\n");
|
||||
failed = true;
|
||||
break;
|
||||
}
|
||||
anchor_edge = edge;
|
||||
notches(host, figure);
|
||||
record(1, figure, series);
|
||||
break;
|
||||
}
|
||||
case 2:
|
||||
pan(host, figure);
|
||||
record(2, figure, series);
|
||||
@@ -396,8 +412,15 @@ struct SyncScript {
|
||||
const bool following = live[0] && live[1] && live[2] && live[3];
|
||||
// (a) the steady state sits on the live edge.
|
||||
const bool at_edge = std::fabs(behind[0]) <= kBatchS;
|
||||
// (b) the zoom, made on the bottom row, narrowed the one shared window.
|
||||
// (b) the zoom, made on the bottom row, narrowed the one shared window -- and, because
|
||||
// the anchor outside `axes` is the live edge, left the window *ending on that edge*,
|
||||
// to the bit. `zoom_on_edge` is the same statement with the slack a frame of arriving
|
||||
// data costs: the edge moves on while the frame runs, so the window is within 5 % of
|
||||
// its own width of the newest sample -- where anchoring on the extrapolated coordinate
|
||||
// left it 30 % behind.
|
||||
const bool zoomed = span[1] < span[0] * 0.99;
|
||||
const bool anchor_kept = near(right[1], anchor_edge, 1e-9 * std::fabs(anchor_edge) + 1e-12);
|
||||
const bool zoom_on_edge = behind[1] <= 0.05 * span[1];
|
||||
// (c) the pan, made on the middle row, moved the window back and left the width alone.
|
||||
const bool panned_back = behind[2] > behind[1] + 1e-3;
|
||||
const bool width_kept = near(span[2], span[1], 1e-9 * span[1] + 1e-12);
|
||||
@@ -408,15 +431,18 @@ struct SyncScript {
|
||||
const bool tracked = near((right[3] - right[2]) - (newest[3] - newest[2]), 0.0, kBatchS);
|
||||
const bool behind_kept = near(behind[3], behind[2], kBatchS);
|
||||
const bool width_kept_after_slide = near(span[3], span[1], 1e-9 * span[1] + 1e-12);
|
||||
failed = !(one_range && following && at_edge && zoomed && panned_back && width_kept && slid &&
|
||||
tracked && behind_kept && width_kept_after_slide);
|
||||
failed = !(one_range && following && at_edge && zoomed && anchor_kept && zoom_on_edge &&
|
||||
panned_back && width_kept && slid && tracked && behind_kept &&
|
||||
width_kept_after_slide);
|
||||
completed = true;
|
||||
std::printf(
|
||||
"[multiplot] sync checks: one range across the rows %s; following armed %s; at the live"
|
||||
" edge %s; the zoom narrowed the shared window %s; the pan moved the window back %s;"
|
||||
" the right edge tracked the newest sample %s\n",
|
||||
" edge %s; the zoom narrowed the shared window %s; the zoom kept the window ending on the"
|
||||
" live edge %s (to the bit %s, behind %.6f s); the pan moved the window back %s; the right"
|
||||
" edge tracked the newest sample %s\n",
|
||||
one_range ? "yes" : "NO", following ? "yes" : "NO", at_edge ? "yes" : "NO",
|
||||
zoomed ? "yes" : "NO", panned_back && width_kept ? "yes" : "NO",
|
||||
zoomed ? "yes" : "NO", zoom_on_edge ? "yes" : "NO", anchor_kept ? "yes" : "NO", behind[1],
|
||||
panned_back && width_kept ? "yes" : "NO",
|
||||
tracked && slid && behind_kept && width_kept_after_slide ? "yes" : "NO");
|
||||
std::fflush(stdout);
|
||||
}
|
||||
@@ -635,21 +661,26 @@ int main() {
|
||||
static_cast<unsigned long long>(series[kSine]->size()),
|
||||
static_cast<f64>(series[kSine]->size()) * kDx, kSampleRate * 1.0e-3, yr0.min,
|
||||
yr0.max);
|
||||
// `x_link_count()` counts link *groups* (excluding the unlinked group 0), so it is
|
||||
// labelled as one and the members are listed separately: the readout a reader checks the
|
||||
// link with must not say "rows" about a number of groups.
|
||||
u32 members = 0;
|
||||
for (u32 row = 0; row < kRows; ++row) members += figure.x_link(row) == link ? 1u : 0u;
|
||||
std::printf("[multiplot] x link group %u: range [%.6f, %.6f] s (width %.6f), follow %s,"
|
||||
" rows in it %u\n",
|
||||
link, shared.min, shared.max, shared.span(), figure.x_link_follow(link) ==
|
||||
plot::XFollow::Live
|
||||
? "Live"
|
||||
: "Off",
|
||||
figure.x_link_count());
|
||||
" %u member row%s of %u link group%s\n",
|
||||
link, shared.min, shared.max, shared.span(),
|
||||
figure.x_link_follow(link) == plot::XFollow::Live ? "Live" : "Off", members,
|
||||
members == 1u ? "" : "s", figure.x_link_count(),
|
||||
figure.x_link_count() == 1u ? "" : "s");
|
||||
for (u32 row = 0; row < kRows; ++row) {
|
||||
const plot::Plot& p = figure.plot(row);
|
||||
const plot::Rect ra = p.layout().axes;
|
||||
std::printf("[multiplot] row %u x [%.6f, %.6f] s, axes %.0fx%.0f at (%.0f, %.0f),"
|
||||
" traces %u, legend rows %u\n",
|
||||
" traces %u, legend rows %u, in link group %u\n",
|
||||
row, p.x_range(0).min, p.x_range(0).max, static_cast<f64>(ra.w),
|
||||
static_cast<f64>(ra.h), static_cast<f64>(ra.x), static_cast<f64>(ra.y),
|
||||
p.trace_count(), static_cast<u32>(p.layout().legend.rows.size()));
|
||||
p.trace_count(), static_cast<u32>(p.layout().legend.rows.size()),
|
||||
figure.x_link(row));
|
||||
}
|
||||
std::printf("[multiplot] region %u: x [%g, %g] s, n %llu, mean %.4f, rms %.4f, min"
|
||||
" %.4f, max %.4f, integral %.4f\n",
|
||||
|
||||
+21
-2
@@ -316,6 +316,11 @@ int main(int argc, char** argv) {
|
||||
const auto keep_focus = [widget] { widget->setFocus(); };
|
||||
|
||||
Meter meter;
|
||||
/// The scripted input check's verdict, read after `exec()`: a FAIL that is only printed
|
||||
/// is a check no caller can act on -- the exit status is what a headless run reads. Same
|
||||
/// for the exit frame's screenshot: the headless mode exists to produce that file.
|
||||
bool inputs_ok = true;
|
||||
bool shot_ok = true;
|
||||
// The cursors ride the live window until the user takes over: the first
|
||||
// ctrl+drag on a cursor, or a pan/zoom that clears the x axis' auto scale,
|
||||
// stops them where the user put them. The region's bounds ride the same window.
|
||||
@@ -343,7 +348,12 @@ int main(int argc, char** argv) {
|
||||
const plot::Range xr = p.x_range(axis_x);
|
||||
p.set_cursor_x(cursor_a, xr.min + 0.35 * xr.span());
|
||||
p.set_cursor_x(cursor_b, xr.min + 0.65 * xr.span());
|
||||
p.set_region(region, xr.min + 0.40 * xr.span(), xr.min + 0.60 * xr.span());
|
||||
// The left sixth of the window, not its middle: a region is a grabbable handle, so
|
||||
// one laid across the centre turns the pan this demo's scripted input check makes
|
||||
// (and the pan a user makes) into a region drag -- the same trap the GLFW demo hit.
|
||||
// The cursors are not within a grab radius of the centre either, which is what
|
||||
// keeps the check's press a pan.
|
||||
p.set_region(region, xr.min + 0.06 * xr.span(), xr.min + 0.18 * xr.span());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,7 +470,9 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
if (screenshot != nullptr && screenshot[0] != '\0') {
|
||||
const bool written = dump_widget_ppm(image, screenshot);
|
||||
shot_ok = written;
|
||||
std::printf("[qt_demo] screenshot %s: %s\n", screenshot, written ? "written" : "FAILED");
|
||||
if (!written) std::fprintf(stderr, "plot_qt_demo: the screenshot could not be written\n");
|
||||
}
|
||||
std::printf("[qt_demo] init %s: %s\n", plot::to_string(widget->init_status().code),
|
||||
widget->init_status().message);
|
||||
@@ -534,9 +546,15 @@ int main(int argc, char** argv) {
|
||||
static_cast<double>(check_axes.w), static_cast<double>(check_axes.h));
|
||||
std::printf("[qt_demo] input check box-zoom %s expected x [%.9f, %.9f], got [%.9f, %.9f]\n",
|
||||
box_ok ? "PASS" : "FAIL", box_expect_min, box_expect_max, box_got.min, box_got.max);
|
||||
inputs_ok = box_ok && drag_ok;
|
||||
std::printf("[qt_demo] input check pan %s expected x [%.9f, %.9f], got [%.9f, %.9f]\n",
|
||||
drag_ok ? "PASS" : "FAIL", drag_expect_min, drag_expect_min + before_drag.span(),
|
||||
drag_got.min, drag_got.max);
|
||||
if (!inputs_ok) {
|
||||
std::fprintf(stderr,
|
||||
"plot_qt_demo: the scripted input check failed (see the 'input check' lines"
|
||||
" above)\n");
|
||||
}
|
||||
|
||||
// The two gestures above left the plot zoomed and panned (that is what makes
|
||||
// them checks). Put the live framing back before the run summary reads the
|
||||
@@ -594,5 +612,6 @@ int main(int argc, char** argv) {
|
||||
// (so the plot's GL resources are released while their context is still alive,
|
||||
// and before `graph`, which the widget points at), then `graph`, then the
|
||||
// already-joined `producer`, then `stream` itself.
|
||||
return app_result;
|
||||
if (app_result != 0) return app_result;
|
||||
return inputs_ok && shot_ok ? 0 : 1;
|
||||
}
|
||||
Reference in new issue
Block a user