feat(plot): the figure crosshair marks the hovered row, and the stroke is thin, dashed and configurable
The figure sync learns which row the pointer is in and that row's local y, so the shared x goes to every row and the horizontal line only to the hovered one -- a crosshair that finally marks a row where its y means something. The stroke gains Style::crosshair_width/_dash_on/_dash_off (logical; 0.75 / 4 / 4), a per-Crosshair override, and a fragment-shader dash on v_along (off == 0 is solid), anchored to the axes edge each line is staged from so the phase does not crawl.
This commit is contained in:
1 parent
eee7c34dd5
commit
f8a364e963
3 files changed
+506
No files matched your search
@@ -860,3 +860,103 @@ PLOT_TEST(figure_crosshair_applies_one_x_to_every_row) {
|
||||
PLOT_CHECK_NEAR(figure.plot(row).crosshair().x, 2.0, 1e-12);
|
||||
}
|
||||
}
|
||||
|
||||
/// Spec §17, the reported defect: a figure's crosshair showed **no horizontal line** because its
|
||||
/// sync was vertical-only. The fix is not a figure-wide y -- which would mean a different value in
|
||||
/// every row -- but the *pointer row's own* y: the shared x goes to every row, and the horizontal
|
||||
/// line to the hovered row alone, where a y is a value of that row's own scale.
|
||||
///
|
||||
/// The rows here carry **different** y ranges on purpose, so "the hovered row's y" and "a sibling's
|
||||
/// y" are genuinely different pixel rows; the exclusivity asserted below is therefore about the
|
||||
/// fix and not a coincidence of equal axes.
|
||||
PLOT_TEST(figure_crosshair_draws_a_horizontal_line_only_in_the_hovered_row) {
|
||||
plot::Figure figure(3u);
|
||||
figure.set_viewport(plot::Rect{0.0f, 0.0f, 640.0f, 480.0f});
|
||||
static plot::data::ArraySeries ramp(std::vector<plot::f32>{0.0f, 10.0f, 20.0f, 30.0f, 40.0f,
|
||||
50.0f, 60.0f, 70.0f, 80.0f, 90.0f},
|
||||
0.0, 1.0);
|
||||
static plot::render::EmbeddedMonoFont font;
|
||||
const plot::Range y_range[3] = {{0.0, 100.0}, {0.0, 1000.0}, {-50.0, 50.0}};
|
||||
for (plot::u32 row = 0; row < 3u; ++row) {
|
||||
plot::Plot& p = figure.plot(row);
|
||||
p.set_text_backend(&font);
|
||||
p.add_x_axis({"t", {0.0, 99.0}, false, plot::AxisPlace::Bottom, false});
|
||||
p.add_y_axis({"v", y_range[row], false, plot::AxisPlace::Left, false});
|
||||
p.add_trace({&ramp, plot::Color::hex(0x40C0FF), 1.5f, 0u, 0u, "ramp"});
|
||||
}
|
||||
figure.layout();
|
||||
|
||||
// The pointer in row 1 at y = 150: inside row 1's [0, 1000] scale, and *outside* rows 0 and 2.
|
||||
figure.set_crosshair(3.0, 1u, 150.0, true);
|
||||
figure.layout();
|
||||
|
||||
// Every row carries the shared x's vertical line, all at one pixel column; no row but the
|
||||
// hovered one draws a horizontal line at all (a zero-sized box, not a line at y = 0).
|
||||
plot::f32 column = 0.0f;
|
||||
for (plot::u32 row = 0; row < 3u; ++row) {
|
||||
const plot::CrosshairLayout& c = figure.plot(row).layout().crosshair;
|
||||
PLOT_CHECK(c.active);
|
||||
PLOT_CHECK(c.vertical.w > 0.0f);
|
||||
PLOT_CHECK(c.vertical.h > 0.0f);
|
||||
if (row == 0u) {
|
||||
column = c.vertical.x;
|
||||
} else {
|
||||
PLOT_CHECK_NEAR(c.vertical.x, column, 1e-6);
|
||||
}
|
||||
if (row != 1u) {
|
||||
PLOT_CHECK(c.horizontal.w == 0.0f);
|
||||
PLOT_CHECK(c.horizontal.h == 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
// Row 1 draws it, at *its own* y: the stroke box is centred where 150 maps on row 1's y axis.
|
||||
const plot::CrosshairLayout& hovered = figure.plot(1u).layout().crosshair;
|
||||
PLOT_CHECK(hovered.horizontal.w > 0.0f);
|
||||
PLOT_CHECK(hovered.horizontal.h > 0.0f);
|
||||
const plot::Transform ty1 = figure.plot(1u).y_transform(0u);
|
||||
const plot::f32 row1_at = hovered.horizontal.y + 0.5f * hovered.horizontal.h;
|
||||
PLOT_CHECK_NEAR(row1_at, static_cast<plot::f32>(ty1.to_screen_y(150.0)), 0.75);
|
||||
|
||||
// The pointer moves to row 2: the horizontal line moves *there*, to row 2's own y, and the
|
||||
// vertical lines are left exactly where they were -- one hover, one row's y, N rows' x.
|
||||
figure.set_crosshair(3.0, 2u, 0.0, true);
|
||||
figure.layout();
|
||||
PLOT_CHECK(figure.plot(1u).layout().crosshair.horizontal.h == 0.0f);
|
||||
PLOT_CHECK(figure.plot(1u).layout().crosshair.horizontal.w == 0.0f);
|
||||
const plot::CrosshairLayout& second = figure.plot(2u).layout().crosshair;
|
||||
PLOT_CHECK(second.horizontal.h > 0.0f);
|
||||
const plot::Transform ty2 = figure.plot(2u).y_transform(0u);
|
||||
const plot::f32 row2_at = second.horizontal.y + 0.5f * second.horizontal.h;
|
||||
PLOT_CHECK_NEAR(row2_at, static_cast<plot::f32>(ty2.to_screen_y(0.0)), 0.75);
|
||||
PLOT_CHECK(row2_at != row1_at); // a different row of the figure, not the same line re-placed
|
||||
for (plot::u32 row = 0; row < 3u; ++row) {
|
||||
PLOT_CHECK_NEAR(figure.plot(row).layout().crosshair.vertical.x, column, 1e-6);
|
||||
}
|
||||
|
||||
// A row that does not exist -- the pointer in the gap between rows -- is "no row under the
|
||||
// pointer": every row keeps the vertical line and none gets a horizontal one.
|
||||
figure.set_crosshair(3.0, 9u, 10.0, true);
|
||||
figure.layout();
|
||||
for (plot::u32 row = 0; row < 3u; ++row) {
|
||||
const plot::CrosshairLayout& c = figure.plot(row).layout().crosshair;
|
||||
PLOT_CHECK(c.vertical.w > 0.0f);
|
||||
PLOT_CHECK(c.horizontal.h == 0.0f);
|
||||
}
|
||||
|
||||
// The two-argument overload stays vertical-only on every row -- a caller with only a coordinate
|
||||
// gets no horizontal line, because there is a y it never gave.
|
||||
figure.set_crosshair(3.0, true);
|
||||
figure.layout();
|
||||
for (plot::u32 row = 0; row < 3u; ++row) {
|
||||
const plot::CrosshairLayout& c = figure.plot(row).layout().crosshair;
|
||||
PLOT_CHECK(c.vertical.w > 0.0f);
|
||||
PLOT_CHECK(c.horizontal.h == 0.0f);
|
||||
}
|
||||
|
||||
// A y that is not a number leaves the hovered row vertical-only rather than clearing the whole
|
||||
// hover: the pointer is still in that row, it just has no y to mark.
|
||||
figure.set_crosshair(3.0, 1u, std::nan(""), true);
|
||||
figure.layout();
|
||||
PLOT_CHECK(figure.plot(1u).layout().crosshair.vertical.w > 0.0f);
|
||||
PLOT_CHECK(figure.plot(1u).layout().crosshair.horizontal.h == 0.0f);
|
||||
}
|
||||
Reference in new issue
Block a user