From 7f4afd58bea7e833173a4892b7f244ba22e2382f Mon Sep 17 00:00:00 2001 From: Matt Keeter Date: Wed, 9 Oct 2024 08:29:38 -0400 Subject: [PATCH 1/5] Working on camera abstraction --- Cargo.lock | 8 +++ Cargo.toml | 1 + demos/camera/Cargo.toml | 7 ++ demos/camera/src/lib.rs | 101 +++++++++++++++++++++++++++++ demos/viewer/Cargo.toml | 1 + demos/viewer/src/main.rs | 136 ++++++++++++++++++--------------------- 6 files changed, 179 insertions(+), 75 deletions(-) create mode 100644 demos/camera/Cargo.toml create mode 100644 demos/camera/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index eafb16f3..d7b43f31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -290,6 +290,13 @@ dependencies = [ "wayland-client", ] +[[package]] +name = "camera" +version = "0.1.0" +dependencies = [ + "nalgebra", +] + [[package]] name = "cast" version = "0.3.0" @@ -906,6 +913,7 @@ name = "fidget-viewer" version = "0.1.0" dependencies = [ "anyhow", + "camera", "clap", "crossbeam-channel", "eframe", diff --git a/Cargo.toml b/Cargo.toml index 91e853f5..0035b40c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "demos/constraints", "demos/cli", "demos/viewer", + "demos/camera", "workspace-hack", ] exclude = ["demos/web-editor/crate"] diff --git a/demos/camera/Cargo.toml b/demos/camera/Cargo.toml new file mode 100644 index 00000000..6ac7638e --- /dev/null +++ b/demos/camera/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "camera" +version = "0.1.0" +edition = "2021" + +[dependencies] +nalgebra.workspace = true diff --git a/demos/camera/src/lib.rs b/demos/camera/src/lib.rs new file mode 100644 index 00000000..e7dcbca8 --- /dev/null +++ b/demos/camera/src/lib.rs @@ -0,0 +1,101 @@ +use nalgebra::{Point2, Vector2}; + +#[derive(Copy, Clone, Debug)] +pub struct Camera2D { + scale: f32, + + /// Offset in world units + offset: Vector2, + + /// Position in screen units + drag_start: Option>, + + /// Size of the viewport in screen units + rect: Rect, + + /// Size of the viewport in world units + uv: Rect, +} + +#[derive(Copy, Clone, Debug)] +pub struct Rect { + pub min: Vector2, + pub max: Vector2, +} + +impl Camera2D { + /// Builds a new camera with the given viewport + pub fn new(rect: Rect, uv: Rect) -> Self { + Camera2D { + drag_start: None, + scale: 1.0, + offset: Vector2::zeros(), + rect, + uv, + } + } + + pub fn offset(&self) -> Vector2 { + self.offset + } + + pub fn scale(&self) -> f32 { + self.scale + } + + /// Updates the screen and world viewport sizes + pub fn set_viewport(&mut self, rect: Rect, uv: Rect) { + self.rect = rect; + self.uv = uv; + } + + /// Converts from mouse position to a UV position within the render window + fn mouse_to_uv(&self, p: Point2) -> Vector2 { + let r = (p - self.rect.min) + .coords + .component_div(&(self.rect.max - self.rect.min)); + const ONE: Vector2 = Vector2::new(1.0, 1.0); + let pos = self.uv.min.component_mul(&(ONE - r)) + + self.uv.max.component_mul(&r); + let out = ((pos * 2.0) - ONE) * self.scale; + println!("{p:?} -> {:?}", out + self.offset); + out + self.offset + } + + /// Performs a new drag operation + /// + /// Returns `true` if the camera has changed + pub fn drag(&mut self, pos: Point2) -> bool { + if let Some(start) = self.drag_start { + self.offset = Vector2::zeros(); + let pos = self.mouse_to_uv(pos); + println!("{start:?} -> {pos:?}"); + self.offset = start - pos; + true + } else { + let pos = self.mouse_to_uv(pos); + self.drag_start = Some(pos); + false + } + } + + /// Releases the drag + pub fn release(&mut self) { + self.drag_start = None + } + + /// Called when the mouse is scrolled + pub fn scroll(&mut self, pos: Option>, scroll: f32) -> bool { + if scroll != 0.0 { + let pos_before = pos.map(|p| self.mouse_to_uv(p)); + self.scale /= (scroll / 100.0).exp2(); + if let Some(pos_before) = pos_before { + let pos_after = self.mouse_to_uv(pos.unwrap()); + self.offset += pos_before - pos_after; + } + true + } else { + false + } + } +} diff --git a/demos/viewer/Cargo.toml b/demos/viewer/Cargo.toml index 79d9132b..12618500 100644 --- a/demos/viewer/Cargo.toml +++ b/demos/viewer/Cargo.toml @@ -16,6 +16,7 @@ notify.workspace = true rhai.workspace = true fidget = { path = "../../fidget", default-features = false, features = ["render", "rhai"] } +camera = { path = "../camera" } workspace-hack = { version = "0.1", path = "../../workspace-hack" } [features] diff --git a/demos/viewer/src/main.rs b/demos/viewer/src/main.rs index 5836be7c..f1cc6423 100644 --- a/demos/viewer/src/main.rs +++ b/demos/viewer/src/main.rs @@ -5,7 +5,7 @@ use eframe::egui; use env_logger::Env; use fidget::render::RenderConfig; use log::{debug, error, info, warn}; -use nalgebra::{Vector2, Vector3}; +use nalgebra::{Point2, Vector2, Vector3}; use notify::Watcher; use std::{error::Error, path::Path}; @@ -163,14 +163,14 @@ fn render( image_size, tile_sizes: F::tile_sizes_2d().to_vec(), bounds: fidget::shape::Bounds { - center: Vector2::new(camera.offset.x, camera.offset.y), - size: camera.scale, + center: camera.offset(), + size: camera.scale(), }, ..RenderConfig::default() }; match mode { - TwoDMode::Color => { + Mode2D::Color => { let image = fidget::render::render2d::< _, fidget::render::BitRenderMode, @@ -188,7 +188,7 @@ fn render( } } - TwoDMode::Sdf => { + Mode2D::Sdf => { let image = fidget::render::render2d::< _, fidget::render::SdfRenderMode, @@ -198,7 +198,7 @@ fn render( } } - TwoDMode::Debug => { + Mode2D::Debug => { let image = fidget::render::render2d::< _, fidget::render::DebugRenderMode, @@ -294,7 +294,7 @@ fn main() -> Result<(), Box> { // Automatically select the best implementation for your platform. let mut watcher = notify::recommended_watcher(move |res| match res { Ok(event) => { - println!("{event:?}"); + info!("file watcher: {event:?}"); file_watcher_tx.send(()).unwrap(); } Err(e) => panic!("watch error: {:?}", e), @@ -312,6 +312,7 @@ fn main() -> Result<(), Box> { // Run a worker thread which listens for wake events and pokes the // UI whenever they come in. let egui_ctx = cc.egui_ctx.clone(); + let rect = egui_ctx.available_rect(); std::thread::spawn(move || { while let Ok(()) = wake_rx.recv() { egui_ctx.request_repaint(); @@ -319,7 +320,7 @@ fn main() -> Result<(), Box> { info!("wake thread is done"); }); - Box::new(ViewerApp::new(config_tx, render_rx)) + Box::new(ViewerApp::new(rect, config_tx, render_rx)) }), )?; @@ -328,42 +329,8 @@ fn main() -> Result<(), Box> { //////////////////////////////////////////////////////////////////////////////// -#[derive(Copy, Clone)] -struct TwoDCamera { - // 2D camera parameters - scale: f32, - offset: egui::Vec2, - drag_start: Option, -} - -impl TwoDCamera { - /// Converts from mouse position to a UV position within the render window - fn mouse_to_uv( - &self, - rect: egui::Rect, - uv: egui::Rect, - p: egui::Pos2, - ) -> egui::Vec2 { - let r = (p - rect.min) / (rect.max - rect.min); - const ONE: egui::Vec2 = egui::Vec2::new(1.0, 1.0); - let pos = uv.min.to_vec2() * (ONE - r) + uv.max.to_vec2() * r; - let out = ((pos * 2.0) - ONE) * self.scale; - egui::Vec2::new(out.x, -out.y) + self.offset - } -} - -impl Default for TwoDCamera { - fn default() -> Self { - TwoDCamera { - drag_start: None, - scale: 1.0, - offset: egui::Vec2::ZERO, - } - } -} - #[derive(Copy, Clone, Eq, PartialEq)] -enum TwoDMode { +enum Mode2D { Color, Sdf, Debug, @@ -412,12 +379,12 @@ enum ThreeDMode { #[derive(Copy, Clone)] enum RenderMode { - TwoD(TwoDCamera, TwoDMode), + TwoD(camera::Camera2D, Mode2D), ThreeD(ThreeDCamera, ThreeDMode), } impl RenderMode { - fn set_2d_mode(&mut self, mode: TwoDMode) -> bool { + fn set_2d_mode(&mut self, mode: Mode2D) -> bool { match self { RenderMode::TwoD(.., m) => { let changed = *m != mode; @@ -425,7 +392,20 @@ impl RenderMode { changed } RenderMode::ThreeD(..) => { - *self = RenderMode::TwoD(TwoDCamera::default(), mode); + *self = RenderMode::TwoD( + // TODO get parameters from 3D camera here? + camera::Camera2D::new( + camera::Rect { + min: Vector2::new(0.0, 0.0), + max: Vector2::new(640.0, 480.0), + }, + camera::Rect { + min: Vector2::new(-1.0, -1.0), + max: Vector2::new(1.0, 1.0), + }, + ), + mode, + ); true } } @@ -466,6 +446,7 @@ struct ViewerApp { impl ViewerApp { fn new( + rect: egui::Rect, config_tx: Sender, image_rx: Receiver>, ) -> Self { @@ -479,7 +460,19 @@ impl ViewerApp { config_tx, image_rx, - mode: RenderMode::TwoD(TwoDCamera::default(), TwoDMode::Color), + mode: RenderMode::TwoD( + camera::Camera2D::new( + camera::Rect { + min: Vector2::new(rect.min.x, rect.min.y), + max: Vector2::new(rect.max.x, rect.max.y), + }, + camera::Rect { + min: Vector2::new(-1.0, -1.0), + max: Vector2::new(1.0, 1.0), + }, + ), + Mode2D::Color, + ), } } @@ -512,13 +505,13 @@ impl ViewerApp { }; ui.radio_value( &mut mode_2d, - Some(TwoDMode::Debug), + Some(Mode2D::Debug), "2D debug", ); - ui.radio_value(&mut mode_2d, Some(TwoDMode::Sdf), "2D SDF"); + ui.radio_value(&mut mode_2d, Some(Mode2D::Sdf), "2D SDF"); ui.radio_value( &mut mode_2d, - Some(TwoDMode::Color), + Some(Mode2D::Color), "2D Color", ); @@ -679,37 +672,30 @@ impl eframe::App for ViewerApp { // Handle pan and zoom match &mut self.mode { RenderMode::TwoD(camera, ..) => { + let rect = camera::Rect { + min: Vector2::new(rect.min.x, rect.min.y), + max: Vector2::new(rect.max.x, rect.max.y), + }; + let uv = camera::Rect { + min: Vector2::new(uv.min.x, uv.min.y), + max: Vector2::new(uv.max.x, uv.max.y), + }; + camera.set_viewport(rect, uv); + if let Some(pos) = r.interact_pointer_pos() { - if let Some(start) = camera.drag_start { - camera.offset = egui::Vec2::ZERO; - let pos = camera.mouse_to_uv(rect, uv, pos); - camera.offset = start - pos; - render_changed = true; - } else { - let pos = camera.mouse_to_uv(rect, uv, pos); - camera.drag_start = Some(pos); - } + let pos = Point2::new(pos.x, pos.y); + render_changed |= camera.drag(pos); } else { - camera.drag_start = None; + camera.release(); } if r.hovered() { let scroll = ctx.input(|i| i.smooth_scroll_delta.y); - if scroll != 0.0 { - let mouse_pos = ctx.input(|i| i.pointer.hover_pos()); - let pos_before = - mouse_pos.map(|p| camera.mouse_to_uv(rect, uv, p)); - render_changed = true; - camera.scale /= (scroll / 100.0).exp2(); - if let Some(pos_before) = pos_before { - let pos_after = camera.mouse_to_uv( - rect, - uv, - mouse_pos.unwrap(), - ); - camera.offset += pos_before - pos_after; - } - } + let mouse_pos = ctx.input(|i| i.pointer.hover_pos()); + render_changed |= camera.scroll( + mouse_pos.map(|p| Point2::new(p.x, p.y)), + scroll, + ); } } RenderMode::ThreeD(..) => { From 1818d170d6139642f65d198e07951bc7a9c5829f Mon Sep 17 00:00:00 2001 From: Matt Keeter Date: Wed, 9 Oct 2024 11:26:25 -0400 Subject: [PATCH 2/5] Camera is working --- demos/camera/src/lib.rs | 91 +++++++++++++++++++++++++--------------- demos/viewer/src/main.rs | 85 +++++++++++++------------------------ 2 files changed, 87 insertions(+), 89 deletions(-) diff --git a/demos/camera/src/lib.rs b/demos/camera/src/lib.rs index e7dcbca8..b00ff537 100644 --- a/demos/camera/src/lib.rs +++ b/demos/camera/src/lib.rs @@ -1,20 +1,18 @@ -use nalgebra::{Point2, Vector2}; +use nalgebra::Vector2; #[derive(Copy, Clone, Debug)] pub struct Camera2D { + /// Scale to translate between screen and world units scale: f32, /// Offset in world units offset: Vector2, - /// Position in screen units + /// Starting position for a drag, in world units drag_start: Option>, /// Size of the viewport in screen units - rect: Rect, - - /// Size of the viewport in world units - uv: Rect, + viewport: Rect, } #[derive(Copy, Clone, Debug)] @@ -23,17 +21,25 @@ pub struct Rect { pub max: Vector2, } -impl Camera2D { - /// Builds a new camera with the given viewport - pub fn new(rect: Rect, uv: Rect) -> Self { +impl Default for Camera2D { + fn default() -> Self { Camera2D { drag_start: None, scale: 1.0, offset: Vector2::zeros(), - rect, - uv, + viewport: Rect { + min: Vector2::new(f32::NAN, f32::NAN), + max: Vector2::new(f32::NAN, f32::NAN), + }, } } +} + +impl Camera2D { + /// Builds a new camera with the given viewport + pub fn new() -> Self { + Self::default() + } pub fn offset(&self) -> Vector2 { self.offset @@ -43,37 +49,56 @@ impl Camera2D { self.scale } + pub fn viewport(&self) -> Rect { + self.viewport + } + + /// Returns UV coordinates that cover the window + pub fn uv(&self) -> Rect { + let size = self.viewport.max - self.viewport.min; + if size.x > size.y { + let r = (1.0 - (size.y / size.x)) / 2.0; + Rect { + min: Vector2::new(0.0, r), + max: Vector2::new(1.0, 1.0 - r), + } + } else { + let r = (1.0 - (size.x / size.y)) / 2.0; + Rect { + min: Vector2::new(r, 0.0), + max: Vector2::new(1.0 - r, 1.0), + } + } + } + /// Updates the screen and world viewport sizes - pub fn set_viewport(&mut self, rect: Rect, uv: Rect) { - self.rect = rect; - self.uv = uv; + pub fn set_viewport(&mut self, viewport: Rect) { + self.viewport = viewport; } /// Converts from mouse position to a UV position within the render window - fn mouse_to_uv(&self, p: Point2) -> Vector2 { - let r = (p - self.rect.min) - .coords - .component_div(&(self.rect.max - self.rect.min)); - const ONE: Vector2 = Vector2::new(1.0, 1.0); - let pos = self.uv.min.component_mul(&(ONE - r)) - + self.uv.max.component_mul(&r); - let out = ((pos * 2.0) - ONE) * self.scale; - println!("{p:?} -> {:?}", out + self.offset); - out + self.offset + fn screen_to_world(&self, p: Vector2) -> Vector2 { + let size = self.viewport.max - self.viewport.min; + let out = (p - (self.viewport.min + self.viewport.max) / 2.0) + * self.scale + / size.x.max(size.y); + self.offset + out.component_mul(&Vector2::new(2.0, -2.0)) } /// Performs a new drag operation /// /// Returns `true` if the camera has changed - pub fn drag(&mut self, pos: Point2) -> bool { + pub fn drag(&mut self, pos: Vector2) -> bool { if let Some(start) = self.drag_start { + let prev_offset = self.offset; self.offset = Vector2::zeros(); - let pos = self.mouse_to_uv(pos); - println!("{start:?} -> {pos:?}"); - self.offset = start - pos; - true + let pos = self.screen_to_world(pos); + let new_offset = start - pos; + let changed = prev_offset != new_offset; + self.offset = new_offset; + changed } else { - let pos = self.mouse_to_uv(pos); + let pos = self.screen_to_world(pos); self.drag_start = Some(pos); false } @@ -85,12 +110,12 @@ impl Camera2D { } /// Called when the mouse is scrolled - pub fn scroll(&mut self, pos: Option>, scroll: f32) -> bool { + pub fn scroll(&mut self, pos: Option>, scroll: f32) -> bool { if scroll != 0.0 { - let pos_before = pos.map(|p| self.mouse_to_uv(p)); + let pos_before = pos.map(|p| self.screen_to_world(p)); self.scale /= (scroll / 100.0).exp2(); if let Some(pos_before) = pos_before { - let pos_after = self.mouse_to_uv(pos.unwrap()); + let pos_after = self.screen_to_world(pos.unwrap()); self.offset += pos_before - pos_after; } true diff --git a/demos/viewer/src/main.rs b/demos/viewer/src/main.rs index f1cc6423..c2246218 100644 --- a/demos/viewer/src/main.rs +++ b/demos/viewer/src/main.rs @@ -5,7 +5,7 @@ use eframe::egui; use env_logger::Env; use fidget::render::RenderConfig; use log::{debug, error, info, warn}; -use nalgebra::{Point2, Vector2, Vector3}; +use nalgebra::{Vector2, Vector3}; use notify::Watcher; use std::{error::Error, path::Path}; @@ -304,7 +304,10 @@ fn main() -> Result<(), Box> { .watch(Path::new(&args.target), notify::RecursiveMode::NonRecursive) .unwrap(); - let options = eframe::NativeOptions::default(); + let mut options = eframe::NativeOptions::default(); + let size = egui::Vec2::new(640.0, 480.0); + options.viewport.inner_size = Some(size); + eframe::run_native( "Fidget", options, @@ -312,7 +315,6 @@ fn main() -> Result<(), Box> { // Run a worker thread which listens for wake events and pokes the // UI whenever they come in. let egui_ctx = cc.egui_ctx.clone(); - let rect = egui_ctx.available_rect(); std::thread::spawn(move || { while let Ok(()) = wake_rx.recv() { egui_ctx.request_repaint(); @@ -320,7 +322,7 @@ fn main() -> Result<(), Box> { info!("wake thread is done"); }); - Box::new(ViewerApp::new(rect, config_tx, render_rx)) + Box::new(ViewerApp::new(config_tx, render_rx)) }), )?; @@ -394,16 +396,7 @@ impl RenderMode { RenderMode::ThreeD(..) => { *self = RenderMode::TwoD( // TODO get parameters from 3D camera here? - camera::Camera2D::new( - camera::Rect { - min: Vector2::new(0.0, 0.0), - max: Vector2::new(640.0, 480.0), - }, - camera::Rect { - min: Vector2::new(-1.0, -1.0), - max: Vector2::new(1.0, 1.0), - }, - ), + camera::Camera2D::new(), mode, ); true @@ -446,7 +439,6 @@ struct ViewerApp { impl ViewerApp { fn new( - rect: egui::Rect, config_tx: Sender, image_rx: Receiver>, ) -> Self { @@ -460,19 +452,7 @@ impl ViewerApp { config_tx, image_rx, - mode: RenderMode::TwoD( - camera::Camera2D::new( - camera::Rect { - min: Vector2::new(rect.min.x, rect.min.y), - max: Vector2::new(rect.max.x, rect.max.y), - }, - camera::Rect { - min: Vector2::new(-1.0, -1.0), - max: Vector2::new(1.0, 1.0), - }, - ), - Mode2D::Color, - ), + mode: RenderMode::TwoD(camera::Camera2D::new(), Mode2D::Color), } } @@ -562,12 +542,7 @@ impl ViewerApp { } } - fn paint_image( - &self, - rect: egui::Rect, - uv: egui::Rect, - ui: &mut egui::Ui, - ) -> egui::Response { + fn paint_image(&self, ui: &mut egui::Ui) -> egui::Response { let pos = ui.next_widget_position(); let size = ui.available_size(); let painter = ui.painter_at(egui::Rect { @@ -576,10 +551,25 @@ impl ViewerApp { }); const PADDING: egui::Vec2 = egui::Vec2 { x: 10.0, y: 10.0 }; + let RenderMode::TwoD(camera, ..) = &self.mode else { + panic!("can't render in 3D"); + }; + let rect = camera.viewport(); + let rect = egui::Rect { + min: egui::Pos2::new(rect.min.x, rect.min.y), + max: egui::Pos2::new(rect.max.x, rect.max.y), + }; + let uv = camera.uv(); + let uv = egui::Rect { + min: egui::Pos2::new(uv.min.x, uv.min.y), + max: egui::Pos2::new(uv.max.x, uv.max.y), + }; + if let Some((dt, image_size)) = self.stats { // Only draw the image if we have valid stats (i.e. no error) if let Some(t) = self.texture.as_ref() { let mut mesh = egui::Mesh::with_texture(t.id()); + mesh.add_rect_with_uv(rect, uv, egui::Color32::WHITE); painter.add(mesh); } @@ -649,24 +639,10 @@ impl eframe::App for ViewerApp { render_changed = true; } - let uv = if size.x > size.y { - let r = (1.0 - (size.y / size.x)) / 2.0; - egui::Rect { - min: egui::Pos2::new(0.0, r), - max: egui::Pos2::new(1.0, 1.0 - r), - } - } else { - let r = (1.0 - (size.x / size.y)) / 2.0; - egui::Rect { - min: egui::Pos2::new(r, 0.0), - max: egui::Pos2::new(1.0 - r, 1.0), - } - }; - // Draw the current image and/or error let r = egui::CentralPanel::default() .frame(egui::Frame::none().fill(egui::Color32::BLACK)) - .show(ctx, |ui| self.paint_image(rect, uv, ui)) + .show(ctx, |ui| self.paint_image(ui)) .inner; // Handle pan and zoom @@ -676,14 +652,11 @@ impl eframe::App for ViewerApp { min: Vector2::new(rect.min.x, rect.min.y), max: Vector2::new(rect.max.x, rect.max.y), }; - let uv = camera::Rect { - min: Vector2::new(uv.min.x, uv.min.y), - max: Vector2::new(uv.max.x, uv.max.y), - }; - camera.set_viewport(rect, uv); + + camera.set_viewport(rect); if let Some(pos) = r.interact_pointer_pos() { - let pos = Point2::new(pos.x, pos.y); + let pos = Vector2::new(pos.x, pos.y); render_changed |= camera.drag(pos); } else { camera.release(); @@ -693,7 +666,7 @@ impl eframe::App for ViewerApp { let scroll = ctx.input(|i| i.smooth_scroll_delta.y); let mouse_pos = ctx.input(|i| i.pointer.hover_pos()); render_changed |= camera.scroll( - mouse_pos.map(|p| Point2::new(p.x, p.y)), + mouse_pos.map(|p| Vector2::new(p.x, p.y)), scroll, ); } From 8802d480b2f15533cc1b33b3604444aae5cd9ef8 Mon Sep 17 00:00:00 2001 From: Matt Keeter Date: Thu, 10 Oct 2024 08:57:34 -0400 Subject: [PATCH 3/5] Add workspace-hack dep --- Cargo.lock | 1 + demos/camera/Cargo.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index d7b43f31..ffa15734 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -295,6 +295,7 @@ name = "camera" version = "0.1.0" dependencies = [ "nalgebra", + "workspace-hack", ] [[package]] diff --git a/demos/camera/Cargo.toml b/demos/camera/Cargo.toml index 6ac7638e..300abd63 100644 --- a/demos/camera/Cargo.toml +++ b/demos/camera/Cargo.toml @@ -5,3 +5,4 @@ edition = "2021" [dependencies] nalgebra.workspace = true +workspace-hack = { version = "0.1", path = "../../workspace-hack" } From e18d0b029a13f7386855b3be015ed65de02b28ad Mon Sep 17 00:00:00 2001 From: Matt Keeter Date: Thu, 10 Oct 2024 09:06:09 -0400 Subject: [PATCH 4/5] Add some comments --- demos/camera/src/lib.rs | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/demos/camera/src/lib.rs b/demos/camera/src/lib.rs index b00ff537..d529e59a 100644 --- a/demos/camera/src/lib.rs +++ b/demos/camera/src/lib.rs @@ -36,19 +36,22 @@ impl Default for Camera2D { } impl Camera2D { - /// Builds a new camera with the given viewport + /// Builds a new camera with an empty viewport pub fn new() -> Self { Self::default() } + /// Returns the camera's current offset, in world units pub fn offset(&self) -> Vector2 { self.offset } + /// Returns the camera's current scale, mapping from screen to world units pub fn scale(&self) -> f32 { self.scale } + /// Returns the current viewport, in screen units pub fn viewport(&self) -> Rect { self.viewport } @@ -71,7 +74,7 @@ impl Camera2D { } } - /// Updates the screen and world viewport sizes + /// Updates the screen viewport size pub fn set_viewport(&mut self, viewport: Rect) { self.viewport = viewport; } @@ -85,7 +88,7 @@ impl Camera2D { self.offset + out.component_mul(&Vector2::new(2.0, -2.0)) } - /// Performs a new drag operation + /// Updates the camera position when the mouse is held and dragged /// /// Returns `true` if the camera has changed pub fn drag(&mut self, pos: Vector2) -> bool { @@ -109,14 +112,25 @@ impl Camera2D { self.drag_start = None } - /// Called when the mouse is scrolled + /// Updates the camera zoom when the mouse is scrolled + /// + /// If the mouse cursor position is provided (in screen units), then the + /// camera offset is updated to keep the same point under the cursor. + /// + /// Returns `true` if the camera has changed pub fn scroll(&mut self, pos: Option>, scroll: f32) -> bool { if scroll != 0.0 { - let pos_before = pos.map(|p| self.screen_to_world(p)); - self.scale /= (scroll / 100.0).exp2(); - if let Some(pos_before) = pos_before { - let pos_after = self.screen_to_world(pos.unwrap()); - self.offset += pos_before - pos_after; + let new_scale = self.scale / (scroll / 100.0).exp2(); + match pos { + Some(p) => { + let pos_before = self.screen_to_world(p); + self.scale = new_scale; + let pos_after = self.screen_to_world(p); + self.offset += pos_before - pos_after; + } + None => { + self.scale = new_scale; + } } true } else { From cd0a34bcf3d5c215b6a455f1c44f890646b7d924 Mon Sep 17 00:00:00 2001 From: Matt Keeter Date: Fri, 11 Oct 2024 08:26:18 -0400 Subject: [PATCH 5/5] Add some tests --- demos/camera/src/lib.rs | 72 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/demos/camera/src/lib.rs b/demos/camera/src/lib.rs index d529e59a..4d7659ea 100644 --- a/demos/camera/src/lib.rs +++ b/demos/camera/src/lib.rs @@ -138,3 +138,75 @@ impl Camera2D { } } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_drag() { + let mut c = Camera2D::new(); + c.set_viewport(Rect { + min: Vector2::new(0.0, 0.0), + max: Vector2::new(100.0, 100.0), + }); + let p = c.screen_to_world(Vector2::new(0.0, 0.0)); + assert_eq!(p, Vector2::new(-1.0, 1.0)); + let p = c.screen_to_world(Vector2::new(100.0, 0.0)); + assert_eq!(p, Vector2::new(1.0, 1.0)); + let p = c.screen_to_world(Vector2::new(0.0, 100.0)); + assert_eq!(p, Vector2::new(-1.0, -1.0)); + let p = c.screen_to_world(Vector2::new(100.0, 100.0)); + assert_eq!(p, Vector2::new(1.0, -1.0)); + + c.drag(Vector2::new(50.0, 50.0)); + c.drag(Vector2::new(100.0, 50.0)); + c.release(); + + let p = c.screen_to_world(Vector2::new(50.0, 50.0)); + assert_eq!(p, Vector2::new(-1.0, 0.0)); + + c.drag(Vector2::new(50.0, 50.0)); + c.drag(Vector2::new(50.0, 100.0)); + c.release(); + + let p = c.screen_to_world(Vector2::new(50.0, 50.0)); + assert_eq!(p, Vector2::new(-1.0, 1.0)); + } + + #[test] + fn test_zoom() { + let mut c = Camera2D::new(); + c.set_viewport(Rect { + min: Vector2::new(0.0, 0.0), + max: Vector2::new(100.0, 100.0), + }); + + let points = [ + (75.0, 75.0), + (75.0, 25.0), + (25.0, 75.0), + (25.0, 25.0), + (50.0, 50.0), + ] + .map(|(x, y)| Vector2::new(x, y)); + for &p in &points { + let corner = Vector2::zeros(); + let prev_p = c.screen_to_world(p); + let prev_corner = c.screen_to_world(corner); + c.scroll(Some(p), 100.0); + let next_p = c.screen_to_world(p); + let next_corner = c.screen_to_world(corner); + + assert_eq!(prev_p, next_p); + assert_ne!(prev_corner, next_corner); + } + + // Undo the zooming to put us back at the origin + assert_ne!(c.offset, Vector2::new(0.0, 0.0)); + for &p in points.iter().rev() { + c.scroll(Some(p), -100.0); + } + assert_eq!(c.offset, Vector2::new(0.0, 0.0)); + } +}