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
@@ -608,10 +608,18 @@ void Controller::axis_zoom(const PointerEvent& e) {
|
||||
// A pointer drag reports pixels, not wheel notches, so it uses its own
|
||||
// sensitivity: 200 px is one decade. The gain is a physical distance the hand
|
||||
// moves, so the device-pixel delta is converted to logical pixels first -- the
|
||||
// same drag must zoom the same amount on a 2x display. A scale no host should
|
||||
// ever set (0, or negative) makes the factor non-positive, and the guard below
|
||||
// turns that into no zoom at all rather than an axis re-centred on infinity.
|
||||
const f64 logical_dy = static_cast<f64>(e.dy) / static_cast<f64>(plot_->ui_scale());
|
||||
// same drag must zoom the same amount on a 2x display.
|
||||
//
|
||||
// A scale that is not a positive number is not a scale -- a host that has not
|
||||
// measured its display, a field nobody filled in -- the same state
|
||||
// `Style::device()` tolerates. Here it means *no zoom*: dividing by it would make
|
||||
// `logical_dy` infinite and the factor infinite (or NaN), and that factor reaches
|
||||
// `set_x_range()`/`set_y_range()`, which store it verbatim and clear `auto_scale`
|
||||
// -- an axis left non-finite until an auto-fit. A negative scale would silently
|
||||
// invert the zoom direction too, so the whole class is refused here.
|
||||
const f64 scale = static_cast<f64>(plot_->ui_scale());
|
||||
if (!(scale > 0.0) || !std::isfinite(scale)) return;
|
||||
const f64 logical_dy = static_cast<f64>(e.dy) / scale;
|
||||
const f64 factor = std::exp(-logical_dy * kZoomPerPixel);
|
||||
if (!(factor > 0.0)) return;
|
||||
if (mode_ == DragMode::AxisZoomX) {
|
||||
|
||||
Reference in new issue
Block a user