egui/emath/src/lib.rs

385 lines
11 KiB
Rust
Raw Normal View History

2021-01-17 09:52:01 +00:00
//! Opinionated 2D math library for building GUIs.
//!
//! Includes vectors, positions, rectangles etc.
//!
//! Conventions (unless otherwise specified):
2021-01-10 10:37:47 +00:00
//!
//! * All angles are in radians
2021-01-10 10:37:47 +00:00
//! * X+ is right and Y+ is down.
//! * (0,0) is left top.
//! * Dimension order is always `x y`
// Forbid warnings in release builds:
#![cfg_attr(not(debug_assertions), deny(warnings))]
2021-01-10 10:37:47 +00:00
#![forbid(unsafe_code)]
#![warn(
clippy::all,
clippy::await_holding_lock,
2021-05-09 12:13:09 +00:00
clippy::char_lit_as_u8,
clippy::checked_conversions,
2021-01-10 10:37:47 +00:00
clippy::dbg_macro,
2021-03-23 18:53:31 +00:00
clippy::debug_assert_with_mut_call,
2021-10-20 14:30:39 +00:00
clippy::disallowed_method,
2021-01-10 10:37:47 +00:00
clippy::doc_markdown,
clippy::empty_enum,
clippy::enum_glob_use,
clippy::exit,
2021-05-09 12:13:09 +00:00
clippy::expl_impl_clone_on_copy,
clippy::explicit_deref_methods,
2021-03-23 18:53:31 +00:00
clippy::explicit_into_iter_loop,
2021-05-09 12:13:09 +00:00
clippy::fallible_impl_from,
2021-01-10 10:37:47 +00:00
clippy::filter_map_next,
2021-10-20 14:30:39 +00:00
clippy::flat_map_option,
2021-05-09 12:13:09 +00:00
clippy::float_cmp_const,
2021-01-10 10:37:47 +00:00
clippy::fn_params_excessive_bools,
2021-10-20 14:30:39 +00:00
clippy::from_iter_instead_of_collect,
2021-01-10 10:37:47 +00:00
clippy::if_let_mutex,
2021-10-20 14:30:39 +00:00
clippy::implicit_clone,
2021-01-10 10:37:47 +00:00
clippy::imprecise_flops,
clippy::inefficient_to_string,
2021-05-09 12:13:09 +00:00
clippy::invalid_upcast_comparisons,
2021-10-20 14:30:39 +00:00
clippy::large_digit_groups,
clippy::large_stack_arrays,
2021-03-23 18:53:31 +00:00
clippy::large_types_passed_by_value,
clippy::let_unit_value,
2021-01-10 10:37:47 +00:00
clippy::linkedlist,
clippy::lossy_float_literal,
clippy::macro_use_imports,
2021-05-09 12:13:09 +00:00
clippy::manual_ok_or,
2021-03-23 18:53:31 +00:00
clippy::map_err_ignore,
clippy::map_flatten,
2021-10-20 14:30:39 +00:00
clippy::map_unwrap_or,
2021-01-10 10:37:47 +00:00
clippy::match_on_vec_items,
2021-03-23 18:53:31 +00:00
clippy::match_same_arms,
2021-10-20 14:30:39 +00:00
clippy::match_wild_err_arm,
2021-01-10 10:37:47 +00:00
clippy::match_wildcard_for_single_variants,
clippy::mem_forget,
clippy::mismatched_target_os,
clippy::missing_errors_doc,
clippy::missing_safety_doc,
2021-05-09 12:13:09 +00:00
clippy::mut_mut,
clippy::mutex_integer,
2021-01-10 10:37:47 +00:00
clippy::needless_borrow,
clippy::needless_continue,
2021-10-20 14:30:39 +00:00
clippy::needless_for_each,
2021-01-10 10:37:47 +00:00
clippy::needless_pass_by_value,
clippy::option_option,
2021-05-09 12:13:09 +00:00
clippy::path_buf_push_overwrite,
clippy::ptr_as_ptr,
2021-03-23 18:53:31 +00:00
clippy::ref_option_ref,
2021-01-10 10:37:47 +00:00
clippy::rest_pat_in_fully_bound_structs,
2021-05-09 12:13:09 +00:00
clippy::same_functions_in_if_condition,
2021-10-20 14:30:39 +00:00
clippy::semicolon_if_nothing_returned,
clippy::single_match_else,
2021-03-23 18:53:31 +00:00
clippy::string_add_assign,
clippy::string_add,
2021-05-09 12:13:09 +00:00
clippy::string_lit_as_bytes,
2021-03-23 18:53:31 +00:00
clippy::string_to_string,
2021-01-10 10:37:47 +00:00
clippy::todo,
2021-05-09 12:13:09 +00:00
clippy::trait_duplication_in_bounds,
2021-01-10 10:37:47 +00:00
clippy::unimplemented,
clippy::unnested_or_patterns,
2021-03-23 18:53:31 +00:00
clippy::unused_self,
2021-05-09 12:13:09 +00:00
clippy::useless_transmute,
2021-01-10 10:37:47 +00:00
clippy::verbose_file_reads,
2021-05-09 12:13:09 +00:00
clippy::zero_sized_map_values,
2021-01-10 10:37:47 +00:00
future_incompatible,
nonstandard_style,
rust_2018_idioms,
rustdoc::missing_crate_level_docs
2021-01-10 10:37:47 +00:00
)]
#![allow(clippy::float_cmp)]
2021-01-10 10:37:47 +00:00
#![allow(clippy::manual_range_contains)]
2020-08-09 15:24:32 +00:00
use std::ops::{Add, Div, Mul, RangeInclusive, Sub};
// ----------------------------------------------------------------------------
2021-01-10 09:54:34 +00:00
pub mod align;
2021-03-27 15:03:11 +00:00
mod numeric;
mod pos2;
mod rect;
mod rect_transform;
2020-12-25 11:22:10 +00:00
mod rot2;
pub mod smart_aim;
mod vec2;
2020-04-21 05:39:23 +00:00
2021-01-10 09:54:34 +00:00
pub use {
align::{Align, Align2},
2021-03-27 15:03:11 +00:00
numeric::*,
2021-01-10 09:54:34 +00:00
pos2::*,
rect::*,
rect_transform::*,
2021-01-10 09:54:34 +00:00
rot2::*,
vec2::*,
};
2020-04-21 05:39:23 +00:00
2019-01-19 16:09:00 +00:00
// ----------------------------------------------------------------------------
/// Helper trait to implement [`lerp`] and [`remap`].
pub trait One {
fn one() -> Self;
}
impl One for f32 {
#[inline(always)]
fn one() -> Self {
1.0
}
}
impl One for f64 {
#[inline(always)]
fn one() -> Self {
1.0
}
}
/// Helper trait to implement [`lerp`] and [`remap`].
pub trait Real:
Copy
+ PartialEq
+ PartialOrd
+ One
+ Add<Self, Output = Self>
+ Sub<Self, Output = Self>
+ Mul<Self, Output = Self>
+ Div<Self, Output = Self>
{
}
impl Real for f32 {}
impl Real for f64 {}
// ----------------------------------------------------------------------------
2020-08-09 15:24:32 +00:00
/// Linear interpolation.
#[inline(always)]
pub fn lerp<R, T>(range: RangeInclusive<R>, t: T) -> R
2019-04-25 16:07:36 +00:00
where
T: Real + Mul<R, Output = R>,
R: Copy + Add<R, Output = R>,
2019-04-25 16:07:36 +00:00
{
(T::one() - t) * *range.start() + t * *range.end()
2018-12-26 16:01:46 +00:00
}
2020-08-09 15:24:32 +00:00
/// Linearly remap a value from one range to another,
/// so that when `x == from.start()` returns `to.start()`
/// and when `x == from.end()` returns `to.end()`.
pub fn remap<T>(x: T, from: RangeInclusive<T>, to: RangeInclusive<T>) -> T
where
T: Real,
{
crate::emath_assert!(from.start() != from.end());
let t = (x - *from.start()) / (*from.end() - *from.start());
2020-04-25 09:14:32 +00:00
lerp(to, t)
2019-01-05 14:28:07 +00:00
}
2021-01-17 09:52:01 +00:00
/// Like [`remap`], but also clamps the value so that the returned value is always in the `to` range.
pub fn remap_clamp<T>(x: T, from: RangeInclusive<T>, to: RangeInclusive<T>) -> T
where
T: Real,
{
if from.end() < from.start() {
return remap_clamp(x, *from.end()..=*from.start(), *to.end()..=*to.start());
}
2020-05-08 19:31:27 +00:00
if x <= *from.start() {
*to.start()
} else if *from.end() <= x {
*to.end()
2018-12-26 16:01:46 +00:00
} else {
crate::emath_assert!(from.start() != from.end());
let t = (x - *from.start()) / (*from.end() - *from.start());
// Ensure no numerical inaccuracies sneak in:
if T::one() <= t {
2020-05-08 19:31:27 +00:00
*to.end()
} else {
lerp(to, t)
}
}
2018-12-26 16:01:46 +00:00
}
2019-01-05 14:28:07 +00:00
2020-08-09 15:24:32 +00:00
/// Round a value to the given number of decimal places.
pub fn round_to_decimals(value: f64, decimal_places: usize) -> f64 {
// This is a stupid way of doing this, but stupid works.
2020-08-09 15:24:32 +00:00
format!("{:.*}", decimal_places, value)
.parse()
2020-11-20 11:28:24 +00:00
.unwrap_or(value)
}
2021-01-10 10:37:47 +00:00
pub fn format_with_minimum_decimals(value: f64, decimals: usize) -> String {
format_with_decimals_in_range(value, decimals..=6)
}
2021-01-10 10:37:47 +00:00
pub fn format_with_decimals_in_range(value: f64, decimal_range: RangeInclusive<usize>) -> String {
let min_decimals = *decimal_range.start();
let max_decimals = *decimal_range.end();
crate::emath_assert!(min_decimals <= max_decimals);
crate::emath_assert!(max_decimals < 100);
let max_decimals = max_decimals.min(16);
let min_decimals = min_decimals.min(max_decimals);
2021-06-22 21:25:54 +00:00
if min_decimals != max_decimals {
// Ugly/slow way of doing this. TODO: clean up precision.
for decimals in min_decimals..max_decimals {
let text = format!("{:.*}", decimals, value);
let epsilon = 16.0 * f32::EPSILON; // margin large enough to handle most peoples round-tripping needs
if almost_equal(text.parse::<f32>().unwrap(), value as f32, epsilon) {
// Enough precision to show the value accurately - good!
return text;
}
}
// The value has more precision than we expected.
// Probably the value was set not by the slider, but from outside.
// In any case: show the full value
}
2021-06-22 21:25:54 +00:00
format!("{:.*}", max_decimals, value)
}
/// Return true when arguments are the same within some rounding error.
///
/// For instance `almost_equal(x, x.to_degrees().to_radians(), f32::EPSILON)` should hold true for all x.
/// The `epsilon` can be `f32::EPSILON` to handle simple transforms (like degrees -> radians)
/// but should be higher to handle more complex transformations.
pub fn almost_equal(a: f32, b: f32, epsilon: f32) -> bool {
if a == b {
true // handle infinites
} else {
let abs_max = a.abs().max(b.abs());
abs_max <= epsilon || ((a - b).abs() / abs_max) <= epsilon
}
}
2020-10-17 09:00:58 +00:00
#[allow(clippy::approx_constant)]
#[test]
fn test_format() {
assert_eq!(format_with_minimum_decimals(1_234_567.0, 0), "1234567");
assert_eq!(format_with_minimum_decimals(1_234_567.0, 1), "1234567.0");
assert_eq!(format_with_minimum_decimals(3.14, 2), "3.14");
assert_eq!(format_with_minimum_decimals(3.14, 3), "3.140");
assert_eq!(
format_with_minimum_decimals(std::f64::consts::PI, 2),
"3.14159"
);
}
#[test]
fn test_almost_equal() {
for &x in &[
0.0_f32,
f32::MIN_POSITIVE,
1e-20,
1e-10,
f32::EPSILON,
0.1,
0.99,
1.0,
1.001,
1e10,
f32::MAX / 100.0,
// f32::MAX, // overflows in rad<->deg test
f32::INFINITY,
] {
for &x in &[-x, x] {
for roundtrip in &[
|x: f32| x.to_degrees().to_radians(),
|x: f32| x.to_radians().to_degrees(),
] {
let epsilon = f32::EPSILON;
assert!(
almost_equal(x, roundtrip(x), epsilon),
"{} vs {}",
x,
roundtrip(x)
);
}
}
}
}
#[test]
fn test_remap() {
assert_eq!(remap_clamp(1.0, 0.0..=1.0, 0.0..=16.0), 16.0);
assert_eq!(remap_clamp(1.0, 1.0..=0.0, 16.0..=0.0), 16.0);
assert_eq!(remap_clamp(0.5, 1.0..=0.0, 16.0..=0.0), 8.0);
}
// ----------------------------------------------------------------------------
/// Extends `f32`, `Vec2` etc with `at_least` and `at_most` as aliases for `max` and `min`.
pub trait NumExt {
/// More readable version of `self.max(lower_limit)`
fn at_least(self, lower_limit: Self) -> Self;
/// More readable version of `self.min(upper_limit)`
fn at_most(self, upper_limit: Self) -> Self;
}
macro_rules! impl_num_ext {
($t: ty) => {
impl NumExt for $t {
#[inline(always)]
fn at_least(self, lower_limit: Self) -> Self {
self.max(lower_limit)
}
#[inline(always)]
fn at_most(self, upper_limit: Self) -> Self {
self.min(upper_limit)
}
}
};
}
impl_num_ext!(f32);
impl_num_ext!(f64);
impl_num_ext!(usize);
impl_num_ext!(Vec2);
impl_num_ext!(Pos2);
2021-05-08 21:42:17 +00:00
// ----------------------------------------------------------------------------
/// Wrap angle to `[-PI, PI]` range.
pub fn normalized_angle(mut angle: f32) -> f32 {
use std::f32::consts::{PI, TAU};
angle %= TAU;
if angle > PI {
angle -= TAU;
} else if angle < -PI {
angle += TAU;
}
angle
}
#[test]
fn test_normalized_angle() {
macro_rules! almost_eq {
2021-10-02 19:08:00 +00:00
($left: expr, $right: expr) => {
2021-05-08 21:42:17 +00:00
let left = $left;
let right = $right;
assert!((left - right).abs() < 1e-6, "{} != {}", left, right);
};
}
use std::f32::consts::TAU;
almost_eq!(normalized_angle(-3.0 * TAU), 0.0);
almost_eq!(normalized_angle(-2.3 * TAU), -0.3 * TAU);
almost_eq!(normalized_angle(-TAU), 0.0);
almost_eq!(normalized_angle(0.0), 0.0);
almost_eq!(normalized_angle(TAU), 0.0);
almost_eq!(normalized_angle(2.7 * TAU), -0.3 * TAU);
}
// ----------------------------------------------------------------------------
/// An assert that is only active when `emath` is compiled with the `extra_asserts` feature
/// or with the `extra_debug_asserts` feature in debug builds.
#[macro_export]
macro_rules! emath_assert {
2021-10-02 19:08:00 +00:00
($($arg: tt)*) => {
if cfg!(any(
feature = "extra_asserts",
all(feature = "extra_debug_asserts", debug_assertions),
)) {
assert!($($arg)*);
}
}
}