1#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
20#![allow(clippy::float_cmp)]
23
24use std::ops::{Add, Div, Mul, RangeInclusive, Sub};
25
26pub mod align;
29pub mod easing;
30mod gui_rounding;
31mod history;
32mod numeric;
33mod ordered_float;
34mod pos2;
35mod range;
36mod rect;
37mod rect_transform;
38mod rot2;
39pub mod smart_aim;
40mod ts_transform;
41mod vec2;
42mod vec2b;
43
44pub use self::{
45 align::{Align, Align2},
46 gui_rounding::{GuiRounding, GUI_ROUNDING},
47 history::History,
48 numeric::*,
49 ordered_float::*,
50 pos2::*,
51 range::Rangef,
52 rect::*,
53 rect_transform::*,
54 rot2::*,
55 ts_transform::*,
56 vec2::*,
57 vec2b::*,
58};
59
60pub trait One {
64 const ONE: Self;
65}
66
67impl One for f32 {
68 const ONE: Self = 1.0;
69}
70
71impl One for f64 {
72 const ONE: Self = 1.0;
73}
74
75pub trait Real:
77 Copy
78 + PartialEq
79 + PartialOrd
80 + One
81 + Add<Self, Output = Self>
82 + Sub<Self, Output = Self>
83 + Mul<Self, Output = Self>
84 + Div<Self, Output = Self>
85{
86}
87
88impl Real for f32 {}
89
90impl Real for f64 {}
91
92#[inline(always)]
104pub fn lerp<R, T>(range: impl Into<RangeInclusive<R>>, t: T) -> R
105where
106 T: Real + Mul<R, Output = R>,
107 R: Copy + Add<R, Output = R>,
108{
109 let range = range.into();
110 (T::ONE - t) * *range.start() + t * *range.end()
111}
112
113#[inline]
128pub fn inverse_lerp<R>(range: RangeInclusive<R>, value: R) -> Option<R>
129where
130 R: Copy + PartialEq + Sub<R, Output = R> + Div<R, Output = R>,
131{
132 let min = *range.start();
133 let max = *range.end();
134 if min == max {
135 None
136 } else {
137 Some((value - min) / (max - min))
138 }
139}
140
141pub fn remap<T>(x: T, from: impl Into<RangeInclusive<T>>, to: impl Into<RangeInclusive<T>>) -> T
145where
146 T: Real,
147{
148 let from = from.into();
149 let to = to.into();
150 debug_assert!(from.start() != from.end());
151 let t = (x - *from.start()) / (*from.end() - *from.start());
152 lerp(to, t)
153}
154
155pub fn remap_clamp<T>(
157 x: T,
158 from: impl Into<RangeInclusive<T>>,
159 to: impl Into<RangeInclusive<T>>,
160) -> T
161where
162 T: Real,
163{
164 let from = from.into();
165 let to = to.into();
166 if from.end() < from.start() {
167 return remap_clamp(x, *from.end()..=*from.start(), *to.end()..=*to.start());
168 }
169 if x <= *from.start() {
170 *to.start()
171 } else if *from.end() <= x {
172 *to.end()
173 } else {
174 debug_assert!(from.start() != from.end());
175 let t = (x - *from.start()) / (*from.end() - *from.start());
176 if T::ONE <= t {
178 *to.end()
179 } else {
180 lerp(to, t)
181 }
182 }
183}
184
185pub fn round_to_decimals(value: f64, decimal_places: usize) -> f64 {
187 format!("{value:.decimal_places$}").parse().unwrap_or(value)
189}
190
191pub fn format_with_minimum_decimals(value: f64, decimals: usize) -> String {
192 format_with_decimals_in_range(value, decimals..=6)
193}
194
195pub fn format_with_decimals_in_range(value: f64, decimal_range: RangeInclusive<usize>) -> String {
199 let min_decimals = *decimal_range.start();
200 let max_decimals = *decimal_range.end();
201 debug_assert!(min_decimals <= max_decimals);
202 debug_assert!(max_decimals < 100);
203 let max_decimals = max_decimals.min(16);
204 let min_decimals = min_decimals.min(max_decimals);
205
206 if min_decimals < max_decimals {
207 for decimals in min_decimals..max_decimals {
209 let text = format!("{value:.decimals$}");
210 let epsilon = 16.0 * f32::EPSILON; if almost_equal(text.parse::<f32>().unwrap(), value as f32, epsilon) {
212 return text;
214 }
215 }
216 }
220 format!("{value:.max_decimals$}")
221}
222
223pub fn almost_equal(a: f32, b: f32, epsilon: f32) -> bool {
229 if a == b {
230 true } else {
232 let abs_max = a.abs().max(b.abs());
233 abs_max <= epsilon || ((a - b).abs() / abs_max) <= epsilon
234 }
235}
236
237#[allow(clippy::approx_constant)]
238#[test]
239fn test_format() {
240 assert_eq!(format_with_minimum_decimals(1_234_567.0, 0), "1234567");
241 assert_eq!(format_with_minimum_decimals(1_234_567.0, 1), "1234567.0");
242 assert_eq!(format_with_minimum_decimals(3.14, 2), "3.14");
243 assert_eq!(format_with_minimum_decimals(3.14, 3), "3.140");
244 assert_eq!(
245 format_with_minimum_decimals(std::f64::consts::PI, 2),
246 "3.14159"
247 );
248}
249
250#[test]
251fn test_almost_equal() {
252 for &x in &[
253 0.0_f32,
254 f32::MIN_POSITIVE,
255 1e-20,
256 1e-10,
257 f32::EPSILON,
258 0.1,
259 0.99,
260 1.0,
261 1.001,
262 1e10,
263 f32::MAX / 100.0,
264 f32::INFINITY,
266 ] {
267 for &x in &[-x, x] {
268 for roundtrip in &[
269 |x: f32| x.to_degrees().to_radians(),
270 |x: f32| x.to_radians().to_degrees(),
271 ] {
272 let epsilon = f32::EPSILON;
273 assert!(
274 almost_equal(x, roundtrip(x), epsilon),
275 "{} vs {}",
276 x,
277 roundtrip(x)
278 );
279 }
280 }
281 }
282}
283
284#[test]
285fn test_remap() {
286 assert_eq!(remap_clamp(1.0, 0.0..=1.0, 0.0..=16.0), 16.0);
287 assert_eq!(remap_clamp(1.0, 1.0..=0.0, 16.0..=0.0), 16.0);
288 assert_eq!(remap_clamp(0.5, 1.0..=0.0, 16.0..=0.0), 8.0);
289}
290
291pub trait NumExt {
295 #[must_use]
297 fn at_least(self, lower_limit: Self) -> Self;
298
299 #[must_use]
301 fn at_most(self, upper_limit: Self) -> Self;
302}
303
304macro_rules! impl_num_ext {
305 ($t: ty) => {
306 impl NumExt for $t {
307 #[inline(always)]
308 fn at_least(self, lower_limit: Self) -> Self {
309 self.max(lower_limit)
310 }
311
312 #[inline(always)]
313 fn at_most(self, upper_limit: Self) -> Self {
314 self.min(upper_limit)
315 }
316 }
317 };
318}
319
320impl_num_ext!(u8);
321impl_num_ext!(u16);
322impl_num_ext!(u32);
323impl_num_ext!(u64);
324impl_num_ext!(u128);
325impl_num_ext!(usize);
326impl_num_ext!(i8);
327impl_num_ext!(i16);
328impl_num_ext!(i32);
329impl_num_ext!(i64);
330impl_num_ext!(i128);
331impl_num_ext!(isize);
332impl_num_ext!(f32);
333impl_num_ext!(f64);
334impl_num_ext!(Vec2);
335impl_num_ext!(Pos2);
336
337pub fn normalized_angle(mut angle: f32) -> f32 {
341 use std::f32::consts::{PI, TAU};
342 angle %= TAU;
343 if angle > PI {
344 angle -= TAU;
345 } else if angle < -PI {
346 angle += TAU;
347 }
348 angle
349}
350
351#[test]
352fn test_normalized_angle() {
353 macro_rules! almost_eq {
354 ($left: expr, $right: expr) => {
355 let left = $left;
356 let right = $right;
357 assert!((left - right).abs() < 1e-6, "{} != {}", left, right);
358 };
359 }
360
361 use std::f32::consts::TAU;
362 almost_eq!(normalized_angle(-3.0 * TAU), 0.0);
363 almost_eq!(normalized_angle(-2.3 * TAU), -0.3 * TAU);
364 almost_eq!(normalized_angle(-TAU), 0.0);
365 almost_eq!(normalized_angle(0.0), 0.0);
366 almost_eq!(normalized_angle(TAU), 0.0);
367 almost_eq!(normalized_angle(2.7 * TAU), -0.3 * TAU);
368}
369
370pub fn exponential_smooth_factor(
385 reach_this_fraction: f32,
386 in_this_many_seconds: f32,
387 dt: f32,
388) -> f32 {
389 1.0 - (1.0 - reach_this_fraction).powf(dt / in_this_many_seconds)
390}
391
392pub fn interpolation_factor(
413 (start_time, end_time): (f64, f64),
414 current_time: f64,
415 dt: f32,
416 easing: impl Fn(f32) -> f32,
417) -> f32 {
418 let animation_duration = (end_time - start_time) as f32;
419 let prev_time = current_time - dt as f64;
420 let prev_t = easing((prev_time - start_time) as f32 / animation_duration);
421 let end_t = easing((current_time - start_time) as f32 / animation_duration);
422 if end_t < 1.0 {
423 (end_t - prev_t) / (1.0 - prev_t)
424 } else {
425 1.0
426 }
427}
428
429#[inline]
433pub fn ease_in_ease_out(t: f32) -> f32 {
434 let t = t.clamp(0.0, 1.0);
435 (3.0 * t * t - 2.0 * t * t * t).clamp(0.0, 1.0)
436}