egui/crates/emath/src/smart_aim.rs

237 lines
8.3 KiB
Rust
Raw Permalink 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 = 15;
/// 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.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
}