Another project
0

Configure Feed

Select the types of activity you want to include in your feed.

at main 12 kB View raw
1use bone_types::{AxisAngle, Camera3, OrbitState, Result, ZoomFactor}; 2 3use crate::camera::ViewportExtent; 4use crate::camera3::{ 5 ViewportPoint, arcball_rotation, orbit_about_point, pan_pixels, roll_about_view, 6 zoom_about_pixel, 7}; 8 9const ZOOM_DRAG_PER_PIXEL: f64 = 1.0075; 10 11#[derive(Copy, Clone, Debug, PartialEq, Eq)] 12pub struct DragModifiers { 13 ctrl: bool, 14 shift: bool, 15 alt: bool, 16} 17 18impl DragModifiers { 19 pub const NONE: Self = Self { 20 ctrl: false, 21 shift: false, 22 alt: false, 23 }; 24 25 #[must_use] 26 pub const fn with_ctrl(self) -> Self { 27 Self { 28 ctrl: true, 29 shift: self.shift, 30 alt: self.alt, 31 } 32 } 33 34 #[must_use] 35 pub const fn with_shift(self) -> Self { 36 Self { 37 ctrl: self.ctrl, 38 shift: true, 39 alt: self.alt, 40 } 41 } 42 43 #[must_use] 44 pub const fn with_alt(self) -> Self { 45 Self { 46 ctrl: self.ctrl, 47 shift: self.shift, 48 alt: true, 49 } 50 } 51 52 #[must_use] 53 pub const fn gesture(self) -> NavGesture { 54 if self.ctrl { 55 NavGesture::Pan 56 } else if self.shift { 57 NavGesture::Zoom 58 } else if self.alt { 59 NavGesture::Roll 60 } else { 61 NavGesture::Orbit 62 } 63 } 64} 65 66#[derive(Copy, Clone, Debug, PartialEq, Eq)] 67pub enum NavGesture { 68 Orbit, 69 Pan, 70 Roll, 71 Zoom, 72} 73 74#[derive(Copy, Clone, Debug, PartialEq)] 75struct Drag { 76 gesture: NavGesture, 77 last: ViewportPoint, 78} 79 80#[derive(Copy, Clone, Debug, PartialEq)] 81pub struct ViewportNavigator { 82 drag: Option<Drag>, 83 orbit: OrbitState, 84} 85 86impl ViewportNavigator { 87 #[must_use] 88 pub fn new() -> Self { 89 Self { 90 drag: None, 91 orbit: OrbitState::identity(), 92 } 93 } 94 95 #[must_use] 96 pub fn orbit_rotation(&self) -> AxisAngle { 97 self.orbit.rotation() 98 } 99 100 #[must_use] 101 pub fn is_dragging(&self) -> bool { 102 self.drag.is_some() 103 } 104 105 pub fn begin_drag(&mut self, gesture: NavGesture, cursor: ViewportPoint) { 106 self.drag = Some(Drag { 107 gesture, 108 last: cursor, 109 }); 110 } 111 112 pub fn end_drag(&mut self) { 113 self.drag = None; 114 } 115 116 pub fn drag_to( 117 &mut self, 118 cursor: ViewportPoint, 119 camera: Camera3, 120 extent: ViewportExtent, 121 ) -> Result<Camera3> { 122 let Some(drag) = self.drag else { 123 return Ok(camera); 124 }; 125 let next = match drag.gesture { 126 NavGesture::Orbit => self.orbit_step(camera, extent, cursor, drag.last)?, 127 NavGesture::Pan => pan_pixels(camera, extent, drag.last, cursor)?, 128 NavGesture::Roll => roll_about_view(camera, extent, cursor, drag.last)?, 129 NavGesture::Zoom => { 130 let factor = ZoomFactor::new(ZOOM_DRAG_PER_PIXEL.powf(drag.last.y() - cursor.y()))?; 131 zoom_about_pixel(camera, extent, cursor, factor)? 132 } 133 }; 134 self.drag = Some(Drag { 135 gesture: drag.gesture, 136 last: cursor, 137 }); 138 Ok(next) 139 } 140 141 pub fn orbit_pixels( 142 &mut self, 143 camera: Camera3, 144 extent: ViewportExtent, 145 dx: f64, 146 dy: f64, 147 ) -> Result<Camera3> { 148 let cx = f64::from(extent.width().value()) * 0.5; 149 let cy = f64::from(extent.height().value()) * 0.5; 150 let from = ViewportPoint::new(cx, cy)?; 151 let to = ViewportPoint::new(cx + dx, cy + dy)?; 152 self.orbit_step(camera, extent, to, from) 153 } 154 155 fn orbit_step( 156 &mut self, 157 camera: Camera3, 158 extent: ViewportExtent, 159 from: ViewportPoint, 160 to: ViewportPoint, 161 ) -> Result<Camera3> { 162 let delta = arcball_rotation(camera, extent, from, to)?; 163 let oriented = orbit_about_point(camera, camera.target(), delta)?; 164 self.orbit = self.orbit.rotated(delta); 165 Ok(oriented) 166 } 167} 168 169impl Default for ViewportNavigator { 170 fn default() -> Self { 171 Self::new() 172 } 173} 174 175#[cfg(test)] 176mod tests { 177 use super::*; 178 use crate::camera::ViewportPx; 179 use bone_types::{Point3, Projection, UnitVec3}; 180 use uom::si::f64::Length; 181 use uom::si::length::millimeter; 182 183 fn extent() -> ViewportExtent { 184 ViewportExtent::square(ViewportPx::new(256)) 185 } 186 187 fn camera() -> Camera3 { 188 let Ok(projection) = Projection::orthographic(Length::new::<millimeter>(2.0)) else { 189 panic!("half height is positive"); 190 }; 191 let Ok(camera) = Camera3::new( 192 Point3::from_mm(0.0, -10.0, 0.0), 193 Point3::origin(), 194 UnitVec3::z_axis(), 195 projection, 196 ) else { 197 panic!("camera is non-degenerate"); 198 }; 199 camera 200 } 201 202 fn vp(x: f64, y: f64) -> ViewportPoint { 203 let Ok(point) = ViewportPoint::new(x, y) else { 204 panic!("pixel coordinates are finite"); 205 }; 206 point 207 } 208 209 fn close(a: Point3, b: Point3, tol: f64) -> bool { 210 let (ax, ay, az) = a.coords_mm(); 211 let (bx, by, bz) = b.coords_mm(); 212 (ax - bx).abs() < tol && (ay - by).abs() < tol && (az - bz).abs() < tol 213 } 214 215 #[test] 216 fn modifiers_select_the_gesture() { 217 assert_eq!(DragModifiers::NONE.gesture(), NavGesture::Orbit); 218 assert_eq!(DragModifiers::NONE.with_ctrl().gesture(), NavGesture::Pan); 219 assert_eq!(DragModifiers::NONE.with_shift().gesture(), NavGesture::Zoom); 220 assert_eq!(DragModifiers::NONE.with_alt().gesture(), NavGesture::Roll); 221 } 222 223 #[test] 224 fn ctrl_outranks_shift_outranks_alt() { 225 assert_eq!( 226 DragModifiers::NONE.with_ctrl().with_shift().gesture(), 227 NavGesture::Pan, 228 "ctrl pans even when shift is also held" 229 ); 230 assert_eq!( 231 DragModifiers::NONE.with_shift().with_alt().gesture(), 232 NavGesture::Zoom, 233 "shift zooms even when alt is also held" 234 ); 235 assert_eq!( 236 DragModifiers::NONE.with_alt().with_ctrl().gesture(), 237 NavGesture::Pan, 238 "the precedence is independent of the order the modifiers were set" 239 ); 240 } 241 242 #[test] 243 fn drag_without_a_begin_is_a_no_op() { 244 let mut nav = ViewportNavigator::new(); 245 assert!(!nav.is_dragging()); 246 let Ok(after) = nav.drag_to(vp(10.0, 10.0), camera(), extent()) else { 247 panic!("an idle navigator passes the camera through"); 248 }; 249 assert_eq!(after, camera()); 250 } 251 252 #[test] 253 fn orbit_drag_holds_the_target_and_accumulates_rotation() { 254 let mut nav = ViewportNavigator::new(); 255 nav.begin_drag(NavGesture::Orbit, vp(128.0, 128.0)); 256 assert!(nav.is_dragging()); 257 let Ok(orbited) = nav.drag_to(vp(190.0, 128.0), camera(), extent()) else { 258 panic!("an orbit drag transforms the camera"); 259 }; 260 assert!( 261 close(orbited.target(), camera().target(), 1e-9), 262 "the orbit pivots on the target, so it cannot move" 263 ); 264 assert_ne!( 265 orbited.eye(), 266 camera().eye(), 267 "a horizontal orbit drag must move the eye" 268 ); 269 let rotated = nav.orbit_rotation().angle().get::<uom::si::angle::radian>(); 270 assert!( 271 rotated.abs() > 1e-3, 272 "the orbit state accumulates the drag rotation: {rotated}" 273 ); 274 } 275 276 #[test] 277 fn orbit_drag_right_pulls_the_grabbed_model_with_the_cursor() { 278 let mut nav = ViewportNavigator::new(); 279 nav.begin_drag(NavGesture::Orbit, vp(128.0, 128.0)); 280 let Ok(orbited) = nav.drag_to(vp(190.0, 128.0), camera(), extent()) else { 281 panic!("an orbit drag transforms the camera"); 282 }; 283 let (ex, _, _) = orbited.eye().coords_mm(); 284 assert!( 285 ex < 0.0, 286 "dragging right swings the eye to -x so the grabbed face follows the cursor like pan: {ex}" 287 ); 288 } 289 290 #[test] 291 fn pan_drag_keeps_the_grabbed_point_under_the_cursor() { 292 let mut nav = ViewportNavigator::new(); 293 let from = vp(100.0, 110.0); 294 let to = vp(160.0, 140.0); 295 let Ok(grabbed) = crate::camera3::world_on_focal_plane(camera(), extent(), from) else { 296 panic!("the grabbed pixel has a focal point"); 297 }; 298 nav.begin_drag(NavGesture::Pan, from); 299 let Ok(panned) = nav.drag_to(to, camera(), extent()) else { 300 panic!("a pan drag transforms the camera"); 301 }; 302 let Ok(landed) = crate::camera3::world_on_focal_plane(panned, extent(), to) else { 303 panic!("the released pixel has a focal point"); 304 }; 305 assert!( 306 close(grabbed, landed, 1e-6), 307 "the grabbed world point should sit under the release cursor" 308 ); 309 } 310 311 #[test] 312 fn roll_drag_spins_up_about_the_view_without_moving_the_eye() { 313 let mut nav = ViewportNavigator::new(); 314 nav.begin_drag(NavGesture::Roll, vp(200.0, 128.0)); 315 let Ok(rolled) = nav.drag_to(vp(128.0, 200.0), camera(), extent()) else { 316 panic!("a roll drag transforms the camera"); 317 }; 318 assert!( 319 close(rolled.eye(), camera().eye(), 1e-9) 320 && close(rolled.target(), camera().target(), 1e-9), 321 "a roll keeps the eye and target fixed" 322 ); 323 assert_ne!(rolled.up(), camera().up(), "a roll reorients the up vector"); 324 } 325 326 #[test] 327 fn scroll_orbit_holds_the_target_and_accumulates_rotation() { 328 let mut nav = ViewportNavigator::new(); 329 let Ok(orbited) = nav.orbit_pixels(camera(), extent(), 60.0, 0.0) else { 330 panic!("a scroll delta orbits the camera"); 331 }; 332 assert!( 333 close(orbited.target(), camera().target(), 1e-9), 334 "a scroll orbit pivots on the target" 335 ); 336 assert_ne!( 337 orbited.eye(), 338 camera().eye(), 339 "a horizontal scroll orbit moves the eye" 340 ); 341 let rotated = nav.orbit_rotation().angle().get::<uom::si::angle::radian>(); 342 assert!( 343 rotated.abs() > 1e-3, 344 "a scroll orbit accumulates rotation like a drag: {rotated}" 345 ); 346 } 347 348 #[test] 349 fn zoom_drag_down_enlarges_the_orthographic_view() { 350 use bone_types::ProjectionKind; 351 let mut nav = ViewportNavigator::new(); 352 nav.begin_drag(NavGesture::Zoom, vp(128.0, 100.0)); 353 let Ok(zoomed) = nav.drag_to(vp(128.0, 160.0), camera(), extent()) else { 354 panic!("a zoom drag transforms the camera"); 355 }; 356 let ProjectionKind::Orthographic { half_height } = zoomed.projection().kind() else { 357 panic!("the seed camera is orthographic"); 358 }; 359 assert!( 360 half_height.get::<uom::si::length::millimeter>() > 2.0, 361 "dragging the zoom gesture downward zooms out, growing the half height" 362 ); 363 } 364 365 #[test] 366 fn end_drag_clears_the_active_gesture() { 367 let mut nav = ViewportNavigator::new(); 368 nav.begin_drag(NavGesture::Orbit, vp(40.0, 40.0)); 369 assert!(nav.is_dragging()); 370 nav.end_drag(); 371 assert!( 372 !nav.is_dragging(), 373 "losing focus or releasing the button drops the in-flight drag" 374 ); 375 let Ok(after) = nav.drag_to(vp(80.0, 80.0), camera(), extent()) else { 376 panic!("a cleared navigator passes the camera through"); 377 }; 378 assert_eq!( 379 after, 380 camera(), 381 "a move after the drag clears must not orbit" 382 ); 383 } 384}