1#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
20#![expect(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_align;
38mod rect_transform;
39mod rot2;
40pub mod smart_aim;
41mod ts_transform;
42mod vec2;
43mod vec2b;
44
45pub use self::{
46 align::{Align, Align2},
47 gui_rounding::{GUI_ROUNDING, GuiRounding},
48 history::History,
49 numeric::*,
50 ordered_float::*,
51 pos2::*,
52 range::Rangef,
53 rect::*,
54 rect_align::RectAlign,
55 rect_transform::*,
56 rot2::*,
57 ts_transform::*,
58 vec2::*,
59 vec2b::*,
60};
61
62pub trait One {
66 const ONE: Self;
67}
68
69impl One for f32 {
70 const ONE: Self = 1.0;
71}
72
73impl One for f64 {
74 const ONE: Self = 1.0;
75}
76
77pub trait Real:
79 Copy
80 + PartialEq
81 + PartialOrd
82 + One
83 + Add<Self, Output = Self>
84 + Sub<Self, Output = Self>
85 + Mul<Self, Output = Self>
86 + Div<Self, Output = Self>
87{
88}
89
90impl Real for f32 {}
91
92impl Real for f64 {}
93
94#[inline(always)]
106pub fn lerp<R, T>(range: impl Into<RangeInclusive<R>>, t: T) -> R
107where
108 T: Real + Mul<R, Output = R>,
109 R: Copy + Add<R, Output = R>,
110{
111 let range = range.into();
112 (T::ONE - t) * *range.start() + t * *range.end()
113}
114
115#[inline(always)]
122pub fn fast_midpoint<R>(a: R, b: R) -> R
123where
124 R: Copy + Add<R, Output = R> + Div<R, Output = R> + One,
125{
126 let two = R::ONE + R::ONE;
127 (a + b) / two
128}
129
130#[inline]
145pub fn inverse_lerp<R>(range: RangeInclusive<R>, value: R) -> Option<R>
146where
147 R: Copy + PartialEq + Sub<R, Output = R> + Div<R, Output = R>,
148{
149 let min = *range.start();
150 let max = *range.end();
151 if min == max {
152 None
153 } else {
154 Some((value - min) / (max - min))
155 }
156}
157
158pub fn remap<T>(x: T, from: impl Into<RangeInclusive<T>>, to: impl Into<RangeInclusive<T>>) -> T
162where
163 T: Real,
164{
165 let from = from.into();
166 let to = to.into();
167 debug_assert!(
168 from.start() != from.end(),
169 "from.start() and from.end() should not be equal"
170 );
171 let t = (x - *from.start()) / (*from.end() - *from.start());
172 lerp(to, t)
173}
174
175pub fn remap_clamp<T>(
177 x: T,
178 from: impl Into<RangeInclusive<T>>,
179 to: impl Into<RangeInclusive<T>>,
180) -> T
181where
182 T: Real,
183{
184 let from = from.into();
185 let to = to.into();
186 if from.end() < from.start() {
187 return remap_clamp(x, *from.end()..=*from.start(), *to.end()..=*to.start());
188 }
189 if x <= *from.start() {
190 *to.start()
191 } else if *from.end() <= x {
192 *to.end()
193 } else {
194 debug_assert!(
195 from.start() != from.end(),
196 "from.start() and from.end() should not be equal"
197 );
198 let t = (x - *from.start()) / (*from.end() - *from.start());
199 if T::ONE <= t { *to.end() } else { lerp(to, t) }
201 }
202}
203
204pub fn round_to_decimals(value: f64, decimal_places: usize) -> f64 {
206 format!("{value:.decimal_places$}").parse().unwrap_or(value)
208}
209
210pub fn format_with_minimum_decimals(value: f64, decimals: usize) -> String {
211 format_with_decimals_in_range(value, decimals..=6)
212}
213
214pub fn format_with_decimals_in_range(value: f64, decimal_range: RangeInclusive<usize>) -> String {
218 let min_decimals = *decimal_range.start();
219 let max_decimals = *decimal_range.end();
220 debug_assert!(
221 min_decimals <= max_decimals,
222 "min_decimals should be <= max_decimals, but got min_decimals: {min_decimals}, max_decimals: {max_decimals}"
223 );
224 debug_assert!(
225 max_decimals < 100,
226 "max_decimals should be < 100, but got {max_decimals}"
227 );
228 let max_decimals = max_decimals.min(16);
229 let min_decimals = min_decimals.min(max_decimals);
230
231 if min_decimals < max_decimals {
232 for decimals in min_decimals..max_decimals {
234 let text = format!("{value:.decimals$}");
235 let epsilon = 16.0 * f32::EPSILON; if let Ok(parsed_value) = text.parse::<f32>()
237 && almost_equal(parsed_value, value as f32, epsilon)
238 {
239 return text;
241 }
242 }
243 }
247 format!("{value:.max_decimals$}")
248}
249
250pub fn almost_equal(a: f32, b: f32, epsilon: f32) -> bool {
256 if a == b {
257 true } else {
259 let abs_max = a.abs().max(b.abs());
260 abs_max <= epsilon || ((a - b).abs() / abs_max) <= epsilon
261 }
262}
263
264#[expect(clippy::approx_constant)]
265#[test]
266fn test_format() {
267 assert_eq!(format_with_minimum_decimals(1_234_567.0, 0), "1234567");
268 assert_eq!(format_with_minimum_decimals(1_234_567.0, 1), "1234567.0");
269 assert_eq!(format_with_minimum_decimals(3.14, 2), "3.14");
270 assert_eq!(format_with_minimum_decimals(3.14, 3), "3.140");
271 assert_eq!(
272 format_with_minimum_decimals(std::f64::consts::PI, 2),
273 "3.14159"
274 );
275}
276
277#[test]
278fn test_almost_equal() {
279 for &x in &[
280 0.0_f32,
281 f32::MIN_POSITIVE,
282 1e-20,
283 1e-10,
284 f32::EPSILON,
285 0.1,
286 0.99,
287 1.0,
288 1.001,
289 1e10,
290 f32::MAX / 100.0,
291 f32::INFINITY,
293 ] {
294 for &x in &[-x, x] {
295 for roundtrip in &[
296 |x: f32| x.to_degrees().to_radians(),
297 |x: f32| x.to_radians().to_degrees(),
298 ] {
299 let epsilon = f32::EPSILON;
300 assert!(
301 almost_equal(x, roundtrip(x), epsilon),
302 "{} vs {}",
303 x,
304 roundtrip(x)
305 );
306 }
307 }
308 }
309}
310
311#[test]
312fn test_remap() {
313 assert_eq!(remap_clamp(1.0, 0.0..=1.0, 0.0..=16.0), 16.0);
314 assert_eq!(remap_clamp(1.0, 1.0..=0.0, 16.0..=0.0), 16.0);
315 assert_eq!(remap_clamp(0.5, 1.0..=0.0, 16.0..=0.0), 8.0);
316}
317
318pub trait NumExt {
322 #[must_use]
324 fn at_least(self, lower_limit: Self) -> Self;
325
326 #[must_use]
328 fn at_most(self, upper_limit: Self) -> Self;
329}
330
331macro_rules! impl_num_ext {
332 ($t: ty) => {
333 impl NumExt for $t {
334 #[inline(always)]
335 fn at_least(self, lower_limit: Self) -> Self {
336 self.max(lower_limit)
337 }
338
339 #[inline(always)]
340 fn at_most(self, upper_limit: Self) -> Self {
341 self.min(upper_limit)
342 }
343 }
344 };
345}
346
347impl_num_ext!(u8);
348impl_num_ext!(u16);
349impl_num_ext!(u32);
350impl_num_ext!(u64);
351impl_num_ext!(u128);
352impl_num_ext!(usize);
353impl_num_ext!(i8);
354impl_num_ext!(i16);
355impl_num_ext!(i32);
356impl_num_ext!(i64);
357impl_num_ext!(i128);
358impl_num_ext!(isize);
359impl_num_ext!(f32);
360impl_num_ext!(f64);
361impl_num_ext!(Vec2);
362impl_num_ext!(Pos2);
363
364pub fn normalized_angle(mut angle: f32) -> f32 {
368 use std::f32::consts::{PI, TAU};
369 angle %= TAU;
370 if angle > PI {
371 angle -= TAU;
372 } else if angle < -PI {
373 angle += TAU;
374 }
375 angle
376}
377
378#[test]
379fn test_normalized_angle() {
380 macro_rules! almost_eq {
381 ($left: expr, $right: expr) => {
382 let left = $left;
383 let right = $right;
384 assert!((left - right).abs() < 1e-6, "{} != {}", left, right);
385 };
386 }
387
388 use std::f32::consts::TAU;
389 almost_eq!(normalized_angle(-3.0 * TAU), 0.0);
390 almost_eq!(normalized_angle(-2.3 * TAU), -0.3 * TAU);
391 almost_eq!(normalized_angle(-TAU), 0.0);
392 almost_eq!(normalized_angle(0.0), 0.0);
393 almost_eq!(normalized_angle(TAU), 0.0);
394 almost_eq!(normalized_angle(2.7 * TAU), -0.3 * TAU);
395}
396
397pub fn exponential_smooth_factor(
412 reach_this_fraction: f32,
413 in_this_many_seconds: f32,
414 dt: f32,
415) -> f32 {
416 1.0 - (1.0 - reach_this_fraction).powf(dt / in_this_many_seconds)
417}
418
419pub fn interpolation_factor(
440 (start_time, end_time): (f64, f64),
441 current_time: f64,
442 dt: f32,
443 easing: impl Fn(f32) -> f32,
444) -> f32 {
445 let animation_duration = (end_time - start_time) as f32;
446 let prev_time = current_time - dt as f64;
447 let prev_t = easing((prev_time - start_time) as f32 / animation_duration);
448 let end_t = easing((current_time - start_time) as f32 / animation_duration);
449 if end_t < 1.0 {
450 (end_t - prev_t) / (1.0 - prev_t)
451 } else {
452 1.0
453 }
454}
455
456#[inline]
460pub fn ease_in_ease_out(t: f32) -> f32 {
461 let t = t.clamp(0.0, 1.0);
462 (3.0 * t * t - 2.0 * t * t * t).clamp(0.0, 1.0)
463}