Skip to main content

viewport_lib/interaction/input/
controller.rs

1//! High-level orbit/pan/zoom camera controller.
2//!
3//! [`OrbitCameraController`] is the ergonomic entry point for standard viewport
4//! camera navigation. It wraps [`super::viewport_input::ViewportInput`] and
5//! applies resolved orbit / pan / zoom actions directly to a [`crate::Camera`].
6
7use crate::Camera;
8
9use super::action_frame::ActionFrame;
10use super::context::ViewportContext;
11use super::event::ViewportEvent;
12use super::preset::{BindingPreset, viewport_all_bindings, viewport_primitives_bindings};
13use super::viewport_input::ViewportInput;
14
15/// High-level orbit / pan / zoom camera controller.
16///
17/// Wraps the lower-level [`ViewportInput`] resolver and applies semantic
18/// camera actions to a [`Camera`] in a single `apply_to_camera` call.
19///
20/// # Integration pattern (winit / single window)
21///
22/// ```text
23/// // --- AppState construction ---
24/// controller.begin_frame(ViewportContext { hovered: true, focused: true, viewport_size });
25///
26/// // --- window_event ---
27/// controller.push_event(translated_event);
28///
29/// // --- RedrawRequested ---
30/// controller.apply_to_camera(&mut state.camera);
31/// controller.begin_frame(ViewportContext { hovered: true, focused: true, viewport_size });
32/// // ... render ...
33/// ```
34///
35/// # Integration pattern (eframe / egui)
36///
37/// ```text
38/// // --- update() ---
39/// controller.begin_frame(ViewportContext {
40///     hovered: response.hovered(),
41///     focused: response.has_focus(),
42///     viewport_size: [rect.width(), rect.height()],
43/// });
44/// // push events from ui.input(|i| { ... })
45/// controller.apply_to_camera(&mut self.camera);
46/// ```
47pub struct OrbitCameraController {
48    input: ViewportInput,
49    /// Sensitivity for drag-based orbit (radians per pixel).
50    pub orbit_sensitivity: f32,
51    /// Sensitivity for scroll-based zoom (scale factor per pixel).
52    pub zoom_sensitivity: f32,
53    /// Current viewport size (cached from the last `begin_frame`).
54    viewport_size: [f32; 2],
55}
56
57impl OrbitCameraController {
58    /// Default drag orbit sensitivity: 0.005 radians per pixel.
59    pub const DEFAULT_ORBIT_SENSITIVITY: f32 = 0.005;
60    /// Default scroll zoom sensitivity: 0.001 scale per pixel.
61    pub const DEFAULT_ZOOM_SENSITIVITY: f32 = 0.001;
62
63    /// Create a controller from the given binding preset.
64    pub fn new(preset: BindingPreset) -> Self {
65        let bindings = match preset {
66            BindingPreset::ViewportPrimitives => viewport_primitives_bindings(),
67            BindingPreset::ViewportAll => viewport_all_bindings(),
68        };
69        Self {
70            input: ViewportInput::new(bindings),
71            orbit_sensitivity: Self::DEFAULT_ORBIT_SENSITIVITY,
72            zoom_sensitivity: Self::DEFAULT_ZOOM_SENSITIVITY,
73            viewport_size: [1.0, 1.0],
74        }
75    }
76
77    /// Create a controller with the [`BindingPreset::ViewportPrimitives`] preset.
78    ///
79    /// This is the canonical control scheme matching `examples/winit_primitives`.
80    pub fn viewport_primitives() -> Self {
81        Self::new(BindingPreset::ViewportPrimitives)
82    }
83
84    /// Create a controller with the [`BindingPreset::ViewportAll`] preset.
85    ///
86    /// Includes all camera navigation bindings plus keyboard shortcuts for
87    /// normal mode, fly mode, and manipulation mode. Use this to replace
88    /// [`crate::InputSystem`] entirely.
89    pub fn viewport_all() -> Self {
90        Self::new(BindingPreset::ViewportAll)
91    }
92
93    /// Begin a new frame.
94    ///
95    /// Resets per-frame accumulators and records viewport context (hover/focus
96    /// state and size). Call this at the **end** of each rendered frame — after
97    /// `apply_to_camera` — so the accumulator is ready for the next batch of
98    /// events.
99    ///
100    /// Also call once immediately after construction to prime the accumulator.
101    pub fn begin_frame(&mut self, ctx: ViewportContext) {
102        self.viewport_size = ctx.viewport_size;
103        self.input.begin_frame(ctx);
104    }
105
106    /// Push a single viewport-scoped event into the accumulator.
107    ///
108    /// Call this from the host's event handler whenever a relevant native event
109    /// arrives, after translating it to a [`ViewportEvent`].
110    pub fn push_event(&mut self, event: ViewportEvent) {
111        self.input.push_event(event);
112    }
113
114    /// Resolve accumulated events into an [`ActionFrame`] without applying any
115    /// camera navigation.
116    ///
117    /// Use this when the caller needs to inspect actions but camera movement
118    /// should be suppressed — for example during gizmo manipulation or fly mode
119    /// where the camera is driven by other logic.
120    pub fn resolve(&self) -> ActionFrame {
121        self.input.resolve()
122    }
123
124    /// Resolve accumulated events, apply camera navigation, and return the
125    /// [`ActionFrame`] for this frame.
126    ///
127    /// Call this in the render / update step, **before** `begin_frame` for the
128    /// next frame.
129    pub fn apply_to_camera(&self, camera: &mut Camera) -> ActionFrame {
130        let frame = self.input.resolve();
131        let nav = &frame.navigation;
132
133        let h = self.viewport_size[1];
134
135        if nav.orbit != glam::Vec2::ZERO {
136            camera.orbit(
137                nav.orbit.x * self.orbit_sensitivity,
138                nav.orbit.y * self.orbit_sensitivity,
139            );
140        }
141
142        if nav.pan != glam::Vec2::ZERO {
143            camera.pan_pixels(nav.pan, h);
144        }
145
146        if nav.zoom != 0.0 {
147            camera.zoom_by_factor(1.0 - nav.zoom * self.zoom_sensitivity);
148        }
149
150        frame
151    }
152}