fix(input): refuse non-finite region bounds, keep narrow regions movable, republish hot handles

This commit is contained in:
Martino Ferrari committed 2026-09-28 14:08:11 +02:00
1 parent 4681d7015b
commit 7cc06f07f5
7 files changed
+263 -19

No files matched your search

+20
View File
@@ -65,6 +65,14 @@ bool covered_samples(const data::Series& series, f64 lo, f64 hi, u64& begin, u64
return true;
}
/// True when a pair of bounds can be a region's bounds: both of them finite. The one place
/// this is decided, so `add_region()` and `set_region()` cannot disagree about what they
/// will hold -- and so no NaN ever reaches the `std::min`/`std::max` below, which propagate
/// it into *both* stored bounds and would leave a region no drag could recover from.
bool finite_bounds(f64 x0, f64 x1) noexcept {
return std::isfinite(x0) && std::isfinite(x1);
}
const Measurement& invalid_measurement() noexcept {
static const Measurement measurement{};
return measurement;
@@ -175,6 +183,12 @@ u32 Plot::add_annotation(const Annotation& annotation) {
}
u32 Plot::add_region(const Region& region) {
// A region whose bounds are not numbers is not an interval this plot can hold, and a
// `Free` one stored that way could never be dragged back (every motion re-derives both
// bounds from the stored ones), so it is refused rather than stored: nothing is added,
// and the id handed back names no region -- `region()` reads a default for it and
// `region_count()` does not count it.
if (!finite_bounds(region.x0, region.x1)) return static_cast<u32>(kInvalid);
// Ordered on the way in, so no caller -- and no drag -- can leave `x0 > x1`
// behind: the store is the one place this invariant is enforced.
Region stored = region;
@@ -505,6 +519,12 @@ void Plot::invalidate_measurements() noexcept {
void Plot::set_region(u32 id, f64 x0, f64 x1) {
if (id >= regions_.size()) return;
// A bound that is not a number is not a bound, and `std::min`/`std::max` would carry it
// through the whole pair: accepting one would replace the finite partner as well, so the
// region could never be dragged back. Refused outright -- as the other bound setters in
// this file refuse one -- which leaves the interval it already held, and with it the
// statistics cached for that interval.
if (!finite_bounds(x0, x1)) return;
// min/max, so an inverted pair is stored ordered rather than clamped: a drag that
// crosses one bound over the other *swaps* the interval, which is what keeps the
// bound the pointer holds movable instead of pinned at the other one.