game/sandbox/gameplay/freeform/
area_spawner.rs1use geom::{Polygon, Pt2D};
2use map_model::{BuildingID, IntersectionID};
3use widgetry::mapspace::{ObjectID, World, WorldOutcome};
4use widgetry::tools::ChooseSomething;
5use widgetry::{
6 Choice, Color, Drawable, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment, Key, Line, Outcome,
7 Panel, State, TextExt, VerticalAlignment, Widget,
8};
9
10use crate::app::{App, Transition};
11
12pub struct AreaSpawner {
13 areas: Vec<Area>,
14 panel: Panel,
15 world: World<Obj>,
16 mode: Mode,
17}
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
20struct Obj(usize);
21impl ObjectID for Obj {}
22
23enum Mode {
24 Neutral,
25 DrawingArea(SelectRectangle),
26 PickingDestination { source: usize },
27}
28
29impl AreaSpawner {
30 pub fn new_state(ctx: &mut EventCtx) -> Box<dyn State<App>> {
31 Box::new(AreaSpawner {
32 areas: Vec::new(),
33 panel: Panel::new_builder(Widget::col(vec![
34 Widget::row(vec![
35 Line("Specify traffic patterns")
36 .small_heading()
37 .into_widget(ctx),
38 ctx.style().btn_close_widget(ctx),
39 ]),
40 ctx.style()
41 .btn_outline
42 .text("Draw new area")
43 .hotkey(Key::A)
44 .build_def(ctx),
45 "".text_widget(ctx).named("instructions"),
46 ]))
47 .aligned(HorizontalAlignment::Left, VerticalAlignment::Top)
48 .build(ctx),
49 world: World::new(),
50 mode: Mode::Neutral,
51 })
52 }
53
54 fn rebuild_world(&mut self, ctx: &mut EventCtx) {
55 let mut world = World::new();
56 let picking_destination = match self.mode {
57 Mode::PickingDestination { source } => Some(source),
58 _ => None,
59 };
60
61 for (idx, area) in self.areas.iter().enumerate() {
62 if picking_destination == Some(idx) {
63 world
64 .add(Obj(idx))
65 .hitbox(area.polygon.clone())
66 .draw_color(Color::RED.alpha(0.5))
67 .build(ctx);
68 } else {
69 world
70 .add(Obj(idx))
71 .hitbox(area.polygon.clone())
72 .draw_color(Color::BLUE.alpha(0.5))
73 .hover_alpha(0.8)
74 .clickable()
75 .build(ctx);
76 }
77 }
78 world.initialize_hover(ctx);
79 self.world = world;
80 }
81}
82
83impl State<App> for AreaSpawner {
84 fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
85 match self.mode {
86 Mode::Neutral => {
87 if let Outcome::Clicked(x) = self.panel.event(ctx) {
88 match x.as_ref() {
89 "close" => {
90 return Transition::Pop;
91 }
92 "Draw new area" => {
93 self.mode = Mode::DrawingArea(SelectRectangle::new(ctx));
94 let label = "Click and drag to select an area".text_widget(ctx);
95 self.panel.replace(ctx, "instructions", label);
96 }
97 _ => unreachable!(),
98 }
99 }
100
101 if let WorldOutcome::ClickedObject(Obj(idx)) = self.world.event(ctx) {
102 let area = &self.areas[idx];
103 return Transition::Push(ChooseSomething::new_state(
104 ctx,
105 format!(
106 "This area has {} buildings and {} borders",
107 area.buildings.len(),
108 area.borders.len()
109 ),
110 vec![
111 Choice::string("spawn traffic from here"),
112 Choice::string("delete"),
113 ],
114 Box::new(move |resp, _, _| {
115 Transition::Multi(vec![
116 Transition::Pop,
117 Transition::ModifyState(Box::new(move |state, ctx, _| {
118 let state = state.downcast_mut::<AreaSpawner>().unwrap();
119 if resp == "delete" {
120 state.areas.remove(idx);
121 state.rebuild_world(ctx);
122 } else if resp == "spawn traffic from here" {
123 state.mode = Mode::PickingDestination { source: idx };
124 state.rebuild_world(ctx);
125 let label = "Choose where traffic will go".text_widget(ctx);
126 state.panel.replace(ctx, "instructions", label);
127 }
128 })),
129 ])
130 }),
131 ));
132 }
133 }
134 Mode::DrawingArea(ref mut select) => {
135 if select.event(ctx) {
136 if let Some(polygon) = select.rect.take() {
137 self.areas.push(Area::new(app, polygon));
138 self.rebuild_world(ctx);
139 }
140 self.mode = Mode::Neutral;
141 let label = "".text_widget(ctx);
142 self.panel.replace(ctx, "instructions", label);
143 }
144 }
145 Mode::PickingDestination { .. } => {
146 if let WorldOutcome::ClickedObject(Obj(_destination)) = self.world.event(ctx) {
147 self.mode = Mode::Neutral;
149 self.rebuild_world(ctx);
150 let label = "".text_widget(ctx);
151 self.panel.replace(ctx, "instructions", label);
152 }
153 }
154 }
155
156 Transition::Keep
157 }
158
159 fn draw(&self, g: &mut GfxCtx, _: &App) {
160 self.panel.draw(g);
161 self.world.draw(g);
162 if let Mode::DrawingArea(ref select) = self.mode {
163 select.draw(g);
164 }
165 }
166}
167
168struct SelectRectangle {
169 pt1: Option<Pt2D>,
170 rect: Option<Polygon>,
171 preview: Drawable,
172}
173
174impl SelectRectangle {
175 fn new(ctx: &mut EventCtx) -> SelectRectangle {
176 SelectRectangle {
177 pt1: None,
178 rect: None,
179 preview: Drawable::empty(ctx),
180 }
181 }
182
183 fn event(&mut self, ctx: &mut EventCtx) -> bool {
185 let pt = if let Some(pt) = ctx.canvas.get_cursor_in_map_space() {
186 pt
187 } else {
188 return false;
189 };
190 if let Some(pt1) = self.pt1 {
191 if ctx.redo_mouseover() {
192 self.rect = Polygon::rectangle_two_corners(pt1, pt);
193 let mut batch = GeomBatch::new();
194 if let Some(ref poly) = self.rect {
195 batch.push(Color::RED.alpha(0.5), poly.clone());
196 }
197 self.preview = batch.upload(ctx);
198 }
199 if ctx.input.left_mouse_button_released() {
200 return true;
201 }
202 } else if ctx.input.left_mouse_button_pressed() {
203 self.pt1 = Some(pt);
204 }
205 false
206 }
207
208 fn draw(&self, g: &mut GfxCtx) {
209 g.redraw(&self.preview);
210 }
211}
212
213struct Area {
214 polygon: Polygon,
215 borders: Vec<IntersectionID>,
216 buildings: Vec<BuildingID>,
217}
218
219impl Area {
220 fn new(app: &App, polygon: Polygon) -> Area {
221 let mut borders = Vec::new();
222 for i in app.primary.map.all_intersections() {
223 if i.is_border() && polygon.contains_pt(i.polygon.center()) {
224 borders.push(i.id);
225 }
226 }
227 let mut buildings = Vec::new();
228 for b in app.primary.map.all_buildings() {
229 if polygon.contains_pt(b.polygon.center()) {
230 buildings.push(b.id);
231 }
232 }
233 Area {
234 polygon,
235 borders,
236 buildings,
237 }
238 }
239}