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
7pub struct EditPolygon {
9 points: Vec<Pt2D>,
10 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 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 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 pub fn event(&mut self, ctx: &mut EventCtx) -> bool {
122 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 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 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 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 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 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 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}