1use std::cmp::Ordering;
17
18#[derive(Clone, Copy, Debug)]
24pub struct SliderSpec {
25 pub logarithmic: bool,
26 pub smallest_positive: f64,
29 pub largest_finite: f64,
32}
33
34impl Default for SliderSpec {
35 fn default() -> Self {
36 Self {
37 logarithmic: false,
38 smallest_positive: 1e-6,
39 largest_finite: f64::INFINITY,
40 }
41 }
42}
43
44const INFINITY: f64 = f64::INFINITY;
45
46const INF_RANGE_MAGNITUDE: f64 = 10.0;
49
50#[inline]
51fn lerp(a: f64, b: f64, t: f64) -> f64 {
52 a + (b - a) * t
53}
54
55#[inline]
56fn remap(x: f64, from_lo: f64, from_hi: f64, to_lo: f64, to_hi: f64) -> f64 {
57 let t = (x - from_lo) / (from_hi - from_lo);
58 lerp(to_lo, to_hi, t)
59}
60
61#[inline]
62fn remap_clamp(x: f64, from_lo: f64, from_hi: f64, to_lo: f64, to_hi: f64) -> f64 {
63 if x <= from_lo.min(from_hi) {
64 if from_lo <= from_hi {
65 to_lo
66 } else {
67 to_hi
68 }
69 } else if x >= from_lo.max(from_hi) {
70 if from_lo <= from_hi {
71 to_hi
72 } else {
73 to_lo
74 }
75 } else {
76 remap(x, from_lo, from_hi, to_lo, to_hi)
77 }
78}
79
80pub fn clamp_value_to_range(x: f64, min: f64, max: f64) -> f64 {
83 let (mut lo, mut hi) = (min, max);
84 if lo.total_cmp(&hi) == Ordering::Greater {
85 std::mem::swap(&mut lo, &mut hi);
86 }
87 match x.total_cmp(&lo) {
88 Ordering::Less | Ordering::Equal => lo,
89 Ordering::Greater => match x.total_cmp(&hi) {
90 Ordering::Greater | Ordering::Equal => hi,
91 Ordering::Less => x,
92 },
93 }
94}
95
96pub fn normalized_from_value(value: f64, min: f64, max: f64, spec: &SliderSpec) -> f64 {
98 if min.is_nan() || max.is_nan() {
99 f64::NAN
100 } else if min == max {
101 0.5 } else if min > max {
103 1.0 - normalized_from_value(value, max, min, spec)
104 } else if value <= min {
105 0.0
106 } else if value >= max {
107 1.0
108 } else if spec.logarithmic {
109 if max <= 0.0 {
110 normalized_from_value(-value, -min, -max, spec)
112 } else if 0.0 <= min {
113 let (min_log, max_log) = range_log10(min, max, spec);
114 let value_log = value.log10();
115 remap_clamp(value_log, min_log, max_log, 0.0, 1.0)
116 } else {
117 let zero_cutoff = logarithmic_zero_cutoff(min, max);
118 if value < 0.0 {
119 remap(
120 normalized_from_value(value, min, 0.0, spec),
121 0.0,
122 1.0,
123 0.0,
124 zero_cutoff,
125 )
126 } else {
127 remap(
128 normalized_from_value(value, 0.0, max, spec),
129 0.0,
130 1.0,
131 zero_cutoff,
132 1.0,
133 )
134 }
135 }
136 } else {
137 remap_clamp(value, min, max, 0.0, 1.0)
138 }
139}
140
141pub fn value_from_normalized(normalized: f64, min: f64, max: f64, spec: &SliderSpec) -> f64 {
144 if min.is_nan() || max.is_nan() {
145 f64::NAN
146 } else if min == max {
147 min
148 } else if min > max {
149 value_from_normalized(1.0 - normalized, max, min, spec)
150 } else if normalized <= 0.0 {
151 min
152 } else if normalized >= 1.0 {
153 max
154 } else if spec.logarithmic {
155 if max <= 0.0 {
156 -value_from_normalized(normalized, -min, -max, spec)
158 } else if 0.0 <= min {
159 let (min_log, max_log) = range_log10(min, max, spec);
160 let log = lerp(min_log, max_log, normalized);
161 10.0_f64.powf(log)
162 } else {
163 let zero_cutoff = logarithmic_zero_cutoff(min, max);
164 if normalized < zero_cutoff {
165 value_from_normalized(
167 remap(normalized, 0.0, zero_cutoff, 0.0, 1.0),
168 min,
169 0.0,
170 spec,
171 )
172 } else {
173 value_from_normalized(
175 remap(normalized, zero_cutoff, 1.0, 0.0, 1.0),
176 0.0,
177 max,
178 spec,
179 )
180 }
181 }
182 } else {
183 lerp(min, max, normalized.clamp(0.0, 1.0))
184 }
185}
186
187fn range_log10(min: f64, max: f64, spec: &SliderSpec) -> (f64, f64) {
188 debug_assert!(spec.logarithmic, "spec must be logarithmic");
189 debug_assert!(min <= max, "min must be <= max, got min={min} max={max}");
190
191 if min == 0.0 && max == INFINITY {
192 (spec.smallest_positive.log10(), INF_RANGE_MAGNITUDE)
193 } else if min == 0.0 {
194 if spec.smallest_positive < max {
195 (spec.smallest_positive.log10(), max.log10())
196 } else {
197 (max.log10() - INF_RANGE_MAGNITUDE, max.log10())
198 }
199 } else if max == INFINITY {
200 if min < spec.largest_finite {
201 (min.log10(), spec.largest_finite.log10())
202 } else {
203 (min.log10(), min.log10() + INF_RANGE_MAGNITUDE)
204 }
205 } else {
206 (min.log10(), max.log10())
207 }
208}
209
210fn logarithmic_zero_cutoff(min: f64, max: f64) -> f64 {
213 debug_assert!(
214 min < 0.0 && 0.0 < max,
215 "min must be negative and max positive, got min={min} max={max}"
216 );
217
218 let min_magnitude = if min == -INFINITY {
219 INF_RANGE_MAGNITUDE
220 } else {
221 min.abs().log10().abs()
222 };
223 let max_magnitude = if max == INFINITY {
224 INF_RANGE_MAGNITUDE
225 } else {
226 max.log10().abs()
227 };
228
229 let cutoff = min_magnitude / (min_magnitude + max_magnitude);
230 debug_assert!(
231 (0.0..=1.0).contains(&cutoff),
232 "Bad cutoff {cutoff:?} for min {min:?} and max {max:?}"
233 );
234 cutoff
235}
236
237const NUM_DECIMALS: usize = 16;
243
244#[inline]
245fn fast_midpoint(a: f64, b: f64) -> f64 {
246 (a + b) / 2.0
247}
248
249#[inline]
250fn u8_midpoint(a: u8, b: u8) -> u8 {
251 ((a as u16 + b as u16) / 2) as u8
252}
253
254pub fn best_in_range_f64(min: f64, max: f64) -> f64 {
257 if min.is_nan() {
259 return max;
260 }
261 if max.is_nan() {
262 return min;
263 }
264
265 if max < min {
266 return best_in_range_f64(max, min);
267 }
268 if min == max {
269 return min;
270 }
271 if min <= 0.0 && 0.0 <= max {
272 return 0.0; }
274 if min < 0.0 {
275 return -best_in_range_f64(-max, -min);
276 }
277
278 debug_assert!(0.0 < min && min < max, "Logic bug");
279
280 if !max.is_finite() {
282 return min;
283 }
284
285 let min_exponent = min.log10();
286 let max_exponent = max.log10();
287
288 if min_exponent.floor() != max_exponent.floor() {
289 let exponent = fast_midpoint(min_exponent, max_exponent);
291 return 10.0_f64.powi(exponent.round() as i32);
292 }
293
294 if is_integer(min_exponent) {
295 return 10.0_f64.powf(min_exponent);
296 }
297 if is_integer(max_exponent) {
298 return 10.0_f64.powf(max_exponent);
299 }
300
301 let scale = NUM_DECIMALS as i32 - max_exponent.floor() as i32 - 1;
303 let scale_factor = 10.0_f64.powi(scale);
304
305 let min_str = to_decimal_string((min * scale_factor).round() as u64);
306 let max_str = to_decimal_string((max * scale_factor).round() as u64);
307
308 let mut ret_str = [0u8; NUM_DECIMALS];
312
313 for i in 0..NUM_DECIMALS {
314 if min_str[i] == max_str[i] {
315 ret_str[i] = min_str[i];
316 } else {
317 let mut deciding_digit_min = min_str[i];
318 let deciding_digit_max = max_str[i];
319
320 debug_assert!(deciding_digit_min < deciding_digit_max, "Bug in smart aim");
321
322 let rest_of_min_is_zeroes = min_str[i + 1..].iter().all(|&c| c == 0);
323
324 if !rest_of_min_is_zeroes {
325 deciding_digit_min += 1;
328 }
329
330 let deciding_digit = if deciding_digit_min == 0 {
331 0
332 } else if deciding_digit_min <= 5 && 5 <= deciding_digit_max {
333 5 } else {
335 u8_midpoint(deciding_digit_min, deciding_digit_max)
336 };
337
338 ret_str[i] = deciding_digit;
339
340 return from_decimal_string(ret_str) as f64 / scale_factor;
341 }
342 }
343
344 min }
346
347fn is_integer(f: f64) -> bool {
348 f.round() == f
349}
350
351fn to_decimal_string(v: u64) -> [u8; NUM_DECIMALS] {
352 let mut ret = [0u8; NUM_DECIMALS];
353 let mut value = v;
354 for i in (0..NUM_DECIMALS).rev() {
355 ret[i] = (value % 10) as u8;
356 value /= 10;
357 }
358 ret
359}
360
361fn from_decimal_string(s: [u8; NUM_DECIMALS]) -> u64 {
362 let mut value = 0u64;
363 for &c in &s {
364 debug_assert!(c <= 9, "Bad number");
365 value = value * 10 + c as u64;
366 }
367 value
368}
369
370#[cfg(test)]
371mod tests {
372 use super::*;
373
374 fn log_spec() -> SliderSpec {
375 SliderSpec {
376 logarithmic: true,
377 smallest_positive: 1e-6,
378 largest_finite: f64::INFINITY,
379 }
380 }
381
382 #[test]
383 fn linear_mapping_roundtrips() {
384 let spec = SliderSpec::default();
385 for &(v, n) in &[(0.0, 0.0), (5.0, 0.5), (10.0, 1.0), (2.5, 0.25)] {
386 assert!((normalized_from_value(v, 0.0, 10.0, &spec) - n).abs() < 1e-9);
387 assert!((value_from_normalized(n, 0.0, 10.0, &spec) - v).abs() < 1e-9);
388 }
389 }
390
391 #[test]
392 fn linear_clamps_outside_range() {
393 let spec = SliderSpec::default();
394 assert_eq!(normalized_from_value(-5.0, 0.0, 10.0, &spec), 0.0);
395 assert_eq!(normalized_from_value(50.0, 0.0, 10.0, &spec), 1.0);
396 }
397
398 #[test]
399 fn logarithmic_midpoint_is_geometric_mean() {
400 let spec = log_spec();
401 let mid = value_from_normalized(0.5, 1.0, 100.0, &spec);
403 assert!((mid - 10.0).abs() < 1e-6, "got {mid}");
404 let n = normalized_from_value(10.0, 1.0, 100.0, &spec);
406 assert!((n - 0.5).abs() < 1e-9, "got {n}");
407 }
408
409 #[test]
410 fn logarithmic_reversed_range() {
411 let spec = log_spec();
412 let v = value_from_normalized(0.0, 100.0, 1.0, &spec);
414 assert!((v - 100.0).abs() < 1e-9, "got {v}");
415 }
416
417 #[test]
418 fn logarithmic_spanning_zero_puts_zero_at_cutoff() {
419 let spec = log_spec();
420 let cutoff = logarithmic_zero_cutoff(-1000.0, 1000.0);
422 assert!((cutoff - 0.5).abs() < 1e-9, "got {cutoff}");
423 let v = value_from_normalized(0.5, -1000.0, 1000.0, &spec);
424 assert!(v.abs() < 1.0, "expected near zero, got {v}");
425 }
426
427 #[test]
428 fn logarithmic_spanning_infinity() {
429 let spec = log_spec();
430 assert_eq!(
432 value_from_normalized(1.0, 0.0, f64::INFINITY, &spec),
433 f64::INFINITY
434 );
435 assert_eq!(value_from_normalized(0.0, 0.0, f64::INFINITY, &spec), 0.0);
436 let mid = value_from_normalized(0.5, 0.0, f64::INFINITY, &spec);
438 assert!(mid.is_finite() && mid > 0.0, "got {mid}");
439 }
440
441 #[test]
447 fn infinite_range_initial_mapping_is_finite() {
448 let spec = log_spec();
449 let n = normalized_from_value(10.0, 0.0, 10000.0, &spec);
451 assert!(n.is_finite() && (0.0..=1.0).contains(&n), "demo n={n}");
452
453 for &v in &[0.0, 10000.0] {
456 let n = normalized_from_value(v, -f64::INFINITY, f64::INFINITY, &spec);
457 assert!(n.is_finite() && (0.0..=1.0).contains(&n), "v={v} n={n}");
458 }
459 let mid = value_from_normalized(0.5, -f64::INFINITY, f64::INFINITY, &spec);
461 assert!(mid.is_finite(), "mid={mid}");
462 }
463
464 #[test]
468 fn one_infinite_bound_roundtrips_finite() {
469 let spec = log_spec();
470 let n = normalized_from_value(10.0, -f64::INFINITY, 10000.0, &spec);
471 assert!(n.is_finite() && (0.0..=1.0).contains(&n), "n={n}");
472 let v = value_from_normalized(n, -f64::INFINITY, 10000.0, &spec);
473 assert!(v.is_finite(), "v={v}");
474
475 let n = normalized_from_value(10.0, 0.0, f64::INFINITY, &spec);
476 assert!(n.is_finite() && (0.0..=1.0).contains(&n), "n={n}");
477 }
478
479 #[test]
480 fn clamp_handles_reversed_range() {
481 assert_eq!(clamp_value_to_range(5.0, 10.0, 0.0), 5.0);
482 assert_eq!(clamp_value_to_range(-1.0, 10.0, 0.0), 0.0);
483 assert_eq!(clamp_value_to_range(11.0, 10.0, 0.0), 10.0);
484 }
485
486 #[test]
488 fn smart_aim_round_numbers() {
489 assert_eq!(best_in_range_f64(-0.2, 0.0), 0.0);
490 assert_eq!(best_in_range_f64(-10_004.23, 3.14), 0.0);
491 assert_eq!(best_in_range_f64(7.8, 17.8), 10.0);
492 assert_eq!(best_in_range_f64(99.0, 300.0), 100.0);
493 assert_eq!(best_in_range_f64(-99.0, -300.0), -100.0);
494 assert_eq!(best_in_range_f64(0.4, 0.9), 0.5);
495 assert_eq!(best_in_range_f64(14.1, 19.99), 15.0);
496 assert_eq!(best_in_range_f64(12.3, 65.9), 50.0);
497 assert_eq!(best_in_range_f64(493.0, 879.0), 500.0);
498 assert_eq!(best_in_range_f64(0.37, 0.48), 0.40);
499 assert_eq!(best_in_range_f64(7.5, 16.3), 10.0);
500 assert_eq!(best_in_range_f64(7.5, 763.3), 100.0);
501 assert_eq!(best_in_range_f64(7.5, 123_456.0), 1000.0);
502 assert_eq!(best_in_range_f64(9.9999, 99.999), 10.0);
503 assert_eq!(best_in_range_f64(10.001, 99.999), 50.0);
504 }
505
506 #[test]
507 fn smart_aim_integers() {
508 assert_eq!(best_in_range_f64(99.0, 300.0), 100.0);
509 assert_eq!(best_in_range_f64(4.0, 9.0), 5.0);
510 assert_eq!(best_in_range_f64(14.0, 19.0), 15.0);
511 assert_eq!(best_in_range_f64(12.0, 65.0), 50.0);
512 assert_eq!(best_in_range_f64(37.0, 48.0), 40.0);
513 assert_eq!(best_in_range_f64(12345.0, 12780.0), 12500.0);
514 }
515
516 #[test]
517 fn smart_aim_nan_and_infinity() {
518 assert!(best_in_range_f64(f64::NAN, f64::NAN).is_nan());
519 assert_eq!(best_in_range_f64(f64::NAN, 1.2), 1.2);
520 assert_eq!(best_in_range_f64(1.2, f64::INFINITY), 1.2);
521 assert_eq!(best_in_range_f64(f64::NEG_INFINITY, 1.2), 0.0);
522 assert_eq!(best_in_range_f64(f64::NEG_INFINITY, -2.7), -2.7);
523 assert_eq!(best_in_range_f64(f64::NEG_INFINITY, f64::INFINITY), 0.0);
524 }
525}