fix(render,input): guard the zoom gain, clip annotation captions, no-throw dump
Three findings reachable by ordinary use: * `Controller::axis_zoom()` divided by `Plot::ui_scale()` directly, though `Style::device()` documents a non-positive scale as host state that must be tolerated. With `ui_scale == 0` and a negative drag delta, `logical_dy` was `-inf`, the factor `+inf`, and the guard `!(factor > 0.0)` did not reject it: `zoom_x_axis`/`zoom_y_axis` computed `anchor + (bound - anchor) * inf`, and `set_y_range()` stored it verbatim and cleared `auto_scale` -- an axis left non-finite until an auto-fit. A negative scale silently inverted the zoom. A scale that is not positive and finite now means no zoom at all. * `draw_annotations()` stages its caption into the frame's single text batch, which `flush_text()` draws *after* `clear_clip()`, so a caption staged while the axes clip was engaged was rasterised with the viewport clip. An annotation anchored outside the visible range kept a caption with no cross, and in a `Figure` -- whose rows tile the figure -- that caption painted into the neighbouring row. A caption whose anchor is outside `axes` is now skipped with its cross, rather than splitting the frame's one text draw call (which would cost a draw call per row and mis-clip the labels staged outside `axes`). * `dump_frame_ppm()` was the one public entry point not `noexcept`, so a `std::bad_alloc` crossed the API boundary. Its buffers are now sized before anything is on disk and a failure is reported as `ErrorCode::OutOfMemory`, exactly as `TrueTypeFont::from_file()` reports it. Tests: `ui_scale` 0 and -1 leave the axis range finite and unchanged with `auto_scale` intact; an annotation anchored outside a bottom figure row's y range leaves no caption ink in the row above, while an in-range one still draws; a `static_assert` pins the dump's `noexcept`.
This commit is contained in:
1 parent
9aff06acee
commit
2fbd1de7d4
5 files changed
+188
-19
No files matched your search
@@ -25,6 +25,7 @@
|
||||
#include <cstdio>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
@@ -113,6 +114,13 @@ void dump_if_requested(const plot::Renderer& r, const char* default_name) {
|
||||
PLOT_CHECK(ok);
|
||||
}
|
||||
|
||||
/// The public-boundary contract (review finding 3d): `dump_frame_ppm()` reports an
|
||||
/// allocation failure through its `Status` -- `ErrorCode::OutOfMemory`, as
|
||||
/// `TrueTypeFont::from_file()` does -- rather than letting `std::bad_alloc` cross the
|
||||
/// boundary, so it is `noexcept` like every other entry point here.
|
||||
static_assert(noexcept(std::declval<const plot::Renderer&>().dump_frame_ppm(nullptr)),
|
||||
"dump_frame_ppm must not throw across the public boundary");
|
||||
|
||||
/// True when `px` is (within a couple of levels) the straight-alpha colour `c`,
|
||||
/// which is what a fully covered text pixel is. The colour comes from the style,
|
||||
/// so no predicate here has to guess what the renderer draws with.
|
||||
@@ -4237,6 +4245,81 @@ PLOT_TEST(figure_row_overshoot_stays_out_of_the_neighbouring_row) {
|
||||
ctx.destroy();
|
||||
}
|
||||
|
||||
/// An annotation's **caption** is the one overlay the per-row clip did not reach: the
|
||||
/// cross is staged as line geometry inside the axes clip, while the caption is staged
|
||||
/// into the frame's single text batch, which is flushed *after* `clear_clip()`. An
|
||||
/// annotation anchored outside the visible range therefore kept a caption with no cross,
|
||||
/// and -- in a `Figure`, whose rows tile the figure -- that caption painted into the
|
||||
/// neighbouring row, the bleeding the clip exists to prevent. The caption is now skipped
|
||||
/// with its cross when the anchor is outside the axes; an in-range annotation still draws
|
||||
/// its caption, so the skip is not a blanket refusal to draw text.
|
||||
PLOT_TEST(figure_annotation_caption_stays_out_of_the_neighbouring_row) {
|
||||
plottest::HeadlessContext ctx(kW, kH);
|
||||
plot::Status st;
|
||||
PLOT_CHECK(ctx.create(&st));
|
||||
PLOT_CHECK(plot::gl::load(nullptr));
|
||||
plot::Renderer r;
|
||||
PLOT_CHECK(r.init(&st));
|
||||
|
||||
const std::vector<plot::f32> samples = wave(640u, 0.0f);
|
||||
plot::data::ArraySeries series(samples, 0.0, 1.0);
|
||||
plot::Figure figure(2u);
|
||||
figure.set_viewport({0.0f, 0.0f, static_cast<plot::f32>(kW), static_cast<plot::f32>(kH)});
|
||||
add_row_trace(figure.plot(0u), series, kRowBlue);
|
||||
|
||||
plot::Plot& bottom = figure.plot(1u);
|
||||
const plot::u32 ax =
|
||||
bottom.add_x_axis({"t", series.x_bounds(), false, plot::AxisPlace::Bottom, false});
|
||||
const plot::u32 ay = bottom.add_y_axis({"v", {-1.0, 1.0}, false, plot::AxisPlace::Left, false});
|
||||
bottom.add_trace({&series, kRowMagenta, 1.5f, ax, ay});
|
||||
|
||||
// The geometry the out-of-range anchor is chosen against: a y whose pixel row lands
|
||||
// *inside the row above*, well above this row's own axes.
|
||||
figure.layout();
|
||||
const plot::FigureLayout layout = figure.layout();
|
||||
PLOT_CHECK(layout.rows.size() == 2u);
|
||||
const plot::Rect& axes0 = figure.plot(0u).layout().axes;
|
||||
const plot::Rect& axes1 = bottom.layout().axes;
|
||||
PLOT_CHECK(axes0.h > 0.0f && axes1.h > 0.0f);
|
||||
const plot::f32 target_y = axes0.y + 0.4f * axes0.h;
|
||||
|
||||
plot::Annotation outside;
|
||||
outside.x = 320.0; // in range for x, so the caption is staged at a real column
|
||||
outside.y = bottom.y_transform(ay).to_data_y(static_cast<plot::f64>(target_y));
|
||||
outside.text = "outside";
|
||||
outside.color = plot::Color::hex(0xE8E8E8);
|
||||
outside.axis_x = ax;
|
||||
outside.axis_y = ay;
|
||||
bottom.add_annotation(outside);
|
||||
|
||||
// The control: an annotation actually inside row 1's axes, whose caption must still
|
||||
// be drawn -- so the assertion below is about the skip, not about text being absent.
|
||||
plot::Annotation inside;
|
||||
inside.x = 320.0;
|
||||
inside.y = 0.0;
|
||||
inside.text = "inside";
|
||||
inside.color = plot::Color::hex(0xE8E8E8);
|
||||
inside.axis_x = ax;
|
||||
inside.axis_y = ay;
|
||||
bottom.add_annotation(inside);
|
||||
|
||||
r.begin_frame(kW, kH, {0, 0, static_cast<plot::f32>(kW), static_cast<plot::f32>(kH)});
|
||||
r.draw(figure);
|
||||
r.end_frame();
|
||||
PLOT_CHECK(!r.last_frame_had_gl_error());
|
||||
dump_if_requested(r, "clip_annotation_caption.ppm");
|
||||
|
||||
const std::vector<plot::u8> frame = read_frame(ctx);
|
||||
|
||||
// Row 0's axes hold no text of their own -- its labels live in its y strip, outside
|
||||
// `axes` -- so any text ink here would be the neighbour's escaped caption.
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, axes0, text_pixel) == 0u);
|
||||
// The in-range caption proves the text pipeline ran and captions are drawn at all.
|
||||
PLOT_CHECK(predicate_pixels(frame, kW, axes1, text_pixel) > 0u);
|
||||
|
||||
ctx.destroy();
|
||||
}
|
||||
|
||||
/// The fix's neighbours: the overlay geometry that already declined to draw over the
|
||||
/// strips did so by its own column rules -- a region's bounds outside the window are
|
||||
/// skipped by `draw_region_edge`, a cursor scrolled out of it by `draw_cursors`, and a
|
||||
|
||||
Reference in new issue
Block a user