Skip to main content

polyscope_ui/
gizmo.rs

1//! Visual 3D gizmo integration using transform-gizmo-egui.
2//!
3//! Shows all gizmo modes (translate, rotate, scale) simultaneously.
4
5use egui::Ui;
6use glam::{DMat4, Mat4, Quat, Vec3};
7use transform_gizmo_egui::{
8    Gizmo, GizmoConfig, GizmoExt, GizmoMode, GizmoOrientation, GizmoVisuals,
9    config::TransformPivotPoint, math::Transform, mint,
10};
11
12/// Wrapper around transform-gizmo-egui for polyscope integration.
13pub struct TransformGizmo {
14    gizmo: Gizmo,
15}
16
17impl Default for TransformGizmo {
18    fn default() -> Self {
19        Self::new()
20    }
21}
22
23impl TransformGizmo {
24    /// Creates a new transform gizmo.
25    #[must_use]
26    pub fn new() -> Self {
27        Self {
28            gizmo: Gizmo::default(),
29        }
30    }
31
32    /// Draws the gizmo and handles interaction.
33    ///
34    /// Shows all modes (translate, rotate, scale) simultaneously.
35    /// Returns the updated transform if the gizmo was manipulated.
36    ///
37    /// # Arguments
38    /// * `ui` - The egui UI context
39    /// * `view_matrix` - Camera view matrix
40    /// * `projection_matrix` - Camera projection matrix
41    /// * `model_matrix` - Current transform of the object
42    /// * `local_space` - If true, use local coordinates; if false, use world coordinates
43    /// * `viewport` - Viewport rect
44    pub fn interact(
45        &mut self,
46        ui: &mut Ui,
47        view_matrix: Mat4,
48        projection_matrix: Mat4,
49        model_matrix: Mat4,
50        local_space: bool,
51        viewport: egui::Rect,
52    ) -> Option<Mat4> {
53        let orientation = if local_space {
54            GizmoOrientation::Local
55        } else {
56            GizmoOrientation::Global
57        };
58
59        // Convert glam Mat4 (f32) to DMat4 (f64) for transform-gizmo
60        let view_f64 = mat4_to_dmat4(view_matrix);
61        let proj_f64 = mat4_to_dmat4(projection_matrix);
62
63        // Convert to row-major mint matrices as required by transform-gizmo
64        let view_mint: mint::RowMatrix4<f64> = dmat4_to_row_mint(view_f64);
65        let proj_mint: mint::RowMatrix4<f64> = dmat4_to_row_mint(proj_f64);
66
67        // Create transform from model matrix
68        let (scale, rotation, translation) = model_matrix.to_scale_rotation_translation();
69        let transform = Transform {
70            translation: mint::Vector3 {
71                x: f64::from(translation.x),
72                y: f64::from(translation.y),
73                z: f64::from(translation.z),
74            },
75            rotation: mint::Quaternion {
76                v: mint::Vector3 {
77                    x: f64::from(rotation.x),
78                    y: f64::from(rotation.y),
79                    z: f64::from(rotation.z),
80                },
81                s: f64::from(rotation.w),
82            },
83            scale: mint::Vector3 {
84                x: f64::from(scale.x),
85                y: f64::from(scale.y),
86                z: f64::from(scale.z),
87            },
88        };
89
90        let config = GizmoConfig {
91            view_matrix: view_mint,
92            projection_matrix: proj_mint,
93            viewport,
94            modes: GizmoMode::all(),
95            mode_override: None,
96            orientation,
97            pivot_point: TransformPivotPoint::MedianPoint,
98            snapping: false,
99            snap_angle: 0.0,
100            snap_distance: 0.0,
101            snap_scale: 0.0,
102            visuals: GizmoVisuals::default(),
103            pixels_per_point: ui.ctx().pixels_per_point(),
104        };
105
106        // Update gizmo configuration
107        self.gizmo.update_config(config);
108
109        // Interact with gizmo
110        if let Some((_result, new_transforms)) = self.gizmo.interact(ui, &[transform]) {
111            if let Some(new_transform) = new_transforms.first() {
112                // Convert back to Mat4
113                let translation = Vec3::new(
114                    new_transform.translation.x as f32,
115                    new_transform.translation.y as f32,
116                    new_transform.translation.z as f32,
117                );
118                let rotation = Quat::from_xyzw(
119                    new_transform.rotation.v.x as f32,
120                    new_transform.rotation.v.y as f32,
121                    new_transform.rotation.v.z as f32,
122                    new_transform.rotation.s as f32,
123                );
124                let scale = Vec3::new(
125                    new_transform.scale.x as f32,
126                    new_transform.scale.y as f32,
127                    new_transform.scale.z as f32,
128                );
129
130                return Some(Mat4::from_scale_rotation_translation(
131                    scale,
132                    rotation,
133                    translation,
134                ));
135            }
136        }
137
138        None
139    }
140
141    /// Decomposes a Mat4 into translation, rotation (Euler degrees), and scale.
142    #[must_use]
143    pub fn decompose_transform(matrix: Mat4) -> (Vec3, Vec3, Vec3) {
144        let (scale, rotation, translation) = matrix.to_scale_rotation_translation();
145        let euler = rotation.to_euler(glam::EulerRot::XYZ);
146        let euler_degrees = Vec3::new(
147            euler.0.to_degrees(),
148            euler.1.to_degrees(),
149            euler.2.to_degrees(),
150        );
151        (translation, euler_degrees, scale)
152    }
153
154    /// Composes a Mat4 from translation, rotation (Euler degrees), and scale.
155    #[must_use]
156    pub fn compose_transform(translation: Vec3, euler_degrees: Vec3, scale: Vec3) -> Mat4 {
157        let rotation = Quat::from_euler(
158            glam::EulerRot::XYZ,
159            euler_degrees.x.to_radians(),
160            euler_degrees.y.to_radians(),
161            euler_degrees.z.to_radians(),
162        );
163        Mat4::from_scale_rotation_translation(scale, rotation, translation)
164    }
165}
166
167/// Convert glam Mat4 (f32) to `DMat4` (f64).
168fn mat4_to_dmat4(m: Mat4) -> DMat4 {
169    DMat4::from_cols_array(&[
170        f64::from(m.x_axis.x),
171        f64::from(m.x_axis.y),
172        f64::from(m.x_axis.z),
173        f64::from(m.x_axis.w),
174        f64::from(m.y_axis.x),
175        f64::from(m.y_axis.y),
176        f64::from(m.y_axis.z),
177        f64::from(m.y_axis.w),
178        f64::from(m.z_axis.x),
179        f64::from(m.z_axis.y),
180        f64::from(m.z_axis.z),
181        f64::from(m.z_axis.w),
182        f64::from(m.w_axis.x),
183        f64::from(m.w_axis.y),
184        f64::from(m.w_axis.z),
185        f64::from(m.w_axis.w),
186    ])
187}
188
189/// Convert `DMat4` to row-major mint matrix.
190fn dmat4_to_row_mint(m: DMat4) -> mint::RowMatrix4<f64> {
191    // glam stores column-major, mint::RowMatrix4 expects row-major
192    mint::RowMatrix4 {
193        x: mint::Vector4 {
194            x: m.x_axis.x,
195            y: m.y_axis.x,
196            z: m.z_axis.x,
197            w: m.w_axis.x,
198        },
199        y: mint::Vector4 {
200            x: m.x_axis.y,
201            y: m.y_axis.y,
202            z: m.z_axis.y,
203            w: m.w_axis.y,
204        },
205        z: mint::Vector4 {
206            x: m.x_axis.z,
207            y: m.y_axis.z,
208            z: m.z_axis.z,
209            w: m.w_axis.z,
210        },
211        w: mint::Vector4 {
212            x: m.x_axis.w,
213            y: m.y_axis.w,
214            z: m.z_axis.w,
215            w: m.w_axis.w,
216        },
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    #[test]
225    fn test_gizmo_creation() {
226        let gizmo = TransformGizmo::new();
227        // Just verify it can be created
228        drop(gizmo);
229    }
230
231    #[test]
232    fn test_decompose_compose_roundtrip() {
233        let translation = Vec3::new(1.0, 2.0, 3.0);
234        let euler_degrees = Vec3::new(45.0, 30.0, 15.0);
235        let scale = Vec3::new(1.0, 2.0, 1.5);
236
237        let matrix = TransformGizmo::compose_transform(translation, euler_degrees, scale);
238        let (t, r, s) = TransformGizmo::decompose_transform(matrix);
239
240        assert!((t - translation).length() < 0.001);
241        assert!((r - euler_degrees).length() < 0.1);
242        assert!((s - scale).length() < 0.001);
243    }
244}