fix(render): keep a cursor parked on an axis bound inside and visible
This commit is contained in:
1 parent
eb7371d7a5
commit
93ad774281
3 files changed
+84
-18
No files matched your search
@@ -455,8 +455,13 @@ class Renderer {
|
|||||||
/// The cursors, both orientations: one stroke per cursor, placed at the coordinate its own
|
/// The cursors, both orientations: one stroke per cursor, placed at the coordinate its own
|
||||||
/// orientation owns (`Plot::cursor_screen_coordinate()`) and spanning the other axis' whole
|
/// orientation owns (`Plot::cursor_screen_coordinate()`) and spanning the other axis' whole
|
||||||
/// range, in its own colour -- or in the accent at handle width while it is hovered or
|
/// range, in its own colour -- or in the accent at handle width while it is hovered or
|
||||||
/// grabbed, which is the same cue a region's hot bound gets. A cursor at the far edge of
|
/// grabbed, which is the same cue a region's hot bound gets. A cursor more than half a
|
||||||
/// its range is skipped rather than drawn as half a stroke over the strip beside it.
|
/// stroke outside its range's mapped span (a marker scrolled past the right edge, a ruler
|
||||||
|
/// past the bottom) is skipped; one on or within half a stroke of a bound is drawn, with
|
||||||
|
/// its stroke centre held half a stroke inside the rect -- so a marker parked on an edge
|
||||||
|
/// stays visible there instead of losing its outside half, and the inside sliver of it, to
|
||||||
|
/// the clip, and a range maximum that undershoots the edge by a ulp is not mistaken for
|
||||||
|
/// off the plot.
|
||||||
void draw_cursors(const Plot& plot, const PlotLayout& layout);
|
void draw_cursors(const Plot& plot, const PlotLayout& layout);
|
||||||
/// One cursor's label: the caller's caption for either orientation, and for a horizontal
|
/// One cursor's label: the caller's caption for either orientation, and for a horizontal
|
||||||
/// ruler the value itself, written in its y axis' own tick format (`Axis::format_value()`)
|
/// ruler the value itself, written in its y axis' own tick format (`Axis::format_value()`)
|
||||||
|
|||||||
+27
-11
@@ -941,28 +941,44 @@ void Renderer::draw_cursors(const Plot& plot, const PlotLayout& layout) {
|
|||||||
// text path -- is this one loop's, which is the point of one type rather than two.
|
// text path -- is this one loop's, which is the point of one type rather than two.
|
||||||
const bool horizontal = cursor.orientation == CursorOrientation::Horizontal;
|
const bool horizontal = cursor.orientation == CursorOrientation::Horizontal;
|
||||||
const f32 at = plot.cursor_screen_coordinate(i);
|
const f32 at = plot.cursor_screen_coordinate(i);
|
||||||
// A cursor at the *far* edge of its own range is off the plot rather than on it: its
|
// A cursor has no position on the plot when it is strictly outside its range's mapped
|
||||||
// stroke straddles that edge by construction, so drawing it would put half a marker
|
// span -- a marker scrolled past the right edge, a ruler past the bottom -- and the
|
||||||
// over the label strip beside it. The rule reads the same on both axes -- a marker
|
// tolerance is the stroke's own half-width: a coordinate *at* a bound is on the edge and
|
||||||
// scrolled past the right of its x range, a ruler dragged to the bottom of its y range
|
// belongs to the plot, and `to_screen_y()` of a range maximum can undershoot `axes.y` by
|
||||||
// -- and the axes clip would hold the ink in anyway, so this is about not drawing
|
// a floating-point ulp, which a bare `<` would mistake for "outside". Only the ones
|
||||||
// something that is a half-stroke and a half-nothing.
|
// beyond that margin are skipped.
|
||||||
if (horizontal ? (at < layout.axes.y || at >= layout.axes.bottom())
|
const f32 half = 0.5f * style_.device(on ? style_.handle_width_px : style_.line_width_px);
|
||||||
: (at < layout.axes.x || at >= layout.axes.right())) {
|
if (horizontal ? (at < layout.axes.y - half || at > layout.axes.bottom() + half)
|
||||||
|
: (at < layout.axes.x - half || at > layout.axes.right() + half)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// The stroke is centred on the coordinate, so a cursor on a bound would paint only the
|
||||||
|
// half of itself inside `axes` -- and where the edge is fractional the clip's inward
|
||||||
|
// rounding can leave that half a sub-pixel sliver or nothing at all, because the
|
||||||
|
// rasteriser keeps a fragment only when its centre lies within the stroke. Clamp the
|
||||||
|
// *drawn* centre by half the stroke so a marker parked on an edge stays visible at it;
|
||||||
|
// the cursor's coordinate, the hit test and its label's value are untouched.
|
||||||
|
const f32 lo_v = layout.axes.x + half;
|
||||||
|
const f32 hi_v = layout.axes.right() - half;
|
||||||
|
const f32 lo_h = layout.axes.y + half;
|
||||||
|
const f32 hi_h = layout.axes.bottom() - half;
|
||||||
|
const f32 drawn =
|
||||||
|
horizontal ? std::clamp(at, std::min(lo_h, hi_h), std::max(lo_h, hi_h))
|
||||||
|
: std::clamp(at, std::min(lo_v, hi_v), std::max(lo_v, hi_v));
|
||||||
|
|
||||||
if (horizontal) {
|
if (horizontal) {
|
||||||
stage_segment(x_range.min, cursor.y, x_range.max, cursor.y, log_x,
|
const f64 y_drawn = plot.y_transform(cursor.axis_y).to_data_y(static_cast<f64>(drawn));
|
||||||
|
stage_segment(x_range.min, y_drawn, x_range.max, y_drawn, log_x,
|
||||||
plot.y_axis(cursor.axis_y).log);
|
plot.y_axis(cursor.axis_y).log);
|
||||||
} else {
|
} else {
|
||||||
stage_segment(cursor.x, y_range.min, cursor.x, y_range.max, plot.x_axis(cursor.axis_x).log,
|
const f64 x_drawn = plot.x_transform(cursor.axis_x).to_data_x(static_cast<f64>(drawn));
|
||||||
|
stage_segment(x_drawn, y_range.min, x_drawn, y_range.max, plot.x_axis(cursor.axis_x).log,
|
||||||
log_y);
|
log_y);
|
||||||
}
|
}
|
||||||
flush_lines(horizontal ? mapping(plot, 0, cursor.axis_y) : mapping(plot, cursor.axis_x, 0),
|
flush_lines(horizontal ? mapping(plot, 0, cursor.axis_y) : mapping(plot, cursor.axis_x, 0),
|
||||||
on ? style_.cursor : cursor.color,
|
on ? style_.cursor : cursor.color,
|
||||||
style_.device(on ? style_.handle_width_px : style_.line_width_px));
|
style_.device(on ? style_.handle_width_px : style_.line_width_px));
|
||||||
draw_cursor_label(plot, layout, cursor, at);
|
draw_cursor_label(plot, layout, cursor, drawn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4332,6 +4332,15 @@ bool ruler_pixel(const plot::u8 px[4]) {
|
|||||||
return px[1] > px[0] + 50 && px[1] > px[2] + 20 && px[1] > 90;
|
return px[1] > px[0] + 50 && px[1] > px[2] + 20 && px[1] > 90;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The same hue at *any* coverage. A stroke centred exactly on `axes.bottom()` -- where the
|
||||||
|
/// clip's inward rounding leaves only the inside half -- can survive as a faint sub-pixel
|
||||||
|
/// sliver when that edge is fractional, which `ruler_pixel`'s brightness floor does not see.
|
||||||
|
/// The hue gate still excludes the grey chrome, the pale text and the (absent here) blue
|
||||||
|
/// trace, so a count of it is still a count of the ruler.
|
||||||
|
bool ruler_ink_any(const plot::u8 px[4]) {
|
||||||
|
return px[1] > px[0] + 15 && px[2] > px[0] + 10 && px[1] > 35 && px[1] >= px[2] - 8;
|
||||||
|
}
|
||||||
|
|
||||||
/// The extent of the ink a predicate accepts anywhere in a read-back frame: its bounding
|
/// The extent of the ink a predicate accepts anywhere in a read-back frame: its bounding
|
||||||
/// box in top-down framebuffer rows/columns (like every `Rect` here), and its pixel count.
|
/// box in top-down framebuffer rows/columns (like every `Rect` here), and its pixel count.
|
||||||
struct InkExtent {
|
struct InkExtent {
|
||||||
@@ -4484,16 +4493,24 @@ PLOT_TEST(horizontal_ruler_stays_inside_the_axes_at_the_edges) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Dragged past the *bottom*: the pointer extrapolates below the axis and the drag clamps
|
// Dragged past the *bottom*: the pointer extrapolates below the axis and the drag clamps
|
||||||
// to the range's minimum rather than storing a value no tick could reach.
|
// to the range's minimum rather than storing a value no tick could reach. That value maps
|
||||||
|
// to the axes' *bottom* row, and the ruler is pinned visible there -- the same outcome as
|
||||||
|
// a drag past the top, which is the point of the far-edge rule being strict: an at-bound
|
||||||
|
// coordinate is inside the rect. (The clip's inward rounding keeps the ink on the last row
|
||||||
|
// inside the edge, so "at the boundary" reads as within a stroke width of it.)
|
||||||
grab();
|
grab();
|
||||||
drag_to(axes.bottom() + 400.0f);
|
drag_to(axes.bottom() + 400.0f);
|
||||||
PLOT_CHECK_NEAR(p.cursor(ruler).y, 0.0, 1e-9);
|
PLOT_CHECK_NEAR(p.cursor(ruler).y, 0.0, 1e-9);
|
||||||
{
|
{
|
||||||
const std::vector<plot::u8> frame = frame_now();
|
const std::vector<plot::u8> frame = frame_now();
|
||||||
PLOT_CHECK(predicate_pixels(frame, kW, kFrameRect, ruler_pixel) ==
|
const InkExtent ink = ink_extent(frame, kW, kH, ruler_ink_any);
|
||||||
predicate_pixels(frame, kW, axes, ruler_pixel));
|
PLOT_CHECK(ink.pixels > 0u); // drawn, not hidden
|
||||||
PLOT_CHECK(predicate_pixels(frame, kW, p.layout().x_label_area, ruler_pixel) == 0u);
|
PLOT_CHECK(static_cast<plot::f32>(ink.bottom) >= axes.bottom() - 2.0f);
|
||||||
PLOT_CHECK(predicate_pixels(frame, kW, p.layout().y_label_area, ruler_pixel) == 0u);
|
PLOT_CHECK(static_cast<plot::f32>(ink.top) <= axes.bottom());
|
||||||
|
PLOT_CHECK(predicate_pixels(frame, kW, kFrameRect, ruler_ink_any) ==
|
||||||
|
predicate_pixels(frame, kW, axes, ruler_ink_any));
|
||||||
|
PLOT_CHECK(predicate_pixels(frame, kW, p.layout().x_label_area, ruler_ink_any) == 0u);
|
||||||
|
PLOT_CHECK(predicate_pixels(frame, kW, p.layout().y_label_area, ruler_ink_any) == 0u);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dragged past the *top*: clamped to the range's maximum, and the clamped ruler is still
|
// Dragged past the *top*: clamped to the range's maximum, and the clamped ruler is still
|
||||||
@@ -4512,6 +4529,34 @@ PLOT_TEST(horizontal_ruler_stays_inside_the_axes_at_the_edges) {
|
|||||||
PLOT_CHECK(predicate_pixels(frame, kW, p.layout().y_label_area, ruler_pixel) == 0u);
|
PLOT_CHECK(predicate_pixels(frame, kW, p.layout().y_label_area, ruler_pixel) == 0u);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A degenerate y range (min == max) is widened by +/-0.5 in the transform, so the one
|
||||||
|
// value it holds maps to the middle of the axes: a ruler there is genuinely in range and
|
||||||
|
// must be drawn -- the strict far-edge rule cannot hide it. An inverted range is widened
|
||||||
|
// the same way, so a value strictly between its bounds also maps inside and draws; only
|
||||||
|
// the two bounds land ~0.5 data units outside the widened span, which is a row outside the
|
||||||
|
// rect and correctly skipped (the old rule skipped them too, so this is not a regression).
|
||||||
|
p.set_y_range(ay, {50.0, 50.0});
|
||||||
|
p.set_cursor_y(ruler, 50.0);
|
||||||
|
PLOT_CHECK_NEAR(p.cursor(ruler).y, 50.0, 1e-9);
|
||||||
|
{
|
||||||
|
const std::vector<plot::u8> frame = frame_now();
|
||||||
|
const InkExtent ink = ink_extent(frame, kW, kH, ruler_pixel);
|
||||||
|
PLOT_CHECK(ink.pixels > 0u);
|
||||||
|
PLOT_CHECK(predicate_pixels(frame, kW, kFrameRect, ruler_pixel) ==
|
||||||
|
predicate_pixels(frame, kW, axes, ruler_pixel));
|
||||||
|
}
|
||||||
|
p.set_y_range(ay, {100.0, 0.0});
|
||||||
|
p.set_cursor_y(ruler, 50.0);
|
||||||
|
PLOT_CHECK_NEAR(p.cursor(ruler).y, 50.0, 1e-9);
|
||||||
|
{
|
||||||
|
const std::vector<plot::u8> frame = frame_now();
|
||||||
|
const InkExtent ink = ink_extent(frame, kW, kH, ruler_pixel);
|
||||||
|
PLOT_CHECK(ink.pixels > 0u);
|
||||||
|
PLOT_CHECK(predicate_pixels(frame, kW, kFrameRect, ruler_pixel) ==
|
||||||
|
predicate_pixels(frame, kW, axes, ruler_pixel));
|
||||||
|
}
|
||||||
|
p.set_y_range(ay, {0.0, 100.0});
|
||||||
|
|
||||||
// The direct setter is verbatim -- a caller may park a ruler off range -- and the frame
|
// The direct setter is verbatim -- a caller may park a ruler off range -- and the frame
|
||||||
// still holds the ink in: an off-range ruler is skipped rather than drawn over a strip.
|
// still holds the ink in: an off-range ruler is skipped rather than drawn over a strip.
|
||||||
p.set_cursor_y(ruler, 500.0);
|
p.set_cursor_y(ruler, 500.0);
|
||||||
|
|||||||
Reference in new issue
Block a user