game/render/
car.rs

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        // Wheels
34        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                // The original body may be fine, but sometimes shifting drastically shortens the
44                // length due to miter threshold chopping. Just give up on wheels in that case
45                // instead of crashing.
46                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            // Simpler shape while appearing from a border
64            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            // If this fails for any reason, just fallback to the simple shape
78            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            // draw intent bubble
101            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 the vehicle is temporarily too short for anything, just omit.
121        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                // Always draw the brake light
162                let (pos, angle) = input.body.must_dist_along(Distance::meters(0.5));
163                // TODO rounded
164                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 the vehicle is temporarily too short, just skip the label.
183            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        // TODO Technically some of the body may need to be at different zorders during
198        // transitions, but that's way too much effort
199        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    // Shouldn't ever fail for a single line
248    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}