1use kurbo::{Affine, Point, Vec2};
4use serde::{Deserialize, Serialize};
5
6pub const BASE_ZOOM: f64 = 1.68;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct Camera {
16 pub offset: Vec2,
18 pub zoom: f64,
20 pub min_zoom: f64,
22 pub max_zoom: f64,
24}
25
26impl Default for Camera {
27 fn default() -> Self {
28 Self {
29 offset: Vec2::ZERO,
30 zoom: BASE_ZOOM,
31 min_zoom: 0.1,
32 max_zoom: 10.0,
33 }
34 }
35}
36
37impl Camera {
38 pub fn new() -> Self {
40 Self::default()
41 }
42
43 pub fn transform(&self) -> Affine {
47 Affine::translate(self.offset) * Affine::scale(self.zoom)
48 }
49
50 pub fn inverse_transform(&self) -> Affine {
54 Affine::scale(1.0 / self.zoom) * Affine::translate(-self.offset)
55 }
56
57 pub fn screen_to_world(&self, screen_point: Point) -> Point {
59 self.inverse_transform() * screen_point
60 }
61
62 pub fn world_to_screen(&self, world_point: Point) -> Point {
64 self.transform() * world_point
65 }
66
67 pub fn pan(&mut self, delta: Vec2) {
69 self.offset += delta;
70 }
71
72 pub fn zoom_at(&mut self, screen_point: Point, factor: f64) {
74 let new_zoom = (self.zoom * factor).clamp(self.min_zoom, self.max_zoom);
75 if (new_zoom - self.zoom).abs() < f64::EPSILON {
76 return;
77 }
78
79 let world_point = self.screen_to_world(screen_point);
81
82 self.zoom = new_zoom;
84
85 let new_screen = self.world_to_screen(world_point);
87 let correction = Vec2::new(
88 screen_point.x - new_screen.x,
89 screen_point.y - new_screen.y,
90 );
91 self.offset += correction;
92 }
93
94 pub fn reset(&mut self) {
96 self.offset = Vec2::ZERO;
97 self.zoom = BASE_ZOOM;
98 }
99
100 pub fn fit_to_bounds(&mut self, bounds: kurbo::Rect, viewport: kurbo::Size, padding: f64) {
102 if bounds.is_zero_area() {
103 self.reset();
104 return;
105 }
106
107 let padded_viewport = kurbo::Size::new(
108 (viewport.width - padding * 2.0).max(1.0),
109 (viewport.height - padding * 2.0).max(1.0),
110 );
111
112 let scale_x = padded_viewport.width / bounds.width();
113 let scale_y = padded_viewport.height / bounds.height();
114 self.zoom = scale_x.min(scale_y).clamp(self.min_zoom, self.max_zoom);
115
116 let bounds_center = bounds.center();
118 let viewport_center = Point::new(viewport.width / 2.0, viewport.height / 2.0);
119
120 self.offset = Vec2::new(
121 viewport_center.x - bounds_center.x * self.zoom,
122 viewport_center.y - bounds_center.y * self.zoom,
123 );
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
132 fn test_default_camera() {
133 let camera = Camera::new();
134 assert_eq!(camera.offset, Vec2::ZERO);
135 assert!((camera.zoom - BASE_ZOOM).abs() < f64::EPSILON);
136 }
137
138 #[test]
139 fn test_screen_to_world_identity() {
140 let camera = Camera::new();
141 let screen = Point::new(100.0, 200.0);
142 let world = camera.screen_to_world(screen);
143 assert!((world.x - screen.x).abs() < f64::EPSILON);
144 assert!((world.y - screen.y).abs() < f64::EPSILON);
145 }
146
147 #[test]
148 fn test_screen_to_world_with_offset() {
149 let mut camera = Camera::new();
150 camera.offset = Vec2::new(50.0, 100.0);
151 let screen = Point::new(100.0, 200.0);
152 let world = camera.screen_to_world(screen);
153 assert!((world.x - 50.0).abs() < f64::EPSILON);
154 assert!((world.y - 100.0).abs() < f64::EPSILON);
155 }
156
157 #[test]
158 fn test_screen_to_world_with_zoom() {
159 let mut camera = Camera::new();
160 camera.zoom = 2.0;
161 let screen = Point::new(100.0, 200.0);
162 let world = camera.screen_to_world(screen);
163 assert!((world.x - 50.0).abs() < f64::EPSILON);
164 assert!((world.y - 100.0).abs() < f64::EPSILON);
165 }
166
167 #[test]
168 fn test_roundtrip_conversion() {
169 let mut camera = Camera::new();
170 camera.offset = Vec2::new(30.0, -20.0);
171 camera.zoom = 1.5;
172
173 let original = Point::new(123.0, 456.0);
174 let world = camera.screen_to_world(original);
175 let back = camera.world_to_screen(world);
176
177 assert!((back.x - original.x).abs() < 1e-10);
178 assert!((back.y - original.y).abs() < 1e-10);
179 }
180
181 #[test]
182 fn test_zoom_clamp() {
183 let mut camera = Camera::new();
184 camera.zoom_at(Point::ZERO, 0.001); assert!((camera.zoom - camera.min_zoom).abs() < f64::EPSILON);
186
187 camera.zoom = 1.0;
188 camera.zoom_at(Point::ZERO, 1000.0); assert!((camera.zoom - camera.max_zoom).abs() < f64::EPSILON);
190 }
191
192 #[test]
193 fn test_pan() {
194 let mut camera = Camera::new();
195 camera.pan(Vec2::new(10.0, 20.0));
196 assert!((camera.offset.x - 10.0).abs() < f64::EPSILON);
197 assert!((camera.offset.y - 20.0).abs() < f64::EPSILON);
198 }
199}