feat(examples): a three-row synced Figure demo, on a GLFW host that draws one
The multi-plot example the phase exists for: three plots stacked in one Figure, sharing one x link with following armed, each row carrying its own named signals and its own legend, driven by six StreamSeries at 500 kS/s. Two cursors and a region with an exact statistics readout reach the direct manipulation of spec §5 in the same frame, and PLOT_DEMO_SYNC=1 is a self-checking mode that exits non-zero unless all three rows hold one range and the shared right edge keeps tracking the newest sample through a zoom, a pan on another row and more arriving data. The GLFW host gains the figure mode the demo needs: set_figure() makes the run loop draw the Figure instead of its own plot and route each pointer, wheel and key event to the row it lands in, through one Controller per row (row_controller()). All of it is opt-in; the single-plot path is untouched.
This commit is contained in:
1 parent
770da4c2b2
commit
942f88c568
9 files changed
+917
-39
No files matched your search
+52
-26
@@ -366,6 +366,10 @@ struct RegionScript {
|
||||
/// 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;
|
||||
/// Set in the first step, where the script places the region's bounds itself: from then
|
||||
/// on the frame callback must leave the region alone, or the per-frame placement of the
|
||||
/// demo's overlay would undo the very gesture this script measures.
|
||||
bool owns_geometry = false;
|
||||
plot::u32 step = 0;
|
||||
plot::Rect before{};
|
||||
plot::Rect after{};
|
||||
@@ -393,6 +397,7 @@ struct RegionScript {
|
||||
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());
|
||||
owns_geometry = true;
|
||||
p.layout();
|
||||
const plot::RegionLayout* rl = region_layout(p, region);
|
||||
if (rl == nullptr) {
|
||||
@@ -502,7 +507,10 @@ int main() {
|
||||
{"time", {-0.5, 0.5}, true, plot::AxisPlace::Bottom, false, plot::TickFormat::Time});
|
||||
const u32 axis_y = p.add_y_axis(
|
||||
{"signal", {-4.0, 4.0}, true, plot::AxisPlace::Left, false, plot::TickFormat::Si});
|
||||
const u32 trace = p.add_trace({&stream, kTraceColor, 1.5f, axis_x, axis_y});
|
||||
// The trace is named, so the plot's legend -- on by default, per `Legend` -- lists it.
|
||||
// An unnamed trace contributes no entry and an all-unnamed plot draws no panel, which is
|
||||
// what keeps the legend free for a caller who never names one.
|
||||
const u32 trace = p.add_trace({&stream, kTraceColor, 1.5f, axis_x, axis_y, "signal"});
|
||||
|
||||
// Snapped cursors sit exactly on a sample, so the readout measures samples
|
||||
// rather than a rounded position between two of them.
|
||||
@@ -521,22 +529,25 @@ int main() {
|
||||
/// as the follow script: a driven run whose verdict is carried out in its `failed`.
|
||||
RegionScript region_script = RegionScript::from_env();
|
||||
|
||||
// The region that script drags, and the element the pixel counts are of. It exists only
|
||||
// while the script is armed: the default run is the frame the four adapters copy, and an
|
||||
// element nothing drives would be a change to it rather than a demonstration. The script
|
||||
// places its bounds once the view it is measured against has been frozen.
|
||||
// The region the frame carries: a direct-manipulation primitive with the statistics
|
||||
// readout that is the point of one, so the demo reaches it without a script as well as
|
||||
// through one. Its bounds ride the live window while the user has not taken the overlay
|
||||
// over (see `overlay_follows`), and `PLOT_DEMO_REGIONS` freezes the view and drives it
|
||||
// instead.
|
||||
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 = region_script.enabled ? p.add_region(region_desc) : 0u;
|
||||
// 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
|
||||
// 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 u32 region = p.add_region(region_desc);
|
||||
// The cursors and the region ride the live window until the user takes the overlay over:
|
||||
// the first drag on either, or a pan or zoom that clears the x axis' auto scale, 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 handle
|
||||
// drag, or following being switched off, is. While `PLOT_DEMO_REGIONS` is running its
|
||||
// script owns the region's geometry instead (it measures one gesture against the next
|
||||
// frame's), so the overlay is left alone for the length of that run.
|
||||
bool overlay_follows = true;
|
||||
|
||||
const plot::glfw::FrameFn frame = [&](plot::Plot& graph, plot::f64 dt) {
|
||||
stream.acquire(); // once per frame, on the render thread
|
||||
@@ -554,16 +565,22 @@ int main() {
|
||||
}
|
||||
if (graph.y_axis(axis_y).auto_scale) graph.auto_fit_y(axis_y);
|
||||
|
||||
if (cursors_follow) {
|
||||
if (overlay_follows && !(region_script.enabled && region_script.owns_geometry)) {
|
||||
const plot::Range xr = graph.x_range(axis_x);
|
||||
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;
|
||||
const plot::input::DragMode mode = host.controller().drag_mode();
|
||||
const bool dragging = mode == plot::input::DragMode::MoveCursor ||
|
||||
mode == plot::input::DragMode::MoveRegion ||
|
||||
mode == plot::input::DragMode::ResizeRegionLeft ||
|
||||
mode == plot::input::DragMode::ResizeRegionRight;
|
||||
if (dragging || x_taken_over) {
|
||||
overlay_follows = false;
|
||||
} else {
|
||||
const plot::Range xr = graph.x_range(axis_x);
|
||||
graph.set_cursor_x(cursor_a, xr.min + 0.35 * xr.span());
|
||||
graph.set_cursor_x(cursor_b, xr.min + 0.65 * xr.span());
|
||||
graph.set_region(region, xr.min + 0.40 * xr.span(), xr.min + 0.60 * xr.span());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -690,22 +707,31 @@ 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);
|
||||
// The region's statistics, in every run: this is the direct-manipulation primitive's
|
||||
// readout, and the numbers the pixel evidence is read against. Exact -- the interval
|
||||
// queries go through `Series::reduce/sum/sumsq`, never the pyramid's bucket forms.
|
||||
if (const plot::RegionStats& rs = p.region_stats(region, trace); rs.valid) {
|
||||
std::printf("[glfw_demo] region %u: x [%g, %g] s, n %llu, mean %g, rms %g, min %g, max"
|
||||
" %g, integral %g\n",
|
||||
region, rs.x0, rs.x1, static_cast<unsigned long long>(rs.count), rs.mean, rs.rms,
|
||||
rs.min, rs.max, rs.integral);
|
||||
}
|
||||
if (const plot::RegionLayout* rl = region_layout(p, region); rl != nullptr) {
|
||||
// The last frame's geometry, as the layout measured it: the rect the fill, the bound
|
||||
// strokes and the 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);
|
||||
}
|
||||
if (region_script.enabled) {
|
||||
std::printf("[glfw_demo] region %u: x [%g, %g] s, snap %s, %s; checks %s\n", region,
|
||||
p.region(region).x0, p.region(region).x1,
|
||||
p.region(region).snap == plot::RegionSnap::Free ? "free" : "nearest sample",
|
||||
region_script.hover ? "bound hovered at exit" : "pointer off the data area at exit",
|
||||
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 rect the fill, the bound
|
||||
// strokes and the 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",
|
||||
|
||||
Reference in new issue
Block a user