abstreet/editor/src/ui.rs

621 lines
24 KiB
Rust
Raw Normal View History

2019-02-09 22:50:39 +03:00
use crate::objects::{DrawCtx, RenderingHints, ID};
use crate::render::{
draw_vehicle, AgentCache, DrawPedestrian, RenderOptions, Renderable, MIN_ZOOM_FOR_DETAIL,
};
2019-02-02 23:49:17 +03:00
use crate::state::UIState;
2019-02-15 00:01:26 +03:00
use abstutil;
use ezgui::{
Canvas, Color, EventCtx, EventLoopMode, Folder, GfxCtx, Key, ModalMenu, Prerender, Text,
TopMenu, BOTTOM_LEFT, GUI,
};
use geom::{Bounds, Circle, Distance, Polygon};
use map_model::{BuildingID, LaneID, Traversable};
use serde_derive::{Deserialize, Serialize};
use std::collections::HashSet;
2018-12-14 01:26:12 +03:00
pub struct UI<S: UIState> {
// TODO Probably move this into DefaultUIState after rewriting Tutorial and the whole UIState
// abstraction.
hints: RenderingHints,
pub state: S,
}
impl<S: UIState> GUI for UI<S> {
fn top_menu(&self, canvas: &Canvas) -> Option<TopMenu> {
2018-12-25 06:31:04 +03:00
let mut folders = Vec::new();
folders.push(Folder::new(
"File",
vec![
(Some(Key::L), "show legend"),
(Some(Key::Escape), "pause game"),
],
2018-12-25 06:31:04 +03:00
));
if self.state.get_state().enable_debug_controls {
folders.push(Folder::new(
"Debug",
vec![
(None, "screenshot everything"),
(Some(Key::F1), "screenshot just this"),
(None, "find chokepoints"),
(None, "validate map geometry"),
(Some(Key::Num1), "show/hide buildings"),
(Some(Key::Num2), "show/hide intersections"),
(Some(Key::Num3), "show/hide lanes"),
(Some(Key::Num5), "show/hide areas"),
(Some(Key::Num6), "show OSM colors"),
(Some(Key::Num7), "show/hide extra shapes"),
(Some(Key::Num9), "show/hide all turn icons"),
(None, "show/hide geometry debug mode"),
2018-12-25 06:31:04 +03:00
],
));
}
folders.extend(vec![
Folder::new(
"Edit",
vec![
(Some(Key::B), "manage A/B tests"),
(None, "configure colors"),
(Some(Key::N), "manage neighborhoods"),
(Some(Key::Q), "manage map edits"),
(Some(Key::E), "edit roads"),
(Some(Key::W), "manage scenarios"),
2019-04-03 05:28:14 +03:00
(None, "show map diffs"),
2018-12-25 06:31:04 +03:00
],
),
Folder::new(
"Simulation",
vec![
(Some(Key::LeftBracket), "slow down sim"),
(Some(Key::RightBracket), "speed up sim"),
(Some(Key::O), "save sim state"),
(Some(Key::Y), "load previous sim state"),
(Some(Key::U), "load next sim state"),
(Some(Key::Space), "run/pause sim"),
(Some(Key::M), "run one step of sim"),
(Some(Key::Dot), "show/hide sim info sidepanel"),
(Some(Key::T), "start time traveling"),
(Some(Key::D), "diff all A/B trips"),
(Some(Key::S), "seed the sim with agents"),
(Some(Key::LeftAlt), "swap the primary/secondary sim"),
2019-04-21 02:24:48 +03:00
(None, "reset sim"),
2018-12-25 06:31:04 +03:00
],
),
Folder::new(
"View",
vec![
(None, "show neighborhood summaries"),
(Some(Key::Slash), "search for something"),
(Some(Key::A), "show lanes with active traffic"),
(Some(Key::J), "warp to an object"),
2018-12-25 06:31:04 +03:00
],
),
]);
Some(TopMenu::new(folders, canvas))
}
fn modal_menus(&self) -> Vec<ModalMenu> {
2018-12-17 23:33:44 +03:00
vec![
ModalMenu::new(
"Traffic Signal Editor",
vec![
(Key::Enter, "quit"),
(Key::D, "change cycle duration"),
(Key::P, "choose a preset signal"),
(Key::K, "move current cycle up"),
(Key::J, "move current cycle down"),
(Key::UpArrow, "select previous cycle"),
(Key::DownArrow, "select next cycle"),
2018-12-17 23:33:44 +03:00
(Key::Backspace, "delete current cycle"),
(Key::N, "add a new empty cycle"),
(Key::M, "add a new pedestrian scramble cycle"),
],
),
ModalMenu::new(
"Scenario Editor",
vec![
(Key::S, "save"),
(Key::E, "edit"),
(Key::I, "instantiate"),
(Key::V, "visualize"),
(Key::Enter, "quit"),
],
2018-12-17 23:33:44 +03:00
),
ModalMenu::new("Road Editor", vec![(Key::Enter, "quit")]),
ModalMenu::new(
"Color Picker",
2018-12-18 03:10:09 +03:00
vec![(Key::Backspace, "revert"), (Key::Enter, "finalize")],
2018-12-17 23:33:44 +03:00
),
ModalMenu::new(
"Stop Sign Editor",
vec![(Key::Enter, "quit"), (Key::R, "reset to default")],
),
ModalMenu::new("A/B Test Editor", vec![(Key::R, "run A/B test")]),
ModalMenu::new(
"Neighborhood Editor",
vec![
2018-12-18 03:48:55 +03:00
(Key::Enter, "save"),
(Key::Escape, "quit"),
2018-12-17 23:33:44 +03:00
(Key::X, "export as an Osmosis polygon filter"),
(Key::P, "add a new point"),
],
),
ModalMenu::new(
"Time Traveler",
vec![
(Key::Enter, "quit"),
(Key::Comma, "rewind"),
(Key::Dot, "forwards"),
],
),
ModalMenu::new(
"Simple Model",
vec![
(Key::Enter, "quit"),
(Key::Comma, "rewind"),
(Key::Dot, "forwards"),
2019-02-08 05:16:18 +03:00
(Key::Space, "toggle forwards play"),
(Key::M, "toggle backwards play"),
2019-02-08 05:34:51 +03:00
(Key::T, "toggle tooltips"),
(Key::E, "exhaustively test instantiation everywhere"),
(Key::D, "debug"),
],
),
ModalMenu::new(
"Even Simpler Model",
vec![
(Key::Enter, "quit"),
(Key::Dot, "forwards"),
(Key::Space, "toggle forwards play"),
2019-02-20 07:11:43 +03:00
(Key::E, "spawn tons of cars everywhere"),
],
),
2018-12-17 23:33:44 +03:00
ModalMenu::new(
"Geometry Debugger",
vec![(Key::Enter, "quit"), (Key::N, "see next problem")],
),
ModalMenu::new("Original Roads", vec![(Key::Enter, "quit")]),
2018-12-17 23:33:44 +03:00
ModalMenu::new("OSM Classifier", vec![(Key::Num6, "quit")]),
ModalMenu::new(
"Floodfiller",
vec![
2018-12-18 03:10:09 +03:00
(Key::Enter, "quit"),
2018-12-17 23:33:44 +03:00
(Key::Space, "step forwards"),
(Key::Tab, "finish floodfilling"),
],
),
ModalMenu::new("Chokepoints Debugger", vec![(Key::Enter, "quit")]),
ModalMenu::new("A/B Trip Explorer", vec![(Key::Enter, "quit")]),
ModalMenu::new("A/B All Trips Explorer", vec![(Key::Enter, "quit")]),
ModalMenu::new("Agent Follower", vec![(Key::F, "quit")]),
ModalMenu::new("Search", vec![(Key::Enter, "quit")]),
ModalMenu::new("Neighborhood Summaries", vec![(Key::Enter, "quit")]),
2018-12-17 23:33:44 +03:00
ModalMenu::new(
"Agent Route Debugger",
2018-12-18 03:10:09 +03:00
vec![(Key::R, "quit"), (Key::L, "show route for all agents")],
2018-12-17 23:33:44 +03:00
),
ModalMenu::new("Active Traffic Visualizer", vec![(Key::A, "quit")]),
2018-12-24 19:13:59 +03:00
ModalMenu::new("Object Hider", vec![(Key::K, "unhide everything")]),
// TODO F1?
ModalMenu::new("Legend", vec![(Key::L, "quit")]),
2019-01-13 02:19:25 +03:00
ModalMenu::new(
"Polygon Debugger",
vec![
(Key::Enter, "quit"),
(Key::Dot, "next item"),
(Key::Comma, "prev item"),
(Key::F, "first item"),
(Key::L, "last item"),
2019-01-13 02:19:25 +03:00
],
),
ModalMenu::new("Agent Spawner", vec![(Key::Enter, "quit")]),
2019-04-03 05:28:14 +03:00
ModalMenu::new("Map Edits Differ", vec![(Key::Enter, "quit")]),
2018-12-17 23:33:44 +03:00
]
}
// TODO This hacky wrapper will soon disappear, when UI stops implementing GUI
fn event(&mut self, ctx: EventCtx) -> EventLoopMode {
self.new_event(ctx).0
}
2018-09-13 03:39:53 +03:00
fn draw(&self, g: &mut GfxCtx, screencap: bool) -> Option<String> {
let state = self.state.get_state();
2019-02-09 22:50:39 +03:00
let ctx = DrawCtx {
cs: &state.cs,
map: &state.primary.map,
draw_map: &state.primary.draw_map,
sim: &state.primary.sim,
hints: &self.hints,
};
let mut sample_intersection: Option<String> = None;
2019-04-14 02:02:33 +03:00
g.clear(state.cs.get_def("true background", Color::BLACK));
g.redraw(&state.primary.draw_map.boundary_polygon);
if g.canvas.cam_zoom < MIN_ZOOM_FOR_DETAIL && !screencap {
// Unzoomed mode
if state.layers.show_areas {
g.redraw(&state.primary.draw_map.draw_all_areas);
}
if state.layers.show_lanes {
g.redraw(&state.primary.draw_map.draw_all_thick_roads);
}
if state.layers.show_intersections {
g.redraw(&state.primary.draw_map.draw_all_unzoomed_intersections);
}
if state.layers.show_buildings {
g.redraw(&state.primary.draw_map.draw_all_buildings);
}
// Still show area selection when zoomed out.
if state.primary.current_flags.debug_areas {
if let Some(ID::Area(id)) = state.primary.current_selection {
g.draw_polygon(
state.cs.get("selected"),
&fill_to_boundary_polygon(ctx.draw_map.get_a(id).get_outline(&ctx.map)),
);
}
}
state.primary.sim.draw_unzoomed(g, &state.primary.map);
} else {
let mut cache = state.primary.draw_map.agents.borrow_mut();
let objects =
self.get_renderables_back_to_front(g.get_screen_bounds(), &g.prerender, &mut cache);
let mut drawn_all_buildings = false;
let mut drawn_all_areas = false;
for obj in objects {
match obj.get_id() {
ID::Building(_) => {
if !drawn_all_buildings {
g.redraw(&state.primary.draw_map.draw_all_buildings);
drawn_all_buildings = true;
}
2019-02-15 03:53:21 +03:00
}
ID::Area(_) => {
if !drawn_all_areas {
g.redraw(&state.primary.draw_map.draw_all_areas);
drawn_all_areas = true;
}
}
_ => {}
};
let opts = RenderOptions {
color: state.color_obj(obj.get_id(), &ctx),
debug_mode: state.layers.debug_mode,
};
obj.draw(g, opts, &ctx);
if state.primary.current_selection == Some(obj.get_id()) {
g.draw_polygon(
state.cs.get_def("selected", Color::YELLOW.alpha(0.4)),
&fill_to_boundary_polygon(obj.get_outline(&ctx.map)),
);
}
if screencap && sample_intersection.is_none() {
if let ID::Intersection(id) = obj.get_id() {
sample_intersection = Some(format!("_i{}", id.0));
}
}
}
}
if !screencap {
self.state.draw(g, &ctx);
// Not happy about cloning, but probably will make the OSD a first-class ezgui concept
// soon, so meh
let mut osd = self.hints.osd.clone();
// TODO Only in some kind of debug mode
osd.add_line(format!(
"{} things uploaded, {} things drawn",
abstutil::prettyprint_usize(g.get_num_uploads()),
abstutil::prettyprint_usize(g.num_draw_calls),
));
g.draw_blocking_text(osd, BOTTOM_LEFT);
}
sample_intersection
2018-10-22 05:23:47 +03:00
}
fn dump_before_abort(&self, canvas: &Canvas) {
2019-02-20 22:24:21 +03:00
println!(
"********************************************************************************"
);
println!("UI broke! Primary sim:");
2018-12-25 05:58:01 +03:00
self.state.get_state().primary.sim.dump_before_abort();
if let Some((s, _)) = &self.state.get_state().secondary {
2019-02-20 22:24:21 +03:00
println!("Secondary sim:");
2018-12-25 05:58:01 +03:00
s.sim.dump_before_abort();
}
self.save_editor_state(canvas);
}
2018-12-17 22:32:52 +03:00
fn before_quit(&self, canvas: &Canvas) {
self.save_editor_state(canvas);
2019-02-01 10:22:36 +03:00
self.state.get_state().cs.save();
2019-02-20 22:24:21 +03:00
println!("Saved color_scheme");
2019-02-15 00:01:26 +03:00
}
fn profiling_enabled(&self) -> bool {
self.state.get_state().primary.current_flags.enable_profiler
2018-12-17 22:32:52 +03:00
}
2018-10-22 05:23:47 +03:00
}
2018-12-14 01:26:12 +03:00
impl<S: UIState> UI<S> {
2019-02-01 10:22:36 +03:00
pub fn new(state: S, canvas: &mut Canvas) -> UI<S> {
match abstutil::read_json::<EditorState>("../editor_state") {
Ok(ref loaded) if state.get_state().primary.map.get_name() == &loaded.map_name => {
2019-02-20 22:24:21 +03:00
println!("Loaded previous editor_state");
canvas.cam_x = loaded.cam_x;
canvas.cam_y = loaded.cam_y;
canvas.cam_zoom = loaded.cam_zoom;
2018-10-22 05:23:47 +03:00
}
_ => {
2019-02-20 22:24:21 +03:00
println!("Couldn't load editor_state or it's for a different map, so just focusing on an arbitrary building");
let focus_pt = ID::Building(BuildingID(0))
.canonical_point(
&state.get_state().primary.map,
&state.get_state().primary.sim,
&state.get_state().primary.draw_map,
)
.or_else(|| {
ID::Lane(LaneID(0)).canonical_point(
&state.get_state().primary.map,
&state.get_state().primary.sim,
&state.get_state().primary.draw_map,
)
})
.expect("Can't get canonical_point of BuildingID(0) or Road(0)");
canvas.center_on_map_pt(focus_pt);
2018-10-22 05:23:47 +03:00
}
}
UI {
state,
hints: RenderingHints {
mode: EventLoopMode::InputOnly,
osd: Text::new(),
suppress_traffic_signal_details: None,
hide_turn_icons: HashSet::new(),
},
}
2018-10-22 05:23:47 +03:00
}
// True if we should pause.
pub fn new_event(&mut self, mut ctx: EventCtx) -> (EventLoopMode, bool) {
self.hints = RenderingHints {
mode: EventLoopMode::InputOnly,
osd: Text::new(),
suppress_traffic_signal_details: None,
hide_turn_icons: HashSet::new(),
};
// First update the camera
ctx.canvas.handle_event(ctx.input);
// Always handle mouseover
if !ctx.canvas.is_dragging() && ctx.input.get_moved_mouse().is_some() {
self.state.mut_state().primary.current_selection = self.mouseover_something(&ctx);
}
if ctx.input.window_lost_cursor() {
self.state.mut_state().primary.current_selection = None;
}
let mut recalculate_current_selection = false;
self.state.event(
&mut ctx,
&mut self.hints,
&mut recalculate_current_selection,
);
if recalculate_current_selection {
self.state.mut_state().primary.current_selection = self.mouseover_something(&ctx);
}
ctx.input.populate_osd(&mut self.hints.osd);
// TODO a plugin should do this, even though it's such a tiny thing
if self.state.get_state().enable_debug_controls {
if ctx.input.action_chosen("screenshot everything") {
let bounds = self.state.get_state().primary.map.get_bounds();
assert!(bounds.min_x == 0.0 && bounds.min_y == 0.0);
self.hints.mode = EventLoopMode::ScreenCaptureEverything {
dir: format!(
"../data/screenshots/pending_{}",
self.state.get_state().primary.map.get_name()
),
zoom: 3.0,
max_x: bounds.max_x,
max_y: bounds.max_y,
};
}
if ctx.input.action_chosen("screenshot just this") {
self.hints.mode = EventLoopMode::ScreenCaptureCurrentShot;
}
}
(
self.hints.mode.clone(),
ctx.input.action_chosen("pause game"),
)
}
fn mouseover_something(&self, ctx: &EventCtx) -> Option<ID> {
2019-02-19 02:21:43 +03:00
// Unzoomed mode. Ignore when debugging areas.
if ctx.canvas.cam_zoom < MIN_ZOOM_FOR_DETAIL
&& !self.state.get_state().primary.current_flags.debug_areas
{
return None;
}
let pt = ctx.canvas.get_cursor_in_map_space()?;
let mut cache = self.state.get_state().primary.draw_map.agents.borrow_mut();
let mut objects = self.get_renderables_back_to_front(
Circle::new(pt, Distance::meters(3.0)).get_bounds(),
ctx.prerender,
&mut cache,
);
objects.reverse();
2019-02-15 05:42:24 +03:00
let debug_areas = self.state.get_state().primary.current_flags.debug_areas;
for obj in objects {
// Don't mouseover areas.
// TODO Might get fancier rules in the future, so we can't mouseover irrelevant things
// in intersection editor mode, for example.
match obj.get_id() {
2019-02-15 05:42:24 +03:00
ID::Area(_) if !debug_areas => {}
// Thick roads are only shown when unzoomed, when we don't mouseover at all.
ID::Road(_) => {}
_ => {
if obj
.get_outline(&self.state.get_state().primary.map)
.contains_pt(pt)
{
return Some(obj.get_id());
}
}
};
}
None
2018-12-25 05:58:01 +03:00
}
fn save_editor_state(&self, canvas: &Canvas) {
let state = EditorState {
2018-12-25 05:58:01 +03:00
map_name: self.state.get_state().primary.map.get_name().clone(),
cam_x: canvas.cam_x,
cam_y: canvas.cam_y,
cam_zoom: canvas.cam_zoom,
};
// TODO maybe make state line up with the map, so loading from a new map doesn't break
abstutil::write_json("../editor_state", &state).expect("Saving editor_state failed");
2019-02-20 22:24:21 +03:00
println!("Saved editor_state");
}
2019-02-02 23:49:17 +03:00
// TODO This could probably belong to DrawMap again, but it's annoying to plumb things that
// State does, like show_icons_for() and show().
fn get_renderables_back_to_front<'a>(
&'a self,
bounds: Bounds,
prerender: &Prerender,
agents: &'a mut AgentCache,
) -> Vec<Box<&'a Renderable>> {
let state = self.state.get_state();
let map = &state.primary.map;
let draw_map = &state.primary.draw_map;
let mut areas: Vec<Box<&Renderable>> = Vec::new();
let mut lanes: Vec<Box<&Renderable>> = Vec::new();
let mut roads: Vec<Box<&Renderable>> = Vec::new();
let mut intersections: Vec<Box<&Renderable>> = Vec::new();
let mut buildings: Vec<Box<&Renderable>> = Vec::new();
let mut extra_shapes: Vec<Box<&Renderable>> = Vec::new();
let mut bus_stops: Vec<Box<&Renderable>> = Vec::new();
let mut turn_icons: Vec<Box<&Renderable>> = Vec::new();
let mut agents_on: Vec<Traversable> = Vec::new();
for id in draw_map.get_matching_objects(bounds) {
if !state.show(id) {
continue;
}
match id {
ID::Area(id) => areas.push(Box::new(draw_map.get_a(id))),
ID::Lane(id) => {
lanes.push(Box::new(draw_map.get_l(id)));
let lane = map.get_l(id);
if state.show_icons_for(lane.dst_i) {
for (t, _) in map.get_next_turns_and_lanes(id, lane.dst_i) {
turn_icons.push(Box::new(draw_map.get_t(t.id)));
}
} else {
// TODO Bug: pedestrians on front paths aren't selectable.
agents_on.push(Traversable::Lane(id));
}
for bs in &lane.bus_stops {
bus_stops.push(Box::new(draw_map.get_bs(*bs)));
}
}
ID::Road(id) => {
roads.push(Box::new(draw_map.get_r(id)));
}
ID::Intersection(id) => {
intersections.push(Box::new(draw_map.get_i(id)));
for t in &map.get_i(id).turns {
if !state.show_icons_for(id) {
agents_on.push(Traversable::Turn(*t));
}
}
}
// TODO front paths will get drawn over buildings, depending on quadtree order.
// probably just need to make them go around other buildings instead of having
// two passes through buildings.
ID::Building(id) => buildings.push(Box::new(draw_map.get_b(id))),
ID::ExtraShape(id) => extra_shapes.push(Box::new(draw_map.get_es(id))),
ID::BusStop(_) | ID::Turn(_) | ID::Car(_) | ID::Pedestrian(_) | ID::Trip(_) => {
panic!("{:?} shouldn't be in the quadtree", id)
}
}
}
// From background to foreground Z-order
let mut borrows: Vec<Box<&Renderable>> = Vec::new();
borrows.extend(areas);
borrows.extend(lanes);
borrows.extend(roads);
borrows.extend(intersections);
borrows.extend(buildings);
borrows.extend(extra_shapes);
borrows.extend(bus_stops);
borrows.extend(turn_icons);
// Expand all of the Traversables into agents, populating the cache if needed.
{
let source = state.get_draw_agents();
2019-02-26 23:50:43 +03:00
let time = source.time();
for on in &agents_on {
2019-02-26 23:50:43 +03:00
if !agents.has(time, *on) {
let mut list: Vec<Box<Renderable>> = Vec::new();
for c in source.get_draw_cars(*on, map).into_iter() {
2019-02-03 00:59:22 +03:00
list.push(draw_vehicle(c, map, prerender, &state.cs));
}
for p in source.get_draw_peds(*on, map).into_iter() {
2019-02-02 23:54:16 +03:00
list.push(Box::new(DrawPedestrian::new(p, map, prerender, &state.cs)));
}
2019-02-26 23:50:43 +03:00
agents.put(time, *on, list);
}
}
}
for on in agents_on {
for obj in agents.get(on) {
borrows.push(obj);
}
}
// This is a stable sort.
borrows.sort_by_key(|r| r.get_zorder());
borrows
}
}
2018-06-22 21:01:44 +03:00
#[derive(Serialize, Deserialize, Debug)]
pub struct EditorState {
pub map_name: String,
2018-06-22 21:01:44 +03:00
pub cam_x: f64,
pub cam_y: f64,
pub cam_zoom: f64,
}
fn fill_to_boundary_polygon(poly: Polygon) -> Polygon {
// TODO This looks awful for lanes, oops.
//PolyLine::make_polygons_for_boundary(poly.points().clone(), Distance::meters(0.5))
poly
}