1use crate::view::{add3, cross3, dot3, len3, norm3, rotate3, scale3, sub3, Projection, ViewCamera};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum Gesture {
18 None,
19 Rotate,
20 Pan,
21}
22
23pub const BUTTON_LEFT: i32 = 0;
25pub const BUTTON_MIDDLE: i32 = 1;
26pub const BUTTON_RIGHT: i32 = 2;
27
28const ZOOM_PER_NOTCH: f64 = 1.08;
49const WHEEL_POINTS_PER_NOTCH: f64 = 70.0;
51const MAX_WHEEL_NOTCHES_PER_EVENT: f64 = 3.0;
56const MIN_ORTHO_HALF_HEIGHT: f64 = 1e-9;
57const MIN_PERSP_DISTANCE: f64 = 1e-6;
58
59#[derive(Debug, Default)]
60pub struct ArcballControls {
61 pub enabled: bool,
62 gesture: GestureState,
63}
64
65#[derive(Debug)]
66struct GestureState {
67 kind: Gesture,
68 last: (f64, f64),
69}
70
71impl Default for GestureState {
72 fn default() -> Self {
73 Self {
74 kind: Gesture::None,
75 last: (0.0, 0.0),
76 }
77 }
78}
79
80impl ArcballControls {
81 pub fn new() -> Self {
82 Self {
83 enabled: true,
84 gesture: GestureState::default(),
85 }
86 }
87
88 pub fn active_gesture(&self) -> Gesture {
89 self.gesture.kind
90 }
91
92 pub fn pointer_down(&mut self, x: f64, y: f64, button: i32) -> bool {
95 if !self.enabled {
96 return false;
97 }
98 self.gesture.kind = match button {
99 BUTTON_LEFT => Gesture::Rotate,
100 BUTTON_MIDDLE | BUTTON_RIGHT => Gesture::Pan,
101 _ => Gesture::None,
102 };
103 self.gesture.last = (x, y);
104 self.gesture.kind != Gesture::None
105 }
106
107 pub fn pointer_move(&mut self, camera: &mut ViewCamera, x: f64, y: f64) -> bool {
110 if !self.enabled || self.gesture.kind == Gesture::None {
111 return false;
112 }
113 let (lx, ly) = self.gesture.last;
114 if (x - lx).abs() < f64::EPSILON && (y - ly).abs() < f64::EPSILON {
115 return false;
116 }
117 match self.gesture.kind {
118 Gesture::Rotate => rotate_arcball(camera, (lx, ly), (x, y)),
119 Gesture::Pan => pan(camera, x - lx, y - ly),
120 Gesture::None => {}
121 }
122 self.gesture.last = (x, y);
123 true
124 }
125
126 pub fn pointer_up(&mut self) -> bool {
128 let was = self.gesture.kind != Gesture::None;
129 self.gesture.kind = Gesture::None;
130 was
131 }
132
133 pub fn touch_navigation(
137 &mut self,
138 camera: &mut ViewCamera,
139 from: [f64; 2],
140 to: [f64; 2],
141 scale: f64,
142 ) -> bool {
143 if !self.enabled
144 || !scale.is_finite()
145 || scale <= 0.0
146 || !from.into_iter().chain(to).all(f64::is_finite)
147 || (from == to && scale == 1.0)
148 {
149 return false;
150 }
151 zoom_toward(camera, scale.recip(), from[0], from[1]);
152 pan(camera, to[0] - from[0], to[1] - from[1]);
153 true
154 }
155
156 pub fn wheel(
160 &mut self,
161 camera: &mut ViewCamera,
162 delta_y: f64,
163 cursor: Option<[f64; 2]>,
164 ) -> bool {
165 if !self.enabled || delta_y == 0.0 {
166 return false;
167 }
168 let notches = (delta_y / WHEEL_POINTS_PER_NOTCH)
172 .clamp(-MAX_WHEEL_NOTCHES_PER_EVENT, MAX_WHEEL_NOTCHES_PER_EVENT);
173 let factor = ZOOM_PER_NOTCH.powf(notches);
174 match cursor {
175 Some([cx, cy]) => zoom_toward(camera, factor, cx, cy),
176 None => zoom(camera, factor),
177 }
178 true
179 }
180}
181
182pub fn zoom_toward(camera: &mut ViewCamera, factor: f64, cx: f64, cy: f64) {
185 let wpp = camera.world_per_pixel();
186 let (right, up, _) = camera.basis();
187 let sx = cx - camera.width * 0.5;
188 let sy = -(cy - camera.height * 0.5); let off = add3(scale3(right, sx * wpp), scale3(up, sy * wpp));
191 match &mut camera.projection {
192 Projection::Orthographic { half_height } => {
193 let old = *half_height;
194 *half_height = (old * factor).max(MIN_ORTHO_HALF_HEIGHT);
195 let f = *half_height / old; let shift = scale3(off, 1.0 - f);
197 camera.target = add3(camera.target, shift);
198 camera.eye = add3(camera.eye, shift);
199 }
200 Projection::Perspective { .. } => {
201 let cursor_world = add3(camera.target, off);
204 let nt = add3(
205 cursor_world,
206 scale3(sub3(camera.target, cursor_world), factor),
207 );
208 let ne = add3(cursor_world, scale3(sub3(camera.eye, cursor_world), factor));
209 let dir = sub3(ne, nt);
210 let dist = len3(dir).max(MIN_PERSP_DISTANCE);
211 camera.target = nt;
212 camera.eye = add3(nt, scale3(norm3(dir), dist));
213 }
214 }
215}
216
217pub fn zoom(camera: &mut ViewCamera, factor: f64) {
219 match &mut camera.projection {
220 Projection::Orthographic { half_height } => {
221 *half_height = (*half_height * factor).max(MIN_ORTHO_HALF_HEIGHT);
222 }
223 Projection::Perspective { .. } => {
224 let dir = sub3(camera.eye, camera.target);
225 let dist = (len3(dir) * factor).max(MIN_PERSP_DISTANCE);
226 camera.eye = add3(camera.target, scale3(norm3(dir), dist));
227 }
228 }
229}
230
231pub fn pan(camera: &mut ViewCamera, dx: f64, dy: f64) {
234 let (right, up, _) = camera.basis();
235 let wpp = camera.world_per_pixel();
236 let offset = add3(scale3(right, -dx * wpp), scale3(up, dy * wpp));
237 camera.eye = add3(camera.eye, offset);
238 camera.target = add3(camera.target, offset);
239}
240
241fn trackball_point(camera: &ViewCamera, x: f64, y: f64) -> [f64; 3] {
245 let radius = 0.5 * camera.width.min(camera.height).max(1.0) * 0.75;
246 let cx = camera.width * 0.5;
247 let cy = camera.height * 0.5;
248 let px = x - cx;
249 let py = cy - y; let r2 = radius * radius;
251 let d2 = px * px + py * py;
252 let pz = if d2 <= r2 * 0.5 {
253 (r2 - d2).sqrt()
254 } else {
255 r2 * 0.5 / d2.sqrt()
257 };
258 norm3([px, py, pz])
259}
260
261fn rotate_arcball(camera: &mut ViewCamera, from: (f64, f64), to: (f64, f64)) {
265 let v0 = trackball_point(camera, from.0, from.1);
266 let v1 = trackball_point(camera, to.0, to.1);
267 let axis_cam = cross3(v0, v1);
268 let axis_len = len3(axis_cam);
269 if axis_len < 1e-12 {
270 return;
271 }
272 let angle = dot3(v0, v1).clamp(-1.0, 1.0).acos();
273 if angle.abs() < 1e-12 {
274 return;
275 }
276 let (right, up, forward) = camera.basis();
279 let axis_cam = scale3(axis_cam, 1.0 / axis_len);
280 let axis_world = norm3(add3(
281 add3(scale3(right, axis_cam[0]), scale3(up, axis_cam[1])),
282 scale3(forward, -axis_cam[2]),
283 ));
284 let offset = sub3(camera.eye, camera.target);
285 camera.eye = add3(camera.target, rotate3(offset, axis_world, -angle));
286 camera.up = norm3(rotate3(camera.up, axis_world, -angle));
287}
288