egui_gizmo/
lib.rs

1//! Provides a 3d transformation gizmo that can be used to manipulate 4x4
2//! transformation matrices. Such gizmos are commonly used in applications
3//! such as game engines and 3d modeling software.
4//!
5//! # Creating a gizmo
6//! For a more complete example, see the online demo at <https://urholaukkarinen.github.io/egui-gizmo/>.
7//! The demo sources can be found at <https://github.com/urholaukkarinen/egui-gizmo/blob/main/demo/src/main.rs>.
8//!
9//! ## A basic example
10//! ```text
11//! let gizmo = Gizmo::new("My gizmo")
12//!     .view_matrix(view_matrix)
13//!     .projection_matrix(projection_matrix)
14//!     .model_matrix(model_matrix)
15//!     .mode(GizmoMode::Rotate);
16//!
17//! if let Some(response) = gizmo.interact(ui) {
18//!     model_matrix = response.transform();
19//! }
20//! ```
21//! The gizmo can be placed inside a container such as a [`egui::Window`] or an [`egui::Area`].
22//! By default, the gizmo will use the ui clip rect as a viewport.
23//! The gizmo will apply transformations to the given model matrix.
24
25use std::cmp::Ordering;
26use std::f32::consts::PI;
27use std::hash::Hash;
28use std::ops::Sub;
29
30use crate::math::{screen_to_world, world_to_screen};
31use egui::{Color32, Context, Id, PointerButton, Pos2, Rect, Sense, Ui};
32use glam::{DMat4, DQuat, DVec3, Mat4, Quat, Vec3, Vec4Swizzles};
33
34use crate::subgizmo::rotation::RotationParams;
35use crate::subgizmo::scale::ScaleParams;
36use crate::subgizmo::translation::TranslationParams;
37use crate::subgizmo::{
38    ArcballSubGizmo, RotationSubGizmo, ScaleSubGizmo, SubGizmo, TransformKind, TranslationSubGizmo,
39};
40
41mod math;
42mod painter;
43mod subgizmo;
44
45/// The default snapping distance for rotation in radians
46pub const DEFAULT_SNAP_ANGLE: f32 = PI / 32.0;
47/// The default snapping distance for translation
48pub const DEFAULT_SNAP_DISTANCE: f32 = 0.1;
49/// The default snapping distance for scale
50pub const DEFAULT_SNAP_SCALE: f32 = 0.1;
51
52pub struct Gizmo {
53    id: Id,
54    config: GizmoConfig,
55    subgizmos: Vec<Box<dyn SubGizmo>>,
56}
57
58impl Gizmo {
59    pub fn new(id_source: impl Hash) -> Self {
60        Self {
61            id: Id::new(id_source),
62            config: GizmoConfig::default(),
63            subgizmos: Default::default(),
64        }
65    }
66
67    /// Matrix that specifies translation and rotation of the gizmo in world space
68    pub fn model_matrix(mut self, model_matrix: mint::ColumnMatrix4<f32>) -> Self {
69        self.config.model_matrix = Mat4::from(model_matrix).as_dmat4();
70        self
71    }
72
73    /// Matrix that specifies translation and rotation of the viewport camera
74    pub fn view_matrix(mut self, view_matrix: mint::ColumnMatrix4<f32>) -> Self {
75        self.config.view_matrix = Mat4::from(view_matrix).as_dmat4();
76        self
77    }
78
79    /// Matrix that specifies projection of the viewport
80    pub fn projection_matrix(mut self, projection_matrix: mint::ColumnMatrix4<f32>) -> Self {
81        self.config.projection_matrix = Mat4::from(projection_matrix).as_dmat4();
82        self
83    }
84
85    /// Bounds of the viewport in pixels
86    pub const fn viewport(mut self, viewport: Rect) -> Self {
87        self.config.viewport = viewport;
88        self
89    }
90
91    /// Gizmo mode to use
92    pub const fn mode(mut self, mode: GizmoMode) -> Self {
93        self.config.mode = mode;
94        self
95    }
96
97    /// Gizmo orientation to use
98    pub const fn orientation(mut self, orientation: GizmoOrientation) -> Self {
99        self.config.orientation = orientation;
100        self
101    }
102
103    /// Whether snapping is enabled
104    pub const fn snapping(mut self, snapping: bool) -> Self {
105        self.config.snapping = snapping;
106        self
107    }
108
109    /// Snap angle to use for rotation when snapping is enabled
110    pub const fn snap_angle(mut self, snap_angle: f32) -> Self {
111        self.config.snap_angle = snap_angle;
112        self
113    }
114
115    /// Snap distance to use for translation when snapping is enabled
116    pub const fn snap_distance(mut self, snap_distance: f32) -> Self {
117        self.config.snap_distance = snap_distance;
118        self
119    }
120
121    /// Snap distance to use for scaling when snapping is enabled
122    pub const fn snap_scale(mut self, snap_scale: f32) -> Self {
123        self.config.snap_scale = snap_scale;
124        self
125    }
126
127    /// Visual configuration of the gizmo, such as colors and size
128    pub const fn visuals(mut self, visuals: GizmoVisuals) -> Self {
129        self.config.visuals = visuals;
130        self
131    }
132
133    /// Draw and interact with the gizmo. This consumes the gizmo.
134    ///
135    /// Returns the result of the interaction, which includes a transformed model matrix.
136    /// [`None`] is returned when the gizmo is not active.
137    pub fn interact(mut self, ui: &mut Ui) -> Option<GizmoResult> {
138        self.config.prepare(ui);
139
140        // Choose subgizmos based on the gizmo mode
141        match self.config.mode {
142            GizmoMode::Rotate => {
143                self.add_subgizmos(self.new_rotation());
144                self.add_subgizmos(self.new_arcball());
145            }
146            GizmoMode::Translate => self.add_subgizmos(self.new_translation()),
147            GizmoMode::Scale => self.add_subgizmos(self.new_scale()),
148        };
149
150        let mut result = None;
151        let mut active_subgizmo = None;
152        let mut state = GizmoState::load(ui.ctx(), self.id);
153
154        if let Some(pointer_ray) = self.pointer_ray(ui) {
155            let viewport = self.config.viewport;
156            let id = self.id;
157
158            // If there is no active subgizmo, find which one of them
159            // is under the mouse pointer, if any.
160            if state.active_subgizmo_id.is_none() {
161                if let Some(subgizmo) = self.pick_subgizmo(ui, pointer_ray) {
162                    subgizmo.set_focused(true);
163
164                    let interaction = ui.interact(viewport, id, Sense::click_and_drag());
165                    let dragging = interaction.dragged_by(PointerButton::Primary);
166                    if interaction.drag_started() && dragging {
167                        state.active_subgizmo_id = Some(subgizmo.id());
168                    }
169                }
170            }
171
172            active_subgizmo = state.active_subgizmo_id.and_then(|id| {
173                self.subgizmos
174                    .iter_mut()
175                    .find(|subgizmo| subgizmo.id() == id)
176            });
177
178            if let Some(subgizmo) = active_subgizmo.as_mut() {
179                if ui.input(|i| i.pointer.primary_down()) {
180                    subgizmo.set_active(true);
181                    subgizmo.set_focused(true);
182                    result = subgizmo.update(ui, pointer_ray);
183                } else {
184                    state.active_subgizmo_id = None;
185                }
186            }
187        }
188
189        if let Some((_, result)) = active_subgizmo.zip(result) {
190            self.config.translation = Vec3::from(result.translation).as_dvec3();
191            self.config.rotation = Quat::from(result.rotation).as_dquat();
192            self.config.scale = Vec3::from(result.scale).as_dvec3();
193        }
194
195        state.save(ui.ctx(), self.id);
196
197        self.draw_subgizmos(ui, &mut state);
198
199        result
200    }
201
202    fn draw_subgizmos(&mut self, ui: &mut Ui, state: &mut GizmoState) {
203        for subgizmo in &mut self.subgizmos {
204            if state.active_subgizmo_id.is_none() || subgizmo.is_active() {
205                subgizmo.draw(ui);
206            }
207        }
208    }
209
210    /// Picks the subgizmo that is closest to the mouse pointer
211    fn pick_subgizmo(&mut self, ui: &Ui, ray: Ray) -> Option<&mut Box<dyn SubGizmo>> {
212        self.subgizmos
213            .iter_mut()
214            .filter_map(|subgizmo| subgizmo.pick(ui, ray).map(|t| (t, subgizmo)))
215            .min_by(|(first, _), (second, _)| first.partial_cmp(second).unwrap_or(Ordering::Equal))
216            .map(|(_, subgizmo)| subgizmo)
217    }
218
219    /// Create arcball subgizmo
220    fn new_arcball(&self) -> [ArcballSubGizmo; 1] {
221        [ArcballSubGizmo::new(self.id.with("arc"), self.config, ())]
222    }
223
224    /// Create subgizmos for rotation
225    fn new_rotation(&self) -> [RotationSubGizmo; 4] {
226        [
227            RotationSubGizmo::new(
228                self.id.with("rx"),
229                self.config,
230                RotationParams {
231                    direction: GizmoDirection::X,
232                },
233            ),
234            RotationSubGizmo::new(
235                self.id.with("ry"),
236                self.config,
237                RotationParams {
238                    direction: GizmoDirection::Y,
239                },
240            ),
241            RotationSubGizmo::new(
242                self.id.with("rz"),
243                self.config,
244                RotationParams {
245                    direction: GizmoDirection::Z,
246                },
247            ),
248            RotationSubGizmo::new(
249                self.id.with("rs"),
250                self.config,
251                RotationParams {
252                    direction: GizmoDirection::View,
253                },
254            ),
255        ]
256    }
257
258    /// Create subgizmos for translation
259    fn new_translation(&self) -> [TranslationSubGizmo; 7] {
260        [
261            TranslationSubGizmo::new(
262                self.id.with("txs"),
263                self.config,
264                TranslationParams {
265                    direction: GizmoDirection::View,
266                    transform_kind: TransformKind::Plane,
267                },
268            ),
269            TranslationSubGizmo::new(
270                self.id.with("tx"),
271                self.config,
272                TranslationParams {
273                    direction: GizmoDirection::X,
274                    transform_kind: TransformKind::Axis,
275                },
276            ),
277            TranslationSubGizmo::new(
278                self.id.with("ty"),
279                self.config,
280                TranslationParams {
281                    direction: GizmoDirection::Y,
282                    transform_kind: TransformKind::Axis,
283                },
284            ),
285            TranslationSubGizmo::new(
286                self.id.with("tz"),
287                self.config,
288                TranslationParams {
289                    direction: GizmoDirection::Z,
290                    transform_kind: TransformKind::Axis,
291                },
292            ),
293            TranslationSubGizmo::new(
294                self.id.with("tyz"),
295                self.config,
296                TranslationParams {
297                    direction: GizmoDirection::X,
298                    transform_kind: TransformKind::Plane,
299                },
300            ),
301            TranslationSubGizmo::new(
302                self.id.with("txz"),
303                self.config,
304                TranslationParams {
305                    direction: GizmoDirection::Y,
306                    transform_kind: TransformKind::Plane,
307                },
308            ),
309            TranslationSubGizmo::new(
310                self.id.with("txy"),
311                self.config,
312                TranslationParams {
313                    direction: GizmoDirection::Z,
314                    transform_kind: TransformKind::Plane,
315                },
316            ),
317        ]
318    }
319
320    /// Create subgizmos for scale
321    fn new_scale(&self) -> [ScaleSubGizmo; 7] {
322        [
323            ScaleSubGizmo::new(
324                self.id.with("txs"),
325                self.config,
326                ScaleParams {
327                    direction: GizmoDirection::View,
328                    transform_kind: TransformKind::Plane,
329                },
330            ),
331            ScaleSubGizmo::new(
332                self.id.with("sx"),
333                self.config,
334                ScaleParams {
335                    direction: GizmoDirection::X,
336                    transform_kind: TransformKind::Axis,
337                },
338            ),
339            ScaleSubGizmo::new(
340                self.id.with("sy"),
341                self.config,
342                ScaleParams {
343                    direction: GizmoDirection::Y,
344                    transform_kind: TransformKind::Axis,
345                },
346            ),
347            ScaleSubGizmo::new(
348                self.id.with("sz"),
349                self.config,
350                ScaleParams {
351                    direction: GizmoDirection::Z,
352                    transform_kind: TransformKind::Axis,
353                },
354            ),
355            ScaleSubGizmo::new(
356                self.id.with("syz"),
357                self.config,
358                ScaleParams {
359                    direction: GizmoDirection::X,
360                    transform_kind: TransformKind::Plane,
361                },
362            ),
363            ScaleSubGizmo::new(
364                self.id.with("sxz"),
365                self.config,
366                ScaleParams {
367                    direction: GizmoDirection::Y,
368                    transform_kind: TransformKind::Plane,
369                },
370            ),
371            ScaleSubGizmo::new(
372                self.id.with("sxy"),
373                self.config,
374                ScaleParams {
375                    direction: GizmoDirection::Z,
376                    transform_kind: TransformKind::Plane,
377                },
378            ),
379        ]
380    }
381
382    /// Add given subgizmos to this gizmo
383    fn add_subgizmos<T: SubGizmo, const N: usize>(&mut self, subgizmos: [T; N]) {
384        for subgizmo in subgizmos {
385            self.subgizmos.push(Box::new(subgizmo));
386        }
387    }
388
389    /// Calculate a world space ray from current mouse position
390    fn pointer_ray(&self, ui: &Ui) -> Option<Ray> {
391        let screen_pos = ui.input(|i| i.pointer.hover_pos())?;
392
393        let mat = self.config.view_projection.inverse();
394        let origin = screen_to_world(self.config.viewport, mat, screen_pos, -1.0);
395        let target = screen_to_world(self.config.viewport, mat, screen_pos, 1.0);
396
397        let direction = target.sub(origin).normalize();
398
399        Some(Ray {
400            screen_pos,
401            origin,
402            direction,
403        })
404    }
405}
406
407/// Result of an active transformation
408#[derive(Debug, Copy, Clone)]
409pub struct GizmoResult {
410    /// Updated scale
411    pub scale: mint::Vector3<f32>,
412    /// Updated rotation
413    pub rotation: mint::Quaternion<f32>,
414    /// Updated translation
415    pub translation: mint::Vector3<f32>,
416    /// Mode of the active subgizmo
417    pub mode: GizmoMode,
418    /// Total scale, rotation or translation of the current gizmo activation, depending on mode
419    pub value: Option<[f32; 3]>,
420}
421
422impl GizmoResult {
423    /// Updated transformation matrix in column major order.
424    pub fn transform(&self) -> mint::ColumnMatrix4<f32> {
425        Mat4::from_scale_rotation_translation(
426            self.scale.into(),
427            self.rotation.into(),
428            self.translation.into(),
429        )
430        .into()
431    }
432}
433
434#[derive(Debug, Copy, Clone, Eq, PartialEq)]
435pub enum GizmoMode {
436    /// Only rotation
437    Rotate,
438    /// Only translation
439    Translate,
440    /// Only scale
441    Scale,
442}
443
444#[derive(Debug, Copy, Clone, Eq, PartialEq)]
445pub enum GizmoOrientation {
446    /// Transformation axes are aligned to world space. Rotation of the
447    /// gizmo does not change.
448    Global,
449    /// Transformation axes are aligned to local space. Rotation of the
450    /// gizmo matches the rotation represented by the model matrix.
451    Local,
452}
453
454#[derive(Debug, Copy, Clone, Eq, PartialEq)]
455pub enum GizmoDirection {
456    /// Gizmo points in the X-direction
457    X,
458    /// Gizmo points in the Y-direction
459    Y,
460    /// Gizmo points in the Z-direction
461    Z,
462    /// Gizmo points in the view direction
463    View,
464}
465
466/// Controls the visual style of the gizmo
467#[derive(Debug, Copy, Clone)]
468pub struct GizmoVisuals {
469    /// Color of the x axis
470    pub x_color: Color32,
471    /// Color of the y axis
472    pub y_color: Color32,
473    /// Color of the z axis
474    pub z_color: Color32,
475    /// Color of the forward axis
476    pub s_color: Color32,
477    /// Alpha of the gizmo color when inactive
478    pub inactive_alpha: f32,
479    /// Alpha of the gizmo color when highlighted/active
480    pub highlight_alpha: f32,
481    /// Color to use for highlighted and active axes. By default, the axis color is used with `highlight_alpha`
482    pub highlight_color: Option<Color32>,
483    /// Width (thickness) of the gizmo strokes
484    pub stroke_width: f32,
485    /// Gizmo size in pixels
486    pub gizmo_size: f32,
487}
488
489impl Default for GizmoVisuals {
490    fn default() -> Self {
491        Self {
492            x_color: Color32::from_rgb(255, 50, 0),
493            y_color: Color32::from_rgb(50, 255, 0),
494            z_color: Color32::from_rgb(0, 50, 255),
495            s_color: Color32::from_rgb(255, 255, 255),
496            inactive_alpha: 0.5,
497            highlight_alpha: 0.9,
498            highlight_color: None,
499            stroke_width: 4.0,
500            gizmo_size: 75.0,
501        }
502    }
503}
504
505#[derive(Debug, Copy, Clone)]
506pub(crate) struct GizmoConfig {
507    pub view_matrix: DMat4,
508    pub projection_matrix: DMat4,
509    pub model_matrix: DMat4,
510    pub viewport: Rect,
511    pub mode: GizmoMode,
512    pub orientation: GizmoOrientation,
513    pub snapping: bool,
514    pub snap_angle: f32,
515    pub snap_distance: f32,
516    pub snap_scale: f32,
517    pub visuals: GizmoVisuals,
518    //----------------------------------//
519    pub rotation: DQuat,
520    pub translation: DVec3,
521    pub scale: DVec3,
522    pub view_projection: DMat4,
523    pub mvp: DMat4,
524    pub gizmo_view_forward: DVec3,
525    pub scale_factor: f32,
526    /// How close the mouse pointer needs to be to a subgizmo before it is focused
527    pub focus_distance: f32,
528    pub left_handed: bool,
529}
530
531impl Default for GizmoConfig {
532    fn default() -> Self {
533        Self {
534            view_matrix: DMat4::IDENTITY,
535            projection_matrix: DMat4::IDENTITY,
536            model_matrix: DMat4::IDENTITY,
537            viewport: Rect::NOTHING,
538            mode: GizmoMode::Rotate,
539            orientation: GizmoOrientation::Global,
540            snapping: false,
541            snap_angle: DEFAULT_SNAP_ANGLE,
542            snap_distance: DEFAULT_SNAP_DISTANCE,
543            snap_scale: DEFAULT_SNAP_SCALE,
544            visuals: GizmoVisuals::default(),
545            //----------------------------------//
546            rotation: DQuat::IDENTITY,
547            translation: DVec3::ZERO,
548            scale: DVec3::ONE,
549            view_projection: DMat4::IDENTITY,
550            mvp: DMat4::IDENTITY,
551            gizmo_view_forward: DVec3::ONE,
552            scale_factor: 0.0,
553            focus_distance: 0.0,
554            left_handed: false,
555        }
556    }
557}
558
559impl GizmoConfig {
560    /// Prepare the gizmo configuration for interaction and rendering.
561    /// Some values are precalculated for better performance at the cost of memory usage.
562    fn prepare(&mut self, ui: &Ui) {
563        // Use ui clip rect if the user has not specified a viewport
564        if self.viewport.is_negative() {
565            self.viewport = ui.clip_rect();
566        }
567
568        let (scale, rotation, translation) = self.model_matrix.to_scale_rotation_translation();
569        self.rotation = rotation;
570        self.translation = translation;
571        self.scale = scale;
572        self.view_projection = self.projection_matrix * self.view_matrix;
573        self.mvp = self.projection_matrix * self.view_matrix * self.model_matrix;
574
575        self.scale_factor = self.mvp.as_ref()[15] as f32
576            / self.projection_matrix.as_ref()[0] as f32
577            / self.viewport.width()
578            * 2.0;
579
580        self.focus_distance = self.scale_factor * (self.visuals.stroke_width / 2.0 + 5.0);
581
582        self.left_handed = if self.projection_matrix.z_axis.w == 0.0 {
583            self.projection_matrix.z_axis.z > 0.0
584        } else {
585            self.projection_matrix.z_axis.w > 0.0
586        };
587
588        let gizmo_screen_pos =
589            world_to_screen(self.viewport, self.mvp, self.translation).unwrap_or_default();
590
591        let gizmo_view_near = screen_to_world(
592            self.viewport,
593            self.view_projection.inverse(),
594            gizmo_screen_pos,
595            -1.0,
596        );
597
598        self.gizmo_view_forward = (gizmo_view_near - self.translation).normalize_or_zero();
599    }
600
601    /// Forward vector of the view camera
602    pub(crate) fn view_forward(&self) -> DVec3 {
603        self.view_matrix.row(2).xyz()
604    }
605
606    /// Up vector of the view camera
607    pub(crate) fn view_up(&self) -> DVec3 {
608        self.view_matrix.row(1).xyz()
609    }
610
611    /// Right vector of the view camera
612    pub(crate) fn view_right(&self) -> DVec3 {
613        self.view_matrix.row(0).xyz()
614    }
615
616    /// Whether local orientation is used
617    pub(crate) fn local_space(&self) -> bool {
618        // Scale mode only works in local space
619        self.orientation == GizmoOrientation::Local || self.mode == GizmoMode::Scale
620    }
621}
622
623#[derive(Debug, Copy, Clone)]
624pub(crate) struct Ray {
625    screen_pos: Pos2,
626    origin: DVec3,
627    direction: DVec3,
628}
629
630/// Gizmo state that is saved between frames
631#[derive(Default, Debug, Copy, Clone)]
632struct GizmoState {
633    active_subgizmo_id: Option<Id>,
634}
635
636pub(crate) trait WidgetData: Sized + Default + Copy + Clone + Send + Sync + 'static {
637    fn load(ctx: &Context, gizmo_id: Id) -> Self {
638        ctx.memory_mut(|mem| *mem.data.get_temp_mut_or_default::<Self>(gizmo_id))
639    }
640
641    fn save(self, ctx: &Context, gizmo_id: Id) {
642        ctx.memory_mut(|mem| mem.data.insert_temp(gizmo_id, self));
643    }
644}
645
646impl WidgetData for GizmoState {}