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
@@ -697,6 +697,63 @@ PLOT_TEST(drag_zoom_is_logical_the_wheel_is_not_scaled) {
|
||||
}
|
||||
}
|
||||
|
||||
/// `Style::device()` documents a non-positive `ui_scale` as host state that must be
|
||||
/// tolerated, but `axis_zoom()` divided by it directly. With `ui_scale == 0` and a
|
||||
/// negative drag delta, `logical_dy` was `-inf`, the factor `+inf`, and the
|
||||
/// `!(factor > 0.0)` guard did not reject it: `zoom_x_axis`/`zoom_y_axis` computed
|
||||
/// `anchor + (bound - anchor) * inf`, and `set_y_range()` stored `inf`/`NaN` verbatim and
|
||||
/// cleared `auto_scale` -- an axis left non-finite until an auto-fit. A negative scale
|
||||
/// silently inverted the zoom direction. Both now mean no zoom at all, exactly the state
|
||||
/// `device()` documents for a scale that is not one.
|
||||
PLOT_TEST(axis_zoom_refuses_a_non_positive_ui_scale) {
|
||||
for (const f32 scale : {0.0f, -1.0f}) {
|
||||
plot::Plot p;
|
||||
p.set_ui_scale(scale);
|
||||
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
|
||||
// `auto_scale` on, and no trace bound: `layout()` has nothing to fit, so the range is
|
||||
// the one below and clearing `auto_scale` is a change this case can see.
|
||||
const u32 ax = p.add_x_axis({"t", {0.0, 100.0}, true, plot::AxisPlace::Bottom, false});
|
||||
const u32 ay = p.add_y_axis({"v", {0.0, 100.0}, true, plot::AxisPlace::Left, false});
|
||||
plot::input::Controller c(p);
|
||||
c.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
|
||||
const plot::Range xb = p.x_range(ax);
|
||||
const plot::Range yb = p.y_range(ay);
|
||||
const plot::PlotLayout& l = p.layout();
|
||||
|
||||
// A middle drag on the x label strip, dragged *up* (a negative dy): the sign that
|
||||
// made the factor infinite before.
|
||||
const f32 strip_x = l.axes.x + 0.5f * l.axes.w;
|
||||
const f32 strip_y = l.x_label_area.y + 0.5f * l.x_label_area.h;
|
||||
c.on_pointer({strip_x, strip_y, 0, 0, plot::input::Button::Middle, 0, true, true, false});
|
||||
PLOT_CHECK(c.drag_mode() == plot::input::DragMode::AxisZoomX);
|
||||
c.on_pointer({strip_x, strip_y - 30.0f, 0, -30.0f, plot::input::Button::Middle, 0, true, false,
|
||||
false});
|
||||
c.on_pointer({strip_x, strip_y - 30.0f, 0, 0, plot::input::Button::Middle, 0, false, false,
|
||||
true});
|
||||
PLOT_CHECK(std::isfinite(p.x_range(ax).min));
|
||||
PLOT_CHECK(std::isfinite(p.x_range(ax).max));
|
||||
PLOT_CHECK(p.x_range(ax).min == xb.min);
|
||||
PLOT_CHECK(p.x_range(ax).max == xb.max);
|
||||
|
||||
// And the y strip, where the infinite factor was stored verbatim.
|
||||
const f32 y_strip_x = l.y_label_area.x + 0.5f * l.y_label_area.w;
|
||||
const f32 y_strip_y = l.axes.y + 0.5f * l.axes.h;
|
||||
c.on_pointer({y_strip_x, y_strip_y, 0, 0, plot::input::Button::Middle, 0, true, true, false});
|
||||
PLOT_CHECK(c.drag_mode() == plot::input::DragMode::AxisZoomY);
|
||||
c.on_pointer({y_strip_x, y_strip_y - 30.0f, 0, -30.0f, plot::input::Button::Middle, 0, true,
|
||||
false, false});
|
||||
c.on_pointer({y_strip_x, y_strip_y - 30.0f, 0, 0, plot::input::Button::Middle, 0, false, false,
|
||||
true});
|
||||
PLOT_CHECK(std::isfinite(p.y_range(ay).min));
|
||||
PLOT_CHECK(std::isfinite(p.y_range(ay).max));
|
||||
PLOT_CHECK(p.y_range(ay).min == yb.min);
|
||||
PLOT_CHECK(p.y_range(ay).max == yb.max);
|
||||
// The range setters never ran, so the axes' own auto scale stands untouched.
|
||||
PLOT_CHECK(p.x_axis(ax).auto_scale);
|
||||
PLOT_CHECK(p.y_axis(ay).auto_scale);
|
||||
}
|
||||
}
|
||||
|
||||
/// The consequences of a degenerate range on a live axis, from the gesture side: the
|
||||
/// wheel, the pan and the axis zoom all refuse a span that is not a span, so the
|
||||
/// frozen axis a stored zero-span left behind was one no interaction could move --
|
||||
|
||||
Reference in new issue
Block a user