1use geom::{Angle, ArrowCap, Distance, PolyLine, Polygon, Pt2D, Ring, Tessellation};
2use map_gui::colors::ColorScheme;
3use map_gui::render::{DrawOptions, OUTLINE_THICKNESS};
4use map_gui::AppLike;
5use map_model::{Map, TurnType};
6use sim::{CarID, CarStatus, DrawCarInput, Intent, Sim, VehicleType};
7use widgetry::{Color, Drawable, GeomBatch, GfxCtx, Line, Prerender, Text};
8
9use crate::render::{grey_out_unhighlighted_people, GameRenderable};
10use crate::ID;
11
12const CAR_WIDTH: Distance = Distance::const_meters(1.75);
13
14pub struct DrawCar {
15 pub id: CarID,
16 body: PolyLine,
17 body_polygon: Polygon,
18 zorder: isize,
19
20 draw_default: Drawable,
21}
22
23impl DrawCar {
24 pub fn new(
25 input: DrawCarInput,
26 map: &Map,
27 sim: &Sim,
28 prerender: &Prerender,
29 cs: &ColorScheme,
30 ) -> DrawCar {
31 let mut draw_default = GeomBatch::new();
32
33 for side in vec![
35 input.body.shift_right(CAR_WIDTH / 2.0),
36 input.body.shift_left(CAR_WIDTH / 2.0),
37 ]
38 .into_iter()
39 .flatten()
40 {
41 let len = side.length();
42 if len <= Distance::meters(2.0) {
43 continue;
47 }
48 draw_default.push(
49 cs.bike_frame,
50 side.exact_slice(Distance::meters(0.5), Distance::meters(1.0))
51 .make_polygons(OUTLINE_THICKNESS / 2.0),
52 );
53 draw_default.push(
54 cs.bike_frame,
55 side.exact_slice(len - Distance::meters(2.0), len - Distance::meters(1.5))
56 .make_polygons(OUTLINE_THICKNESS / 2.0),
57 );
58 }
59
60 let body_polygon = input.body.make_polygons(CAR_WIDTH);
61
62 let draw_body = if input.body.length() < Distance::meters(1.1) {
63 Tessellation::from(body_polygon.clone())
65 } else {
66 let front_corner = input.body.length() - Distance::meters(1.0);
67 let thick_line = Tessellation::from(
68 input
69 .body
70 .exact_slice(Distance::ZERO, front_corner)
71 .make_polygons(CAR_WIDTH),
72 );
73
74 let (corner_pt, corner_angle) = input.body.must_dist_along(front_corner);
75 let tip_pt = input.body.last_pt();
76 let tip_angle = input.body.last_line().angle();
77 match Ring::new(vec![
79 corner_pt.project_away(CAR_WIDTH / 2.0, corner_angle.rotate_degs(90.0)),
80 corner_pt.project_away(CAR_WIDTH / 2.0, corner_angle.rotate_degs(-90.0)),
81 tip_pt.project_away(CAR_WIDTH / 4.0, tip_angle.rotate_degs(-90.0)),
82 tip_pt.project_away(CAR_WIDTH / 4.0, tip_angle.rotate_degs(90.0)),
83 corner_pt.project_away(CAR_WIDTH / 2.0, corner_angle.rotate_degs(90.0)),
84 ]) {
85 Ok(front) => Tessellation::from(front.into_polygon()).union(thick_line),
86 Err(_) => thick_line,
87 }
88 };
89 draw_default.push(zoomed_color_car(&input, sim, cs), draw_body);
90
91 if input.status == CarStatus::Parked {
92 draw_default.append(
93 GeomBatch::load_svg(prerender, "system/assets/map/parked_car.svg")
94 .scale(0.01)
95 .centered_on(input.body.middle()),
96 );
97 }
98
99 if input.intent == Some(Intent::Parking) {
100 let bubble_z = -0.0001;
102 let mut bubble_batch =
103 GeomBatch::load_svg(prerender, "system/assets/map/thought_bubble.svg")
104 .scale(0.05)
105 .centered_on(input.body.middle())
106 .translate(4.0, -4.0)
107 .set_z_offset(bubble_z);
108
109 let intent_batch = GeomBatch::load_svg(prerender, "system/assets/map/parking.svg")
110 .scale(0.015)
111 .centered_on(input.body.middle())
112 .translate(4.5, -4.5)
113 .set_z_offset(bubble_z);
114
115 bubble_batch.append(intent_batch);
116
117 draw_default.append(bubble_batch);
118 }
119
120 if input.body.length() >= Distance::meters(2.5) {
122 let arrow_len = 0.8 * CAR_WIDTH;
123 let arrow_thickness = Distance::meters(0.5);
124
125 if let Some(t) = input.waiting_for_turn {
126 match map.get_t(t).turn_type {
127 TurnType::Left | TurnType::UTurn => {
128 let (pos, angle) = input
129 .body
130 .must_dist_along(input.body.length() - Distance::meters(2.5));
131
132 draw_default.push(
133 cs.turn_arrow,
134 PolyLine::must_new(vec![
135 pos.project_away(arrow_len / 2.0, angle.rotate_degs(90.0)),
136 pos.project_away(arrow_len / 2.0, angle.rotate_degs(-90.0)),
137 ])
138 .make_arrow(arrow_thickness, ArrowCap::Triangle),
139 );
140 }
141 TurnType::Right => {
142 let (pos, angle) = input
143 .body
144 .must_dist_along(input.body.length() - Distance::meters(2.5));
145
146 draw_default.push(
147 cs.turn_arrow,
148 PolyLine::must_new(vec![
149 pos.project_away(arrow_len / 2.0, angle.rotate_degs(-90.0)),
150 pos.project_away(arrow_len / 2.0, angle.rotate_degs(90.0)),
151 ])
152 .make_arrow(arrow_thickness, ArrowCap::Triangle),
153 );
154 }
155 TurnType::Straight => {}
156 TurnType::Crosswalk
157 | TurnType::UnmarkedCrossing
158 | TurnType::SharedSidewalkCorner => unreachable!(),
159 }
160
161 let (pos, angle) = input.body.must_dist_along(Distance::meters(0.5));
163 let window_length_gap = Distance::meters(0.2);
165 let window_thickness = Distance::meters(0.3);
166 draw_default.push(
167 cs.brake_light,
168 thick_line_from_angle(
169 window_thickness,
170 CAR_WIDTH - window_length_gap * 2.0,
171 pos.project_away(
172 CAR_WIDTH / 2.0 - window_length_gap,
173 angle.rotate_degs(-90.0),
174 ),
175 angle.rotate_degs(90.0),
176 ),
177 );
178 }
179 }
180
181 if let Some(line) = input.label {
182 if let Ok((pt, angle)) = input
184 .body
185 .dist_along(input.body.length() - Distance::meters(3.5))
186 {
187 draw_default.append(
188 Text::from(Line(line).fg(cs.bus_label))
189 .render_autocropped(prerender)
190 .scale(0.07)
191 .centered_on(pt)
192 .rotate(angle.reorient()),
193 );
194 }
195 }
196
197 let zorder = input
200 .partly_on
201 .into_iter()
202 .chain(vec![input.on])
203 .map(|on| on.get_zorder(map))
204 .max()
205 .unwrap();
206 DrawCar {
207 id: input.id,
208 body: input.body,
209 body_polygon,
210 zorder,
211 draw_default: prerender.upload(draw_default),
212 }
213 }
214}
215
216impl GameRenderable for DrawCar {
217 fn get_id(&self) -> ID {
218 ID::Car(self.id)
219 }
220
221 fn draw(&self, g: &mut GfxCtx, _: &dyn AppLike, _: &DrawOptions) {
222 g.redraw(&self.draw_default);
223 }
224
225 fn get_outline(&self, _: &Map) -> Tessellation {
226 self.body
227 .to_thick_boundary(CAR_WIDTH, OUTLINE_THICKNESS)
228 .unwrap_or_else(|| Tessellation::from(self.body_polygon.clone()))
229 }
230
231 fn contains_pt(&self, pt: Pt2D, _: &Map) -> bool {
232 self.body_polygon.contains_pt(pt)
233 }
234
235 fn get_zorder(&self) -> isize {
236 self.zorder
237 }
238}
239
240fn thick_line_from_angle(
241 thickness: Distance,
242 line_length: Distance,
243 pt: Pt2D,
244 angle: Angle,
245) -> Polygon {
246 let pt2 = pt.project_away(line_length, angle);
247 PolyLine::must_new(vec![pt, pt2]).make_polygons(thickness)
249}
250
251fn zoomed_color_car(input: &DrawCarInput, sim: &Sim, cs: &ColorScheme) -> Color {
252 if input.id.vehicle_type == VehicleType::Bus {
253 cs.bus_body
254 } else if input.id.vehicle_type == VehicleType::Train {
255 cs.train_body
256 } else {
257 let color = match input.status {
258 CarStatus::Moving => cs.rotating_color_agents(input.id.id),
259 CarStatus::Parked => cs.parked_car,
260 };
261 grey_out_unhighlighted_people(color, &input.person, sim)
262 }
263}