widgetry/mapspace/
unzoomed.rs

1use std::cell::RefCell;
2
3use geom::{Circle, Distance, PolyLine, Pt2D};
4
5use crate::{Color, Drawable, GeomBatch, GfxCtx};
6
7/// Draw `Circles` and `PolyLines` in map-space that scale their size as the canvas is zoomed. The
8/// goal is to appear with roughly constant screen-space size, but for the moment, this is
9/// approximated by discretizing into 10 buckets. The scaling only happens when the canvas is
10/// zoomed out less than a value of 1.0.
11pub struct DrawUnzoomedShapes {
12    shapes: Vec<Shape>,
13    per_zoom: RefCell<[Option<Drawable>; 11]>,
14}
15
16enum Shape {
17    Line {
18        polyline: PolyLine,
19        width: Distance,
20        color: Color,
21    },
22    Circle {
23        center: Pt2D,
24        radius: Distance,
25        color: Color,
26    },
27}
28
29impl Shape {
30    fn render(&self, batch: &mut GeomBatch, thickness: f64) {
31        match self {
32            Shape::Line {
33                polyline,
34                width,
35                color,
36            } => {
37                batch.push(*color, polyline.make_polygons(thickness * *width));
38            }
39            Shape::Circle {
40                center,
41                radius,
42                color,
43            } => {
44                // TODO Here especially if we're drawing lots of circles with the same radius,
45                // generating the shape once and translating it is much more efficient.
46                // UnzoomedAgents does this.
47                batch.push(
48                    *color,
49                    Circle::new(*center, thickness * *radius).to_polygon(),
50                );
51            }
52        }
53    }
54}
55
56pub struct DrawUnzoomedShapesBuilder {
57    shapes: Vec<Shape>,
58}
59
60impl DrawUnzoomedShapes {
61    pub fn empty() -> Self {
62        Self {
63            shapes: Vec::new(),
64            per_zoom: Default::default(),
65        }
66    }
67
68    pub fn builder() -> DrawUnzoomedShapesBuilder {
69        DrawUnzoomedShapesBuilder { shapes: Vec::new() }
70    }
71
72    pub fn draw(&self, g: &mut GfxCtx) {
73        let (zoom, idx) = discretize_zoom(g.canvas.cam_zoom);
74        let value = &mut self.per_zoom.borrow_mut()[idx];
75        if value.is_none() {
76            // Never shrink past the original size -- always at least 1.0.
77            // zoom ranges between [0.0, 1.0], and we want thicker shapes as zoom approaches 0.
78            let max = 5.0;
79            // So thickness ranges between [1.0, 5.0]
80            let thickness = 1.0 + (max - 1.0) * (1.0 - zoom);
81
82            let mut batch = GeomBatch::new();
83            for shape in &self.shapes {
84                shape.render(&mut batch, thickness);
85            }
86            *value = Some(g.upload(batch));
87        }
88        g.redraw(value.as_ref().unwrap());
89    }
90}
91
92impl DrawUnzoomedShapesBuilder {
93    pub fn add_line(&mut self, polyline: PolyLine, width: Distance, color: Color) {
94        self.shapes.push(Shape::Line {
95            polyline,
96            width,
97            color,
98        });
99    }
100
101    pub fn add_circle(&mut self, center: Pt2D, radius: Distance, color: Color) {
102        self.shapes.push(Shape::Circle {
103            center,
104            radius,
105            color,
106        });
107    }
108
109    pub fn build(self) -> DrawUnzoomedShapes {
110        DrawUnzoomedShapes {
111            shapes: self.shapes,
112            per_zoom: Default::default(),
113        }
114    }
115}
116
117// Continuously changing road width as we zoom looks great, but it's terribly slow. We'd have to
118// move line thickening into the shader to do it better. So recalculate with less granularity.
119//
120// Returns ([0.0, 1.0], [0, 10])
121fn discretize_zoom(zoom: f64) -> (f64, usize) {
122    if zoom >= 1.0 {
123        return (1.0, 10);
124    }
125    let rounded = (zoom * 10.0).round();
126    let idx = rounded as usize;
127    (rounded / 10.0, idx)
128}
129
130/// Draw custom objects that scale their size as the canvas is zoomed.
131///
132/// In all honesty I'm completely lost on the math here. By trial and error, I got something that
133/// works reasonably for the one use case. Of course I'd love to properly understand how to do this
134/// pattern, unify with the above, etc.
135pub struct DrawCustomUnzoomedShapes {
136    shapes: Vec<Box<dyn Fn(&mut GeomBatch, f64)>>,
137    per_zoom: RefCell<PerZoom>,
138}
139
140pub struct DrawCustomUnzoomedShapesBuilder {
141    shapes: Vec<Box<dyn Fn(&mut GeomBatch, f64)>>,
142}
143
144impl DrawCustomUnzoomedShapes {
145    pub fn empty() -> Self {
146        Self {
147            shapes: Vec::new(),
148            per_zoom: RefCell::new(PerZoom::new(1.0, 0.1)),
149        }
150    }
151
152    pub fn builder() -> DrawCustomUnzoomedShapesBuilder {
153        DrawCustomUnzoomedShapesBuilder { shapes: Vec::new() }
154    }
155
156    // If the zoom level is insufficient, return false
157    pub fn maybe_draw(&self, g: &mut GfxCtx) -> bool {
158        let mut per_zoom = self.per_zoom.borrow_mut();
159
160        if g.canvas.cam_zoom >= per_zoom.min_zoom_for_detail {
161            return false;
162        }
163
164        let (zoom, idx) = per_zoom.discretize_zoom(g.canvas.cam_zoom);
165        let value = &mut per_zoom.draw_per_zoom[idx];
166        if value.is_none() {
167            let thickness = 1.0 / zoom;
168
169            let mut batch = GeomBatch::new();
170            for shape in &self.shapes {
171                (shape)(&mut batch, thickness);
172            }
173            *value = Some(g.upload(batch));
174        }
175        g.redraw(value.as_ref().unwrap());
176
177        true
178    }
179}
180
181impl DrawCustomUnzoomedShapesBuilder {
182    pub fn add_custom(&mut self, f: Box<dyn Fn(&mut GeomBatch, f64)>) {
183        self.shapes.push(f);
184    }
185
186    pub fn build(self, per_zoom: PerZoom) -> DrawCustomUnzoomedShapes {
187        DrawCustomUnzoomedShapes {
188            shapes: self.shapes,
189            per_zoom: RefCell::new(per_zoom),
190        }
191    }
192}
193
194// TODO There may be an off-by-one floating around here. Watch what this does at extremely low zoom
195// levels near 0.
196pub struct PerZoom {
197    // TODO Maybe keep private and take the rendering callback here. Share more behavior with
198    // DrawRoadLabels.
199    pub draw_per_zoom: Vec<Option<Drawable>>,
200    step_size: f64,
201    min_zoom_for_detail: f64,
202}
203
204impl PerZoom {
205    pub fn new(min_zoom_for_detail: f64, step_size: f64) -> Self {
206        let num_buckets = (min_zoom_for_detail / step_size) as usize;
207        Self {
208            draw_per_zoom: std::iter::repeat_with(|| None).take(num_buckets).collect(),
209            step_size,
210            min_zoom_for_detail,
211        }
212    }
213
214    // Takes the current canvas zoom, rounds it to the nearest step_size, and returns the index of
215    // the bucket to fill out
216    pub fn discretize_zoom(&self, zoom: f64) -> (f64, usize) {
217        let bucket = (zoom / self.step_size).floor() as usize;
218        // It's a bit weird to have the same zoom behavior for < 0.1 and 0.1 to 0.2, but unclear
219        // what to do otherwise -- an effective zoom of 0 is broken
220        let rounded = (bucket.max(1) as f64) * self.step_size;
221        (rounded, bucket)
222    }
223}