From 36c15c4e41bbc6d99ad9db48ed986558f112cb3b Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 25 Dec 2020 12:22:10 +0100 Subject: [PATCH] Add egui::math::Rot2 rotation helper --- CHANGELOG.md | 1 + egui/src/containers/collapsing_header.rs | 6 +- egui/src/demos/fractal_clock.rs | 8 +- egui/src/math/mod.rs | 3 +- egui/src/math/rot2.rs | 183 +++++++++++++++++++++++ egui/src/math/vec2.rs | 12 +- egui/src/painter.rs | 18 +-- 7 files changed, 197 insertions(+), 34 deletions(-) create mode 100644 egui/src/math/rot2.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 6189af21..604d7939 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). * Mouse-over explanation to duplicate ID warning. * You can now easily constrain Egui to a portion of the screen using `RawInput::screen_rect`. * You can now control the minimum and maixumum number of decimals to show in a `Slider` or `DragValue`. +* Add `egui::math::Rot2`: rotation helper. ### Changed 🔧 diff --git a/egui/src/containers/collapsing_header.rs b/egui/src/containers/collapsing_header.rs index afa4d416..8c6f6694 100644 --- a/egui/src/containers/collapsing_header.rs +++ b/egui/src/containers/collapsing_header.rs @@ -109,11 +109,9 @@ pub fn paint_icon(ui: &mut Ui, openness: f32, response: &Response) { // Draw a pointy triangle arrow: let rect = Rect::from_center_size(rect.center(), vec2(rect.width(), rect.height()) * 0.75); let mut points = vec![rect.left_top(), rect.right_top(), rect.center_bottom()]; - let rotation = Vec2::angled(remap(openness, 0.0..=1.0, -TAU / 4.0..=0.0)); + let rotation = Rot2::from_angle(remap(openness, 0.0..=1.0, -TAU / 4.0..=0.0)); for p in &mut points { - let v = *p - rect.center(); - let v = rotation.rotate_other(v); - *p = rect.center() + v; + *p = rect.center() + rotation * (*p - rect.center()); } ui.painter().add(PaintCmd::closed_line(points, stroke)); diff --git a/egui/src/demos/fractal_clock.rs b/egui/src/demos/fractal_clock.rs index f2e14114..a170d401 100644 --- a/egui/src/demos/fractal_clock.rs +++ b/egui/src/demos/fractal_clock.rs @@ -141,8 +141,8 @@ impl FractalClock { ]; let hand_rotors = [ - hands[0].length * Vec2::angled(hand_rotations[0]), - hands[1].length * Vec2::angled(hand_rotations[1]), + hands[0].length * Rot2::from_angle(hand_rotations[0]), + hands[1].length * Rot2::from_angle(hand_rotations[1]), ]; #[derive(Clone, Copy)] @@ -179,9 +179,9 @@ impl FractalClock { let luminance_u8 = (255.0 * luminance).round() as u8; - for rotor in &hand_rotors { + for &rotor in &hand_rotors { for a in &nodes { - let new_dir = rotor.rotate_other(a.dir); + let new_dir = rotor * a.dir; let b = Node { pos: a.pos + new_dir, dir: new_dir, diff --git a/egui/src/math/mod.rs b/egui/src/math/mod.rs index f43ca576..dbb5d218 100644 --- a/egui/src/math/mod.rs +++ b/egui/src/math/mod.rs @@ -6,10 +6,11 @@ use std::ops::{Add, Div, Mul, RangeInclusive, Sub}; mod pos2; mod rect; +mod rot2; pub mod smart_aim; mod vec2; -pub use {pos2::*, rect::*, vec2::*}; +pub use {pos2::*, rect::*, rot2::*, vec2::*}; // ---------------------------------------------------------------------------- diff --git a/egui/src/math/rot2.rs b/egui/src/math/rot2.rs new file mode 100644 index 00000000..0782c941 --- /dev/null +++ b/egui/src/math/rot2.rs @@ -0,0 +1,183 @@ +use super::Vec2; + +// {s,c} represents the rotation matrix: +// +// | c -s | +// | s c | +// +// `vec2(c,s)` represents where the X axis will end up after rotation. +// +/// Represents a rotation in the 2D plane. +/// A rotation of 90° rotates the X axis to the Y axis. +/// Normally a `Rot2` is normalized (unit-length). +/// If not, it will also scale vectors. +#[derive(Clone, Copy, PartialEq)] +pub struct Rot2 { + /// angle.sin() + s: f32, + /// angle.cos() + c: f32, +} + +/// Identity rotation +impl Default for Rot2 { + /// Identity rotation + fn default() -> Self { + Self { s: 0.0, c: 1.0 } + } +} + +impl Rot2 { + pub fn identity() -> Self { + Self { s: 0.0, c: 1.0 } + } + + /// A `TAU / 4.0` rotation means rotating the X axis to the Y axis. + pub fn from_angle(angle: f32) -> Self { + let (s, c) = angle.sin_cos(); + Self { s, c } + } + + pub fn angle(self) -> f32 { + self.s.atan2(self.c) + } + + /// The factor by which vectors will be scaled. + pub fn length(self) -> f32 { + self.c.hypot(self.s) + } + + pub fn length_squared(self) -> f32 { + self.c.powi(2) + self.s.powi(2) + } + + pub fn is_finite(self) -> bool { + self.c.is_finite() && self.s.is_finite() + } + + #[must_use] + pub fn inverse(self) -> Rot2 { + Self { + s: -self.s, + c: self.c, + } / self.length_squared() + } + + #[must_use] + pub fn normalized(self) -> Self { + let l = self.length(); + let ret = Self { + c: self.c / l, + s: self.s / l, + }; + debug_assert!(ret.is_finite()); + ret + } +} + +impl std::fmt::Debug for Rot2 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Rot2 {{ angle: {:.1}°, length: {} }}", + self.angle().to_degrees(), + self.length() + ) + } +} + +impl std::ops::Mul for Rot2 { + type Output = Rot2; + fn mul(self, r: Rot2) -> Rot2 { + /* + |lc -ls| * |rc -rs| + |ls lc| |rs rc| + */ + Rot2 { + c: self.c * r.c - self.s * r.s, + s: self.s * r.c + self.c * r.s, + } + } +} + +impl std::ops::Mul for Rot2 { + type Output = Vec2; + fn mul(self, v: Vec2) -> Vec2 { + Vec2 { + x: self.c * v.x - self.s * v.y, + y: self.s * v.x + self.c * v.y, + } + } +} + +impl std::ops::Mul for f32 { + type Output = Rot2; + fn mul(self, r: Rot2) -> Rot2 { + Rot2 { + c: self * r.c, + s: self * r.s, + } + } +} + +impl std::ops::Mul for Rot2 { + type Output = Rot2; + fn mul(self, r: f32) -> Rot2 { + Rot2 { + c: self.c * r, + s: self.s * r, + } + } +} + +impl std::ops::Div for Rot2 { + type Output = Rot2; + fn div(self, r: f32) -> Rot2 { + Rot2 { + c: self.c / r, + s: self.s / r, + } + } +} + +#[cfg(test)] +mod test { + use super::Rot2; + use crate::vec2; + + #[test] + fn test_rotation2() { + { + let angle = std::f32::consts::TAU / 6.0; + let rot = Rot2::from_angle(angle); + assert!((rot.angle() - angle).abs() < 1e-5); + assert!((rot * rot.inverse()).angle().abs() < 1e-5); + assert!((rot.inverse() * rot).angle().abs() < 1e-5); + } + + { + let angle = std::f32::consts::TAU / 4.0; + let rot = Rot2::from_angle(angle); + assert!(((rot * vec2(1.0, 0.0)) - vec2(0.0, 1.0)).length() < 1e-5); + } + + { + // Test rotation and scaling + let angle = std::f32::consts::TAU / 4.0; + let rot = 3.0 * Rot2::from_angle(angle); + let rotated = rot * vec2(1.0, 0.0); + let expected = vec2(0.0, 3.0); + assert!( + (rotated - expected).length() < 1e-5, + "Expected {:?} to equal {:?}. rot: {:?}", + rotated, + expected, + rot, + ); + + let undone = rot.inverse() * rot; + assert!(undone.angle().abs() < 1e-5); + assert!((undone.length() - 1.0).abs() < 1e-5,); + } + } +} diff --git a/egui/src/math/vec2.rs b/egui/src/math/vec2.rs index dac13fd4..3c2dcd76 100644 --- a/egui/src/math/vec2.rs +++ b/egui/src/math/vec2.rs @@ -60,6 +60,8 @@ impl Vec2 { } } + /// Rotates the vector by 90°, i.e positive X to positive Y + /// (clockwise in Egui coordinates). #[inline(always)] pub fn rot90(self) -> Self { vec2(self.y, -self.x) @@ -85,16 +87,6 @@ impl Vec2 { vec2(angle.cos(), angle.sin()) } - /// Use this vector as a rotor, rotating something else. - /// Example: `Vec2::angled(angle).rotate_other(some_vec)` - #[must_use] - pub fn rotate_other(self, v: Vec2) -> Self { - Self { - x: v.x * self.x + v.y * -self.y, - y: v.x * self.y + v.y * self.x, - } - } - #[must_use] pub fn floor(self) -> Self { vec2(self.x.floor(), self.y.floor()) diff --git a/egui/src/painter.rs b/egui/src/painter.rs index 8969d70f..dd2372d0 100644 --- a/egui/src/painter.rs +++ b/egui/src/painter.rs @@ -159,22 +159,10 @@ impl Painter { let tip_length = full_length / 3.0; let dir = dir.normalized(); let tip = origin + dir * full_length; - let angle = TAU / 10.0; + let rot = Rot2::from_angle(TAU / 10.0); self.line_segment([origin, tip], stroke); - self.line_segment( - [ - tip, - tip - tip_length * Vec2::angled(angle).rotate_other(dir), - ], - stroke, - ); - self.line_segment( - [ - tip, - tip - tip_length * Vec2::angled(-angle).rotate_other(dir), - ], - stroke, - ); + self.line_segment([tip, tip - tip_length * (rot * dir)], stroke); + self.line_segment([tip, tip - tip_length * (rot.inverse() * dir)], stroke); } }