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
@@ -52,6 +52,15 @@ if(TARGET plot::imgui)
|
||||
target_link_libraries(plot_tests PRIVATE plot::imgui imgui)
|
||||
endif()
|
||||
|
||||
# The GLFW host's figure-mode routing -- which row's controller a pointer pixel reaches --
|
||||
# is this project's logic too, and it needs no window: the cases drive `controller_at()`
|
||||
# against the rects `Figure::layout()` computes. Guarded on the adapter's target for the
|
||||
# same reason as the ImGui block above.
|
||||
if(TARGET plot::glfw)
|
||||
target_sources(plot_tests PRIVATE test_glfw_host.cpp)
|
||||
target_link_libraries(plot_tests PRIVATE plot::glfw)
|
||||
endif()
|
||||
|
||||
# The Qt adapter's pointer anchor is deliberately toolkit-free (see
|
||||
# adapters/qt6/qt_pointer.hpp), so this one piece of that adapter's translation is
|
||||
# tested without linking Qt -- and without `PLOT_BUILD_QT6`, since no Qt target has
|
||||
|
||||
@@ -1388,3 +1388,124 @@ PLOT_TEST(a_middle_drag_on_a_figures_bottom_row_zooms_the_shared_x_axis) {
|
||||
PLOT_CHECK_NEAR(figure.plot(0u).x_range(0u).span(), shared_before.span(), 1e-12);
|
||||
PLOT_CHECK_NEAR(figure.x_link_range(group).span(), shared_before.span(), 1e-12);
|
||||
}
|
||||
|
||||
// The live edge a *link group* follows, seen by the input layer.
|
||||
//
|
||||
// A figure row's x axis is `XFollow::Off` by construction -- the group owns the shared
|
||||
// window and its follow state, and `link_x()` refuses a `Live` member -- so before a
|
||||
// following group published its edge, `Controller::x_zoom_anchor()` could not see that the
|
||||
// axis was following at all. A wheel over the row's axis strip then anchored on the
|
||||
// coordinate the pointer's x extrapolates to (the middle of the axes for a strip-centre
|
||||
// pointer) rather than on the live edge, and the window walked back off the newest sample
|
||||
// by a fifth of its width. These two cases pin the fix from the two ends: the gesture keeps
|
||||
// the edge, and the hint that makes it possible exists exactly while a group follows.
|
||||
PLOT_TEST(a_wheel_on_a_following_figures_axis_strip_keeps_the_live_edge) {
|
||||
constexpr u32 kFigureRows = 3u;
|
||||
constexpr f64 kWheelDx = 1.0 / 1000.0;
|
||||
plot::data::StreamSeries series(1u << 12, 0.0, kWheelDx);
|
||||
plot::f32 samples[512];
|
||||
for (u32 i = 0; i < 512u; ++i) samples[i] = static_cast<plot::f32>(i % 7u);
|
||||
series.push(samples, 512u);
|
||||
series.acquire();
|
||||
|
||||
plot::Figure figure(kFigureRows);
|
||||
figure.set_viewport({0.0f, 0.0f, 800.0f, 600.0f});
|
||||
for (u32 row = 0; row < kFigureRows; ++row) {
|
||||
plot::Plot& p = figure.plot(row);
|
||||
p.add_x_axis({"t", {0.0, 1.0}, false, plot::AxisPlace::Bottom, false});
|
||||
p.add_y_axis({"v", {-1.0, 1.0}, false, plot::AxisPlace::Left, false});
|
||||
p.add_trace({&series, plot::Color::hex(0x4FA3FF), 1.5f, 0u, 0u, "s"});
|
||||
PLOT_CHECK(p.trace_count() == 1u);
|
||||
}
|
||||
const u32 group = figure.add_x_link();
|
||||
for (u32 row = 0; row < kFigureRows; ++row) PLOT_CHECK(figure.link_x(row, group));
|
||||
PLOT_CHECK(figure.set_x_link_range(group, {0.0, 0.25}));
|
||||
PLOT_CHECK(figure.set_x_link_follow(group, plot::XFollow::Live));
|
||||
figure.layout();
|
||||
|
||||
const f64 newest = series.x_bounds().max;
|
||||
PLOT_CHECK_NEAR(figure.x_link_range(group).max, newest, 1e-12);
|
||||
for (u32 row = 0; row < kFigureRows; ++row) {
|
||||
f64 edge = 0.0;
|
||||
PLOT_CHECK(figure.plot(row).x_live_owner(0u, edge));
|
||||
PLOT_CHECK_NEAR(edge, newest, 1e-12);
|
||||
// The member's own axis is still Off: the group is what follows.
|
||||
PLOT_CHECK(figure.plot(row).x_follow(0u) == plot::XFollow::Off);
|
||||
}
|
||||
|
||||
const plot::PlotLayout& bottom = figure.plot(kFigureRows - 1u).layout();
|
||||
const f32 strip_x = bottom.axes.x + 0.5f * bottom.axes.w;
|
||||
const f32 strip_y = bottom.x_label_area.y + 0.5f * bottom.x_label_area.h;
|
||||
PLOT_CHECK(bottom.x_label_area.contains(strip_x, strip_y));
|
||||
const plot::Range before = figure.x_link_range(group);
|
||||
|
||||
plot::input::Controller c(figure.plot(kFigureRows - 1u));
|
||||
c.set_viewport(figure.viewport());
|
||||
const plot::input::WheelEvent wheel{strip_x, strip_y, 0.0f, 1.0f, 0u};
|
||||
for (u32 i = 0; i < 3u; ++i) c.on_wheel(wheel);
|
||||
figure.layout();
|
||||
|
||||
const plot::Range after = figure.x_link_range(group);
|
||||
// The zoom reached the group's one window and narrowed it...
|
||||
PLOT_CHECK(after.span() < before.span() * 0.99);
|
||||
// ... and left it ending on the live edge, which is the anchor a gesture outside `axes`
|
||||
// must use. Pre-fix this is `newest - 0.2 span` or so: the extrapolated anchor.
|
||||
PLOT_CHECK_NEAR(after.max, newest, 1e-12);
|
||||
for (u32 row = 0; row < kFigureRows; ++row) {
|
||||
PLOT_CHECK_NEAR(figure.plot(row).x_range(0u).max, after.max, 1e-12);
|
||||
PLOT_CHECK_NEAR(figure.plot(row).x_range(0u).min, after.min, 1e-12);
|
||||
}
|
||||
}
|
||||
|
||||
PLOT_TEST(a_figures_live_edge_hint_is_published_only_while_the_group_follows) {
|
||||
constexpr u32 kFigureRows = 2u;
|
||||
plot::data::StreamSeries series(1u << 10, 0.0, 1.0 / 100.0);
|
||||
plot::f32 samples[64];
|
||||
for (u32 i = 0; i < 64u; ++i) samples[i] = 1.0f;
|
||||
series.push(samples, 64u);
|
||||
series.acquire();
|
||||
|
||||
plot::Figure figure(kFigureRows);
|
||||
figure.set_viewport({0.0f, 0.0f, 400.0f, 400.0f});
|
||||
for (u32 row = 0; row < kFigureRows; ++row) {
|
||||
plot::Plot& p = figure.plot(row);
|
||||
p.add_x_axis({"t", {0.0, 1.0}, false, plot::AxisPlace::Bottom, false});
|
||||
p.add_y_axis({"v", {-1.0, 1.0}, false, plot::AxisPlace::Left, false});
|
||||
p.add_trace({&series, plot::Color::hex(0x4FA3FF), 1.5f, 0u, 0u, "s"});
|
||||
}
|
||||
const u32 group = figure.add_x_link();
|
||||
for (u32 row = 0; row < kFigureRows; ++row) PLOT_CHECK(figure.link_x(row, group));
|
||||
PLOT_CHECK(figure.set_x_link_range(group, {0.0, 0.5}));
|
||||
PLOT_CHECK(figure.set_x_link_follow(group, plot::XFollow::Live));
|
||||
figure.layout();
|
||||
|
||||
f64 edge = 0.0;
|
||||
PLOT_CHECK(figure.plot(0u).x_live_owner(0u, edge));
|
||||
PLOT_CHECK_NEAR(edge, series.x_bounds().max, 1e-12);
|
||||
|
||||
// Disarming takes the edge away at once -- nothing may anchor on an edge no group is
|
||||
// following -- and leaves the range exactly where it was.
|
||||
const plot::Range held = figure.x_link_range(group);
|
||||
PLOT_CHECK(figure.set_x_link_follow(group, plot::XFollow::Off));
|
||||
PLOT_CHECK(!figure.plot(0u).x_live_owner(0u, edge));
|
||||
PLOT_CHECK_NEAR(figure.x_link_range(group).max, held.max, 1e-12);
|
||||
|
||||
// Arming publishes it again, and a row that leaves the group loses it.
|
||||
PLOT_CHECK(figure.set_x_link_follow(group, plot::XFollow::Live));
|
||||
figure.layout();
|
||||
PLOT_CHECK(figure.plot(1u).x_live_owner(0u, edge));
|
||||
PLOT_CHECK(figure.link_x(1u, 0u)); // back to the unlinked group
|
||||
PLOT_CHECK(!figure.plot(1u).x_live_owner(0u, edge));
|
||||
PLOT_CHECK(figure.plot(0u).x_live_owner(0u, edge)); // row 0 is still in the group
|
||||
|
||||
// A plot no figure owns is never claimed, `Live` or not: the hint is an owner's, and a
|
||||
// single plot anchors on its own edge through `x_live_edge()`, which is unchanged.
|
||||
plot::Plot solo;
|
||||
solo.add_x_axis({"t", {0.0, 1.0}, false, plot::AxisPlace::Bottom, false});
|
||||
solo.add_y_axis({"v", {-1.0, 1.0}, false, plot::AxisPlace::Left, false});
|
||||
solo.add_trace({&series, plot::Color::hex(0x4FA3FF), 1.5f, 0u, 0u, "s"});
|
||||
solo.set_x_follow(0u, plot::XFollow::Live);
|
||||
solo.layout();
|
||||
PLOT_CHECK(solo.x_follow(0u) == plot::XFollow::Live);
|
||||
PLOT_CHECK(!solo.x_live_owner(0u, edge));
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// The GLFW host's event routing in figure mode: which row's controller a pointer pixel
|
||||
// reaches.
|
||||
//
|
||||
// This is the adapter's own logic and not GLFW's -- a figure is several plots stacked in
|
||||
// one framebuffer, and the host has to decide which of them a gesture was aimed at -- so it
|
||||
// is tested here rather than through a window. Nothing below calls `run()`: the case needs
|
||||
// no GL context, no display and no window, only the rects `Figure::layout()` computes and
|
||||
// the controllers the host builds from them.
|
||||
#include "harness.hpp"
|
||||
|
||||
#include <glfw/glfw_host.hpp>
|
||||
#include <plot/plot/figure.hpp>
|
||||
|
||||
namespace {
|
||||
|
||||
/// A figure of `rows` rows over a known rect, with an x and a y axis in every row (so
|
||||
/// each row lays out as a plot rather than as an empty rect), handed to a host.
|
||||
struct Routed {
|
||||
explicit Routed(plot::u32 rows, plot::f32 gap = 0.0f) : figure(rows) {
|
||||
figure.set_viewport({0.0f, 0.0f, 300.0f, 600.0f});
|
||||
figure.set_row_gap(gap);
|
||||
for (plot::u32 row = 0; row < rows; ++row) {
|
||||
plot::Plot& p = figure.plot(row);
|
||||
p.add_x_axis({"t", {0.0, 1.0}, false, plot::AxisPlace::Bottom, false});
|
||||
p.add_y_axis({"v", {-1.0, 1.0}, false, plot::AxisPlace::Left, false});
|
||||
}
|
||||
host.set_figure(&figure);
|
||||
figure.layout();
|
||||
}
|
||||
|
||||
plot::Figure figure;
|
||||
plot::glfw::Host host;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
PLOT_TEST(the_host_routes_a_pixel_to_the_row_it_is_in) {
|
||||
Routed r(3u);
|
||||
const std::vector<plot::Rect> rows = r.figure.layout().rows;
|
||||
PLOT_CHECK(rows.size() == 3u);
|
||||
for (plot::u32 row = 0; row < 3u; ++row) {
|
||||
PLOT_CHECK(r.host.row_controller(row) != nullptr);
|
||||
const plot::f32 x = rows[row].x + 0.5f * rows[row].w;
|
||||
const plot::f32 y = rows[row].y + 0.5f * rows[row].h;
|
||||
PLOT_CHECK(&r.host.controller_at(x, y) == r.host.row_controller(row));
|
||||
}
|
||||
// Above the first row and below the last one (the figure's own margins) are still the
|
||||
// nearest row rather than a dropped event: a drag that leaves the data area must end
|
||||
// where it should.
|
||||
PLOT_CHECK(&r.host.controller_at(150.0f, 0.0f) == r.host.row_controller(0u));
|
||||
PLOT_CHECK(&r.host.controller_at(150.0f, 599.0f) == r.host.row_controller(2u));
|
||||
}
|
||||
|
||||
PLOT_TEST(the_host_routes_a_pixel_in_a_row_gap_to_the_nearest_row) {
|
||||
// A gap is what `set_row_gap()` asks for. Rows tile the figure at the default of 0, so
|
||||
// this is the only way a pixel can land in none of them -- and the routing must then
|
||||
// answer with the row it is *near*, not with row 0, which is what a "first row that
|
||||
// matches, else whatever `row` started at" loop would do.
|
||||
Routed r(3u, 40.0f);
|
||||
const std::vector<plot::Rect> rows = r.figure.layout().rows;
|
||||
PLOT_CHECK(rows.size() == 3u);
|
||||
for (plot::u32 row = 0; row + 1u < 3u; ++row) {
|
||||
const plot::f32 gap = rows[row + 1u].y - rows[row].bottom();
|
||||
PLOT_CHECK(gap > 0.0f);
|
||||
const plot::f32 middle = rows[row].bottom() + 0.5f * gap;
|
||||
// Just past a row's bottom edge the gap pixel is that row's...
|
||||
PLOT_CHECK(&r.host.controller_at(150.0f, rows[row].bottom() + 1.0f) ==
|
||||
r.host.row_controller(row));
|
||||
// ... and just above the next row's top edge it is the next row's.
|
||||
PLOT_CHECK(&r.host.controller_at(150.0f, rows[row + 1u].y - 1.0f) ==
|
||||
r.host.row_controller(row + 1u));
|
||||
// The middle of the gap goes to the nearer of the two rows -- and, whatever it picks,
|
||||
// to a row of *this* figure rather than to a default: this is the assertion the
|
||||
// "first match, else whatever `row` started at" loop failed, since it answered row 0
|
||||
// for every gap pixel.
|
||||
plot::input::Controller& in_gap = r.host.controller_at(150.0f, middle);
|
||||
PLOT_CHECK(&in_gap == r.host.row_controller(row) || &in_gap == r.host.row_controller(row + 1u));
|
||||
PLOT_CHECK(&in_gap != r.host.row_controller(2u) || row + 1u == 2u);
|
||||
}
|
||||
}
|
||||
|
||||
PLOT_TEST(the_host_keeps_its_own_controller_while_no_figure_is_set) {
|
||||
plot::glfw::Host host;
|
||||
PLOT_CHECK(host.figure() == nullptr);
|
||||
PLOT_CHECK(host.row_controller(0u) == nullptr);
|
||||
// With no figure the routing is the single-plot loop's: one controller for every pixel.
|
||||
PLOT_CHECK(&host.controller_at(10.0f, 10.0f) == &host.controller());
|
||||
PLOT_CHECK(&host.controller_at(1000.0f, 1000.0f) == &host.controller());
|
||||
|
||||
plot::Figure figure(2u);
|
||||
figure.set_viewport({0.0f, 0.0f, 300.0f, 600.0f});
|
||||
host.set_figure(&figure);
|
||||
PLOT_CHECK(host.figure() == &figure);
|
||||
PLOT_CHECK(host.row_controller(0u) != nullptr);
|
||||
PLOT_CHECK(host.row_controller(2u) == nullptr); // a row that does not exist
|
||||
// And clearing it goes back to the single-plot answer.
|
||||
host.set_figure(nullptr);
|
||||
PLOT_CHECK(host.figure() == nullptr);
|
||||
PLOT_CHECK(host.row_controller(0u) == nullptr);
|
||||
PLOT_CHECK(&host.controller_at(10.0f, 10.0f) == &host.controller());
|
||||
}
|
||||
Reference in new issue
Block a user