Skip to main content

transform_gizmo/
config.rs

1use std::ops::{Deref, DerefMut};
2
3pub use ecolor::Color32;
4
5use emath::Rect;
6use enumset::{EnumSet, EnumSetType, enum_set};
7
8use crate::math::{
9    DMat4, DQuat, DVec3, DVec4, Transform, Vec4Swizzles, screen_to_world, world_to_screen,
10};
11
12/// The default snapping distance for rotation in radians
13pub const DEFAULT_SNAP_ANGLE: f32 = std::f32::consts::PI / 32.0;
14/// The default snapping distance for translation
15pub const DEFAULT_SNAP_DISTANCE: f32 = 0.1;
16/// The default snapping distance for scale
17pub const DEFAULT_SNAP_SCALE: f32 = 0.1;
18
19/// Configuration of a gizmo.
20///
21/// Defines how the gizmo is drawn to the screen and
22/// how it can be interacted with.
23#[derive(Debug, Copy, Clone)]
24pub struct GizmoConfig {
25    /// View matrix for the gizmo, aligning it with the camera's viewpoint.
26    pub view_matrix: mint::RowMatrix4<f64>,
27    /// Projection matrix for the gizmo, determining how it is projected onto the screen.
28    pub projection_matrix: mint::RowMatrix4<f64>,
29    /// Screen area where the gizmo is displayed.
30    pub viewport: Rect,
31    /// The gizmo's operation modes.
32    pub modes: EnumSet<GizmoMode>,
33    /// If set, this mode is forced active and other modes are disabled
34    pub mode_override: Option<GizmoMode>,
35    /// Determines the gizmo's orientation relative to global or local axes.
36    pub orientation: GizmoOrientation,
37    /// Pivot point for transformations
38    pub pivot_point: TransformPivotPoint,
39    /// Toggles snapping to predefined increments during transformations for precision.
40    pub snapping: bool,
41    /// Angle increment for snapping rotations, in radians.
42    pub snap_angle: f32,
43    /// Distance increment for snapping translations.
44    pub snap_distance: f32,
45    /// Scale increment for snapping scalings.
46    pub snap_scale: f32,
47    /// Visual settings for the gizmo, affecting appearance and visibility.
48    pub visuals: GizmoVisuals,
49    /// Ratio of window's physical size to logical size.
50    pub pixels_per_point: f32,
51}
52
53impl Default for GizmoConfig {
54    fn default() -> Self {
55        Self {
56            view_matrix: DMat4::IDENTITY.into(),
57            projection_matrix: DMat4::IDENTITY.into(),
58            viewport: Rect::NOTHING,
59            modes: GizmoMode::all(),
60            mode_override: None,
61            orientation: GizmoOrientation::default(),
62            pivot_point: TransformPivotPoint::default(),
63            snapping: false,
64            snap_angle: DEFAULT_SNAP_ANGLE,
65            snap_distance: DEFAULT_SNAP_DISTANCE,
66            snap_scale: DEFAULT_SNAP_SCALE,
67            visuals: GizmoVisuals::default(),
68            pixels_per_point: 1.0,
69        }
70    }
71}
72
73impl GizmoConfig {
74    /// Forward vector of the view camera
75    pub(crate) fn view_forward(&self) -> DVec3 {
76        DVec4::from(self.view_matrix.z).xyz()
77    }
78
79    /// Up vector of the view camera
80    pub(crate) fn view_up(&self) -> DVec3 {
81        DVec4::from(self.view_matrix.y).xyz()
82    }
83
84    /// Right vector of the view camera
85    pub(crate) fn view_right(&self) -> DVec3 {
86        DVec4::from(self.view_matrix.x).xyz()
87    }
88
89    /// Whether local orientation is used
90    pub(crate) fn local_space(&self) -> bool {
91        self.orientation() == GizmoOrientation::Local
92    }
93
94    /// Transform orientation of the gizmo
95    pub(crate) fn orientation(&self) -> GizmoOrientation {
96        self.orientation
97    }
98
99    /// Whether the modes have changed, compared to given other config
100    pub(crate) fn modes_changed(&self, other: &Self) -> bool {
101        (self.modes != other.modes && self.mode_override.is_none())
102            || (self.mode_override != other.mode_override)
103    }
104}
105
106#[derive(Debug, Copy, Clone, Default)]
107pub(crate) struct PreparedGizmoConfig {
108    config: GizmoConfig,
109    /// Rotation of the gizmo
110    pub(crate) rotation: DQuat,
111    /// Translation of the gizmo
112    pub(crate) translation: DVec3,
113    /// Scale of the gizmo
114    pub(crate) scale: DVec3,
115    /// Combined view-projection matrix
116    pub(crate) view_projection: DMat4,
117    /// Model matrix from targets
118    pub(crate) model_matrix: DMat4,
119    /// Combined model-view-projection matrix
120    pub(crate) mvp: DMat4,
121    /// Scale factor for the gizmo rendering
122    pub(crate) scale_factor: f32,
123    /// How close the mouse pointer needs to be to a subgizmo before it is focused
124    pub(crate) focus_distance: f32,
125    /// Whether left-handed projection is used
126    pub(crate) left_handed: bool,
127    /// Direction from the camera to the gizmo in world space
128    pub(crate) eye_to_model_dir: DVec3,
129}
130
131impl Deref for PreparedGizmoConfig {
132    type Target = GizmoConfig;
133
134    fn deref(&self) -> &Self::Target {
135        &self.config
136    }
137}
138
139impl DerefMut for PreparedGizmoConfig {
140    fn deref_mut(&mut self) -> &mut Self::Target {
141        &mut self.config
142    }
143}
144
145impl PreparedGizmoConfig {
146    pub(crate) fn update_for_config(&mut self, config: GizmoConfig) {
147        let projection_matrix = DMat4::from(config.projection_matrix);
148        let view_matrix = DMat4::from(config.view_matrix);
149
150        let view_projection = projection_matrix * view_matrix;
151
152        let left_handed = if projection_matrix.z_axis.w == 0.0 {
153            // A positive Z scale normally indicates left-handed coords,
154            // but some engines (e.g. Bevy) swap near/far for reverse-Z depth, which
155            // also flips the Z scale sign while remaining right-handed.
156            // Disambiguate by checking the view matrix's actual handedness.
157            if projection_matrix.z_axis.z > 0.0 {
158                let vx = view_matrix.x_axis.xyz();
159                let vy = view_matrix.y_axis.xyz();
160                let vz = view_matrix.z_axis.xyz();
161                vx.cross(vy).dot(vz) < 0.0
162            } else {
163                false
164            }
165        } else {
166            projection_matrix.z_axis.w > 0.0
167        };
168
169        self.config = config;
170        self.view_projection = view_projection;
171        self.left_handed = left_handed;
172
173        self.update_transform(Transform {
174            scale: self.scale.into(),
175            rotation: self.rotation.into(),
176            translation: self.translation.into(),
177        });
178    }
179
180    pub(crate) fn update_for_targets(&mut self, targets: &[Transform]) {
181        let mut scale = DVec3::ZERO;
182        let mut translation = DVec3::ZERO;
183        let mut rotation = DQuat::IDENTITY;
184
185        let mut target_count = 0;
186        for target in targets {
187            scale += DVec3::from(target.scale);
188            translation += DVec3::from(target.translation);
189            rotation = DQuat::from(target.rotation);
190
191            target_count += 1;
192        }
193
194        if target_count == 0 {
195            scale = DVec3::ONE;
196        } else {
197            translation /= target_count as f64;
198            scale /= target_count as f64;
199        }
200
201        self.update_transform(Transform {
202            scale: scale.into(),
203            rotation: rotation.into(),
204            translation: translation.into(),
205        });
206    }
207
208    pub(crate) fn update_transform(&mut self, transform: Transform) {
209        self.translation = transform.translation.into();
210        self.rotation = transform.rotation.into();
211        self.scale = transform.scale.into();
212        self.model_matrix =
213            DMat4::from_scale_rotation_translation(self.scale, self.rotation, self.translation);
214        self.mvp = self.view_projection * self.model_matrix;
215
216        self.scale_factor = self.mvp.as_ref()[15] as f32
217            / self.projection_matrix.x.x as f32
218            / self.config.viewport.width()
219            * 2.0;
220
221        let gizmo_screen_pos =
222            world_to_screen(self.config.viewport, self.mvp, self.translation).unwrap_or_default();
223
224        let gizmo_view_near = screen_to_world(
225            self.config.viewport,
226            self.view_projection.inverse(),
227            gizmo_screen_pos,
228            -1.0,
229        );
230
231        self.focus_distance = self.scale_factor * (self.config.visuals.stroke_width / 2.0 + 5.0);
232
233        self.eye_to_model_dir = (gizmo_view_near - self.translation).normalize_or_zero();
234    }
235
236    pub(crate) fn as_transform(&self) -> Transform {
237        Transform {
238            scale: self.scale.into(),
239            rotation: self.rotation.into(),
240            translation: self.translation.into(),
241        }
242    }
243}
244
245/// Operation mode of a gizmo.
246#[derive(Debug, EnumSetType, Hash)]
247pub enum GizmoMode {
248    /// Rotate around the view forward axis
249    RotateView,
250    /// Rotate around the X axis
251    RotateX,
252    /// Rotate around the Y axis
253    RotateY,
254    /// Rotate around the Z axis
255    RotateZ,
256    /// Translate along the view forward axis
257    TranslateView,
258    /// Translate along the X axis
259    TranslateX,
260    /// Translate along the Y axis
261    TranslateY,
262    /// Translate along the Z axis
263    TranslateZ,
264    /// Translate along the XY plane
265    TranslateXY,
266    /// Translate along the XZ plane
267    TranslateXZ,
268    /// Translate along the YZ plane
269    TranslateYZ,
270    /// Scale uniformly in all directions
271    ScaleUniform,
272    /// Scale along the X axis
273    ScaleX,
274    /// Scale along the Y axis
275    ScaleY,
276    /// Scale along the Z axis
277    ScaleZ,
278    /// Scale along the XY plane
279    ScaleXY,
280    /// Scale along the XZ plane
281    ScaleXZ,
282    /// Scale along the YZ plane
283    ScaleYZ,
284    /// Rotate using an arcball (trackball)
285    Arcball,
286}
287
288impl GizmoMode {
289    /// All modes
290    pub fn all() -> EnumSet<Self> {
291        EnumSet::all()
292    }
293
294    /// All rotation modes
295    pub const fn all_rotate() -> EnumSet<Self> {
296        enum_set!(Self::RotateX | Self::RotateY | Self::RotateZ | Self::RotateView)
297    }
298
299    /// All translation modes
300    pub const fn all_translate() -> EnumSet<Self> {
301        enum_set!(
302            Self::TranslateX
303                | Self::TranslateY
304                | Self::TranslateZ
305                | Self::TranslateXY
306                | Self::TranslateXZ
307                | Self::TranslateYZ
308                | Self::TranslateView
309        )
310    }
311
312    /// All scaling modes
313    pub const fn all_scale() -> EnumSet<Self> {
314        enum_set!(
315            Self::ScaleX
316                | Self::ScaleY
317                | Self::ScaleZ
318                | Self::ScaleXY
319                | Self::ScaleXZ
320                | Self::ScaleYZ
321                | Self::ScaleUniform
322        )
323    }
324
325    /// Is this mode for rotation
326    pub fn is_rotate(&self) -> bool {
327        self.kind() == GizmoModeKind::Rotate
328    }
329
330    /// Is this mode for translation
331    pub fn is_translate(&self) -> bool {
332        self.kind() == GizmoModeKind::Translate
333    }
334
335    /// Is this mode for scaling
336    pub fn is_scale(&self) -> bool {
337        self.kind() == GizmoModeKind::Scale
338    }
339
340    /// Axes this mode acts on
341    pub fn axes(&self) -> EnumSet<GizmoDirection> {
342        match self {
343            Self::RotateX | Self::TranslateX | Self::ScaleX => {
344                enum_set!(GizmoDirection::X)
345            }
346            Self::RotateY | Self::TranslateY | Self::ScaleY => {
347                enum_set!(GizmoDirection::Y)
348            }
349            Self::RotateZ | Self::TranslateZ | Self::ScaleZ => {
350                enum_set!(GizmoDirection::Z)
351            }
352            Self::RotateView | Self::TranslateView => {
353                enum_set!(GizmoDirection::View)
354            }
355            Self::ScaleUniform | Self::Arcball => {
356                enum_set!(GizmoDirection::X | GizmoDirection::Y | GizmoDirection::Z)
357            }
358            Self::TranslateXY | Self::ScaleXY => {
359                enum_set!(GizmoDirection::X | GizmoDirection::Y)
360            }
361            Self::TranslateXZ | Self::ScaleXZ => {
362                enum_set!(GizmoDirection::X | GizmoDirection::Z)
363            }
364            Self::TranslateYZ | Self::ScaleYZ => {
365                enum_set!(GizmoDirection::Y | GizmoDirection::Z)
366            }
367        }
368    }
369
370    /// Returns the modes that match to given axes exactly
371    pub fn all_from_axes(axes: EnumSet<GizmoDirection>) -> EnumSet<Self> {
372        EnumSet::<Self>::all()
373            .iter()
374            .filter(|mode| mode.axes() == axes)
375            .collect()
376    }
377
378    pub fn kind(&self) -> GizmoModeKind {
379        match self {
380            Self::RotateX | Self::RotateY | Self::RotateZ | Self::RotateView => {
381                GizmoModeKind::Rotate
382            }
383            Self::TranslateX
384            | Self::TranslateY
385            | Self::TranslateZ
386            | Self::TranslateXY
387            | Self::TranslateXZ
388            | Self::TranslateYZ
389            | Self::TranslateView => GizmoModeKind::Translate,
390            Self::ScaleX
391            | Self::ScaleY
392            | Self::ScaleZ
393            | Self::ScaleXY
394            | Self::ScaleXZ
395            | Self::ScaleYZ
396            | Self::ScaleUniform => GizmoModeKind::Scale,
397            Self::Arcball => GizmoModeKind::Arcball,
398        }
399    }
400
401    pub fn all_from_kind(kind: GizmoModeKind) -> EnumSet<Self> {
402        EnumSet::<Self>::all()
403            .iter()
404            .filter(|mode| mode.kind() == kind)
405            .collect()
406    }
407
408    pub fn from_kind_and_axes(kind: GizmoModeKind, axes: EnumSet<GizmoDirection>) -> Option<Self> {
409        EnumSet::<Self>::all()
410            .iter()
411            .find(|mode| mode.kind() == kind && mode.axes() == axes)
412    }
413}
414
415#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd)]
416pub enum GizmoModeKind {
417    Rotate,
418    Translate,
419    Scale,
420    Arcball,
421}
422
423/// The point in space around which all rotations are centered.
424#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
425pub enum TransformPivotPoint {
426    /// Pivot around the median point of targets
427    #[default]
428    MedianPoint,
429    /// Pivot around each target's own origin
430    IndividualOrigins,
431}
432
433/// Orientation of a gizmo.
434#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
435pub enum GizmoOrientation {
436    /// Transformation axes are aligned to world space.
437    #[default]
438    Global,
439    /// Transformation axes are aligned to the last target's orientation.
440    Local,
441}
442
443#[derive(Debug, EnumSetType, Hash)]
444pub enum GizmoDirection {
445    /// Gizmo points in the X-direction
446    X,
447    /// Gizmo points in the Y-direction
448    Y,
449    /// Gizmo points in the Z-direction
450    Z,
451    /// Gizmo points in the view direction
452    View,
453}
454
455/// Controls the visual style of the gizmo
456#[derive(Debug, Copy, Clone)]
457pub struct GizmoVisuals {
458    /// Color of the x axis
459    pub x_color: Color32,
460    /// Color of the y axis
461    pub y_color: Color32,
462    /// Color of the z axis
463    pub z_color: Color32,
464    /// Color of the forward axis
465    pub s_color: Color32,
466    /// Alpha of the gizmo color when inactive
467    pub inactive_alpha: f32,
468    /// Alpha of the gizmo color when highlighted/active
469    pub highlight_alpha: f32,
470    /// Color to use for highlighted and active axes. By default, the axis color is used with `highlight_alpha`
471    pub highlight_color: Option<Color32>,
472    /// Width (thickness) of the gizmo strokes
473    pub stroke_width: f32,
474    /// Gizmo size in pixels
475    pub gizmo_size: f32,
476}
477
478impl Default for GizmoVisuals {
479    fn default() -> Self {
480        Self {
481            x_color: Color32::from_rgb(255, 0, 125),
482            y_color: Color32::from_rgb(0, 255, 125),
483            z_color: Color32::from_rgb(0, 125, 255),
484            s_color: Color32::from_rgb(255, 255, 255),
485            inactive_alpha: 0.7,
486            highlight_alpha: 1.0,
487            highlight_color: None,
488            stroke_width: 4.0,
489            gizmo_size: 75.0,
490        }
491    }
492}