feat(input): draggable cursors, regions and measurement-band edges

This commit is contained in:
Martino Ferrari committed 2026-09-28 13:47:35 +02:00
1 parent f423cf6f99
commit 354e79eea2
2 files changed
+289

No files matched your search

+227
View File
@@ -37,6 +37,7 @@
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <numbers>
#include <thread>
#include <utility>
@@ -73,6 +74,19 @@ constexpr f64 kSpikeAmp = 3.0;
constexpr plot::Color kTraceColor = plot::Color::hex(0x40C0FF);
constexpr plot::Color kCursorAColor = plot::Color::hex(0xFFB020);
constexpr plot::Color kCursorBColor = plot::Color::hex(0x35D0A8);
/// The region the direct-manipulation script drags: a fill and an edge no other element
/// of the frame produces -- the trace is blue, the chrome grey, cursor A the accent and
/// cursor B a teal -- so a pixel count of one of them can only be about the region. The
/// two are the render smoke test's own colours, so its predicates read this screenshot
/// too.
constexpr plot::Color kRegionFill = plot::Color::hex(0x33FF00, 0.35f);
constexpr plot::Color kRegionEdge = plot::Color::hex(0xFF00FF);
/// The pan the region script takes, in framebuffer pixels, and the window it freezes the
/// view on, in seconds: 20 ms of this 1 MS/s stream is ~20 000 samples, wider than the
/// axes, so the trace is drawn from the pyramid and the region's interval is hundreds of
/// pixels across.
constexpr plot::u32 kRegionPanPx = 100u;
constexpr plot::f64 kRegionWindowS = 0.020;
/// y(t) for sample index `i`: sine + deterministic noise + a two-sample spike.
///
@@ -303,6 +317,177 @@ struct FollowScript {
}
};
/// The region's geometry as `Plot::layout()` measured it -- the rects the renderer paints
/// and the controller hit-tests, so what this reports is what the frame drew -- or null
/// when the layout holds no such region.
const plot::RegionLayout* region_layout(const plot::Plot& p, plot::u32 region) {
for (const plot::RegionLayout& r : std::as_const(p).layout().regions) {
if (r.region == region) return &r;
}
return nullptr;
}
/// The shaded interval a region occupies right now, or an empty rect when the layout
/// holds no such region.
plot::Rect region_fill(const plot::Plot& p, plot::u32 region) {
const plot::RegionLayout* r = region_layout(p, region);
return r != nullptr ? r->fill : plot::Rect{};
}
/// The scripted direct-manipulation sequence `PLOT_DEMO_REGIONS` runs: the region half of
/// spec §5, on a real host, in the shape the follow script above established.
///
/// A region is geometry in *data* coordinates while what a pointer grabs and a frame
/// paints is its *pixel* rect, so the claims worth making on a live stream are that the
/// rect the layout measured is the rect the data moved, and that it tracks the view
/// rather than the screen. Three frames measure exactly that:
///
/// (a) freeze the x view on a fixed window behind the live edge -- a user gesture does
/// the same by clearing auto scale, and it is what stops the live fit from sliding
/// the window under the region on every frame -- and place the region on a fraction
/// of what that window shows;
/// (b) pan 100 px with the press *outside* the region's interval, which is the press
/// that means "move the view", and check that the region's fill rect moved by those
/// 100 px and by nothing else, with its width untouched: the region is data-anchored,
/// so the pan moved the data *under* it and the region came along;
/// (c) leave the pointer on the region's left bound (`PLOT_DEMO_REGIONS=hot`) or off the
/// data area (any other value), which is the A/B the screenshot's emphasized handle
/// is counted from.
///
/// The verdict is carried in `failed`, so `main()` exits non-zero when a check said no,
/// and the run's screenshot is what the pixel counts are read from.
struct RegionScript {
bool enabled = false;
/// Leave a bound hovered as the final pointer state: `PLOT_DEMO_REGIONS=hot`. Any other
/// value leaves the pointer where nothing is hot, so running twice -- with and without
/// `hot` -- is a pixel-level A/B of the emphasis.
bool hover = false;
bool failed = false;
/// Set once the third measurement has been taken: a run that ended before it has not
/// passed, it just has not finished.
bool completed = false;
plot::u32 step = 0;
plot::Rect before{};
plot::Rect after{};
plot::RegionGrab grab = plot::RegionGrab::None;
static RegionScript from_env() {
RegionScript s;
const char* v = std::getenv("PLOT_DEMO_REGIONS");
s.enabled = v != nullptr && v[0] != '\0' && v[0] != '0';
s.hover = v != nullptr && std::strcmp(v, "hot") == 0;
return s;
}
void advance(plot::glfw::Host& host, plot::data::StreamSeries& stream, plot::Plot& p,
plot::u32 axis, plot::u32 region) {
switch (step) {
case 0: {
// The producer starts while the window is being built, so the first frame can
// already hold every sample this needs -- and the renderer installs the font the
// strips and the labels are measured with on its *first* draw. Measuring the
// region's rect against a layout computed before that would measure the default
// strips, so the script waits for a frame the renderer has drawn.
if (stream.size() < 262144u || p.text_backend() == nullptr) return;
const plot::Range live = stream.x_bounds();
p.set_x_range(axis, {live.max - kRegionWindowS, live.max});
const plot::Range w = p.x_range(axis);
p.set_region(region, w.min + 0.30 * w.span(), w.min + 0.55 * w.span());
p.layout();
const plot::RegionLayout* rl = region_layout(p, region);
if (rl == nullptr) {
failed = true;
break;
}
before = rl->fill;
std::printf("[glfw_demo] region %u: frozen x [%.6f, %.6f] s, fill x %.1f w %.1f h"
" %.1f, bounds at %.1f and %.1f\n",
region, w.min, w.max, before.x, before.w, before.h,
static_cast<plot::f64>(plot::handle_column(rl->left_handle)),
static_cast<plot::f64>(plot::handle_column(rl->right_handle)));
break;
}
case 1: {
// The rect the pan starts from, from the settled layout of this frame rather than
// from the frame the region was placed on: the strip the chrome reserves settles
// with the tick labels the frozen window produces.
p.layout();
before = region_fill(p, region);
const plot::RegionLayout* rl = region_layout(p, region);
if (rl == nullptr) {
failed = true;
break;
}
const plot::Rect axes = std::as_const(p).layout().axes;
// Left of the region and inside the axes: a press on no handle, so this is the
// pan and not a region grab.
const plot::f32 x = rl->fill.x - 40.0f;
const plot::f32 y = axes.y + 0.5f * axes.h;
const plot::f64 held = p.x_transform(axis).to_data_x(x);
host.controller().on_pointer(
{x, y, 0.0f, 0.0f, plot::input::Button::Left, 0u, true, true, false});
const bool panned = host.controller().drag_mode() == plot::input::DragMode::Pan;
host.controller().on_pointer({x + static_cast<plot::f32>(kRegionPanPx), y,
static_cast<plot::f32>(kRegionPanPx), 0.0f,
plot::input::Button::Left, 0u, true, false, false});
host.controller().on_pointer({x + static_cast<plot::f32>(kRegionPanPx), y, 0.0f, 0.0f,
plot::input::Button::Left, 0u, false, false, true});
p.layout();
after = region_fill(p, region);
// The invariant the pan has always held, which is what makes a data-anchored
// region track the view: the data under the press is under the pointer afterwards.
const plot::f64 under = p.x_transform(axis).to_data_x(x + static_cast<plot::f32>(kRegionPanPx));
const plot::f64 delta = static_cast<plot::f64>(after.x) - static_cast<plot::f64>(before.x);
const bool tracked = std::fabs(delta - static_cast<plot::f64>(kRegionPanPx)) <= 1.0;
const bool width_kept = std::fabs(static_cast<plot::f64>(after.w) -
static_cast<plot::f64>(before.w)) <= 0.5;
const bool data_kept = std::fabs(under - held) <= 1e-9;
failed = failed || !panned || !tracked || !width_kept || !data_kept;
std::printf("[glfw_demo] region pan %u px: drag mode %s, fill x %.1f -> %.1f (delta"
" %.1f, wanted %u), width %.1f -> %.1f, data under the press %.9f -> %.9f\n",
kRegionPanPx, panned ? "Pan" : "NOT PAN", before.x, after.x, delta,
kRegionPanPx, before.w, after.w, held, under);
break;
}
case 2: {
const plot::Rect axes = std::as_const(p).layout().axes;
if (hover) {
const plot::RegionLayout* rl = region_layout(p, region);
if (rl == nullptr) {
failed = true;
break;
}
const plot::f32 col = plot::handle_column(rl->left_handle);
host.controller().on_pointer({col, axes.y + 0.5f * axes.h, 0.0f, 0.0f,
plot::input::Button::Left, 0u, false, false, false});
grab = host.controller().region_grab(region);
failed = failed || grab != plot::RegionGrab::Left;
} else {
// Inside the window, outside the data area: no handle is hot, so a frame drawn
// from here is the baseline the emphasized one is compared against.
const plot::Rect strip = std::as_const(p).layout().x_label_area;
host.controller().on_pointer({strip.x + 0.5f * strip.w, strip.y + 0.5f * strip.h,
0.0f, 0.0f, plot::input::Button::Left, 0u, false, false,
false});
grab = host.controller().region_grab(region);
failed = failed || grab != plot::RegionGrab::None;
}
const plot::HotHandles hot = p.hot_handles();
std::printf("[glfw_demo] region final pointer: grab zone %u, hot region %llu, hot"
" zone %u, hovered cursor %llu\n",
static_cast<unsigned>(grab), static_cast<unsigned long long>(hot.region),
static_cast<unsigned>(hot.zone),
static_cast<unsigned long long>(hot.cursor));
completed = true;
break;
}
default:
return;
}
++step;
}
};
} // namespace
int main() {
@@ -327,11 +512,23 @@ int main() {
p.set_cursor_snap(cursor_b, plot::SnapMode::NearestSample);
const u32 measure = p.add_measure(cursor_a, cursor_b, trace);
// The region the direct-manipulation script drags and the pixels are counted from. Its
// bounds are placed by the script, once the view it is measured against has been frozen.
plot::Region region_desc;
region_desc.snap = plot::RegionSnap::Free;
region_desc.fill = kRegionFill;
region_desc.edge = kRegionEdge;
region_desc.label = "roi";
const u32 region = p.add_region(region_desc);
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 scripted region sequence, off unless `PLOT_DEMO_REGIONS` asks for it. Same shape
/// as the follow script: a driven run whose verdict is carried out in its `failed`.
RegionScript region_script = RegionScript::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. Under `Live` the follow path owns the x
@@ -343,6 +540,7 @@ int main() {
stream.acquire(); // once per frame, on the render thread
if (script.enabled) script.advance(host, stream, graph, axis_x);
if (region_script.enabled) region_script.advance(host, stream, graph, axis_x, region);
// 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
@@ -420,6 +618,20 @@ int main() {
// verification and did not get a passing one fails, whether a check said NO or the
// sequence never got far enough to say anything (a deadline too short to reach the
// fourth measurement -- a run that measured nothing has not passed).
if (region_script.enabled) {
if (region_script.failed) {
std::fprintf(stderr,
"plot_glfw_demo: the region checks failed (see the 'region' lines above)\n");
return 1;
}
if (!region_script.completed) {
std::fprintf(stderr,
"plot_glfw_demo: the region script did not complete (raise"
" PLOT_DEMO_EXIT_AFTER_MS)\n");
return 1;
}
}
if (script.enabled) {
if (script.failed) {
std::fprintf(stderr,
@@ -476,6 +688,21 @@ int main() {
stats.draw_calls, stats.lod_traces, stats.raw_traces);
std::printf("[glfw_demo] measurement %s dx %g s dy %g rms %g mean %g\n",
m.valid ? "valid:" : "INVALID", m.dx, m.dy, m.rms, m.mean);
std::printf("[glfw_demo] region x [%g, %g] s, snap %s, %s; checks %s\n",
p.region(region).x0, p.region(region).x1,
p.region(region).snap == plot::RegionSnap::Free ? "free" : "nearest sample",
region_script.enabled ? (region_script.hover ? "bound hovered at exit" : "pointer off the data area at exit") : "not driven",
region_script.enabled ? (region_script.failed ? "FAILED" : "passed") : "-");
if (const plot::RegionLayout* rl = region_layout(p, region); rl != nullptr) {
// The last frame's geometry, as the layout measured it: the rects the fills, bounds
// and pixel counts all have to agree with.
std::printf("[glfw_demo] region fill x %.1f y %.1f w %.1f h %.1f, bounds at %.1f and"
" %.1f, tab w %.1f, label w %.1f\n",
rl->fill.x, rl->fill.y, rl->fill.w, rl->fill.h,
static_cast<plot::f64>(plot::handle_column(rl->left_handle)),
static_cast<plot::f64>(plot::handle_column(rl->right_handle)),
rl->left_handle.w, rl->label.w);
}
std::printf("[glfw_demo] upload path %s, gl error on the last frame %d\n",
r != nullptr && r->using_persistent_upload() ? "persistent-mapped ring" : "orphan",
r != nullptr && r->last_frame_had_gl_error() ? 1 : 0);