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 /// Sensitivity applied to two-finger trackpad rotation gesture (radians per radian).
54 /// Default: `1.0` (gesture angle applied directly to camera yaw).
55 /// Set to `0.0` to suppress the gesture entirely.
56 pub gesture_sensitivity: f32,
57 /// Current viewport size (cached from the last `begin_frame`).
58 viewport_size: [f32; 2],
59}
60
61impl OrbitCameraController {
62 /// Default drag orbit sensitivity: 0.005 radians per pixel.
63 pub const DEFAULT_ORBIT_SENSITIVITY: f32 = 0.005;
64 /// Default scroll zoom sensitivity: 0.001 scale per pixel.
65 pub const DEFAULT_ZOOM_SENSITIVITY: f32 = 0.001;
66 /// Default gesture sensitivity: 1.0 (gesture radians applied 1:1 to camera yaw).
67 pub const DEFAULT_GESTURE_SENSITIVITY: f32 = 1.0;
68
69 /// Create a controller from the given binding preset.
70 pub fn new(preset: BindingPreset) -> Self {
71 let bindings = match preset {
72 BindingPreset::ViewportPrimitives => viewport_primitives_bindings(),
73 BindingPreset::ViewportAll => viewport_all_bindings(),
74 };
75 Self {
76 input: ViewportInput::new(bindings),
77 orbit_sensitivity: Self::DEFAULT_ORBIT_SENSITIVITY,
78 zoom_sensitivity: Self::DEFAULT_ZOOM_SENSITIVITY,
79 gesture_sensitivity: Self::DEFAULT_GESTURE_SENSITIVITY,
80 viewport_size: [1.0, 1.0],
81 }
82 }
83
84 /// Create a controller with the [`BindingPreset::ViewportPrimitives`] preset.
85 ///
86 /// This is the canonical control scheme matching `examples/winit_primitives`.
87 pub fn viewport_primitives() -> Self {
88 Self::new(BindingPreset::ViewportPrimitives)
89 }
90
91 /// Create a controller with the [`BindingPreset::ViewportAll`] preset.
92 ///
93 /// Includes all camera navigation bindings plus keyboard shortcuts for
94 /// normal mode, fly mode, and manipulation mode. Use this to replace
95 /// [`crate::InputSystem`] entirely.
96 pub fn viewport_all() -> Self {
97 Self::new(BindingPreset::ViewportAll)
98 }
99
100 /// Begin a new frame.
101 ///
102 /// Resets per-frame accumulators and records viewport context (hover/focus
103 /// state and size). Call this at the **end** of each rendered frame — after
104 /// `apply_to_camera` — so the accumulator is ready for the next batch of
105 /// events.
106 ///
107 /// Also call once immediately after construction to prime the accumulator.
108 pub fn begin_frame(&mut self, ctx: ViewportContext) {
109 self.viewport_size = ctx.viewport_size;
110 self.input.begin_frame(ctx);
111 }
112
113 /// Push a single viewport-scoped event into the accumulator.
114 ///
115 /// Call this from the host's event handler whenever a relevant native event
116 /// arrives, after translating it to a [`ViewportEvent`].
117 pub fn push_event(&mut self, event: ViewportEvent) {
118 self.input.push_event(event);
119 }
120
121 /// Resolve accumulated events into an [`ActionFrame`] without applying any
122 /// camera navigation.
123 ///
124 /// Use this when the caller needs to inspect actions but camera movement
125 /// should be suppressed — for example during gizmo manipulation or fly mode
126 /// where the camera is driven by other logic.
127 pub fn resolve(&self) -> ActionFrame {
128 self.input.resolve()
129 }
130
131 /// Resolve accumulated events, apply camera navigation, and return the
132 /// [`ActionFrame`] for this frame.
133 ///
134 /// Call this in the render / update step, **before** `begin_frame` for the
135 /// next frame.
136 pub fn apply_to_camera(&self, camera: &mut Camera) -> ActionFrame {
137 let frame = self.input.resolve();
138 let nav = &frame.navigation;
139
140 let h = self.viewport_size[1];
141
142 if nav.orbit != glam::Vec2::ZERO {
143 camera.orbit(
144 nav.orbit.x * self.orbit_sensitivity,
145 nav.orbit.y * self.orbit_sensitivity,
146 );
147 }
148
149 // Two-finger trackpad rotation: already in radians, apply via gesture_sensitivity.
150 if nav.twist != 0.0 && self.gesture_sensitivity != 0.0 {
151 camera.orbit(nav.twist * self.gesture_sensitivity, 0.0);
152 }
153
154 if nav.pan != glam::Vec2::ZERO {
155 camera.pan_pixels(nav.pan, h);
156 }
157
158 if nav.zoom != 0.0 {
159 camera.zoom_by_factor(1.0 - nav.zoom * self.zoom_sensitivity);
160 }
161
162 frame
163 }
164}