Skip to content

Commit a20e6c2

Browse files
Initial implementation of panning and zooming
1 parent acfc3bb commit a20e6c2

6 files changed

Lines changed: 176 additions & 5 deletions

File tree

src/renderer/app_state.rs

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::{
22
image::grammar::Image,
33
renderer::{
4+
camera::Camera,
45
draw_uniform::DrawUniform,
56
effect_pipeline::EffectPipeline,
67
feature_uniform::{FeatureUniform, TransformAction},
@@ -14,7 +15,7 @@ use crate::{
1415
use anyhow::Result;
1516
use winit::{
1617
dpi::PhysicalSize,
17-
event::{ElementState, Event, KeyEvent, Modifiers, MouseButton, WindowEvent},
18+
event::{ElementState, Event, KeyEvent, Modifiers, MouseButton, MouseScrollDelta, WindowEvent},
1819
event_loop::EventLoop,
1920
keyboard::{KeyCode, PhysicalKey},
2021
window::{CursorIcon, Window, WindowBuilder},
@@ -30,6 +31,7 @@ pub struct AppState<'a> {
3031

3132
pub feature_uniform: FeatureUniform,
3233
pub draw_uniform: DrawUniform,
34+
pub camera: Camera,
3335
pub mouse_state: MouseState,
3436
pub editor_state: EditorState,
3537
pub modifiers: Modifiers,
@@ -61,6 +63,8 @@ impl<'a> AppState<'a> {
6163
let draw_uniform_resource =
6264
gpu_allocator.create_uniform_resource("draw_uniform", draw_uniform)?;
6365

66+
let camera = Camera::new();
67+
6468
let shape_render_texture =
6569
gpu_allocator.create_render_texture("shape_texture", size.width, size.height);
6670

@@ -147,6 +151,7 @@ impl<'a> AppState<'a> {
147151
size,
148152
feature_uniform,
149153
draw_uniform,
154+
camera,
150155
mouse_state,
151156
editor_state,
152157
modifiers,
@@ -179,14 +184,48 @@ impl<'a> AppState<'a> {
179184
let feature_uniform = &mut self.feature_uniform;
180185
let draw_uniform = &mut self.draw_uniform;
181186

187+
// the state
188+
// note to future self: we are probably due for a refactor
189+
// right now, application state, inputs, windowing are all coupled together
190+
// it would be good to be able to reuse certain commands for instance, think about cut (cmd + x)
191+
let mut super_key_pressed = if cfg!(target_os = "macos") {
192+
self.modifiers.state().super_key()
193+
} else {
194+
self.modifiers.state().control_key()
195+
};
196+
197+
let mut draw_mode = draw_uniform.crosshair();
198+
199+
// some global rules
200+
// maybe keep a stack of actions?
201+
draw_mode = if super_key_pressed { false } else { draw_mode };
202+
182203
match event {
183204
WindowEvent::MouseInput { state, button, .. } => {
184205
if *button == MouseButton::Left {
185206
let prev_state = self.mouse_state.pressed();
186207
self.mouse_state
187208
.set_pressed(matches!(state, ElementState::Pressed));
188209

189-
if draw_uniform.crosshair() {
210+
// camera panning
211+
if super_key_pressed && !draw_mode {
212+
self.window.set_cursor_icon(CursorIcon::Grab);
213+
214+
match (prev_state, self.mouse_state.pressed()) {
215+
(false, true) => {
216+
let (x, y) = self.mouse_state.position();
217+
self.mouse_state.set_camera_pan_start(Some((x, y)));
218+
}
219+
(true, false) => {
220+
self.mouse_state.set_camera_pan_start(None);
221+
self.window.set_cursor_icon(CursorIcon::Default);
222+
}
223+
_ => {}
224+
}
225+
return true;
226+
}
227+
228+
if draw_mode {
190229
// Crosshair mode: Draw new circles
191230
match (prev_state, self.mouse_state.pressed()) {
192231
(false, true) => {
@@ -265,10 +304,49 @@ impl<'a> AppState<'a> {
265304
}
266305
}
267306
}
307+
WindowEvent::MouseWheel { delta, .. } => {
308+
match delta {
309+
MouseScrollDelta::LineDelta(_, y) => {
310+
let zoom_speed = 0.1;
311+
let zoom_factor = if *y > 0.0 {
312+
1.0 + zoom_speed
313+
} else if *y < 0.0 {
314+
1.0 - zoom_speed
315+
} else {
316+
1.0
317+
};
318+
self.camera.zoom(zoom_factor);
319+
}
320+
MouseScrollDelta::PixelDelta(pos) => {
321+
// Zoom with two-finger scroll
322+
let zoom_speed = 0.1;
323+
let y = pos.y as f32;
324+
let zoom_factor = if y > 0.0 {
325+
1.0 + zoom_speed * 0.01 * y
326+
} else if y < 0.0 {
327+
1.0 + zoom_speed * 0.01 * y
328+
} else {
329+
1.0
330+
};
331+
self.camera.zoom(zoom_factor);
332+
}
333+
};
334+
}
268335
WindowEvent::CursorMoved { position, .. } => {
269336
let (x, y) = (position.x as f32, position.y as f32);
270337

271-
if draw_uniform.crosshair() {
338+
if super_key_pressed && self.mouse_state.pressed() {
339+
if let Some((start_x, start_y)) = self.mouse_state.camera_pan_start() {
340+
let delta_x = (x - start_x) / (self.size.width as f32) * 2.0;
341+
let delta_y = -(y - start_y) / (self.size.height as f32) * 2.0; // Invert Y
342+
343+
self.camera.pan(delta_x, delta_y);
344+
345+
self.mouse_state.set_camera_pan_start(Some((x, y)));
346+
}
347+
}
348+
349+
if draw_mode {
272350
// Crosshair mode: Update the preview circle radius
273351
if let Some(center) = self.mouse_state.start_drag() {
274352
let radius = compute_distance(center, (x, y));
@@ -404,6 +482,9 @@ impl<'a> AppState<'a> {
404482
}
405483
}
406484
}
485+
(KeyCode::KeyR, ElementState::Pressed) => {
486+
self.camera.reset();
487+
}
407488
_ => return false,
408489
}
409490
}
@@ -421,6 +502,10 @@ impl<'a> AppState<'a> {
421502
self.feature_uniform.gamma(),
422503
);
423504

505+
// Update camera in draw uniform
506+
self.draw_uniform
507+
.update_camera(self.camera.view_projection_matrix());
508+
424509
// Update image shader uniforms
425510
let uniform_resources = &self.image_shader.uniform_resources;
426511
self.gpu_allocator

src/renderer/camera.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#[derive(Debug, Clone, Copy)]
2+
pub struct Camera {
3+
pub pan_x: f32,
4+
pub pan_y: f32,
5+
pub zoom: f32,
6+
}
7+
8+
impl Camera {
9+
pub fn new() -> Self {
10+
Self {
11+
pan_x: 0.0,
12+
pan_y: 0.0,
13+
zoom: 1.0,
14+
}
15+
}
16+
17+
pub fn pan(&mut self, delta_x: f32, delta_y: f32) {
18+
self.pan_x += delta_x / self.zoom;
19+
self.pan_y += delta_y / self.zoom;
20+
}
21+
22+
// factor > 1.0 zooms in, factor < 1.0 zooms out
23+
pub fn zoom(&mut self, factor: f32) {
24+
self.zoom *= factor;
25+
// Clamp zoom to reasonable bounds
26+
self.zoom = self.zoom.clamp(0.1, 10.0);
27+
}
28+
29+
pub fn _set_zoom(&mut self, zoom: f32) {
30+
self.zoom = zoom.clamp(0.1, 10.0);
31+
}
32+
33+
pub fn reset(&mut self) {
34+
self.pan_x = 0.0;
35+
self.pan_y = 0.0;
36+
self.zoom = 1.0;
37+
}
38+
39+
// generate a view-proj matrix for the camera
40+
pub fn view_projection_matrix(&self) -> [[f32; 4]; 4] {
41+
[
42+
[self.zoom, 0.0, 0.0, 0.0],
43+
[0.0, self.zoom, 0.0, 0.0],
44+
[0.0, 0.0, 1.0, 0.0],
45+
[self.pan_x, self.pan_y, 0.0, 1.0],
46+
]
47+
}
48+
}
49+
50+
impl Default for Camera {
51+
fn default() -> Self {
52+
Self::new()
53+
}
54+
}

src/renderer/draw_uniform.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
#[repr(C)]
2-
#[derive(Debug, Default, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
2+
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
33
pub struct DrawUniform {
44
pub crosshair: u32,
55
pub circle_center_x: f32,
66
pub circle_center_y: f32,
77
pub circle_radius: f32,
8+
pub camera_view_proj: [[f32; 4]; 4],
9+
}
10+
11+
impl Default for DrawUniform {
12+
fn default() -> Self {
13+
Self::new()
14+
}
815
}
916

1017
impl DrawUniform {
@@ -14,9 +21,19 @@ impl DrawUniform {
1421
circle_center_x: 0.0,
1522
circle_center_y: 0.0,
1623
circle_radius: 0.0,
24+
camera_view_proj: [
25+
[1.0, 0.0, 0.0, 0.0],
26+
[0.0, 1.0, 0.0, 0.0],
27+
[0.0, 0.0, 1.0, 0.0],
28+
[0.0, 0.0, 0.0, 1.0],
29+
],
1730
}
1831
}
1932

33+
pub(crate) fn update_camera(&mut self, view_proj: [[f32; 4]; 4]) {
34+
self.camera_view_proj = view_proj;
35+
}
36+
2037
pub(crate) const fn crosshair(&self) -> bool {
2138
self.crosshair == 1
2239
}

src/renderer/image_shader.wgsl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ struct DrawUniform {
1919
circle_center_x: f32,
2020
circle_center_y: f32,
2121
circle_radius: f32,
22+
camera_view_proj: mat4x4<f32>,
2223
}
2324

2425

@@ -53,9 +54,13 @@ fn vs_main(
5354
) -> VertexOutput {
5455
var out: VertexOutput;
5556

57+
// Apply feature transform (flip X/Y)
5658
var transformed = transform_matrix() * vec3<f32>(model.position.x, model.position.y, 1.0);
5759

58-
out.clip_position = vec4<f32>(transformed.xy, model.position.z, 1.0);
60+
// Apply camera transform (pan and zoom)
61+
var camera_transformed = draw_uniform.camera_view_proj * vec4<f32>(transformed.xy, model.position.z, 1.0);
62+
63+
out.clip_position = camera_transformed;
5964
out.tex_coords = model.tex_coords;
6065

6166
return out;

src/renderer/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pub(crate) use texture::*;
33
pub(crate) use vertex::*;
44

55
mod app_state;
6+
mod camera;
67
mod compute_effect;
78
mod draw_uniform;
89
mod effect_pipeline;

src/renderer/mouse_state.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ pub struct MouseState {
66
position_x: f32,
77
position_y: f32,
88
start_drag: Option<(f32, f32)>,
9+
camera_pan_start: Option<(f32, f32)>,
910
selected_shape: Option<usize>, // index to the shape stack
1011
clipboard_shape: Option<Circle>, // index to the shape stack
1112
dragging_shape: bool,
@@ -70,4 +71,12 @@ impl MouseState {
7071
pub(crate) const fn set_clipboard_shape(&mut self, element_id: Option<Circle>) {
7172
self.clipboard_shape = element_id;
7273
}
74+
75+
pub(crate) const fn camera_pan_start(&self) -> Option<(f32, f32)> {
76+
self.camera_pan_start
77+
}
78+
79+
pub(crate) const fn set_camera_pan_start(&mut self, position: Option<(f32, f32)>) {
80+
self.camera_pan_start = position;
81+
}
7382
}

0 commit comments

Comments
 (0)