transform_gizmo/
gizmo.rs

1use ecolor::Rgba;
2use emath::Pos2;
3use enumset::EnumSet;
4use std::ops::{Add, AddAssign, Sub};
5
6use crate::GizmoOrientation;
7use crate::config::{
8    GizmoConfig, GizmoDirection, GizmoMode, PreparedGizmoConfig, TransformPivotPoint,
9};
10use crate::math::{Transform, screen_to_world};
11use epaint::Mesh;
12use glam::{DMat4, DQuat, DVec3};
13
14use crate::subgizmo::rotation::RotationParams;
15use crate::subgizmo::scale::ScaleParams;
16use crate::subgizmo::translation::TranslationParams;
17use crate::subgizmo::{
18    ArcballSubGizmo, RotationSubGizmo, ScaleSubGizmo, SubGizmo, SubGizmoControl,
19    TranslationSubGizmo, common::TransformKind,
20};
21
22/// A 3D transformation gizmo.
23#[derive(Clone, Debug, Default)]
24pub struct Gizmo {
25    /// Prepared configuration of the gizmo.
26    /// Includes the original [`GizmoConfig`] as well as
27    /// various other values calculated from it, used for
28    /// interaction and drawing the gizmo.
29    config: PreparedGizmoConfig,
30    /// Subgizmos used in the gizmo.
31    subgizmos: Vec<SubGizmo>,
32    active_subgizmo_id: Option<u64>,
33
34    target_start_transforms: Vec<Transform>,
35
36    gizmo_start_transform: Transform,
37}
38
39impl Gizmo {
40    /// Creates a new gizmo from given configuration
41    pub fn new(config: GizmoConfig) -> Self {
42        let mut gizmo = Self::default();
43        gizmo.update_config(config);
44        gizmo
45    }
46
47    /// Current configuration used by the gizmo.
48    pub fn config(&self) -> &GizmoConfig {
49        &self.config
50    }
51
52    /// Updates the configuration used by the gizmo.
53    pub fn update_config(&mut self, config: GizmoConfig) {
54        if config.modes_changed(&self.config) {
55            self.subgizmos.clear();
56            self.active_subgizmo_id = None;
57        }
58
59        self.config.update_for_config(config);
60
61        if self.subgizmos.is_empty() {
62            self.add_rotation();
63            self.add_translation();
64            self.add_scale();
65        }
66    }
67
68    /// Was this gizmo focused after the latest [`Gizmo::update`] call.
69    pub fn is_focused(&self) -> bool {
70        self.subgizmos.iter().any(|subgizmo| subgizmo.is_focused())
71    }
72
73    /// Updates the gizmo based on given interaction information.
74    ///
75    /// # Examples
76    ///
77    /// ```
78    /// # // Dummy values
79    /// # use transform_gizmo::GizmoInteraction;
80    /// # let mut gizmo = transform_gizmo::Gizmo::default();
81    /// # let cursor_pos = Default::default();
82    /// # let drag_started = true;
83    /// # let dragging = true;
84    /// # let hovered = true;
85    /// # let mut transforms = vec![];
86    ///
87    /// let interaction = GizmoInteraction {
88    ///     cursor_pos,
89    ///     hovered,
90    ///     drag_started,
91    ///     dragging
92    /// };
93    ///
94    /// if let Some((_result, new_transforms)) = gizmo.update(interaction, &transforms) {
95    ///                 for (new_transform, transform) in
96    ///     // Update transforms
97    ///     new_transforms.iter().zip(&mut transforms)
98    ///     {
99    ///         *transform = *new_transform;
100    ///     }
101    /// }
102    /// ```
103    ///
104    /// Returns the result of the interaction with the updated transformation.
105    ///
106    /// [`Some`] is returned when any of the subgizmos is being dragged, [`None`] otherwise.
107    pub fn update(
108        &mut self,
109        interaction: GizmoInteraction,
110        targets: &[Transform],
111    ) -> Option<(GizmoResult, Vec<Transform>)> {
112        if !self.config.viewport.is_finite() {
113            return None;
114        }
115
116        // Update the gizmo based on the given target transforms,
117        // unless the gizmo is currently being interacted with.
118        if self.active_subgizmo_id.is_none() {
119            self.config.update_for_targets(targets);
120        }
121
122        for subgizmo in &mut self.subgizmos {
123            // Update current configuration to each subgizmo.
124            subgizmo.update_config(self.config);
125            // All subgizmos are initially considered unfocused.
126            subgizmo.set_focused(false);
127        }
128
129        let force_active = self.config.mode_override.is_some();
130
131        let pointer_ray = self.pointer_ray(Pos2::from(interaction.cursor_pos));
132
133        // If there is no active subgizmo, find which one of them
134        // is under the mouse pointer, if any.
135        if self.active_subgizmo_id.is_none() && interaction.hovered {
136            if let Some(subgizmo) = self.pick_subgizmo(pointer_ray) {
137                subgizmo.set_focused(true);
138
139                // If we started dragging from one of the subgizmos, mark it as active.
140                if interaction.drag_started || force_active {
141                    self.active_subgizmo_id = Some(subgizmo.id());
142                    self.target_start_transforms = targets.to_vec();
143                    self.gizmo_start_transform = self.config.as_transform();
144                }
145            }
146        }
147
148        let mut result = None;
149
150        if let Some(subgizmo) = self.active_subgizmo_mut() {
151            if interaction.dragging || force_active {
152                subgizmo.set_active(true);
153                subgizmo.set_focused(true);
154                result = subgizmo.update(pointer_ray);
155            } else {
156                subgizmo.set_active(false);
157                subgizmo.set_focused(false);
158                self.active_subgizmo_id = None;
159            }
160        }
161
162        let Some(result) = result else {
163            // No interaction, no result.
164
165            self.config.update_for_targets(targets);
166
167            for subgizmo in &mut self.subgizmos {
168                subgizmo.update_config(self.config);
169            }
170
171            return None;
172        };
173
174        self.update_config_with_result(result);
175
176        let updated_targets =
177            self.update_transforms_with_result(result, targets, &self.target_start_transforms);
178
179        Some((result, updated_targets))
180    }
181
182    /// Return all the necessary data to draw the latest gizmo interaction.
183    ///
184    /// The gizmo draw data consists of vertices in viewport coordinates.
185    pub fn draw(&self) -> GizmoDrawData {
186        if !self.config.viewport.is_finite() {
187            return GizmoDrawData::default();
188        }
189
190        let mut draw_data = GizmoDrawData::default();
191        for subgizmo in &self.subgizmos {
192            if self.active_subgizmo_id.is_none() || subgizmo.is_active() {
193                draw_data += subgizmo.draw();
194            }
195        }
196
197        draw_data
198    }
199
200    /// Checks all sub-gizmos for intersections with the cursor. If there is one, return true.
201    pub fn pick_preview(&self, cursor_pos: (f32, f32)) -> bool {
202        let pointer_ray = self.pointer_ray(Pos2::from(cursor_pos));
203        self.subgizmos.iter().any(|x| x.pick_preview(pointer_ray))
204    }
205
206    fn active_subgizmo_mut(&mut self) -> Option<&mut SubGizmo> {
207        self.active_subgizmo_id.and_then(|id| {
208            self.subgizmos
209                .iter_mut()
210                .find(|subgizmo| subgizmo.id() == id)
211        })
212    }
213
214    fn update_transforms_with_result(
215        &self,
216        result: GizmoResult,
217        transforms: &[Transform],
218        start_transforms: &[Transform],
219    ) -> Vec<Transform> {
220        transforms
221            .iter()
222            .zip(start_transforms)
223            .map(|(transform, start_transform)| match result {
224                GizmoResult::Rotation {
225                    axis,
226                    delta,
227                    total: _,
228                    is_view_axis,
229                } => self.update_rotation(transform, axis, delta, is_view_axis),
230                GizmoResult::Translation { delta, total: _ } => {
231                    self.update_translation(delta, transform, start_transform)
232                }
233                GizmoResult::Scale { total } => {
234                    self.update_scale(transform, start_transform, total)
235                }
236                GizmoResult::Arcball { delta, total: _ } => {
237                    self.update_rotation_quat(transform, delta.into())
238                }
239            })
240            .collect()
241    }
242
243    fn update_rotation(
244        &self,
245        transform: &Transform,
246        axis: mint::Vector3<f64>,
247        delta: f64,
248        is_view_axis: bool,
249    ) -> Transform {
250        let axis = match self.config.orientation() {
251            GizmoOrientation::Local if !is_view_axis => {
252                (DQuat::from(transform.rotation) * DVec3::from(axis)).normalize()
253            }
254            _ => DVec3::from(axis),
255        };
256
257        let delta = DQuat::from_axis_angle(axis, delta);
258
259        self.update_rotation_quat(transform, delta)
260    }
261
262    fn update_rotation_quat(&self, transform: &Transform, delta: DQuat) -> Transform {
263        let translation = match self.config.pivot_point {
264            TransformPivotPoint::MedianPoint => (self.config.translation
265                + delta * (DVec3::from(transform.translation) - self.config.translation))
266                .into(),
267            TransformPivotPoint::IndividualOrigins => transform.translation,
268        };
269
270        let new_rotation = (delta * DQuat::from(transform.rotation)).normalize();
271
272        Transform {
273            scale: transform.scale,
274            rotation: new_rotation.into(),
275            translation,
276        }
277    }
278
279    fn update_translation(
280        &self,
281        delta: mint::Vector3<f64>,
282        transform: &Transform,
283        start_transform: &Transform,
284    ) -> Transform {
285        let delta = match self.config.orientation() {
286            GizmoOrientation::Global => DVec3::from(delta),
287            GizmoOrientation::Local => DQuat::from(start_transform.rotation) * DVec3::from(delta),
288        };
289
290        Transform {
291            scale: start_transform.scale,
292            rotation: start_transform.rotation,
293            translation: (delta + DVec3::from(transform.translation)).into(),
294        }
295    }
296
297    fn update_scale(
298        &self,
299        transform: &Transform,
300        start_transform: &Transform,
301        scale: mint::Vector3<f64>,
302    ) -> Transform {
303        let new_scale = match self.config.orientation() {
304            GizmoOrientation::Global => {
305                let scaled_transform_mat = DMat4::from_scale(scale.into())
306                    * DMat4::from_scale_rotation_translation(
307                        DVec3::from(start_transform.scale),
308                        DQuat::from(start_transform.rotation),
309                        DVec3::from(start_transform.translation),
310                    );
311                let (scale, _, _) = scaled_transform_mat.to_scale_rotation_translation();
312                scale
313            }
314            GizmoOrientation::Local => DVec3::from(start_transform.scale) * DVec3::from(scale),
315        };
316
317        Transform {
318            scale: new_scale.into(),
319            ..*transform
320        }
321    }
322
323    fn update_config_with_result(&mut self, result: GizmoResult) {
324        let new_config_transform = self.update_transforms_with_result(
325            result,
326            &[self.config.as_transform()],
327            &[self.gizmo_start_transform],
328        )[0];
329
330        self.config.update_transform(new_config_transform);
331    }
332
333    /// Picks the subgizmo that is closest to the given world space ray.
334    #[allow(clippy::manual_inspect)]
335    fn pick_subgizmo(&mut self, ray: Ray) -> Option<&mut SubGizmo> {
336        // If mode is overridden, assume we only have that mode, and choose it.
337        if self.config.mode_override.is_some() {
338            return self.subgizmos.first_mut().map(|subgizmo| {
339                subgizmo.pick(ray);
340
341                subgizmo
342            });
343        }
344
345        self.subgizmos
346            .iter_mut()
347            .filter_map(|subgizmo| subgizmo.pick(ray).map(|t| (t, subgizmo)))
348            .min_by(|(first, _), (second, _)| {
349                first
350                    .partial_cmp(second)
351                    .unwrap_or(std::cmp::Ordering::Equal)
352            })
353            .map(|(_, subgizmo)| subgizmo)
354    }
355
356    /// Get all modes that are currently enabled
357    fn enabled_modes(&self) -> EnumSet<GizmoMode> {
358        self.config
359            .mode_override
360            .map_or(self.config.modes, EnumSet::only)
361    }
362
363    /// Adds rotation subgizmos
364    fn add_rotation(&mut self) {
365        let modes = self.enabled_modes();
366
367        if modes.contains(GizmoMode::RotateX) {
368            self.subgizmos.push(
369                RotationSubGizmo::new(
370                    self.config,
371                    RotationParams {
372                        direction: GizmoDirection::X,
373                    },
374                )
375                .into(),
376            );
377        }
378
379        if modes.contains(GizmoMode::RotateY) {
380            self.subgizmos.push(
381                RotationSubGizmo::new(
382                    self.config,
383                    RotationParams {
384                        direction: GizmoDirection::Y,
385                    },
386                )
387                .into(),
388            );
389        }
390
391        if modes.contains(GizmoMode::RotateZ) {
392            self.subgizmos.push(
393                RotationSubGizmo::new(
394                    self.config,
395                    RotationParams {
396                        direction: GizmoDirection::Z,
397                    },
398                )
399                .into(),
400            );
401        }
402
403        if modes.contains(GizmoMode::RotateView) {
404            self.subgizmos.push(
405                RotationSubGizmo::new(
406                    self.config,
407                    RotationParams {
408                        direction: GizmoDirection::View,
409                    },
410                )
411                .into(),
412            );
413        }
414
415        if modes.contains(GizmoMode::Arcball) {
416            self.subgizmos
417                .push(ArcballSubGizmo::new(self.config, ()).into());
418        }
419    }
420
421    /// Adds translation subgizmos
422    fn add_translation(&mut self) {
423        let modes = self.enabled_modes();
424
425        if modes.contains(GizmoMode::TranslateX) {
426            self.subgizmos.push(
427                TranslationSubGizmo::new(
428                    self.config,
429                    TranslationParams {
430                        mode: GizmoMode::TranslateX,
431                        direction: GizmoDirection::X,
432                        transform_kind: TransformKind::Axis,
433                    },
434                )
435                .into(),
436            );
437        }
438
439        if modes.contains(GizmoMode::TranslateY) {
440            self.subgizmos.push(
441                TranslationSubGizmo::new(
442                    self.config,
443                    TranslationParams {
444                        mode: GizmoMode::TranslateY,
445                        direction: GizmoDirection::Y,
446                        transform_kind: TransformKind::Axis,
447                    },
448                )
449                .into(),
450            );
451        }
452
453        if modes.contains(GizmoMode::TranslateZ) {
454            self.subgizmos.push(
455                TranslationSubGizmo::new(
456                    self.config,
457                    TranslationParams {
458                        mode: GizmoMode::TranslateZ,
459                        direction: GizmoDirection::Z,
460                        transform_kind: TransformKind::Axis,
461                    },
462                )
463                .into(),
464            );
465        }
466
467        if modes.contains(GizmoMode::TranslateView) {
468            self.subgizmos.push(
469                TranslationSubGizmo::new(
470                    self.config,
471                    TranslationParams {
472                        mode: GizmoMode::TranslateView,
473                        direction: GizmoDirection::View,
474                        transform_kind: TransformKind::Plane,
475                    },
476                )
477                .into(),
478            );
479        }
480
481        if modes.contains(GizmoMode::TranslateXY) {
482            self.subgizmos.push(
483                TranslationSubGizmo::new(
484                    self.config,
485                    TranslationParams {
486                        mode: GizmoMode::TranslateXY,
487                        direction: GizmoDirection::X,
488                        transform_kind: TransformKind::Plane,
489                    },
490                )
491                .into(),
492            );
493        }
494
495        if modes.contains(GizmoMode::TranslateXZ) {
496            self.subgizmos.push(
497                TranslationSubGizmo::new(
498                    self.config,
499                    TranslationParams {
500                        mode: GizmoMode::TranslateXZ,
501                        direction: GizmoDirection::Y,
502                        transform_kind: TransformKind::Plane,
503                    },
504                )
505                .into(),
506            );
507        }
508
509        if modes.contains(GizmoMode::TranslateYZ) {
510            self.subgizmos.push(
511                TranslationSubGizmo::new(
512                    self.config,
513                    TranslationParams {
514                        mode: GizmoMode::TranslateYZ,
515                        direction: GizmoDirection::Z,
516                        transform_kind: TransformKind::Plane,
517                    },
518                )
519                .into(),
520            );
521        }
522    }
523
524    /// Adds scale subgizmos
525    fn add_scale(&mut self) {
526        let modes = self.enabled_modes();
527
528        if modes.contains(GizmoMode::ScaleX) {
529            self.subgizmos.push(
530                ScaleSubGizmo::new(
531                    self.config,
532                    ScaleParams {
533                        mode: GizmoMode::ScaleX,
534                        direction: GizmoDirection::X,
535                        transform_kind: TransformKind::Axis,
536                    },
537                )
538                .into(),
539            );
540        }
541
542        if modes.contains(GizmoMode::ScaleY) {
543            self.subgizmos.push(
544                ScaleSubGizmo::new(
545                    self.config,
546                    ScaleParams {
547                        mode: GizmoMode::ScaleY,
548                        direction: GizmoDirection::Y,
549                        transform_kind: TransformKind::Axis,
550                    },
551                )
552                .into(),
553            );
554        }
555
556        if modes.contains(GizmoMode::ScaleZ) {
557            self.subgizmos.push(
558                ScaleSubGizmo::new(
559                    self.config,
560                    ScaleParams {
561                        mode: GizmoMode::ScaleZ,
562                        direction: GizmoDirection::Z,
563                        transform_kind: TransformKind::Axis,
564                    },
565                )
566                .into(),
567            );
568        }
569
570        if modes.contains(GizmoMode::ScaleUniform) && !modes.contains(GizmoMode::RotateView) {
571            self.subgizmos.push(
572                ScaleSubGizmo::new(
573                    self.config,
574                    ScaleParams {
575                        mode: GizmoMode::ScaleUniform,
576                        direction: GizmoDirection::View,
577                        transform_kind: TransformKind::Plane,
578                    },
579                )
580                .into(),
581            );
582        }
583
584        if modes.contains(GizmoMode::ScaleXY) && !modes.contains(GizmoMode::TranslateXY) {
585            self.subgizmos.push(
586                ScaleSubGizmo::new(
587                    self.config,
588                    ScaleParams {
589                        mode: GizmoMode::ScaleXY,
590                        direction: GizmoDirection::X,
591                        transform_kind: TransformKind::Plane,
592                    },
593                )
594                .into(),
595            );
596        }
597
598        if modes.contains(GizmoMode::ScaleXZ) && !modes.contains(GizmoMode::TranslateXZ) {
599            self.subgizmos.push(
600                ScaleSubGizmo::new(
601                    self.config,
602                    ScaleParams {
603                        mode: GizmoMode::ScaleXZ,
604                        direction: GizmoDirection::Y,
605                        transform_kind: TransformKind::Plane,
606                    },
607                )
608                .into(),
609            );
610        }
611
612        if modes.contains(GizmoMode::ScaleYZ) && !modes.contains(GizmoMode::TranslateYZ) {
613            self.subgizmos.push(
614                ScaleSubGizmo::new(
615                    self.config,
616                    ScaleParams {
617                        mode: GizmoMode::ScaleYZ,
618                        direction: GizmoDirection::Z,
619                        transform_kind: TransformKind::Plane,
620                    },
621                )
622                .into(),
623            );
624        }
625    }
626
627    /// Calculate a world space ray from given screen space position
628    fn pointer_ray(&self, screen_pos: Pos2) -> Ray {
629        let mat = self.config.view_projection.inverse();
630        let origin = screen_to_world(self.config.viewport, mat, screen_pos, -1.0);
631        let target = screen_to_world(self.config.viewport, mat, screen_pos, 1.0);
632
633        let direction = target.sub(origin).normalize();
634
635        Ray {
636            screen_pos,
637            origin,
638            direction,
639        }
640    }
641}
642
643/// Information needed for interacting with the gizmo.
644#[derive(Default, Clone, Copy, Debug)]
645pub struct GizmoInteraction {
646    /// Current cursor position in window coordinates.
647    pub cursor_pos: (f32, f32),
648    /// Whether the gizmo is hovered this frame.
649    /// Some other UI element might be covering the gizmo,
650    /// and in such case you may not want the gizmo to be
651    /// interactable.
652    pub hovered: bool,
653    /// Whether dragging was started this frame.
654    /// Usually this is set to true if the primary mouse
655    /// button was just pressed.
656    pub drag_started: bool,
657    /// Whether the user is currently dragging.
658    /// Usually this is set to true whenever the primary mouse
659    /// button is being pressed.
660    pub dragging: bool,
661}
662
663/// Result of a gizmo transformation
664#[derive(Debug, Copy, Clone)]
665pub enum GizmoResult {
666    Rotation {
667        /// The rotation axis,
668        axis: mint::Vector3<f64>,
669        /// The latest rotation angle delta
670        delta: f64,
671        /// Total rotation angle of the gizmo interaction
672        total: f64,
673        /// Whether we are rotating along the view axis
674        is_view_axis: bool,
675    },
676    Translation {
677        /// The latest translation delta
678        delta: mint::Vector3<f64>,
679        /// Total translation of the gizmo interaction
680        total: mint::Vector3<f64>,
681    },
682    Scale {
683        /// Total scale of the gizmo interaction
684        total: mint::Vector3<f64>,
685    },
686    Arcball {
687        /// The latest rotation delta
688        delta: mint::Quaternion<f64>,
689        /// Total rotation of the gizmo interaction
690        total: mint::Quaternion<f64>,
691    },
692}
693
694/// Data used to draw [`Gizmo`].
695#[derive(Default, Clone, Debug)]
696pub struct GizmoDrawData {
697    /// Vertices in viewport space.
698    pub vertices: Vec<[f32; 2]>,
699    /// Linear RGBA colors.
700    pub colors: Vec<[f32; 4]>,
701    /// Indices to the vertex data.
702    pub indices: Vec<u32>,
703}
704
705impl From<Mesh> for GizmoDrawData {
706    fn from(mesh: Mesh) -> Self {
707        let (vertices, colors): (Vec<_>, Vec<_>) = mesh
708            .vertices
709            .iter()
710            .map(|vertex| {
711                (
712                    [vertex.pos.x, vertex.pos.y],
713                    Rgba::from(vertex.color).to_array(),
714                )
715            })
716            .unzip();
717
718        Self {
719            vertices,
720            colors,
721            indices: mesh.indices,
722        }
723    }
724}
725
726impl AddAssign for GizmoDrawData {
727    fn add_assign(&mut self, rhs: Self) {
728        let index_offset = self.vertices.len() as u32;
729        self.vertices.extend(rhs.vertices);
730        self.colors.extend(rhs.colors);
731        self.indices
732            .extend(rhs.indices.into_iter().map(|idx| index_offset + idx));
733    }
734}
735
736impl Add for GizmoDrawData {
737    type Output = Self;
738
739    fn add(mut self, rhs: Self) -> Self::Output {
740        self += rhs;
741        self
742    }
743}
744
745#[derive(Debug, Copy, Clone)]
746pub(crate) struct Ray {
747    pub(crate) screen_pos: Pos2,
748    pub(crate) origin: DVec3,
749    pub(crate) direction: DVec3,
750}