1#[cfg(feature = "gpui")]
8mod viewport;
9#[cfg(feature = "gpui")]
10pub use viewport::{Canvas, GridOptions, GridPainter, GridStyle};
11mod input;
12pub use input::PanGesture;
13mod overview;
14mod scene;
15mod snapping;
16mod spatial;
17pub use overview::{Overview, OverviewCell};
18pub use scene::{LevelOfDetail, Scene, SceneItem, VisibleItem};
19pub use snapping::{AlignmentGuide, Axis, SnapResult};
20
21#[derive(Clone, Copy, Debug, Default, PartialEq)]
22pub struct Point {
23 pub x: f64,
24 pub y: f64,
25}
26
27impl Point {
28 pub const fn new(x: f64, y: f64) -> Self {
29 Self { x, y }
30 }
31}
32
33#[derive(Clone, Copy, Debug)]
34pub struct WorldBounds {
35 pub min: Point,
36 pub max: Point,
37}
38
39impl WorldBounds {
40 pub fn translated(self, delta: Point) -> Self {
41 Self {
42 min: Point::new(self.min.x + delta.x, self.min.y + delta.y),
43 max: Point::new(self.max.x + delta.x, self.max.y + delta.y),
44 }
45 }
46 pub fn is_valid(self) -> bool {
47 [self.min.x, self.min.y, self.max.x, self.max.y]
48 .iter()
49 .all(|v| v.is_finite())
50 && self.min.x <= self.max.x
51 && self.min.y <= self.max.y
52 }
53 pub fn contains(self, point: Point) -> bool {
54 point.x >= self.min.x
55 && point.x <= self.max.x
56 && point.y >= self.min.y
57 && point.y <= self.max.y
58 }
59 pub fn intersects(self, other: Self) -> bool {
60 self.min.x <= other.max.x
61 && self.max.x >= other.min.x
62 && self.min.y <= other.max.y
63 && self.max.y >= other.min.y
64 }
65}
66
67#[derive(Clone, Copy, Debug)]
69pub struct Camera2D {
70 center: Point,
71 zoom: f64,
72}
73
74impl Default for Camera2D {
75 fn default() -> Self {
76 Self {
77 center: Point::default(),
78 zoom: 1.0,
79 }
80 }
81}
82
83impl Camera2D {
84 pub fn center_on(&mut self, world: Point) -> bool {
86 if !world.x.is_finite() || !world.y.is_finite() {
87 return false;
88 }
89 self.center = world;
90 true
91 }
92 pub fn zoom(self) -> f64 {
93 self.zoom
94 }
95 pub fn world_to_screen(self, world: Point, viewport: Point) -> Point {
96 Point::new(
97 (world.x - self.center.x) * self.zoom + viewport.x / 2.,
98 (world.y - self.center.y) * self.zoom + viewport.y / 2.,
99 )
100 }
101 pub fn screen_to_world(self, screen: Point, viewport: Point) -> Point {
102 Point::new(
103 self.center.x + (screen.x - viewport.x / 2.) / self.zoom,
104 self.center.y + (screen.y - viewport.y / 2.) / self.zoom,
105 )
106 }
107 pub fn pan_by(&mut self, delta: Point) {
108 if delta.x.is_finite() && delta.y.is_finite() {
109 self.center.x -= delta.x / self.zoom;
110 self.center.y -= delta.y / self.zoom;
111 }
112 }
113 pub fn zoom_at(&mut self, anchor: Point, viewport: Point, factor: f64) {
114 self.zoom_at_with_limits(anchor, viewport, factor, 0.01, 64.);
115 }
116 pub fn zoom_at_with_limits(
118 &mut self,
119 anchor: Point,
120 viewport: Point,
121 factor: f64,
122 min: f64,
123 max: f64,
124 ) {
125 if !min.is_finite() || !max.is_finite() || min <= 0. || max < min {
126 return;
127 }
128 if !factor.is_finite() || factor <= 0. || !anchor.x.is_finite() || !anchor.y.is_finite() {
129 return;
130 }
131 let before = self.screen_to_world(anchor, viewport);
132 self.zoom = (self.zoom * factor).clamp(min, max);
133 let after = self.screen_to_world(anchor, viewport);
134 self.center.x += before.x - after.x;
135 self.center.y += before.y - after.y;
136 }
137 pub fn visible_world_bounds(self, viewport: Point) -> WorldBounds {
138 WorldBounds {
139 min: self.screen_to_world(Point::default(), viewport),
140 max: self.screen_to_world(viewport, viewport),
141 }
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148 fn close(a: Point, b: Point) {
149 assert!(
150 (a.x - b.x).abs() < 1e-5 && (a.y - b.y).abs() < 1e-5,
151 "{a:?} != {b:?}"
152 );
153 }
154 #[test]
155 fn extended_zoom_preserves_anchor_and_rejects_invalid_limits() {
156 let mut camera = Camera2D::default();
157 let viewport = Point::new(1000., 700.);
158 let anchor = Point::new(317., 291.);
159 let expected = camera.screen_to_world(anchor, viewport);
160 camera.zoom_at_with_limits(anchor, viewport, 1e12, 0.01, 1e10);
161 assert_eq!(camera.zoom(), 1e10);
162 close(camera.screen_to_world(anchor, viewport), expected);
163 camera.zoom_at_with_limits(anchor, viewport, 0.5, 2., 1.);
164 assert_eq!(camera.zoom(), 1e10);
165 camera.zoom_at(anchor, viewport, 1.);
166 assert_eq!(camera.zoom(), 64.);
167 }
168 #[test]
169 fn projection_and_anchor_survive_zoom_limits_and_large_world() {
170 let mut camera = Camera2D::default();
171 let viewport = Point::new(1000., 700.);
172 camera.pan_by(Point::new(1e10, -1e10));
173 let anchor = Point::new(137., 492.);
174 let world = camera.screen_to_world(anchor, viewport);
175 for factor in [2., 1e8, 1e-12, 3.] {
176 camera.zoom_at(anchor, viewport, factor);
177 close(camera.screen_to_world(anchor, viewport), world);
178 close(camera.world_to_screen(world, viewport), anchor);
179 }
180 }
181 #[test]
182 fn culling_includes_edges_and_pan_moves_content() {
183 let mut camera = Camera2D::default();
184 let viewport = Point::new(100., 100.);
185 camera.pan_by(Point::new(10., 20.));
186 close(
187 camera.world_to_screen(Point::default(), viewport),
188 Point::new(60., 70.),
189 );
190 let visible = camera.visible_world_bounds(viewport);
191 assert!(visible.intersects(WorldBounds {
192 min: visible.max,
193 max: visible.max
194 }));
195 assert!(!visible.intersects(WorldBounds {
196 min: Point::new(1000., 1000.),
197 max: Point::new(1100., 1100.)
198 }));
199 }
200 #[test]
201 fn invalid_input_does_not_poison_camera() {
202 let mut camera = Camera2D::default();
203 camera.zoom_at(Point::default(), Point::new(100., 100.), f64::NAN);
204 camera.pan_by(Point::new(f64::INFINITY, 0.));
205 assert_eq!(camera.zoom(), 1.);
206 assert_eq!(camera.center, Point::default());
207 }
208
209 #[test]
210 fn resize_preserves_world_center_and_updates_culling_bounds() {
211 let mut camera = Camera2D::default();
212 camera.pan_by(Point::new(123., -456.));
213 let center = camera.screen_to_world(Point::new(500., 350.), Point::new(1000., 700.));
214 for viewport in [
215 Point::new(800., 600.),
216 Point::new(1600., 1200.),
217 Point::default(),
218 ] {
219 close(
220 camera.screen_to_world(Point::new(viewport.x / 2., viewport.y / 2.), viewport),
221 center,
222 );
223 let bounds = camera.visible_world_bounds(viewport);
224 close(
225 camera.world_to_screen(bounds.min, viewport),
226 Point::default(),
227 );
228 close(camera.world_to_screen(bounds.max, viewport), viewport);
229 }
230 }
231}