1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-27 15:13:12 -04:00
Files
egui/crates/emath/src/smart_aim.rs
Carter Schmidt bd63647177 Fix crash when dragging a DragValue through small floats. (#7939)
Increased smart_aim `NUM_DECIMALS` from 15 to 16 to fix crash in
`best_in_range_f64`
The f64 value 0.09999999999999995, when multiplied by `scale_factor` and
rounded, becomes 16 digits (999999999999999.5 -> 1000000000000000) and
the leading 1 is clipped off by `to_decimal_string` resulting in all 0s
and triggering the debug_assert! message "Bug in smart aim code"

<!--
Please read the "Making a PR" section of
[`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/main/CONTRIBUTING.md)
before opening a Pull Request!

* Keep your PR:s small and focused.
* The PR title is what ends up in the changelog, so make it descriptive!
* If applicable, add a screenshot or gif.
* If it is a non-trivial addition, consider adding a demo for it to
`egui_demo_lib`, or a new example.
* Do NOT open PR:s from your `master` branch, as that makes it hard for
maintainers to test and add commits to your PR.
* Remember to run `cargo fmt` and `cargo clippy`.
* Open the PR as a draft until you have self-reviewed it and run
`./scripts/check.sh`.
* When you have addressed a PR comment, mark it as resolved.

Please be patient! I will review your PR, but my time is limited!
-->

* Closes <https://github.com/emilk/egui/issues/7747 >
* [x] I have followed the instructions in the PR template
2026-03-02 08:54:47 +01:00

241 lines
8.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Find "simple" numbers is some range. Used by sliders.
use crate::fast_midpoint;
const NUM_DECIMALS: usize = 16;
/// Find the "simplest" number in a closed range [min, max], i.e. the one with the fewest decimal digits.
///
/// So in the range `[0.83, 1.354]` you will get `1.0`, and for `[0.37, 0.48]` you will get `0.4`.
/// This is used when dragging sliders etc to get the values that users are most likely to desire.
/// This assumes a decimal centric user.
pub fn best_in_range_f64(min: f64, max: f64) -> f64 {
// Avoid NaN if we can:
if min.is_nan() {
return max;
}
if max.is_nan() {
return min;
}
if max < min {
return best_in_range_f64(max, min);
}
if min == max {
return min;
}
if min <= 0.0 && 0.0 <= max {
return 0.0; // always prefer zero
}
if min < 0.0 {
return -best_in_range_f64(-max, -min);
}
debug_assert!(0.0 < min && min < max, "Logic bug");
// Prefer finite numbers:
if !max.is_finite() {
return min;
}
debug_assert!(
min.is_finite() && max.is_finite(),
"min: {min:?}, max: {max:?}"
);
let min_exponent = min.log10();
let max_exponent = max.log10();
if min_exponent.floor() != max_exponent.floor() {
// Different orders of magnitude.
// Pick the geometric center of the two:
let exponent = fast_midpoint(min_exponent, max_exponent);
return 10.0_f64.powi(exponent.round() as i32);
}
if is_integer(min_exponent) {
return 10.0_f64.powf(min_exponent);
}
if is_integer(max_exponent) {
return 10.0_f64.powf(max_exponent);
}
// Find the proper scale, and then convert to integers:
let scale = NUM_DECIMALS as i32 - max_exponent.floor() as i32 - 1;
let scale_factor = 10.0_f64.powi(scale);
let min_str = to_decimal_string((min * scale_factor).round() as u64);
let max_str = to_decimal_string((max * scale_factor).round() as u64);
// We now have two positive integers of the same length.
// We want to find the first non-matching digit,
// which we will call the "deciding digit".
// Everything before it will be the same,
// everything after will be zero,
// and the deciding digit itself will be picked as a "smart average"
// min: 12345
// max: 12780
// output: 12500
let mut ret_str = [0; NUM_DECIMALS];
for i in 0..NUM_DECIMALS {
if min_str[i] == max_str[i] {
ret_str[i] = min_str[i];
} else {
// Found the deciding digit at index `i`
let mut deciding_digit_min = min_str[i];
let deciding_digit_max = max_str[i];
debug_assert!(
deciding_digit_min < deciding_digit_max,
"Bug in smart aim code"
);
let rest_of_min_is_zeroes = min_str[i + 1..].iter().all(|&c| c == 0);
if !rest_of_min_is_zeroes {
// There are more digits coming after `deciding_digit_min`, so we cannot pick it.
// So the true min of what we can pick is one greater:
deciding_digit_min += 1;
}
let deciding_digit = if deciding_digit_min == 0 {
0
} else if deciding_digit_min <= 5 && 5 <= deciding_digit_max {
5 // 5 is the roundest number in the range
} else {
deciding_digit_min.midpoint(deciding_digit_max)
};
ret_str[i] = deciding_digit;
return from_decimal_string(ret_str) as f64 / scale_factor;
}
}
min // All digits are the same. Already handled earlier, but better safe than sorry
}
fn is_integer(f: f64) -> bool {
f.round() == f
}
fn to_decimal_string(v: u64) -> [u8; NUM_DECIMALS] {
let mut ret = [0; NUM_DECIMALS];
let mut value = v;
for i in (0..NUM_DECIMALS).rev() {
ret[i] = (value % 10) as u8;
value /= 10;
}
ret
}
fn from_decimal_string(s: [u8; NUM_DECIMALS]) -> u64 {
let mut value = 0;
for &c in &s {
debug_assert!(c <= 9, "Bad number");
value = value * 10 + c as u64;
}
value
}
#[expect(clippy::approx_constant)]
#[test]
fn test_aim() {
assert_eq!(
best_in_range_f64(0.0799999999999996, 0.09999999999999995),
0.08,
);
assert_eq!(best_in_range_f64(-0.2, 0.0), 0.0, "Prefer zero");
assert_eq!(best_in_range_f64(-10_004.23, 3.14), 0.0, "Prefer zero");
assert_eq!(best_in_range_f64(-0.2, 100.0), 0.0, "Prefer zero");
assert_eq!(best_in_range_f64(0.2, 0.0), 0.0, "Prefer zero");
assert_eq!(best_in_range_f64(7.8, 17.8), 10.0);
assert_eq!(best_in_range_f64(99.0, 300.0), 100.0);
assert_eq!(best_in_range_f64(-99.0, -300.0), -100.0);
assert_eq!(best_in_range_f64(0.4, 0.9), 0.5, "Prefer ending on 5");
assert_eq!(best_in_range_f64(14.1, 19.99), 15.0, "Prefer ending on 5");
assert_eq!(best_in_range_f64(12.3, 65.9), 50.0, "Prefer leading 5");
assert_eq!(best_in_range_f64(493.0, 879.0), 500.0, "Prefer leading 5");
assert_eq!(best_in_range_f64(0.37, 0.48), 0.40);
// assert_eq!(best_in_range_f64(123.71, 123.76), 123.75); // TODO(emilk): we get 123.74999999999999 here
// assert_eq!(best_in_range_f32(123.71, 123.76), 123.75);
assert_eq!(best_in_range_f64(7.5, 16.3), 10.0);
assert_eq!(best_in_range_f64(7.5, 76.3), 10.0);
assert_eq!(best_in_range_f64(7.5, 763.3), 100.0);
assert_eq!(best_in_range_f64(7.5, 1_345.0), 100.0);
assert_eq!(best_in_range_f64(7.5, 123_456.0), 1000.0, "Geometric mean");
assert_eq!(best_in_range_f64(9.9999, 99.999), 10.0);
assert_eq!(best_in_range_f64(10.000, 99.999), 10.0);
assert_eq!(best_in_range_f64(10.001, 99.999), 50.0);
assert_eq!(best_in_range_f64(10.001, 100.000), 100.0);
assert_eq!(best_in_range_f64(99.999, 100.000), 100.0);
assert_eq!(best_in_range_f64(10.001, 100.001), 100.0);
const NAN: f64 = f64::NAN;
const INFINITY: f64 = f64::INFINITY;
const NEG_INFINITY: f64 = f64::NEG_INFINITY;
assert!(best_in_range_f64(NAN, NAN).is_nan());
assert_eq!(best_in_range_f64(NAN, 1.2), 1.2);
assert_eq!(best_in_range_f64(NAN, INFINITY), INFINITY);
assert_eq!(best_in_range_f64(1.2, NAN), 1.2);
assert_eq!(best_in_range_f64(1.2, INFINITY), 1.2);
assert_eq!(best_in_range_f64(INFINITY, 1.2), 1.2);
assert_eq!(best_in_range_f64(NEG_INFINITY, 1.2), 0.0);
assert_eq!(best_in_range_f64(NEG_INFINITY, -2.7), -2.7);
assert_eq!(best_in_range_f64(INFINITY, INFINITY), INFINITY);
assert_eq!(best_in_range_f64(NEG_INFINITY, NEG_INFINITY), NEG_INFINITY);
assert_eq!(best_in_range_f64(NEG_INFINITY, INFINITY), 0.0);
assert_eq!(best_in_range_f64(INFINITY, NEG_INFINITY), 0.0);
#[track_caller]
fn test_f64((min, max): (f64, f64), expected: f64) {
let aimed = best_in_range_f64(min, max);
assert!(
aimed == expected,
"smart_aim({min} {max}) => {aimed}, but expected {expected}"
);
}
#[track_caller]
fn test_i64((min, max): (i64, i64), expected: i64) {
let aimed = best_in_range_f64(min as _, max as _);
assert!(
aimed == expected as f64,
"smart_aim({min} {max}) => {aimed}, but expected {expected}"
);
}
test_i64((99, 300), 100);
test_i64((300, 99), 100);
test_i64((-99, -300), -100);
test_i64((-99, 123), 0); // Prefer zero
test_i64((4, 9), 5); // Prefer ending on 5
test_i64((14, 19), 15); // Prefer ending on 5
test_i64((12, 65), 50); // Prefer leading 5
test_i64((493, 879), 500); // Prefer leading 5
test_i64((37, 48), 40);
test_i64((100, 123), 100);
test_i64((101, 1000), 1000);
test_i64((999, 1000), 1000);
test_i64((123, 500), 500);
test_i64((500, 777), 500);
test_i64((500, 999), 500);
test_i64((12345, 12780), 12500);
test_i64((12371, 12376), 12375);
test_i64((12371, 12376), 12375);
test_f64((7.5, 16.3), 10.0);
test_f64((7.5, 76.3), 10.0);
test_f64((7.5, 763.3), 100.0);
test_f64((7.5, 1_345.0), 100.0); // Geometric mean
test_f64((7.5, 123_456.0), 1_000.0); // Geometric mean
test_f64((-0.2, 0.0), 0.0); // Prefer zero
test_f64((-10_004.23, 4.14), 0.0); // Prefer zero
test_f64((-0.2, 100.0), 0.0); // Prefer zero
test_f64((0.2, 0.0), 0.0); // Prefer zero
test_f64((7.8, 17.8), 10.0);
test_f64((14.1, 19.1), 15.0); // Prefer ending on 5
test_f64((12.3, 65.9), 50.0); // Prefer leading 5
}