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            projection_matrix.z_axis.z > 0.0
154        } else {
155            projection_matrix.z_axis.w > 0.0
156        };
157
158        self.config = config;
159        self.view_projection = view_projection;
160        self.left_handed = left_handed;
161
162        self.update_transform(Transform {
163            scale: self.scale.into(),
164            rotation: self.rotation.into(),
165            translation: self.translation.into(),
166        });
167    }
168
169    pub(crate) fn update_for_targets(&mut self, targets: &[Transform]) {
170        let mut scale = DVec3::ZERO;
171        let mut translation = DVec3::ZERO;
172        let mut rotation = DQuat::IDENTITY;
173
174        let mut target_count = 0;
175        for target in targets {
176            scale += DVec3::from(target.scale);
177            translation += DVec3::from(target.translation);
178            rotation = DQuat::from(target.rotation);
179
180            target_count += 1;
181        }
182
183        if target_count == 0 {
184            scale = DVec3::ONE;
185        } else {
186            translation /= target_count as f64;
187            scale /= target_count as f64;
188        }
189
190        self.update_transform(Transform {
191            scale: scale.into(),
192            rotation: rotation.into(),
193            translation: translation.into(),
194        });
195    }
196
197    pub(crate) fn update_transform(&mut self, transform: Transform) {
198        self.translation = transform.translation.into();
199        self.rotation = transform.rotation.into();
200        self.scale = transform.scale.into();
201        self.model_matrix =
202            DMat4::from_scale_rotation_translation(self.scale, self.rotation, self.translation);
203        self.mvp = self.view_projection * self.model_matrix;
204
205        self.scale_factor = self.mvp.as_ref()[15] as f32
206            / self.projection_matrix.x.x as f32
207            / self.config.viewport.width()
208            * 2.0;
209
210        let gizmo_screen_pos =
211            world_to_screen(self.config.viewport, self.mvp, self.translation).unwrap_or_default();
212
213        let gizmo_view_near = screen_to_world(
214            self.config.viewport,
215            self.view_projection.inverse(),
216            gizmo_screen_pos,
217            -1.0,
218        );
219
220        self.focus_distance = self.scale_factor * (self.config.visuals.stroke_width / 2.0 + 5.0);
221
222        self.eye_to_model_dir = (gizmo_view_near - self.translation).normalize_or_zero();
223    }
224
225    pub(crate) fn as_transform(&self) -> Transform {
226        Transform {
227            scale: self.scale.into(),
228            rotation: self.rotation.into(),
229            translation: self.translation.into(),
230        }
231    }
232}
233
234/// Operation mode of a gizmo.
235#[derive(Debug, EnumSetType, Hash)]
236pub enum GizmoMode {
237    /// Rotate around the view forward axis
238    RotateView,
239    /// Rotate around the X axis
240    RotateX,
241    /// Rotate around the Y axis
242    RotateY,
243    /// Rotate around the Z axis
244    RotateZ,
245    /// Translate along the view forward axis
246    TranslateView,
247    /// Translate along the X axis
248    TranslateX,
249    /// Translate along the Y axis
250    TranslateY,
251    /// Translate along the Z axis
252    TranslateZ,
253    /// Translate along the XY plane
254    TranslateXY,
255    /// Translate along the XZ plane
256    TranslateXZ,
257    /// Translate along the YZ plane
258    TranslateYZ,
259    /// Scale uniformly in all directions
260    ScaleUniform,
261    /// Scale along the X axis
262    ScaleX,
263    /// Scale along the Y axis
264    ScaleY,
265    /// Scale along the Z axis
266    ScaleZ,
267    /// Scale along the XY plane
268    ScaleXY,
269    /// Scale along the XZ plane
270    ScaleXZ,
271    /// Scale along the YZ plane
272    ScaleYZ,
273    /// Rotate using an arcball (trackball)
274    Arcball,
275}
276
277impl GizmoMode {
278    /// All modes
279    pub fn all() -> EnumSet<Self> {
280        EnumSet::all()
281    }
282
283    /// All rotation modes
284    pub const fn all_rotate() -> EnumSet<Self> {
285        enum_set!(Self::RotateX | Self::RotateY | Self::RotateZ | Self::RotateView)
286    }
287
288    /// All translation modes
289    pub const fn all_translate() -> EnumSet<Self> {
290        enum_set!(
291            Self::TranslateX
292                | Self::TranslateY
293                | Self::TranslateZ
294                | Self::TranslateXY
295                | Self::TranslateXZ
296                | Self::TranslateYZ
297                | Self::TranslateView
298        )
299    }
300
301    /// All scaling modes
302    pub const fn all_scale() -> EnumSet<Self> {
303        enum_set!(
304            Self::ScaleX
305                | Self::ScaleY
306                | Self::ScaleZ
307                | Self::ScaleXY
308                | Self::ScaleXZ
309                | Self::ScaleYZ
310                | Self::ScaleUniform
311        )
312    }
313
314    /// Is this mode for rotation
315    pub fn is_rotate(&self) -> bool {
316        self.kind() == GizmoModeKind::Rotate
317    }
318
319    /// Is this mode for translation
320    pub fn is_translate(&self) -> bool {
321        self.kind() == GizmoModeKind::Translate
322    }
323
324    /// Is this mode for scaling
325    pub fn is_scale(&self) -> bool {
326        self.kind() == GizmoModeKind::Scale
327    }
328
329    /// Axes this mode acts on
330    pub fn axes(&self) -> EnumSet<GizmoDirection> {
331        match self {
332            Self::RotateX | Self::TranslateX | Self::ScaleX => {
333                enum_set!(GizmoDirection::X)
334            }
335            Self::RotateY | Self::TranslateY | Self::ScaleY => {
336                enum_set!(GizmoDirection::Y)
337            }
338            Self::RotateZ | Self::TranslateZ | Self::ScaleZ => {
339                enum_set!(GizmoDirection::Z)
340            }
341            Self::RotateView | Self::TranslateView => {
342                enum_set!(GizmoDirection::View)
343            }
344            Self::ScaleUniform | Self::Arcball => {
345                enum_set!(GizmoDirection::X | GizmoDirection::Y | GizmoDirection::Z)
346            }
347            Self::TranslateXY | Self::ScaleXY => {
348                enum_set!(GizmoDirection::X | GizmoDirection::Y)
349            }
350            Self::TranslateXZ | Self::ScaleXZ => {
351                enum_set!(GizmoDirection::X | GizmoDirection::Z)
352            }
353            Self::TranslateYZ | Self::ScaleYZ => {
354                enum_set!(GizmoDirection::Y | GizmoDirection::Z)
355            }
356        }
357    }
358
359    /// Returns the modes that match to given axes exactly
360    pub fn all_from_axes(axes: EnumSet<GizmoDirection>) -> EnumSet<Self> {
361        EnumSet::<Self>::all()
362            .iter()
363            .filter(|mode| mode.axes() == axes)
364            .collect()
365    }
366
367    pub fn kind(&self) -> GizmoModeKind {
368        match self {
369            Self::RotateX | Self::RotateY | Self::RotateZ | Self::RotateView => {
370                GizmoModeKind::Rotate
371            }
372            Self::TranslateX
373            | Self::TranslateY
374            | Self::TranslateZ
375            | Self::TranslateXY
376            | Self::TranslateXZ
377            | Self::TranslateYZ
378            | Self::TranslateView => GizmoModeKind::Translate,
379            Self::ScaleX
380            | Self::ScaleY
381            | Self::ScaleZ
382            | Self::ScaleXY
383            | Self::ScaleXZ
384            | Self::ScaleYZ
385            | Self::ScaleUniform => GizmoModeKind::Scale,
386            Self::Arcball => GizmoModeKind::Arcball,
387        }
388    }
389
390    pub fn all_from_kind(kind: GizmoModeKind) -> EnumSet<Self> {
391        EnumSet::<Self>::all()
392            .iter()
393            .filter(|mode| mode.kind() == kind)
394            .collect()
395    }
396
397    pub fn from_kind_and_axes(kind: GizmoModeKind, axes: EnumSet<GizmoDirection>) -> Option<Self> {
398        EnumSet::<Self>::all()
399            .iter()
400            .find(|mode| mode.kind() == kind && mode.axes() == axes)
401    }
402}
403
404#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd)]
405pub enum GizmoModeKind {
406    Rotate,
407    Translate,
408    Scale,
409    Arcball,
410}
411
412/// The point in space around which all rotations are centered.
413#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
414pub enum TransformPivotPoint {
415    /// Pivot around the median point of targets
416    #[default]
417    MedianPoint,
418    /// Pivot around each target's own origin
419    IndividualOrigins,
420}
421
422/// Orientation of a gizmo.
423#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
424pub enum GizmoOrientation {
425    /// Transformation axes are aligned to world space.
426    #[default]
427    Global,
428    /// Transformation axes are aligned to the last target's orientation.
429    Local,
430}
431
432#[derive(Debug, EnumSetType, Hash)]
433pub enum GizmoDirection {
434    /// Gizmo points in the X-direction
435    X,
436    /// Gizmo points in the Y-direction
437    Y,
438    /// Gizmo points in the Z-direction
439    Z,
440    /// Gizmo points in the view direction
441    View,
442}
443
444/// Controls the visual style of the gizmo
445#[derive(Debug, Copy, Clone)]
446pub struct GizmoVisuals {
447    /// Color of the x axis
448    pub x_color: Color32,
449    /// Color of the y axis
450    pub y_color: Color32,
451    /// Color of the z axis
452    pub z_color: Color32,
453    /// Color of the forward axis
454    pub s_color: Color32,
455    /// Alpha of the gizmo color when inactive
456    pub inactive_alpha: f32,
457    /// Alpha of the gizmo color when highlighted/active
458    pub highlight_alpha: f32,
459    /// Color to use for highlighted and active axes. By default, the axis color is used with `highlight_alpha`
460    pub highlight_color: Option<Color32>,
461    /// Width (thickness) of the gizmo strokes
462    pub stroke_width: f32,
463    /// Gizmo size in pixels
464    pub gizmo_size: f32,
465}
466
467impl Default for GizmoVisuals {
468    fn default() -> Self {
469        Self {
470            x_color: Color32::from_rgb(255, 0, 125),
471            y_color: Color32::from_rgb(0, 255, 125),
472            z_color: Color32::from_rgb(0, 125, 255),
473            s_color: Color32::from_rgb(255, 255, 255),
474            inactive_alpha: 0.7,
475            highlight_alpha: 1.0,
476            highlight_color: None,
477            stroke_width: 4.0,
478            gizmo_size: 75.0,
479        }
480    }
481}