Skip to main content

polyscope_core/
gizmo.rs

1//! Transformation gizmo system for interactive manipulation.
2//!
3//! Provides visual gizmos for translating, rotating, and scaling structures.
4
5use glam::{Mat4, Quat, Vec3};
6
7/// The type of transformation gizmo.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9pub enum GizmoMode {
10    /// Translation gizmo (arrows along axes).
11    #[default]
12    Translate,
13    /// Rotation gizmo (circles around axes).
14    Rotate,
15    /// Scale gizmo (boxes along axes).
16    Scale,
17}
18
19/// The coordinate space for gizmo operations.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
21pub enum GizmoSpace {
22    /// World coordinate system.
23    #[default]
24    World,
25    /// Local coordinate system (relative to object).
26    Local,
27}
28
29/// Configuration for a transformation gizmo.
30#[derive(Debug, Clone)]
31pub struct GizmoConfig {
32    /// The current gizmo mode.
33    pub mode: GizmoMode,
34    /// The coordinate space.
35    pub space: GizmoSpace,
36    /// Whether the gizmo is visible.
37    pub visible: bool,
38    /// Size of the gizmo (in screen-relative units).
39    pub size: f32,
40    /// Snap value for translation (0.0 = disabled).
41    pub snap_translate: f32,
42    /// Snap value for rotation in degrees (0.0 = disabled).
43    pub snap_rotate: f32,
44    /// Snap value for scale (0.0 = disabled).
45    pub snap_scale: f32,
46}
47
48impl Default for GizmoConfig {
49    fn default() -> Self {
50        Self {
51            mode: GizmoMode::Translate,
52            space: GizmoSpace::World,
53            visible: true,
54            size: 100.0,
55            snap_translate: 0.0,
56            snap_rotate: 0.0,
57            snap_scale: 0.0,
58        }
59    }
60}
61
62impl GizmoConfig {
63    /// Creates a new gizmo configuration with default settings.
64    #[must_use]
65    pub fn new() -> Self {
66        Self::default()
67    }
68
69    /// Sets the gizmo mode.
70    #[must_use]
71    pub fn with_mode(mut self, mode: GizmoMode) -> Self {
72        self.mode = mode;
73        self
74    }
75
76    /// Sets the coordinate space.
77    #[must_use]
78    pub fn with_space(mut self, space: GizmoSpace) -> Self {
79        self.space = space;
80        self
81    }
82
83    /// Sets the gizmo size.
84    #[must_use]
85    pub fn with_size(mut self, size: f32) -> Self {
86        self.size = size;
87        self
88    }
89
90    /// Sets the translation snap value.
91    #[must_use]
92    pub fn with_snap_translate(mut self, snap: f32) -> Self {
93        self.snap_translate = snap;
94        self
95    }
96
97    /// Sets the rotation snap value in degrees.
98    #[must_use]
99    pub fn with_snap_rotate(mut self, snap: f32) -> Self {
100        self.snap_rotate = snap;
101        self
102    }
103
104    /// Sets the scale snap value.
105    #[must_use]
106    pub fn with_snap_scale(mut self, snap: f32) -> Self {
107        self.snap_scale = snap;
108        self
109    }
110}
111
112/// A transformation represented as separate components.
113///
114/// This is useful for UI display and incremental manipulation.
115#[derive(Debug, Clone, Copy)]
116pub struct Transform {
117    /// Translation component.
118    pub translation: Vec3,
119    /// Rotation component as a quaternion.
120    pub rotation: Quat,
121    /// Scale component.
122    pub scale: Vec3,
123}
124
125impl Default for Transform {
126    fn default() -> Self {
127        Self {
128            translation: Vec3::ZERO,
129            rotation: Quat::IDENTITY,
130            scale: Vec3::ONE,
131        }
132    }
133}
134
135impl Transform {
136    /// Creates a new identity transform.
137    #[must_use]
138    pub fn identity() -> Self {
139        Self::default()
140    }
141
142    /// Creates a transform from a translation.
143    #[must_use]
144    pub fn from_translation(translation: Vec3) -> Self {
145        Self {
146            translation,
147            ..Default::default()
148        }
149    }
150
151    /// Creates a transform from a rotation.
152    #[must_use]
153    pub fn from_rotation(rotation: Quat) -> Self {
154        Self {
155            rotation,
156            ..Default::default()
157        }
158    }
159
160    /// Creates a transform from a scale.
161    #[must_use]
162    pub fn from_scale(scale: Vec3) -> Self {
163        Self {
164            scale,
165            ..Default::default()
166        }
167    }
168
169    /// Creates a transform from a Mat4.
170    ///
171    /// This decomposition may not be exact for matrices with shear.
172    #[must_use]
173    pub fn from_matrix(matrix: Mat4) -> Self {
174        let (scale, rotation, translation) = matrix.to_scale_rotation_translation();
175        Self {
176            translation,
177            rotation,
178            scale,
179        }
180    }
181
182    /// Converts this transform to a Mat4.
183    #[must_use]
184    pub fn to_matrix(&self) -> Mat4 {
185        Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.translation)
186    }
187
188    /// Returns the rotation as Euler angles (in radians).
189    #[must_use]
190    pub fn euler_angles(&self) -> Vec3 {
191        let (x, y, z) = self.rotation.to_euler(glam::EulerRot::XYZ);
192        Vec3::new(x, y, z)
193    }
194
195    /// Sets the rotation from Euler angles (in radians).
196    pub fn set_euler_angles(&mut self, angles: Vec3) {
197        self.rotation = Quat::from_euler(glam::EulerRot::XYZ, angles.x, angles.y, angles.z);
198    }
199
200    /// Returns the rotation as Euler angles (in degrees).
201    #[must_use]
202    pub fn euler_angles_degrees(&self) -> Vec3 {
203        self.euler_angles() * (180.0 / std::f32::consts::PI)
204    }
205
206    /// Sets the rotation from Euler angles (in degrees).
207    pub fn set_euler_angles_degrees(&mut self, degrees: Vec3) {
208        self.set_euler_angles(degrees * (std::f32::consts::PI / 180.0));
209    }
210
211    /// Translates the transform.
212    pub fn translate(&mut self, delta: Vec3) {
213        self.translation += delta;
214    }
215
216    /// Rotates the transform.
217    pub fn rotate(&mut self, delta: Quat) {
218        self.rotation = delta * self.rotation;
219    }
220
221    /// Scales the transform.
222    pub fn scale_by(&mut self, factor: Vec3) {
223        self.scale *= factor;
224    }
225
226    /// Applies snap to translation.
227    pub fn snap_translation(&mut self, snap: f32) {
228        if snap > 0.0 {
229            self.translation = (self.translation / snap).round() * snap;
230        }
231    }
232
233    /// Applies snap to rotation (in degrees).
234    pub fn snap_rotation(&mut self, snap_degrees: f32) {
235        if snap_degrees > 0.0 {
236            let mut euler = self.euler_angles_degrees();
237            euler = (euler / snap_degrees).round() * snap_degrees;
238            self.set_euler_angles_degrees(euler);
239        }
240    }
241
242    /// Applies snap to scale.
243    pub fn snap_scale(&mut self, snap: f32) {
244        if snap > 0.0 {
245            self.scale = (self.scale / snap).round() * snap;
246        }
247    }
248}
249
250/// Axis for single-axis gizmo operations.
251#[derive(Debug, Clone, Copy, PartialEq, Eq)]
252pub enum GizmoAxis {
253    /// X axis (red).
254    X,
255    /// Y axis (green).
256    Y,
257    /// Z axis (blue).
258    Z,
259    /// XY plane.
260    XY,
261    /// XZ plane.
262    XZ,
263    /// YZ plane.
264    YZ,
265    /// All axes (free movement).
266    All,
267    /// No axis selected.
268    None,
269}
270
271impl GizmoAxis {
272    /// Returns the direction vector for this axis.
273    #[must_use]
274    pub fn direction(&self) -> Option<Vec3> {
275        match self {
276            GizmoAxis::X => Some(Vec3::X),
277            GizmoAxis::Y => Some(Vec3::Y),
278            GizmoAxis::Z => Some(Vec3::Z),
279            _ => None,
280        }
281    }
282
283    /// Returns the color for this axis.
284    #[must_use]
285    pub fn color(&self) -> Vec3 {
286        match self {
287            GizmoAxis::X | GizmoAxis::YZ => Vec3::new(1.0, 0.2, 0.2), // Red
288            GizmoAxis::Y | GizmoAxis::XZ => Vec3::new(0.2, 1.0, 0.2), // Green
289            GizmoAxis::Z | GizmoAxis::XY => Vec3::new(0.2, 0.2, 1.0), // Blue
290            GizmoAxis::All => Vec3::new(1.0, 1.0, 1.0),               // White
291            GizmoAxis::None => Vec3::new(0.5, 0.5, 0.5),              // Gray
292        }
293    }
294}
295
296/// GPU-compatible gizmo uniforms.
297#[repr(C)]
298#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
299pub struct GizmoUniforms {
300    /// Model matrix for the gizmo.
301    pub model: [[f32; 4]; 4],
302    /// Color of the axis being drawn.
303    pub color: [f32; 3],
304    /// Whether the axis is highlighted.
305    pub highlighted: f32,
306}
307
308impl Default for GizmoUniforms {
309    fn default() -> Self {
310        Self {
311            model: Mat4::IDENTITY.to_cols_array_2d(),
312            color: [1.0, 1.0, 1.0],
313            highlighted: 0.0,
314        }
315    }
316}
317
318#[cfg(test)]
319mod tests {
320    use super::*;
321
322    #[test]
323    fn test_transform_matrix_roundtrip() {
324        let t = Transform {
325            translation: Vec3::new(1.0, 2.0, 3.0),
326            rotation: Quat::IDENTITY,
327            scale: Vec3::ONE,
328        };
329        let matrix = t.to_matrix();
330        let back = Transform::from_matrix(matrix);
331        assert!((back.translation - t.translation).length() < 1e-6);
332    }
333
334    #[test]
335    fn test_transform_euler_angles() {
336        let mut t = Transform::identity();
337        t.set_euler_angles_degrees(Vec3::new(0.0, 90.0, 0.0));
338        let angles = t.euler_angles_degrees();
339        assert!((angles.y - 90.0).abs() < 0.1);
340    }
341
342    #[test]
343    fn test_snap_translation() {
344        let mut t = Transform::from_translation(Vec3::new(1.2, 2.7, 3.1));
345        t.snap_translation(0.5);
346        assert_eq!(t.translation, Vec3::new(1.0, 2.5, 3.0));
347    }
348}