From 5e610bc138f91a75335b1d73a22bb8a29ebc24b5 Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Mon, 28 Sep 2026 13:50:52 +0200 Subject: [PATCH] test(input): pin the region snap's once-on-release rule and the live-follow data anchoring --- tests/test_controller.cpp | 49 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/test_controller.cpp b/tests/test_controller.cpp index c71da88..7f0c89d 100644 --- a/tests/test_controller.cpp +++ b/tests/test_controller.cpp @@ -1155,3 +1155,52 @@ PLOT_TEST(region_holds_in_data_space_under_live_follow) { PLOT_CHECK_NEAR(f.graph.region(rid).x0, x0 + delta, 1e-12); PLOT_CHECK_NEAR(f.graph.region(rid).x1, x1 + delta, 1e-12); } + +/// `RegionSnap::NearestSample` is applied **once**, on release -- not on every motion. A +/// body dragged a fraction of a sample would otherwise snap both ends back onto the +/// samples they started on, and the region would never move at all while the pointer did; +/// a region that never snapped would instead end its drag between two samples, which is +/// what the snap exists to prevent. Both halves are observable, and one drag shows both. +PLOT_TEST(region_snap_is_applied_once_on_release) { + constexpr u32 kSamples = 120u; + std::vector samples(kSamples); + for (u32 i = 0; i < kSamples; ++i) samples[i] = static_cast(i); + plot::data::ArraySeries s(std::move(samples), 0.0, 1.0); // the sample grid is the integers + + Fixture f; + f.ay = f.plot.add_y_axis({"v", {0.0, 120.0}, false, plot::AxisPlace::Left, false}); + f.plot.add_trace({&s, plot::Color::hex(0x40C0FF), 1.5f, f.ax, f.ay}); + + plot::Region snapping; // the default: NearestSample + snapping.x0 = 10.4; + snapping.x1 = 20.6; + const u32 rid = f.plot.add_region(snapping); + // On the way in, both bounds land on the grid the traces define. + PLOT_CHECK_NEAR(f.plot.region(rid).x0, 10.0, 1e-9); + PLOT_CHECK_NEAR(f.plot.region(rid).x1, 21.0, 1e-9); + // The contrast: a Free region keeps what it was given. It is placed clear of the snapped + // one, because two bodies under the pointer resolve to the region on top -- the later + // one -- and this case is about the drag of *this* region. + plot::Region kept = snapping; + kept.x0 = 40.4; + kept.x1 = 50.6; + kept.snap = plot::RegionSnap::Free; + const u32 free_id = f.plot.add_region(kept); + PLOT_CHECK_NEAR(f.plot.region(free_id).x0, 40.4, 1e-12); + PLOT_CHECK_NEAR(f.plot.region(free_id).x1, 50.6, 1e-12); + + // Drag the snapped region's body by six tenths of a sample. + const f32 mid = 0.5f * (bound_x(f.plot, rid, true) + bound_x(f.plot, rid, false)); + const f32 to = column_of(f.plot, f.ax, data_at(f.plot, f.ax, mid) + 0.6); + press(f.controller, mid); + PLOT_CHECK(f.controller.drag_mode() == plot::input::DragMode::MoveRegion); + move_to(f.controller, to); + // Mid-drag the bounds are exactly where the pointer put them: no snapping yet. + PLOT_CHECK_NEAR(f.plot.region(rid).x0, 10.6, 1e-3); + PLOT_CHECK_NEAR(f.plot.region(rid).x1, 21.6, 1e-3); + release(f.controller, to); + // On release they are pulled onto the grid, once, and both ends moved. + PLOT_CHECK_NEAR(f.plot.region(rid).x0, 11.0, 1e-9); + PLOT_CHECK_NEAR(f.plot.region(rid).x1, 22.0, 1e-9); + PLOT_CHECK(f.plot.region(rid).x1 > f.plot.region(rid).x0); // the interval survived both ends moving +}