widgetry/geom/
mod.rs

1use geom::{Angle, Bounds, GPSBounds, Polygon, Pt2D, Tessellation};
2
3use crate::{
4    svg, Color, DeferDraw, Drawable, EventCtx, Fill, GfxCtx, JustDraw, Prerender, ScreenDims,
5    Widget,
6};
7
8pub mod geom_batch_stack;
9
10/// A mutable builder for a group of colored tessellated polygons.
11#[derive(Clone)]
12pub struct GeomBatch {
13    // f64 is the z-value offset. This must be in (-1, 0], with values closer to -1.0
14    // rendering above values closer to 0.0.
15    pub(crate) list: Vec<(Fill, Tessellation, f64)>,
16    pub autocrop_dims: bool,
17}
18
19impl std::fmt::Debug for GeomBatch {
20    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21        f.debug_struct("GeomBatch")
22            .field("bounds", &self.get_bounds())
23            .field("items", &self.list.len())
24            .field("autocrop_dims", &self.autocrop_dims)
25            .finish()
26    }
27}
28
29impl GeomBatch {
30    /// Creates an empty batch.
31    pub fn new() -> GeomBatch {
32        GeomBatch {
33            list: Vec::new(),
34            autocrop_dims: true,
35        }
36    }
37
38    /// Adds a single tessellated polygon, painted according to `Fill`
39    pub fn push<F: Into<Fill>, T: Into<Tessellation>>(&mut self, fill: F, p: T) {
40        self.push_with_z(fill, p, 0.0);
41    }
42
43    /// Offset z value to render above/below other polygons.
44    /// z must be in (-1, 0] to ensure we don't traverse layers of the UI - to make
45    /// sure we don't inadvertently render something *above* a tooltip, etc.
46    pub fn push_with_z<F: Into<Fill>, T: Into<Tessellation>>(
47        &mut self,
48        fill: F,
49        p: T,
50        z_offset: f64,
51    ) {
52        debug_assert!(z_offset > -1.0);
53        debug_assert!(z_offset <= 0.0);
54        self.list.push((fill.into(), p.into(), z_offset));
55    }
56
57    /// Adds a single polygon to the front of the batch, painted according to `Fill`
58    pub fn unshift<F: Into<Fill>, T: Into<Tessellation>>(&mut self, fill: F, p: T) {
59        self.list.insert(0, (fill.into(), p.into(), 0.0));
60    }
61
62    /// Removes the first polygon in the batch.
63    pub fn shift(&mut self) {
64        self.list.remove(0);
65    }
66
67    /// Applies one Fill to many polygons.
68    pub fn extend<F: Into<Fill>, T: Into<Tessellation>>(&mut self, fill: F, polys: Vec<T>) {
69        let fill = fill.into();
70        for p in polys {
71            self.list.push((fill.clone(), p.into(), 0.0));
72        }
73    }
74
75    /// Appends all colored polygons from another batch to the current one.
76    pub fn append(&mut self, other: GeomBatch) {
77        self.list.extend(other.list);
78    }
79
80    /// Returns the colored polygons in this batch, destroying the batch.
81    pub fn consume(self) -> Vec<(Fill, Tessellation, f64)> {
82        self.list
83    }
84
85    /// Draws the batch, consuming it. Only use this for drawing things once.
86    pub fn draw(self, g: &mut GfxCtx) {
87        let obj = g.prerender.upload_temporary(self);
88        g.redraw(&obj);
89    }
90
91    /// Upload the batch of polygons to the GPU, returning something that can be cheaply redrawn
92    /// many times later.
93    pub fn upload(self, ctx: &EventCtx) -> Drawable {
94        ctx.prerender.upload(self)
95    }
96
97    /// Wrap in a Widget for layouting, so this batch can become part of a larger one.
98    pub fn batch(self) -> Widget {
99        DeferDraw::new_widget(self)
100    }
101
102    /// Wrap in a Widget, so the batch can be drawn as part of a Panel.
103    pub fn into_widget(self, ctx: &EventCtx) -> Widget {
104        JustDraw::wrap(ctx, self)
105    }
106
107    /// Compute the bounds of all polygons in this batch.
108    pub fn get_bounds(&self) -> Bounds {
109        let mut bounds = Bounds::new();
110        for (_, poly, _) in &self.list {
111            bounds.union(poly.get_bounds());
112        }
113        if !self.autocrop_dims {
114            bounds.update(Pt2D::new(0.0, 0.0));
115        }
116        bounds
117    }
118
119    /// Sets the top-left to 0, 0. Not sure exactly when this should be used.
120    pub fn autocrop(mut self) -> GeomBatch {
121        let bounds = self.get_bounds();
122        if bounds.min_x == 0.0 && bounds.min_y == 0.0 {
123            return self;
124        }
125        for (_, poly, _) in &mut self.list {
126            poly.translate(-bounds.min_x, -bounds.min_y);
127        }
128        self
129    }
130
131    /// True when the batch is empty.
132    pub fn is_empty(&self) -> bool {
133        self.list.is_empty()
134    }
135
136    /// Returns the width and height of all geometry contained in the batch.
137    pub fn get_dims(&self) -> ScreenDims {
138        // TODO Maybe warn about this happening and avoid in the first place? Sometimes we wind up
139        // trying to draw completely empty text.
140        if self.is_empty() {
141            return ScreenDims::new(0.0, 0.0);
142        }
143        let bounds = self.get_bounds();
144        ScreenDims::new(bounds.width(), bounds.height())
145    }
146
147    /// Returns a batch containing an SVG from a file.
148    pub fn load_svg<P: AsRef<Prerender>, I: AsRef<str>>(prerender: &P, filename: I) -> GeomBatch {
149        svg::load_svg(prerender.as_ref(), filename.as_ref()).0
150    }
151
152    /// Returns a GeomBatch from the bytes of a utf8 encoded SVG string.
153    pub fn load_svg_bytes<P: AsRef<Prerender>>(
154        prerender: &P,
155        labeled_bytes: (&str, &[u8]),
156    ) -> GeomBatch {
157        svg::load_svg_bytes(prerender.as_ref(), labeled_bytes.0, labeled_bytes.1)
158            .expect("invalid svg bytes")
159            .0
160    }
161
162    /// Returns a GeomBatch from the bytes of a utf8 encoded SVG string.
163    ///
164    /// Prefer to use `load_svg_bytes`, which caches the parsed SVG, unless
165    /// the SVG was dynamically generated, or is otherwise unlikely to be
166    /// reused.
167    pub fn load_svg_bytes_uncached(raw: &[u8]) -> GeomBatch {
168        svg::load_svg_from_bytes_uncached(raw).unwrap().0
169    }
170
171    /// Transforms all colors in a batch.
172    pub fn color(mut self, transformation: RewriteColor) -> GeomBatch {
173        for (fancy, _, _) in &mut self.list {
174            if let Fill::Color(ref mut c) = fancy {
175                *c = transformation.apply(*c);
176            }
177        }
178        self
179    }
180
181    /// Translates the batch to be centered on some point.
182    pub fn centered_on(self, center: Pt2D) -> GeomBatch {
183        let dims = self.get_dims();
184        let dx = center.x() - dims.width / 2.0;
185        let dy = center.y() - dims.height / 2.0;
186        self.translate(dx, dy)
187    }
188
189    /// Translates the batch by some offset.
190    pub fn translate(mut self, dx: f64, dy: f64) -> GeomBatch {
191        for (_, poly, _) in &mut self.list {
192            poly.translate(dx, dy);
193        }
194        self
195    }
196
197    /// Rotates each polygon in the batch relative to the center of that polygon.
198    pub fn rotate(mut self, angle: Angle) -> GeomBatch {
199        for (_, poly, _) in &mut self.list {
200            poly.rotate(angle);
201        }
202        self
203    }
204
205    /// Rotates each polygon in the batch relative to the center of the entire batch.
206    pub fn rotate_around_batch_center(mut self, angle: Angle) -> GeomBatch {
207        // Bounds won't be defined if so
208        if self.list.is_empty() {
209            return self;
210        }
211
212        let center = self.get_bounds().center();
213        for (_, poly, _) in &mut self.list {
214            poly.rotate_around(angle, center);
215        }
216        self
217    }
218
219    /// Equivalent to
220    /// `self.scale(scale).centered_on(center_on).rotate_around_batch_center(rotate)`, but faster.
221    pub fn multi_transform(mut self, scale: f64, center_on: Pt2D, rotate: Angle) -> GeomBatch {
222        if self.list.is_empty() {
223            return self;
224        }
225
226        let bounds = self.get_bounds().scale(scale);
227        let dx = center_on.x() - bounds.width() / 2.0;
228        let dy = center_on.y() - bounds.height() / 2.0;
229        let rotate_around_pt = bounds.center().offset(dx, dy);
230
231        for (_, poly, _) in &mut self.list {
232            poly.inplace_multi_transform(scale, dx, dy, rotate, rotate_around_pt);
233        }
234
235        self
236    }
237
238    /// Scales the batch by some factor.
239    pub fn scale(self, factor: f64) -> GeomBatch {
240        self.scale_xy(factor, factor)
241    }
242
243    pub fn scale_xy(mut self, x_factor: f64, y_factor: f64) -> GeomBatch {
244        #[allow(clippy::float_cmp)]
245        if x_factor == 1.0 && y_factor == 1.0 {
246            return self;
247        }
248
249        for (_, poly, _) in &mut self.list {
250            poly.scale_xy(x_factor, y_factor);
251        }
252        self
253    }
254
255    /// Scales the batch so that the width matches something, preserving aspect ratio.
256    pub fn scale_to_fit_width(self, width: f64) -> GeomBatch {
257        let ratio = width / self.get_bounds().width();
258        self.scale(ratio)
259    }
260
261    /// Scales the batch so that the height matches something, preserving aspect ratio.
262    pub fn scale_to_fit_height(self, height: f64) -> GeomBatch {
263        let ratio = height / self.get_bounds().height();
264        self.scale(ratio)
265    }
266
267    /// Scales the batch so that the width and height do not exceed some maximum, preserving aspect ratio.
268    pub fn scale_to_fit_square(self, dims: f64) -> GeomBatch {
269        let ratio1 = dims / self.get_bounds().width();
270        let ratio2 = dims / self.get_bounds().height();
271        self.scale(ratio1.min(ratio2))
272    }
273
274    /// Overrides the Z-ordering offset for the batch. Must be in (-1, 0], with values closer to -1
275    /// rendering on top.
276    pub fn set_z_offset(mut self, offset: f64) -> GeomBatch {
277        if offset <= -1.0 || offset > 0.0 {
278            panic!("set_z_offset({}) must be in (-1, 0]", offset);
279        }
280        for (_, _, z) in &mut self.list {
281            *z = offset;
282        }
283        self
284    }
285
286    /// Exports the batch to a list of GeoJSON features, labeling each colored triangle. Note the
287    /// result will be very large and kind of meaningless -- individual triangles are returned; any
288    /// original polygons are lost. Z-values, alpha values from the color, and non-RGB fill
289    /// patterns are lost. The world-space coordinates are optionally translated back to GPS.
290    pub fn into_geojson(self, gps_bounds: Option<&GPSBounds>) -> Vec<geojson::Feature> {
291        let mut features = Vec::new();
292        for (fill, polygon, _) in self.list {
293            if let Fill::Color(color) = fill {
294                let mut properties = serde_json::Map::new();
295                properties.insert("color".to_string(), color.as_hex().into());
296                for triangle in polygon.triangles() {
297                    features.push(geojson::Feature {
298                        bbox: None,
299                        // TODO We could do a bit better and at least emit a MultiPolygon
300                        geometry: Some(Polygon::from_triangle(&triangle).to_geojson(gps_bounds)),
301                        id: None,
302                        properties: Some(properties.clone()),
303                        foreign_members: None,
304                    });
305                }
306            }
307        }
308        features
309    }
310
311    pub fn build(self, ctx: &EventCtx) -> Drawable {
312        ctx.upload(self)
313    }
314}
315
316impl Default for GeomBatch {
317    fn default() -> Self {
318        GeomBatch::new()
319    }
320}
321
322impl<F: Into<Fill>, T: Into<Tessellation>> From<Vec<(F, T)>> for GeomBatch {
323    /// Creates a batch of filled polygons.
324    fn from(list: Vec<(F, T)>) -> GeomBatch {
325        GeomBatch {
326            list: list
327                .into_iter()
328                .map(|(c, p)| (c.into(), p.into(), 0.0))
329                .collect(),
330            autocrop_dims: true,
331        }
332    }
333}
334
335/// A way to transform all colors in a GeomBatch.
336#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
337pub enum RewriteColor {
338    /// Don't do anything
339    NoOp,
340    /// Change every instance of the first color to the second
341    Change(Color, Color),
342    /// Change all colors to the specified value. For this to be interesting, the batch shouldn't
343    /// be a solid block of color. This does not modify Color::CLEAR.
344    ChangeAll(Color),
345    /// Change the alpha value of all colors to this value.
346    ChangeAlpha(f32),
347    /// Convert all colors to greyscale.
348    MakeGrayscale,
349}
350
351impl std::convert::From<Color> for RewriteColor {
352    fn from(color: Color) -> RewriteColor {
353        RewriteColor::ChangeAll(color)
354    }
355}
356
357impl RewriteColor {
358    fn apply(&self, c: Color) -> Color {
359        match self {
360            RewriteColor::NoOp => c,
361            RewriteColor::Change(from, to) => {
362                if c == *from {
363                    *to
364                } else {
365                    c
366                }
367            }
368            RewriteColor::ChangeAll(to) => {
369                if c == Color::CLEAR {
370                    c
371                } else {
372                    *to
373                }
374            }
375            RewriteColor::ChangeAlpha(alpha) => c.alpha(*alpha),
376            RewriteColor::MakeGrayscale => {
377                let avg = (c.r + c.g + c.b) / 3.0;
378                Color::grey(avg).alpha(c.a)
379            }
380        }
381    }
382}