fix(plot,glfw): layer the crosshair under the handles, sync it across a figure, and harden the lookup

This commit is contained in:
Martino Ferrari committed 2026-09-28 19:22:29 +02:00
1 parent 8e27e22ebe
commit 29824b6919
5 files changed
+183 -63

No files matched your search

+64 -44
View File
@@ -4606,18 +4606,19 @@ bool orange_pixel(const plot::u8 px[4]) {
///
/// * an **in-range** trace, whose line and marker are inside it -- the complement that says the
/// clip is the axes rect and not something smaller;
/// * a trace **below the axis**: y = -0.05 on a [0, 1] axis maps some thirteen pixels under the
/// axes' bottom edge, i.e. *into the x label strip*. Nothing in the drawing path tests a
/// sample's y against the visible range, and a crosshair marker is deliberately staged where
/// its trace is drawn rather than skipped, so only Task 1's scissor keeps it off the strip;
/// * a trace **beside the axes**: one bound to a *second* x axis whose range starts past the
/// crosshair's x, so the data x under the pointer maps to a column *left* of the axes -- in
/// the y label strip. The marker is placed through the trace's own axis pair (which is what
/// puts it there, and what the row's geometry asserts as the premise), and again only the clip
/// keeps it out.
/// * a trace **below the axis**: its constant y is five percent under the axis' minimum, so it
/// maps some thirteen pixels beneath the axes' bottom edge -- *into the x label strip*. Nothing
/// in the drawing path tests a sample's y against the visible range, and a crosshair marker is
/// deliberately staged where its trace is drawn rather than skipped, so only Task 1's scissor
/// keeps that ink off the strip;
/// * a trace **beside the axes**: one bound to a *second* x axis whose window starts past the
/// crosshair's x, so the data x under the pointer maps to a column *left* of the axes -- in the
/// y label strip. The marker is placed through the trace's own axis pair (which is what puts it
/// there, and what the row's geometry asserts as the premise), and again only the clip keeps it
/// out.
///
/// A build without the scissor puts marker ink in the x and the y label strip respectively, and
/// this case fails on both; the panel's own placement is asserted too, because a panel that
/// this case fails on both. The panel's own placement is asserted as well, because a panel that
/// overhung `axes` would be the same defect by a different route.
PLOT_TEST(crosshair_ink_stays_inside_the_axes) {
plottest::HeadlessContext ctx(kW, kH);
@@ -4632,6 +4633,45 @@ PLOT_TEST(crosshair_ink_stays_inside_the_axes) {
// in a strip or on the host's own surface, both of which are asserted empty below.
const plot::Rect vp{30.0f, 40.0f, 580.0f, 290.0f};
plot::Plot p;
p.set_viewport(vp);
const plot::u32 ax = p.add_x_axis({"t", {0.0, 639.0}, false, plot::AxisPlace::Bottom, false});
const plot::u32 ay = p.add_y_axis({"v", {0.0, 1.0}, false, plot::AxisPlace::Left, false});
plot::gl::glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
plot::gl::glClear(plot::gl::GL_COLOR_BUFFER_BIT);
// A warm-up frame first, with nothing in the plot: the renderer installs its text backend on the
// first `draw()`, and the strips and the readout panel are measured with it -- so the geometry
// asserted below is a *drawn* frame's, not an empty plot's defaults.
r.begin_frame(kW, kH, vp);
r.draw(p);
r.end_frame();
PLOT_CHECK(!r.last_frame_had_gl_error());
// Nothing added from here on can move the strips: an *untitled* second x axis draws its labels
// on the same strip's single line, and a trace reserves nothing at all. (A titled one would
// reserve a title row and move `axes` -- which is why this axis has no label.)
const plot::Rect axes = p.layout().axes;
const plot::Rect y_strip = p.layout().y_label_area;
PLOT_CHECK(y_strip.w > 4.0f);
const plot::Transform tx = p.x_transform(ax);
const plot::Transform ty = p.y_transform(ay);
const f64 hover_x = tx.to_data_x(axes.right() - 3.0f);
// The second x axis' window is **derived from that geometry rather than guessed**, so this case
// cannot silently stop testing anything when a font or a padding moves the strip: with a span
// `S` starting at `min`, a data x maps to `axes.x + (x - min) / S * axes.w`, so starting the
// window half a strip's width above the hovered x puts that trace's marker halfway into the y
// label strip.
const f64 span2 = 700.0;
const f64 min2 =
hover_x + (0.5 * static_cast<f64>(y_strip.w) / static_cast<f64>(axes.w)) * span2;
const plot::u32 ax2 =
p.add_x_axis({"", {min2, min2 + span2}, false, plot::AxisPlace::Bottom, false});
// The geometry really is the one asserted below, and the second axis really did not move it.
PLOT_CHECK_NEAR(p.layout().axes.h, axes.h, 1e-4);
// The in-range signal: a wave inside the [0, 1] axis, so its trace and its marker are both
// inside the rect.
std::vector<plot::f32> in_range(640u);
@@ -4641,37 +4681,16 @@ PLOT_TEST(crosshair_ink_stays_inside_the_axes) {
plot::data::ArraySeries good(in_range, 0.0, 1.0);
// Five percent below the bottom of the axis: some thirteen pixels, which is the x label strip.
plot::data::ArraySeries below(std::vector<plot::f32>(640u, -0.05f), 0.0, 1.0);
// In range on y, but drawn through an x axis the crosshair's x is outside of: its marker lands
// in the y label strip, beside the axes.
// In range on y, but read through an x axis the hovered x is outside of: its marker lands in the
// y label strip, beside the axes.
plot::data::ArraySeries aside(std::vector<plot::f32>(2000u, 0.25f), 0.0, 1.0);
plot::Plot p;
p.set_viewport(vp);
const plot::u32 ax = p.add_x_axis({"t", {0.0, 639.0}, false, plot::AxisPlace::Bottom, false});
const plot::u32 ay = p.add_y_axis({"v", {0.0, 1.0}, false, plot::AxisPlace::Left, false});
// The second x axis: a window that starts *above* the crosshair's x, so the same data x is a
// column to the left of the axes when the trace reads it through this one.
const plot::u32 ax2 = p.add_x_axis({"t2", {700.0, 1400.0}, false, plot::AxisPlace::Bottom, false});
p.add_trace({&good, kRowMagenta, 1.5f, ax, ay, "good"});
p.add_trace({&below, plot::Color::hex(0x00E070), 1.5f, ax, ay, "below"});
p.add_trace({&aside, plot::Color::hex(0xFFB020), 1.5f, ax2, ay, "aside"});
plot::gl::glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
plot::gl::glClear(plot::gl::GL_COLOR_BUFFER_BIT);
// A warm-up frame first: the renderer installs its text backend on the first `draw()`, and the
// strips and the panel are measured with it -- so the frame under test is placed and asserted
// against the rects a *drawn* frame has, not an empty plot's defaults.
r.begin_frame(kW, kH, vp);
r.draw(p);
r.end_frame();
PLOT_CHECK(!r.last_frame_had_gl_error());
// The crosshair three pixels inside the bottom-right corner of the data area, so its readout
// panel would leave the rect on two sides if it were not flipped and clamped.
const plot::Rect axes = p.layout().axes;
const plot::Transform tx = p.x_transform(ax);
const plot::Transform ty = p.y_transform(ay);
p.set_crosshair(tx.to_data_x(axes.right() - 3.0f), ty.to_data_y(axes.bottom() - 3.0f), true);
p.set_crosshair(hover_x, ty.to_data_y(axes.bottom() - 3.0f), true);
r.begin_frame(kW, kH, vp);
r.draw(p);
@@ -4694,9 +4713,10 @@ PLOT_TEST(crosshair_ink_stays_inside_the_axes) {
PLOT_CHECK(crosshair.panel.right() <= axes.right());
PLOT_CHECK(crosshair.panel.bottom() <= axes.bottom());
// The premises: the two off-scale markers are staged where nothing but the clip is between them
// and a strip. Row 1's is below the axes' bottom edge, within the x label strip; row 2's is left
// of the axes' left edge, within the y label strip.
// The premises, which is what makes every pixel assertion below a statement about the defect and
// not about a marker that never went anywhere: the two off-scale markers are staged where only
// the clip stands between them and a strip -- row 1's below the axes' bottom edge, inside the x
// label strip, row 2's left of the axes' left edge, inside the y label strip.
PLOT_CHECK(crosshair.rows[0].valid);
PLOT_CHECK(crosshair.rows[1].valid);
PLOT_CHECK(crosshair.rows[2].valid);
@@ -4705,8 +4725,8 @@ PLOT_TEST(crosshair_ink_stays_inside_the_axes) {
PLOT_CHECK(crosshair.rows[2].marker.x < axes.x);
PLOT_CHECK(crosshair.rows[2].marker.x >= layout.y_label_area.x);
// Every crosshair-coloured pixel of the frame is inside `axes`, and there are some: not one in
// a label or title strip, not one over the host's surface outside the viewport.
// Every crosshair-coloured pixel of the frame is inside `axes`, and there are some: not one in a
// label or title strip, not one over the host's surface outside the viewport.
const plot::u32 inside = predicate_pixels(frame, kW, axes, crosshair_pixel);
PLOT_CHECK(inside > 0u);
PLOT_CHECK(predicate_pixels(frame, kW, kFrameRect, crosshair_pixel) == inside);
@@ -4723,21 +4743,21 @@ PLOT_TEST(crosshair_ink_stays_inside_the_axes) {
// The trace below the axis: none of its colour anywhere outside `axes` -- the x label strip and
// the host's surface included -- while its own swatch in the readout proves the colour is one
// this frame really draws (so the absence is the clip's doing and not a colour nothing draws).
// this frame really draws, so the absence is the clip's doing and not a colour nothing paints.
PLOT_CHECK(predicate_pixels(frame, kW, layout.x_label_area, green_pixel) == 0u);
PLOT_CHECK(predicate_pixels(frame, kW, kFrameRect, green_pixel) ==
predicate_pixels(frame, kW, axes, green_pixel));
PLOT_CHECK(predicate_pixels(frame, kW, crosshair.rows[1].swatch, green_pixel) > 0u);
// The trace beside the axes: the same statement for the y label strip, where its marker would
// have been drawn.
// The trace beside the axes: the same statement for the y label strip and the title strip beside
// it, where its marker would have been drawn.
PLOT_CHECK(predicate_pixels(frame, kW, layout.y_label_area, orange_pixel) == 0u);
PLOT_CHECK(predicate_pixels(frame, kW, layout.y_title_area, orange_pixel) == 0u);
PLOT_CHECK(predicate_pixels(frame, kW, kFrameRect, orange_pixel) ==
predicate_pixels(frame, kW, axes, orange_pixel));
// The complement: the in-range trace's ink -- its polyline and the marker the crosshair put on
// it -- is inside `axes`, and all of it is, which a clip smaller than the rect could not do.
// The complement: the in-range trace's ink -- its polyline and the marker the crosshair put on it
// -- is inside `axes`, and all of it is, which a clip smaller than the rect could not do.
PLOT_CHECK(predicate_pixels(frame, kW, axes, magenta_pixel) > 0u);
PLOT_CHECK(predicate_pixels(frame, kW, kFrameRect, magenta_pixel) ==
predicate_pixels(frame, kW, axes, magenta_pixel));