1use glam::{Mat4, Quat, Vec2, Vec3};
7
8#[derive(Debug, Clone, Copy, PartialEq)]
10pub enum ProjectionType {
11 Perspective {
12 fov: f32,
13 near: f32,
14 far: f32,
15 },
16 Orthographic {
17 left: f32,
18 right: f32,
19 bottom: f32,
20 top: f32,
21 near: f32,
22 far: f32,
23 },
24}
25
26impl Default for ProjectionType {
27 fn default() -> Self {
28 Self::Perspective {
29 fov: 45.0_f32.to_radians(),
30 near: 0.1,
31 far: 100.0,
32 }
33 }
34}
35
36#[derive(Debug, Clone)]
38pub struct Camera {
39 pub position: Vec3,
41 pub target: Vec3,
42 pub up: Vec3,
43
44 pub projection: ProjectionType,
46 pub aspect_ratio: f32,
47
48 pub zoom: f32,
50 pub rotation: Quat,
51
52 pub pan_sensitivity: f32,
54 pub zoom_sensitivity: f32,
55 pub rotate_sensitivity: f32,
56
57 view_matrix: Mat4,
59 projection_matrix: Mat4,
60 view_proj_dirty: bool,
61}
62
63impl Default for Camera {
64 fn default() -> Self {
65 Self::new()
66 }
67}
68
69impl Camera {
70 pub fn new() -> Self {
72 let mut camera = Self {
73 position: Vec3::new(0.0, 0.0, 5.0),
74 target: Vec3::ZERO,
75 up: Vec3::Y,
76 projection: ProjectionType::default(),
77 aspect_ratio: 16.0 / 9.0,
78 zoom: 1.0,
79 rotation: Quat::IDENTITY,
80 pan_sensitivity: 0.01,
81 zoom_sensitivity: 0.1,
82 rotate_sensitivity: 0.005,
83 view_matrix: Mat4::IDENTITY,
84 projection_matrix: Mat4::IDENTITY,
85 view_proj_dirty: true,
86 };
87 camera.update_matrices();
88 camera
89 }
90
91 pub fn new_2d(bounds: (f32, f32, f32, f32)) -> Self {
93 let (left, right, bottom, top) = bounds;
94 let mut camera = Self {
95 position: Vec3::new(0.0, 0.0, 1.0),
96 target: Vec3::new((left + right) / 2.0, (bottom + top) / 2.0, 0.0),
97 up: Vec3::Y,
98 projection: ProjectionType::Orthographic {
99 left,
100 right,
101 bottom,
102 top,
103 near: -1.0,
104 far: 1.0,
105 },
106 aspect_ratio: (right - left) / (top - bottom),
107 zoom: 1.0,
108 rotation: Quat::IDENTITY,
109 pan_sensitivity: 0.01,
110 zoom_sensitivity: 0.1,
111 rotate_sensitivity: 0.0, view_matrix: Mat4::IDENTITY,
113 projection_matrix: Mat4::IDENTITY,
114 view_proj_dirty: true,
115 };
116 camera.update_matrices();
117 camera
118 }
119
120 pub fn update_aspect_ratio(&mut self, aspect_ratio: f32) {
122 self.aspect_ratio = aspect_ratio;
123 self.view_proj_dirty = true;
124 }
125
126 pub fn view_proj_matrix(&mut self) -> Mat4 {
128 if self.view_proj_dirty {
129 self.update_matrices();
130 }
131 self.projection_matrix * self.view_matrix
132 }
133
134 pub fn mark_dirty(&mut self) {
136 self.view_proj_dirty = true;
137 }
138
139 pub fn view_matrix(&mut self) -> Mat4 {
141 if self.view_proj_dirty {
142 self.update_matrices();
143 }
144 self.view_matrix
145 }
146
147 pub fn projection_matrix(&mut self) -> Mat4 {
149 if self.view_proj_dirty {
150 self.update_matrices();
151 }
152 self.projection_matrix
153 }
154
155 pub fn pan(&mut self, delta: Vec2) {
157 let right = self.view_matrix.x_axis.truncate();
158 let up = self.view_matrix.y_axis.truncate();
159
160 let pan_amount = delta * self.pan_sensitivity * self.zoom;
161 let world_delta = right * pan_amount.x + up * pan_amount.y;
162
163 self.position += world_delta;
164 self.target += world_delta;
165 self.view_proj_dirty = true;
166 }
167
168 pub fn zoom(&mut self, delta: f32) {
170 self.zoom *= 1.0 + delta * self.zoom_sensitivity;
171 self.zoom = self.zoom.clamp(0.01, 100.0);
172
173 match &mut self.projection {
174 ProjectionType::Perspective { .. } => {
175 let direction = (self.position - self.target).normalize();
177 let distance = (self.position - self.target).length();
178 let new_distance = distance * (1.0 + delta * self.zoom_sensitivity);
179 self.position = self.target + direction * new_distance.clamp(0.1, 1000.0);
180 }
181 ProjectionType::Orthographic {
182 left,
183 right,
184 bottom,
185 top,
186 ..
187 } => {
188 let center_x = (*left + *right) / 2.0;
190 let center_y = (*bottom + *top) / 2.0;
191 let width = (*right - *left) * self.zoom;
192 let height = (*top - *bottom) * self.zoom;
193
194 *left = center_x - width / 2.0;
195 *right = center_x + width / 2.0;
196 *bottom = center_y - height / 2.0;
197 *top = center_y + height / 2.0;
198 }
199 }
200
201 self.view_proj_dirty = true;
202 }
203
204 pub fn rotate(&mut self, delta: Vec2) {
206 if self.rotate_sensitivity == 0.0 {
207 return; }
209
210 let yaw_delta = -delta.x * self.rotate_sensitivity;
211 let pitch_delta = -delta.y * self.rotate_sensitivity;
212
213 let yaw_rotation = Quat::from_axis_angle(Vec3::Y, yaw_delta);
215 let pitch_rotation = Quat::from_axis_angle(Vec3::X, pitch_delta);
216
217 self.rotation = yaw_rotation * self.rotation * pitch_rotation;
219
220 let distance = (self.position - self.target).length();
222 let direction = self.rotation * Vec3::new(0.0, 0.0, distance);
223 self.position = self.target + direction;
224
225 self.view_proj_dirty = true;
226 }
227
228 pub fn look_at(&mut self, target: Vec3, distance: Option<f32>) {
230 self.target = target;
231
232 if let Some(dist) = distance {
233 let direction = (self.position - self.target).normalize();
234 self.position = self.target + direction * dist;
235 }
236
237 self.view_proj_dirty = true;
238 }
239
240 pub fn reset(&mut self) {
242 match self.projection {
243 ProjectionType::Perspective { .. } => {
244 self.position = Vec3::new(0.0, 0.0, 5.0);
245 self.target = Vec3::ZERO;
246 self.rotation = Quat::IDENTITY;
247 }
248 ProjectionType::Orthographic { .. } => {
249 self.zoom = 1.0;
250 self.target = Vec3::ZERO;
251 }
252 }
253 self.view_proj_dirty = true;
254 }
255
256 pub fn fit_bounds(&mut self, min_bounds: Vec3, max_bounds: Vec3) {
258 let center = (min_bounds + max_bounds) / 2.0;
259 let size = max_bounds - min_bounds;
260
261 match &mut self.projection {
262 ProjectionType::Perspective { .. } => {
263 let max_size = size.x.max(size.y).max(size.z);
264 let distance = max_size * 2.0; self.target = center;
267 let direction = (self.position - self.target).normalize();
268 self.position = self.target + direction * distance;
269 }
270 ProjectionType::Orthographic {
271 left,
272 right,
273 bottom,
274 top,
275 ..
276 } => {
277 let margin = 0.1; let width = size.x * (1.0 + margin);
279 let height = size.y * (1.0 + margin);
280
281 let display_width = width.max(height * self.aspect_ratio);
283 let display_height = height.max(width / self.aspect_ratio);
284
285 *left = center.x - display_width / 2.0;
286 *right = center.x + display_width / 2.0;
287 *bottom = center.y - display_height / 2.0;
288 *top = center.y + display_height / 2.0;
289
290 self.target = center;
291 }
292 }
293
294 self.view_proj_dirty = true;
295 }
296
297 pub fn screen_to_world(&self, screen_pos: Vec2, screen_size: Vec2, depth: f32) -> Vec3 {
299 let ndc_x = (2.0 * screen_pos.x) / screen_size.x - 1.0;
301 let ndc_y = 1.0 - (2.0 * screen_pos.y) / screen_size.y;
302 let ndc = Vec3::new(ndc_x, ndc_y, depth * 2.0 - 1.0);
303
304 let view_proj_inv = (self.projection_matrix * self.view_matrix).inverse();
306 let world_pos = view_proj_inv * ndc.extend(1.0);
307
308 if world_pos.w != 0.0 {
309 world_pos.truncate() / world_pos.w
310 } else {
311 world_pos.truncate()
312 }
313 }
314
315 fn update_matrices(&mut self) {
317 self.view_matrix = Mat4::look_at_rh(self.position, self.target, self.up);
319
320 self.projection_matrix = match self.projection {
322 ProjectionType::Perspective { fov, near, far } => {
323 Mat4::perspective_rh(fov, self.aspect_ratio, near, far)
324 }
325 ProjectionType::Orthographic {
326 left,
327 right,
328 bottom,
329 top,
330 near,
331 far,
332 } => {
333 println!("ORTHO: Creating matrix with bounds: left={left}, right={right}, bottom={bottom}, top={top}, near={near}, far={far}");
334 println!("ORTHO: Camera aspect_ratio={}", self.aspect_ratio);
335 Mat4::orthographic_rh(left, right, bottom, top, near, far)
336 }
337 };
338
339 self.view_proj_dirty = false;
340 }
341}
342
343#[derive(Debug, Default)]
345pub struct CameraController {
346 pub is_dragging: bool,
347 pub is_panning: bool,
348 pub last_mouse_pos: Vec2,
349 pub mouse_delta: Vec2,
350}
351
352impl CameraController {
353 pub fn new() -> Self {
354 Self::default()
355 }
356
357 pub fn mouse_press(&mut self, position: Vec2, button: MouseButton) {
359 self.last_mouse_pos = position;
360 match button {
361 MouseButton::Left => self.is_dragging = true,
362 MouseButton::Right => self.is_panning = true,
363 _ => {}
364 }
365 }
366
367 pub fn mouse_release(&mut self, _button: MouseButton) {
369 self.is_dragging = false;
370 self.is_panning = false;
371 }
372
373 pub fn mouse_move(&mut self, position: Vec2, camera: &mut Camera) {
375 self.mouse_delta = position - self.last_mouse_pos;
376
377 if self.is_dragging {
378 camera.rotate(self.mouse_delta);
379 } else if self.is_panning {
380 camera.pan(self.mouse_delta);
381 }
382
383 self.last_mouse_pos = position;
384 }
385
386 pub fn mouse_wheel(&mut self, delta: f32, camera: &mut Camera) {
388 camera.zoom(delta);
389 }
390}
391
392#[derive(Debug, Clone, Copy, PartialEq, Eq)]
394pub enum MouseButton {
395 Left,
396 Right,
397 Middle,
398}
399
400#[cfg(test)]
401mod tests {
402 use super::*;
403
404 #[test]
405 fn test_camera_creation() {
406 let camera = Camera::new();
407 assert_eq!(camera.position, Vec3::new(0.0, 0.0, 5.0));
408 assert_eq!(camera.target, Vec3::ZERO);
409 }
410
411 #[test]
412 fn test_2d_camera() {
413 let camera = Camera::new_2d((-10.0, 10.0, -10.0, 10.0));
414 match camera.projection {
415 ProjectionType::Orthographic {
416 left,
417 right,
418 bottom,
419 top,
420 ..
421 } => {
422 assert_eq!(left, -10.0);
423 assert_eq!(right, 10.0);
424 assert_eq!(bottom, -10.0);
425 assert_eq!(top, 10.0);
426 }
427 _ => panic!("Expected orthographic projection"),
428 }
429 }
430
431 #[test]
432 fn test_camera_bounds_fitting() {
433 let mut camera = Camera::new_2d((-1.0, 1.0, -1.0, 1.0));
434 let min_bounds = Vec3::new(-5.0, -3.0, 0.0);
435 let max_bounds = Vec3::new(5.0, 3.0, 0.0);
436
437 camera.fit_bounds(min_bounds, max_bounds);
438
439 match camera.projection {
441 ProjectionType::Orthographic {
442 left,
443 right,
444 bottom,
445 top,
446 ..
447 } => {
448 assert!(left <= -5.0);
449 assert!(right >= 5.0);
450 assert!(bottom <= -3.0);
451 assert!(top >= 3.0);
452 }
453 _ => panic!("Expected orthographic projection"),
454 }
455 }
456}