map_gui/tools/
polygon.rs

1use anyhow::Result;
2
3use geom::{Angle, ArrowCap, Circle, Distance, FindClosest, Line, PolyLine, Pt2D, Ring};
4use widgetry::mapspace::{ObjectID, World, WorldOutcome};
5use widgetry::{Cached, Color, Drawable, EventCtx, GeomBatch, GfxCtx, Key};
6
7// TODO Callers may want to explain the controls -- the D key for the leafblower, in particular.
8pub struct EditPolygon {
9    points: Vec<Pt2D>,
10    // The points change size as we zoom out, so rebuild based on cam_zoom
11    world: Cached<f64, World<Obj>>,
12    polygon_draggable: bool,
13
14    leafblower: Leafblower,
15}
16
17struct Leafblower {
18    cursor: Option<Pt2D>,
19    direction: Option<Angle>,
20    draw: Drawable,
21}
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
24enum Obj {
25    Polygon,
26    Point(usize),
27}
28impl ObjectID for Obj {}
29
30impl EditPolygon {
31    pub fn new(ctx: &EventCtx, mut points: Vec<Pt2D>, polygon_draggable: bool) -> Self {
32        if !points.is_empty() && *points.last().unwrap() == points[0] {
33            points.pop();
34        }
35        let mut edit = Self {
36            points,
37            world: Cached::new(),
38            polygon_draggable,
39            leafblower: Leafblower {
40                cursor: ctx.canvas.get_cursor_in_map_space(),
41                direction: None,
42                draw: Drawable::empty(ctx),
43            },
44        };
45        edit.rebuild_world(ctx);
46        edit
47    }
48
49    fn add_polygon_to_world(&self, ctx: &EventCtx, world: &mut World<Obj>) {
50        if self.points.len() >= 3 {
51            let mut pts = self.points.to_vec();
52            pts.push(pts[0]);
53            if let Ok(ring) = Ring::new(pts) {
54                let obj = world
55                    .add(Obj::Polygon)
56                    .hitbox(ring.into_polygon())
57                    .zorder(0)
58                    .draw_color(Color::BLUE.alpha(0.6));
59                if self.polygon_draggable {
60                    obj.hover_alpha(0.3).draggable().build(ctx);
61                } else {
62                    obj.build(ctx);
63                }
64            }
65        }
66    }
67
68    fn rebuild_world(&mut self, ctx: &EventCtx) {
69        let mut world = World::new();
70
71        self.add_polygon_to_world(ctx, &mut world);
72
73        // Scale the circle as we zoom out
74        let circle =
75            Circle::new(Pt2D::zero(), Distance::meters(10.0) / ctx.canvas.cam_zoom).to_polygon();
76        for (idx, pt) in self.points.iter().enumerate() {
77            world
78                .add(Obj::Point(idx))
79                .hitbox(circle.translate(pt.x(), pt.y()))
80                .zorder(1)
81                .draw_color(Color::RED)
82                .hover_alpha(0.8)
83                .hotkey(Key::Backspace, "delete")
84                .draggable()
85                .build(ctx);
86        }
87
88        world.initialize_hover(ctx);
89
90        if let Some(prev) = self.world.value() {
91            world.rebuilt_during_drag(ctx, prev);
92        }
93        self.world.set(ctx.canvas.cam_zoom, world);
94    }
95
96    fn rebuild_one_point(&mut self, ctx: &EventCtx, idx: usize) {
97        let (_, mut world) = self.world.take().unwrap();
98
99        world.delete_before_replacement(Obj::Polygon);
100        self.add_polygon_to_world(ctx, &mut world);
101
102        // Change the point
103        // TODO Some repeated code, but meh
104        world.delete_before_replacement(Obj::Point(idx));
105        let circle =
106            Circle::new(Pt2D::zero(), Distance::meters(10.0) / ctx.canvas.cam_zoom).to_polygon();
107        world
108            .add(Obj::Point(idx))
109            .hitbox(circle.translate(self.points[idx].x(), self.points[idx].y()))
110            .zorder(1)
111            .draw_color(Color::RED)
112            .hover_alpha(0.8)
113            .hotkey(Key::Backspace, "delete")
114            .draggable()
115            .build(ctx);
116
117        self.world.set(ctx.canvas.cam_zoom, world);
118    }
119
120    /// True if the polygon is modified
121    pub fn event(&mut self, ctx: &mut EventCtx) -> bool {
122        // Recalculate if zoom has changed
123        if self.world.key() != Some(ctx.canvas.cam_zoom) {
124            self.rebuild_world(ctx);
125        }
126
127        match self.world.value_mut().unwrap().event(ctx) {
128            WorldOutcome::ClickedFreeSpace(pt) => {
129                // Insert the new point in the "middle" of the closest line segment
130                let mut closest = FindClosest::new();
131                for (idx, pair) in self.points.windows(2).enumerate() {
132                    closest.add(idx + 1, &[pair[0], pair[1]]);
133                }
134                if let Some((idx, _)) = closest.closest_pt(pt, Distance::meters(1000.0)) {
135                    self.points.insert(idx, pt);
136                } else {
137                    // Just put on the end
138                    self.points.push(pt);
139                }
140
141                self.rebuild_world(ctx);
142                true
143            }
144            WorldOutcome::Dragging {
145                obj: Obj::Point(idx),
146                dx,
147                dy,
148                ..
149            } => {
150                self.points[idx] = self.points[idx].offset(dx, dy);
151                self.rebuild_one_point(ctx, idx);
152                true
153            }
154            WorldOutcome::Dragging {
155                obj: Obj::Polygon,
156                dx,
157                dy,
158                ..
159            } => {
160                for pt in &mut self.points {
161                    *pt = pt.offset(dx, dy);
162                }
163                self.rebuild_world(ctx);
164                true
165            }
166            WorldOutcome::Keypress("delete", Obj::Point(idx)) => {
167                self.points.remove(idx);
168                self.rebuild_world(ctx);
169                true
170            }
171            _ => {
172                // TODO Does World eat the mouse moved event?
173                if self.leafblower.event(
174                    ctx,
175                    &mut self.points,
176                    self.world.value().unwrap().get_hovering().is_some(),
177                ) {
178                    self.rebuild_world(ctx);
179                    return true;
180                }
181                false
182            }
183        }
184    }
185
186    pub fn draw(&self, g: &mut GfxCtx) {
187        self.world.value().unwrap().draw(g);
188        g.redraw(&self.leafblower.draw);
189    }
190
191    /// Could fail if the user edits the ring and makes it invalid
192    pub fn get_ring(&self) -> Result<Ring> {
193        let mut pts = self.points.clone();
194        pts.push(pts[0]);
195        Ring::new(pts)
196    }
197}
198
199impl Leafblower {
200    fn event(&mut self, ctx: &mut EventCtx, points: &mut Vec<Pt2D>, suppress: bool) -> bool {
201        let mut cursor = ctx.canvas.get_cursor_in_map_space();
202        if suppress {
203            cursor = None;
204        }
205        if self.cursor != cursor {
206            self.cursor = cursor;
207            self.update(ctx, points);
208        }
209
210        if ctx.input.pressed(Key::D) {
211            if let Some(angle) = self.direction {
212                let cursor = self.cursor.unwrap();
213                let threshold = Distance::meters(100.0) / ctx.canvas.cam_zoom;
214                for pt in points.iter_mut() {
215                    if pt.dist_to(cursor) <= threshold {
216                        *pt = pt.project_away(0.1 * threshold, angle);
217                    }
218                }
219                // Force an update
220                self.cursor = None;
221                self.update(ctx, points);
222                return true;
223            }
224        }
225
226        false
227    }
228
229    fn update(&mut self, ctx: &EventCtx, points: &[Pt2D]) {
230        self.direction = None;
231        self.draw = Drawable::empty(ctx);
232
233        // TODO Express in pixels?
234        let threshold = Distance::meters(100.0) / ctx.canvas.cam_zoom;
235
236        let cursor = if let Some(pt) = self.cursor {
237            pt
238        } else {
239            return;
240        };
241
242        let mut angles = Vec::new();
243        for pt in points {
244            if let Ok(line) = Line::new(cursor, *pt) {
245                if line.length() <= threshold {
246                    angles.push(line.angle());
247                }
248            }
249        }
250        if !angles.is_empty() {
251            self.direction = Some(Angle::average(angles));
252            self.draw = GeomBatch::from(vec![(
253                Color::BLACK,
254                PolyLine::must_new(vec![
255                    cursor,
256                    cursor.project_away(threshold, self.direction.unwrap()),
257                ])
258                .make_arrow(
259                    Distance::meters(10.0) / ctx.canvas.cam_zoom,
260                    ArrowCap::Triangle,
261                ),
262            )])
263            .upload(ctx);
264        }
265    }
266}