feat(input): live x follow with relative zoom and mouse panning
This commit is contained in:
1 parent
eaae07d8e0
commit
77e861f79e
10 files changed
+971
-28
No files matched your search
+155
-8
@@ -15,7 +15,12 @@
|
||||
// (shaded band plus a `dx/dy/rms/mean` line) -- no drawing code here;
|
||||
// - the headless smoke mode the adapter implements once
|
||||
// (PLOT_DEMO_EXIT_AFTER_MS / PLOT_DEMO_SCREENSHOT), so CI can run this under
|
||||
// `xvfb-run` and check the frame it wrote.
|
||||
// `xvfb-run` and check the frame it wrote;
|
||||
// - `PLOT_DEMO_FOLLOW=1`: the same run with the time axis armed for live following
|
||||
// (`XFollow::Live`), driving three wheel notches and a pan through the host's own
|
||||
// controller and printing, at each step, the range's width, its distance behind
|
||||
// the newest sample and the follow state -- the interaction spec §12 is about,
|
||||
// measured on a real stream rather than in a unit test.
|
||||
//
|
||||
// The producer is joined before the stream it writes into is destroyed, and
|
||||
// before the host closes the GL context (main's destruction order does that).
|
||||
@@ -29,6 +34,7 @@
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <numbers>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
@@ -158,6 +164,133 @@ struct Meter {
|
||||
u64 frames = 0;
|
||||
};
|
||||
|
||||
/// The scripted live-follow sequence `PLOT_DEMO_FOLLOW=1` runs.
|
||||
///
|
||||
/// The follow arithmetic is pinned by the library's tests; "the plot keeps showing
|
||||
/// the newest sample while the user zooms and pans it" is an *interaction*, and this
|
||||
/// is the program that has one -- a real 1 MS/s producer, the real host, the real
|
||||
/// controller. So the sequence arms `XFollow::Live` once the stream holds data and
|
||||
/// then takes four measurements, one per frame:
|
||||
///
|
||||
/// (a) steady state, with the retained window framed;
|
||||
/// (b) after three wheel notches, rolled over the x label strip -- outside `axes`,
|
||||
/// where the wheel's anchor is the live edge, so the zoom changes the width and
|
||||
/// nothing else;
|
||||
/// (c) after a 100 px left-drag, the real pan gesture, which moves the window back
|
||||
/// from the live edge;
|
||||
/// (d) after a further 50 ms of data has arrived.
|
||||
///
|
||||
/// Each measurement prints the range's width, how far its right edge sits behind the
|
||||
/// newest sample, and whether following is still armed. Only the zoom may change the
|
||||
/// width, only the pan may change the distance behind, and following must be armed
|
||||
/// throughout; the last line of the run says whether that held, so the run is a check
|
||||
/// rather than a printout to be read.
|
||||
struct FollowScript {
|
||||
bool enabled = false;
|
||||
plot::u32 step = 0;
|
||||
plot::f64 width[4]{};
|
||||
plot::f64 behind[4]{};
|
||||
bool live[4]{};
|
||||
plot::f64 wait_from = 0.0;
|
||||
|
||||
static FollowScript from_env() {
|
||||
FollowScript s;
|
||||
const char* v = std::getenv("PLOT_DEMO_FOLLOW");
|
||||
s.enabled = v != nullptr && v[0] != '0' && v[0] != '\0';
|
||||
return s;
|
||||
}
|
||||
|
||||
/// One frame per step: the gesture, then the measurement of what it did. The last
|
||||
/// step waits for data to actually arrive, because the last measurement is about
|
||||
/// what more samples do to a window the user has moved.
|
||||
void advance(plot::glfw::Host& host, plot::data::StreamSeries& stream, plot::Plot& p,
|
||||
plot::u32 axis) {
|
||||
switch (step) {
|
||||
case 0:
|
||||
// Enough retained data for the numbers to be worth printing; the producer
|
||||
// starts before the window opens, so this usually fires on the first frame.
|
||||
if (stream.size() < 262144u) return;
|
||||
p.set_x_follow(axis, plot::XFollow::Live);
|
||||
p.auto_fit_x(); // the retained window, ending at the live edge
|
||||
record(0, p, stream, axis);
|
||||
break;
|
||||
case 1:
|
||||
notches(host, p);
|
||||
record(1, p, stream, axis);
|
||||
break;
|
||||
case 2:
|
||||
pan(host, p);
|
||||
record(2, p, stream, axis);
|
||||
wait_from = stream.x_bounds().max;
|
||||
break;
|
||||
case 3:
|
||||
// 50 ms at this demo's 1 MS/s, i.e. ~50000 samples of new data before the
|
||||
// fourth measurement.
|
||||
if (stream.x_bounds().max - wait_from < 0.05) return;
|
||||
record(3, p, stream, axis);
|
||||
report();
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
++step;
|
||||
}
|
||||
|
||||
private:
|
||||
/// Three notches of the wheel over the x label strip centre.
|
||||
static void notches(plot::glfw::Host& host, plot::Plot& p) {
|
||||
const plot::Rect strip = p.layout().x_label_area;
|
||||
const plot::input::WheelEvent w{strip.x + 0.5f * strip.w, strip.y + 0.5f * strip.h,
|
||||
0.0f, 1.0f, 0};
|
||||
for (int i = 0; i < 3; ++i) host.controller().on_wheel(w);
|
||||
}
|
||||
|
||||
/// A 100 px drag to the right across the axes, as press/move/release.
|
||||
static void pan(plot::glfw::Host& host, plot::Plot& p) {
|
||||
const plot::Rect axes = p.layout().axes;
|
||||
const f32 x = axes.x + 0.5f * axes.w;
|
||||
const f32 y = axes.y + 0.5f * axes.h;
|
||||
host.controller().on_pointer({x, y, 0.0f, 0.0f, plot::input::Button::Left, 0, true, true, false});
|
||||
host.controller().on_pointer(
|
||||
{x + 100.0f, y, 100.0f, 0.0f, plot::input::Button::Left, 0, true, false, false});
|
||||
host.controller().on_pointer(
|
||||
{x + 100.0f, y, 0.0f, 0.0f, plot::input::Button::Left, 0, false, false, true});
|
||||
}
|
||||
|
||||
void record(int i, plot::Plot& p, plot::data::StreamSeries& stream, plot::u32 axis) {
|
||||
p.layout();
|
||||
const plot::Range r = p.x_range(axis);
|
||||
width[i] = r.span();
|
||||
behind[i] = stream.x_bounds().max - r.max;
|
||||
live[i] = p.x_follow(axis) == plot::XFollow::Live;
|
||||
std::printf("[glfw_demo] follow %c: width %9.6f s behind %9.6f s follow %s newest %.6f s\n",
|
||||
"abcd"[i], width[i], behind[i], live[i] ? "armed" : "OFF",
|
||||
stream.x_bounds().max);
|
||||
std::fflush(stdout);
|
||||
}
|
||||
|
||||
void report() {
|
||||
// Tolerances rather than `==`: the window is recomputed from a newest x that has
|
||||
// moved between the frames the two numbers were taken on, so the last bits of
|
||||
// the width and the distance behind may differ.
|
||||
const auto same = [](plot::f64 a, plot::f64 b) {
|
||||
return std::fabs(a - b) <= 1e-9 * std::fabs(a) + 1e-12;
|
||||
};
|
||||
const auto at_edge = [](plot::f64 distance) { return std::fabs(distance) < 1e-6; };
|
||||
const bool zoom_only_width =
|
||||
width[0] > width[1] * 1.01 && same(width[1], width[2]) && same(width[2], width[3]);
|
||||
const bool pan_only_behind =
|
||||
at_edge(behind[0]) && at_edge(behind[1]) && behind[2] > 1e-6 && same(behind[2], behind[3]);
|
||||
const bool following = live[0] && live[1] && live[2] && live[3];
|
||||
std::printf(
|
||||
"[glfw_demo] follow checks: width changed only by the zoom: %s; distance behind only"
|
||||
" by the pan: %s; following armed throughout: %s\n",
|
||||
zoom_only_width ? "yes" : "NO", pan_only_behind ? "yes" : "NO",
|
||||
following ? "yes" : "NO");
|
||||
std::fflush(stdout);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
@@ -183,23 +316,37 @@ int main() {
|
||||
const u32 measure = p.add_measure(cursor_a, cursor_b, trace);
|
||||
|
||||
Meter meter;
|
||||
/// The scripted live-follow verification, off unless `PLOT_DEMO_FOLLOW` asks for
|
||||
/// it. It is the same shape as the headless smoke mode above: a driven run whose
|
||||
/// result is a few numbers on stdout.
|
||||
FollowScript script = FollowScript::from_env();
|
||||
// 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.
|
||||
// stops them where the user put them. Under `Live` the follow path owns the x
|
||||
// range whatever auto scale says, so a zoom is not the user taking the x view
|
||||
// over -- only a cursor drag, or following being switched off, is.
|
||||
bool cursors_follow = true;
|
||||
|
||||
const plot::glfw::FrameFn frame = [&](plot::Plot& graph, plot::f64 dt) {
|
||||
stream.acquire(); // once per frame, on the render thread
|
||||
|
||||
// Re-frame the live stream while the user has not overridden the ranges.
|
||||
// `auto_fit_*` re-arms auto scale, so it may only run while auto scale is
|
||||
// still on: calling it after a pan would undo the pan every frame.
|
||||
if (graph.x_axis(axis_x).auto_scale) graph.auto_fit_x();
|
||||
if (script.enabled) script.advance(host, stream, graph, axis_x);
|
||||
|
||||
// Re-frame the live stream while the user has not overridden the ranges, and
|
||||
// while the axis is not following. `auto_fit_*` re-arms auto scale, so it may
|
||||
// only run while auto scale is still on: calling it after a pan would undo the
|
||||
// pan every frame -- and under `Live` a fit means "back to the live edge", so a
|
||||
// per-frame one would undo the offset the user just panned to as well.
|
||||
if (graph.x_follow(axis_x) == plot::XFollow::Off && graph.x_axis(axis_x).auto_scale) {
|
||||
graph.auto_fit_x();
|
||||
}
|
||||
if (graph.y_axis(axis_y).auto_scale) graph.auto_fit_y(axis_y);
|
||||
|
||||
if (cursors_follow) {
|
||||
if (host.controller().drag_mode() == plot::input::DragMode::MoveCursor ||
|
||||
!graph.x_axis(axis_x).auto_scale) {
|
||||
const bool x_taken_over = graph.x_follow(axis_x) == plot::XFollow::Live
|
||||
? false
|
||||
: !graph.x_axis(axis_x).auto_scale;
|
||||
if (host.controller().drag_mode() == plot::input::DragMode::MoveCursor || x_taken_over) {
|
||||
cursors_follow = false;
|
||||
} else {
|
||||
const plot::Range xr = graph.x_range(axis_x);
|
||||
|
||||
Reference in new issue
Block a user