Skip to main content

viewport_lib/runtime/systems/
manipulation.rs

1//! Built-in manipulation system for ViewportRuntime.
2
3use std::collections::HashMap;
4
5use crate::interaction::gizmo::{self, Gizmo, GizmoAxis, GizmoMode};
6use crate::interaction::manipulation::GizmoInfo;
7use crate::interaction::manipulation::{
8    ManipulationContext, ManipulationController, ManipResult, TransformDelta,
9};
10use crate::interaction::selection::{NodeId, Selection};
11use crate::runtime::context::RuntimeFrameContext;
12use crate::runtime::output::{RuntimeOutput, TransformWriteback};
13use crate::scene::scene::Scene;
14
15/// Built-in manipulation system for [`super::super::ViewportRuntime`].
16///
17/// Wraps [`ManipulationController`] and drives it each frame from
18/// [`RuntimeFrameContext`] inputs. Handles G/R/S key sessions and gizmo drag.
19/// Writes transform changes via [`TransformWriteback`]; does not mutate the scene
20/// directly.
21///
22/// To suppress orbit while a session is active, check
23/// [`ViewportRuntime::is_manipulating`](super::super::ViewportRuntime::is_manipulating)
24/// before calling [`OrbitCameraController::apply_to_camera`].
25pub struct ManipulationSystem {
26    controller: ManipulationController,
27    transforms_snapshot: HashMap<NodeId, glam::Mat4>,
28    pub(crate) gizmo: Gizmo,
29    pub(crate) gizmo_center: Option<glam::Vec3>,
30    pub(crate) gizmo_scale: f32,
31}
32
33impl Default for ManipulationSystem {
34    fn default() -> Self {
35        Self::new()
36    }
37}
38
39impl ManipulationSystem {
40    /// Create a new ManipulationSystem with default settings.
41    pub fn new() -> Self {
42        Self {
43            controller: ManipulationController::new(),
44            transforms_snapshot: HashMap::new(),
45            gizmo: Gizmo::default(),
46            gizmo_center: None,
47            gizmo_scale: 1.0,
48        }
49    }
50
51    /// True while a G/R/S session or gizmo drag is in progress.
52    pub fn is_active(&self) -> bool {
53        self.controller.is_active()
54    }
55
56    /// World-space center of the current selection, or None if selection is empty.
57    pub fn gizmo_center(&self) -> Option<glam::Vec3> {
58        self.gizmo_center
59    }
60
61    /// Screen-size scale factor for the gizmo arms.
62    pub fn gizmo_scale(&self) -> f32 {
63        self.gizmo_scale
64    }
65
66    /// Current gizmo mode (Translate / Rotate / Scale).
67    pub fn gizmo_mode(&self) -> GizmoMode {
68        self.gizmo.mode
69    }
70
71    /// Currently hovered gizmo axis.
72    pub fn gizmo_hovered(&self) -> GizmoAxis {
73        self.gizmo.hovered_axis
74    }
75
76    /// Build a GizmoInfo from current state, suitable for InteractionFrame.
77    pub fn gizmo_info(&self) -> Option<GizmoInfo> {
78        self.gizmo_center.map(|center| GizmoInfo {
79            center,
80            scale: self.gizmo_scale,
81            orientation: glam::Quat::IDENTITY,
82            mode: self.gizmo.mode,
83        })
84    }
85
86    pub(crate) fn step(
87        &mut self,
88        frame: &RuntimeFrameContext,
89        scene: &Scene,
90        selection: &Selection,
91        writeback: &mut TransformWriteback,
92        _output: &mut RuntimeOutput,
93    ) {
94        // Compute gizmo center from selection.
95        self.gizmo_center = gizmo::gizmo_center_from_selection(selection, |id| {
96            scene.node(id).map(|n| {
97                let t = n.world_transform();
98                glam::Vec3::new(t.w_axis.x, t.w_axis.y, t.w_axis.z)
99            })
100        });
101
102        // Compute gizmo scale.
103        if let Some(center) = self.gizmo_center {
104            self.gizmo_scale = gizmo::compute_gizmo_scale(
105                center,
106                frame.camera.eye_position(),
107                frame.camera.fov_y,
108                frame.viewport_size.y,
109            );
110        }
111
112        // Gizmo hover hit-test (only when no session is active).
113        if !self.controller.is_active() {
114            if let (Some(center), Some(cursor)) = (self.gizmo_center, frame.cursor_viewport) {
115                let w = frame.viewport_size.x.max(1.0);
116                let h = frame.viewport_size.y.max(1.0);
117                let view_proj = frame.camera.proj_matrix() * frame.camera.view_matrix();
118                let inv_vp = view_proj.inverse();
119                let ndc_x = (cursor.x / w) * 2.0 - 1.0;
120                let ndc_y = 1.0 - (cursor.y / h) * 2.0;
121                let far = inv_vp.project_point3(glam::Vec3::new(ndc_x, ndc_y, 1.0));
122                let ray_origin = frame.camera.eye_position();
123                let ray_dir = (far - ray_origin).normalize_or_zero();
124                self.gizmo.hovered_axis = self.gizmo.hit_test_oriented(
125                    ray_origin,
126                    ray_dir,
127                    center,
128                    self.gizmo_scale,
129                    glam::Quat::IDENTITY,
130                );
131            } else {
132                self.gizmo.hovered_axis = GizmoAxis::None;
133            }
134        }
135
136        // Build ManipulationContext.
137        let gizmo_info = self.gizmo_center.map(|center| GizmoInfo {
138            center,
139            scale: self.gizmo_scale,
140            orientation: glam::Quat::IDENTITY,
141            mode: self.gizmo.mode,
142        });
143        let manip_ctx = ManipulationContext {
144            camera: frame.camera.clone(),
145            viewport_size: frame.viewport_size,
146            cursor_viewport: frame.cursor_viewport,
147            pointer_delta: frame.pointer_delta,
148            selection_center: self.gizmo_center,
149            gizmo: gizmo_info,
150            drag_started: frame.drag_started,
151            dragging: frame.dragging,
152            clicked: frame.clicked,
153        };
154
155        let result = self.controller.update(&frame.input, manip_ctx);
156
157        match result {
158            ManipResult::Update(delta) => {
159                self.apply_delta(&delta, scene, selection, writeback);
160            }
161            ManipResult::Commit => {
162                self.save_snapshots(scene, selection);
163            }
164            ManipResult::Cancel | ManipResult::ConstraintChanged => {
165                self.restore_snapshots(writeback);
166            }
167            ManipResult::None => {
168                if !self.controller.is_active() {
169                    self.save_snapshots(scene, selection);
170                }
171            }
172        }
173    }
174
175    fn save_snapshots(&mut self, scene: &Scene, selection: &Selection) {
176        self.transforms_snapshot.clear();
177        for &id in selection.iter() {
178            if let Some(node) = scene.node(id) {
179                self.transforms_snapshot.insert(id, node.local_transform());
180            }
181        }
182    }
183
184    fn restore_snapshots(&self, writeback: &mut TransformWriteback) {
185        for (&id, &snap) in &self.transforms_snapshot {
186            writeback.set(id, glam::Affine3A::from_mat4(snap));
187        }
188    }
189
190    fn apply_delta(
191        &self,
192        delta: &TransformDelta,
193        scene: &Scene,
194        selection: &Selection,
195        writeback: &mut TransformWriteback,
196    ) {
197        let Some(center) = self.gizmo_center else {
198            return;
199        };
200
201        let has_pos_override = delta.position_override.iter().any(|v| v.is_some());
202        let has_scale_override = delta.scale_override.iter().any(|v| v.is_some());
203
204        let rot_mat = glam::Mat4::from_quat(delta.rotation);
205        let scale_mat = glam::Mat4::from_scale(delta.scale);
206        let translate_mat = glam::Mat4::from_translation(delta.translation);
207        let to_pivot = glam::Mat4::from_translation(-center);
208        let from_pivot = glam::Mat4::from_translation(center);
209
210        for &id in selection.iter() {
211            let base = if has_pos_override || has_scale_override {
212                match self.transforms_snapshot.get(&id) {
213                    Some(&snap) => snap,
214                    None => continue,
215                }
216            } else {
217                match scene.node(id) {
218                    Some(n) => n.local_transform(),
219                    None => continue,
220                }
221            };
222
223            let mut new_t = translate_mat * from_pivot * rot_mat * scale_mat * to_pivot * base;
224
225            // Apply per-axis position overrides (numeric input, e.g. G X 2).
226            for (i, &ov) in delta.position_override.iter().enumerate() {
227                if let Some(v) = ov {
228                    new_t.col_mut(3)[i] = v;
229                }
230            }
231
232            writeback.set(id, glam::Affine3A::from_mat4(new_t));
233        }
234    }
235}