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}