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(remap(normalized, 0.0, zero_cutoff, 0.0, 1.0), min, 0.0, spec)
167 } else {
168 value_from_normalized(remap(normalized, zero_cutoff, 1.0, 0.0, 1.0), 0.0, max, spec)
170 }
171 }
172 } else {
173 lerp(min, max, normalized.clamp(0.0, 1.0))
174 }
175}
176
177fn range_log10(min: f64, max: f64, spec: &SliderSpec) -> (f64, f64) {
178 debug_assert!(spec.logarithmic, "spec must be logarithmic");
179 debug_assert!(min <= max, "min must be <= max, got min={min} max={max}");
180
181 if min == 0.0 && max == INFINITY {
182 (spec.smallest_positive.log10(), INF_RANGE_MAGNITUDE)
183 } else if min == 0.0 {
184 if spec.smallest_positive < max {
185 (spec.smallest_positive.log10(), max.log10())
186 } else {
187 (max.log10() - INF_RANGE_MAGNITUDE, max.log10())
188 }
189 } else if max == INFINITY {
190 if min < spec.largest_finite {
191 (min.log10(), spec.largest_finite.log10())
192 } else {
193 (min.log10(), min.log10() + INF_RANGE_MAGNITUDE)
194 }
195 } else {
196 (min.log10(), max.log10())
197 }
198}
199
200fn logarithmic_zero_cutoff(min: f64, max: f64) -> f64 {
203 debug_assert!(
204 min < 0.0 && 0.0 < max,
205 "min must be negative and max positive, got min={min} max={max}"
206 );
207
208 let min_magnitude = if min == -INFINITY {
209 INF_RANGE_MAGNITUDE
210 } else {
211 min.abs().log10().abs()
212 };
213 let max_magnitude = if max == INFINITY {
214 INF_RANGE_MAGNITUDE
215 } else {
216 max.log10().abs()
217 };
218
219 let cutoff = min_magnitude / (min_magnitude + max_magnitude);
220 debug_assert!(
221 (0.0..=1.0).contains(&cutoff),
222 "Bad cutoff {cutoff:?} for min {min:?} and max {max:?}"
223 );
224 cutoff
225}
226
227const NUM_DECIMALS: usize = 16;
233
234#[inline]
235fn fast_midpoint(a: f64, b: f64) -> f64 {
236 (a + b) / 2.0
237}
238
239#[inline]
240fn u8_midpoint(a: u8, b: u8) -> u8 {
241 ((a as u16 + b as u16) / 2) as u8
242}
243
244pub fn best_in_range_f64(min: f64, max: f64) -> f64 {
247 if min.is_nan() {
249 return max;
250 }
251 if max.is_nan() {
252 return min;
253 }
254
255 if max < min {
256 return best_in_range_f64(max, min);
257 }
258 if min == max {
259 return min;
260 }
261 if min <= 0.0 && 0.0 <= max {
262 return 0.0; }
264 if min < 0.0 {
265 return -best_in_range_f64(-max, -min);
266 }
267
268 debug_assert!(0.0 < min && min < max, "Logic bug");
269
270 if !max.is_finite() {
272 return min;
273 }
274
275 let min_exponent = min.log10();
276 let max_exponent = max.log10();
277
278 if min_exponent.floor() != max_exponent.floor() {
279 let exponent = fast_midpoint(min_exponent, max_exponent);
281 return 10.0_f64.powi(exponent.round() as i32);
282 }
283
284 if is_integer(min_exponent) {
285 return 10.0_f64.powf(min_exponent);
286 }
287 if is_integer(max_exponent) {
288 return 10.0_f64.powf(max_exponent);
289 }
290
291 let scale = NUM_DECIMALS as i32 - max_exponent.floor() as i32 - 1;
293 let scale_factor = 10.0_f64.powi(scale);
294
295 let min_str = to_decimal_string((min * scale_factor).round() as u64);
296 let max_str = to_decimal_string((max * scale_factor).round() as u64);
297
298 let mut ret_str = [0u8; NUM_DECIMALS];
302
303 for i in 0..NUM_DECIMALS {
304 if min_str[i] == max_str[i] {
305 ret_str[i] = min_str[i];
306 } else {
307 let mut deciding_digit_min = min_str[i];
308 let deciding_digit_max = max_str[i];
309
310 debug_assert!(deciding_digit_min < deciding_digit_max, "Bug in smart aim");
311
312 let rest_of_min_is_zeroes = min_str[i + 1..].iter().all(|&c| c == 0);
313
314 if !rest_of_min_is_zeroes {
315 deciding_digit_min += 1;
318 }
319
320 let deciding_digit = if deciding_digit_min == 0 {
321 0
322 } else if deciding_digit_min <= 5 && 5 <= deciding_digit_max {
323 5 } else {
325 u8_midpoint(deciding_digit_min, deciding_digit_max)
326 };
327
328 ret_str[i] = deciding_digit;
329
330 return from_decimal_string(ret_str) as f64 / scale_factor;
331 }
332 }
333
334 min }
336
337fn is_integer(f: f64) -> bool {
338 f.round() == f
339}
340
341fn to_decimal_string(v: u64) -> [u8; NUM_DECIMALS] {
342 let mut ret = [0u8; NUM_DECIMALS];
343 let mut value = v;
344 for i in (0..NUM_DECIMALS).rev() {
345 ret[i] = (value % 10) as u8;
346 value /= 10;
347 }
348 ret
349}
350
351fn from_decimal_string(s: [u8; NUM_DECIMALS]) -> u64 {
352 let mut value = 0u64;
353 for &c in &s {
354 debug_assert!(c <= 9, "Bad number");
355 value = value * 10 + c as u64;
356 }
357 value
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363
364 fn log_spec() -> SliderSpec {
365 SliderSpec {
366 logarithmic: true,
367 smallest_positive: 1e-6,
368 largest_finite: f64::INFINITY,
369 }
370 }
371
372 #[test]
373 fn linear_mapping_roundtrips() {
374 let spec = SliderSpec::default();
375 for &(v, n) in &[(0.0, 0.0), (5.0, 0.5), (10.0, 1.0), (2.5, 0.25)] {
376 assert!((normalized_from_value(v, 0.0, 10.0, &spec) - n).abs() < 1e-9);
377 assert!((value_from_normalized(n, 0.0, 10.0, &spec) - v).abs() < 1e-9);
378 }
379 }
380
381 #[test]
382 fn linear_clamps_outside_range() {
383 let spec = SliderSpec::default();
384 assert_eq!(normalized_from_value(-5.0, 0.0, 10.0, &spec), 0.0);
385 assert_eq!(normalized_from_value(50.0, 0.0, 10.0, &spec), 1.0);
386 }
387
388 #[test]
389 fn logarithmic_midpoint_is_geometric_mean() {
390 let spec = log_spec();
391 let mid = value_from_normalized(0.5, 1.0, 100.0, &spec);
393 assert!((mid - 10.0).abs() < 1e-6, "got {mid}");
394 let n = normalized_from_value(10.0, 1.0, 100.0, &spec);
396 assert!((n - 0.5).abs() < 1e-9, "got {n}");
397 }
398
399 #[test]
400 fn logarithmic_reversed_range() {
401 let spec = log_spec();
402 let v = value_from_normalized(0.0, 100.0, 1.0, &spec);
404 assert!((v - 100.0).abs() < 1e-9, "got {v}");
405 }
406
407 #[test]
408 fn logarithmic_spanning_zero_puts_zero_at_cutoff() {
409 let spec = log_spec();
410 let cutoff = logarithmic_zero_cutoff(-1000.0, 1000.0);
412 assert!((cutoff - 0.5).abs() < 1e-9, "got {cutoff}");
413 let v = value_from_normalized(0.5, -1000.0, 1000.0, &spec);
414 assert!(v.abs() < 1.0, "expected near zero, got {v}");
415 }
416
417 #[test]
418 fn logarithmic_spanning_infinity() {
419 let spec = log_spec();
420 assert_eq!(value_from_normalized(1.0, 0.0, f64::INFINITY, &spec), f64::INFINITY);
422 assert_eq!(value_from_normalized(0.0, 0.0, f64::INFINITY, &spec), 0.0);
423 let mid = value_from_normalized(0.5, 0.0, f64::INFINITY, &spec);
425 assert!(mid.is_finite() && mid > 0.0, "got {mid}");
426 }
427
428 #[test]
434 fn infinite_range_initial_mapping_is_finite() {
435 let spec = log_spec();
436 let n = normalized_from_value(10.0, 0.0, 10000.0, &spec);
438 assert!(n.is_finite() && (0.0..=1.0).contains(&n), "demo n={n}");
439
440 for &v in &[0.0, 10000.0] {
443 let n = normalized_from_value(v, -f64::INFINITY, f64::INFINITY, &spec);
444 assert!(n.is_finite() && (0.0..=1.0).contains(&n), "v={v} n={n}");
445 }
446 let mid = value_from_normalized(0.5, -f64::INFINITY, f64::INFINITY, &spec);
448 assert!(mid.is_finite(), "mid={mid}");
449 }
450
451 #[test]
455 fn one_infinite_bound_roundtrips_finite() {
456 let spec = log_spec();
457 let n = normalized_from_value(10.0, -f64::INFINITY, 10000.0, &spec);
458 assert!(n.is_finite() && (0.0..=1.0).contains(&n), "n={n}");
459 let v = value_from_normalized(n, -f64::INFINITY, 10000.0, &spec);
460 assert!(v.is_finite(), "v={v}");
461
462 let n = normalized_from_value(10.0, 0.0, f64::INFINITY, &spec);
463 assert!(n.is_finite() && (0.0..=1.0).contains(&n), "n={n}");
464 }
465
466 #[test]
467 fn clamp_handles_reversed_range() {
468 assert_eq!(clamp_value_to_range(5.0, 10.0, 0.0), 5.0);
469 assert_eq!(clamp_value_to_range(-1.0, 10.0, 0.0), 0.0);
470 assert_eq!(clamp_value_to_range(11.0, 10.0, 0.0), 10.0);
471 }
472
473 #[test]
475 fn smart_aim_round_numbers() {
476 assert_eq!(best_in_range_f64(-0.2, 0.0), 0.0);
477 assert_eq!(best_in_range_f64(-10_004.23, 3.14), 0.0);
478 assert_eq!(best_in_range_f64(7.8, 17.8), 10.0);
479 assert_eq!(best_in_range_f64(99.0, 300.0), 100.0);
480 assert_eq!(best_in_range_f64(-99.0, -300.0), -100.0);
481 assert_eq!(best_in_range_f64(0.4, 0.9), 0.5);
482 assert_eq!(best_in_range_f64(14.1, 19.99), 15.0);
483 assert_eq!(best_in_range_f64(12.3, 65.9), 50.0);
484 assert_eq!(best_in_range_f64(493.0, 879.0), 500.0);
485 assert_eq!(best_in_range_f64(0.37, 0.48), 0.40);
486 assert_eq!(best_in_range_f64(7.5, 16.3), 10.0);
487 assert_eq!(best_in_range_f64(7.5, 763.3), 100.0);
488 assert_eq!(best_in_range_f64(7.5, 123_456.0), 1000.0);
489 assert_eq!(best_in_range_f64(9.9999, 99.999), 10.0);
490 assert_eq!(best_in_range_f64(10.001, 99.999), 50.0);
491 }
492
493 #[test]
494 fn smart_aim_integers() {
495 assert_eq!(best_in_range_f64(99.0, 300.0), 100.0);
496 assert_eq!(best_in_range_f64(4.0, 9.0), 5.0);
497 assert_eq!(best_in_range_f64(14.0, 19.0), 15.0);
498 assert_eq!(best_in_range_f64(12.0, 65.0), 50.0);
499 assert_eq!(best_in_range_f64(37.0, 48.0), 40.0);
500 assert_eq!(best_in_range_f64(12345.0, 12780.0), 12500.0);
501 }
502
503 #[test]
504 fn smart_aim_nan_and_infinity() {
505 assert!(best_in_range_f64(f64::NAN, f64::NAN).is_nan());
506 assert_eq!(best_in_range_f64(f64::NAN, 1.2), 1.2);
507 assert_eq!(best_in_range_f64(1.2, f64::INFINITY), 1.2);
508 assert_eq!(best_in_range_f64(f64::NEG_INFINITY, 1.2), 0.0);
509 assert_eq!(best_in_range_f64(f64::NEG_INFINITY, -2.7), -2.7);
510 assert_eq!(best_in_range_f64(f64::NEG_INFINITY, f64::INFINITY), 0.0);
511 }
512}