working on a new traffic signal scroller. disabled. decent start.

This commit is contained in:
Dustin Carlino 2019-10-13 18:44:19 -07:00
parent f82000cff4
commit 430b1245c4
11 changed files with 183 additions and 75 deletions

View File

@ -19,18 +19,16 @@
## General ezgui stuff
- optionally limit canvas scrolling/zooming to some map bounds
- when dragging, dont give mouse movement to UI elements
- start context menu when left click releases and we're not dragging
- can we change labels in modal or top menu? show/hide
- distinguish hints from status of modal menus, for hiding purposes
- move context menus out of ezgui
- simplify/remove UserInput.
- maybe separate impls for context, wizard, modal menu make sense.
- arbitrary viewports?!
- tiling wm
## New features
- swap direction of one-way
- convert between one- and two-way if there's enough space
- collapse smaller roads/neighborhoods and just show aggregate stats about them (in/out flow, moving/blocked within)
- undo support for edits

View File

@ -344,14 +344,22 @@ impl GeomBatch {
self.list.extend(other.list.clone());
}
pub fn consume(self) -> Vec<(Color, Polygon)> {
self.list
}
pub fn draw(self, g: &mut GfxCtx) {
let refs = self.list.iter().map(|(color, p)| (*color, p)).collect();
let obj = g.prerender.upload_temporary(refs);
g.redraw(&obj);
}
pub(crate) fn members(&self) -> &Vec<(Color, Polygon)> {
&self.list
pub(crate) fn get_dims(&self) -> ScreenDims {
let mut bounds = Bounds::new();
for (_, poly) in &self.list {
bounds.union(poly.get_bounds());
}
ScreenDims::new(bounds.max_x - bounds.min_x, bounds.max_y - bounds.min_y)
}
}
@ -469,7 +477,7 @@ impl<'a> Prerender<'a> {
}
pub struct MultiText {
list: Vec<(Text, ScreenPt)>,
pub(crate) list: Vec<(Text, ScreenPt)>,
}
impl MultiText {

View File

@ -21,8 +21,8 @@ pub use crate::runner::{run, EventLoopMode, Settings, GUI};
pub use crate::screen_geom::{ScreenDims, ScreenPt, ScreenRectangle};
pub use crate::text::{Line, Text, TextSpan, HOTKEY_COLOR};
pub use crate::widgets::{
Autocomplete, Choice, ItemSlider, ModalMenu, Scroller, Slider, SliderWithTextBox, Warper,
WarpingItemSlider, Wizard, WrappedWizard,
Autocomplete, Choice, ItemSlider, ModalMenu, NewScroller, Scroller, Slider, SliderWithTextBox,
Warper, WarpingItemSlider, Wizard, WrappedWizard,
};
pub enum InputResult<T: Clone> {

View File

@ -1,6 +1,6 @@
use crate::layout::Widget;
use crate::{Color, Drawable, EventCtx, GeomBatch, GfxCtx, ScreenDims, ScreenPt, ScreenRectangle};
use geom::{Bounds, Circle, Distance, Polygon, Pt2D};
use geom::{Circle, Distance, Polygon, Pt2D};
// TODO Tooltips?
// TODO Hotkeys?
@ -17,8 +17,8 @@ pub struct Button {
impl Button {
// Top-left should be at Pt2D::new(0.0, 0.0). Must have same dimensions.
pub fn new(normal: GeomBatch, hovered: GeomBatch, ctx: &EventCtx) -> Button {
let dims = geom_to_dims(&normal);
assert_eq!(dims, geom_to_dims(&hovered));
let dims = normal.get_dims();
assert_eq!(dims, hovered.get_dims());
Button {
draw_normal: ctx.prerender.upload(normal),
draw_hovered: ctx.prerender.upload(hovered),
@ -80,16 +80,6 @@ impl Widget for Button {
}
}
fn geom_to_dims(batch: &GeomBatch) -> ScreenDims {
let mut bounds = Bounds::new();
for (_, poly) in batch.members() {
bounds.union(poly.get_bounds());
}
assert!(bounds.min_x >= 0.0);
assert!(bounds.min_y >= 0.0);
ScreenDims::new(bounds.max_x, bounds.max_y)
}
const ICON_BACKGROUND: Color = Color::grey(0.5);
const ICON_BACKGROUND_SELECTED: Color = Color::YELLOW;
const ICON_SYMBOL: Color = Color::grey(0.8);

View File

@ -17,7 +17,7 @@ pub(crate) use self::context_menu::ContextMenu;
pub use self::modal_menu::ModalMenu;
pub(crate) use self::popup_menu::PopupMenu;
pub(crate) use self::screenshot::{screenshot_current, screenshot_everything};
pub use self::scroller::Scroller;
pub use self::scroller::{NewScroller, Scroller};
pub use self::slider::{ItemSlider, Slider, SliderWithTextBox, WarpingItemSlider};
pub use self::warper::Warper;
pub use self::wizard::{Choice, Wizard, WrappedWizard};

View File

@ -1,4 +1,7 @@
use crate::{Canvas, Color, EventCtx, GfxCtx, Line, ScreenDims, ScreenPt, ScreenRectangle, Text};
use crate::{
text, Canvas, Color, Drawable, EventCtx, GeomBatch, GfxCtx, Line, MultiText, ScreenDims,
ScreenPt, ScreenRectangle, Text,
};
use geom::{Distance, Polygon, Pt2D};
use ordered_float::NotNan;
@ -266,3 +269,86 @@ impl<T: Clone + Copy> Scroller<T> {
self.items.len() - 2
}
}
pub struct NewScroller {
draw: Drawable,
multi_txt: MultiText,
total_dims: ScreenDims,
zoom: f64,
offset: f64,
top_left: ScreenPt,
dims: ScreenDims,
}
impl NewScroller {
// geom and multi_txt should be in screen-space, with the top_left as (0.0, 0.0).
pub fn new(geom: GeomBatch, multi_txt: MultiText, zoom: f64, ctx: &EventCtx) -> NewScroller {
let mut total_dims = geom.get_dims();
for (txt, top_left) in &multi_txt.list {
let (mut w, mut h) = ctx.canvas.text_dims(txt);
w += top_left.x;
h += top_left.y;
if w > total_dims.width {
total_dims.width = w;
}
if h > total_dims.height {
total_dims.height = h;
}
}
NewScroller {
draw: ctx.prerender.upload(geom),
multi_txt,
total_dims,
zoom,
offset: 0.0,
// TODO The layouting is hardcoded
top_left: ScreenPt::new(0.0, 0.0),
dims: ScreenDims::new(100.0, 100.0),
}
}
pub fn event(&mut self, ctx: &mut EventCtx) {
let rect = ScreenRectangle::top_left(self.top_left, self.dims);
if rect.contains(ctx.canvas.get_cursor_in_screen_space()) {
if let Some(scroll) = ctx.input.get_mouse_scroll() {
self.offset -= scroll;
// TODO Clamp... or maybe last minute, based on dims, which'll get updated by
// window resizing and such
}
}
}
pub fn draw(&self, g: &mut GfxCtx) {
let rect = ScreenRectangle::top_left(self.top_left, self.dims);
g.canvas.mark_covered_area(rect);
g.fork_screenspace();
g.draw_polygon(
text::BG_COLOR,
&Polygon::rectangle_topleft(
Pt2D::new(0.0, 0.0),
Distance::meters(self.dims.width),
Distance::meters(self.dims.height),
),
);
g.unfork();
g.fork(Pt2D::new(0.0, self.offset), self.top_left, self.zoom);
g.redraw(&self.draw);
g.unfork();
for (txt, pt) in &self.multi_txt.list {
g.draw_text_at_screenspace_topleft(
txt,
ScreenPt::new(pt.x, pt.y - self.offset * self.zoom),
);
}
// TODO draw scrollbar
}
}

View File

@ -1,6 +1,6 @@
use crate::game::{State, Transition};
use crate::helpers::ID;
use crate::render::{DrawCtx, DrawOptions, DrawTurn, TrafficSignalDiagram};
use crate::render::{DrawOptions, DrawTurn, TrafficSignalDiagram};
use crate::ui::{ShowEverything, UI};
use ezgui::{hotkey, Color, EventCtx, GeomBatch, GfxCtx, Key, ModalMenu};
use map_model::{IntersectionID, LaneID, Map, TurnType};
@ -48,14 +48,14 @@ impl TurnCyclerState {
"Traffic Signal Diagram",
vec![
vec![
(hotkey(Key::UpArrow), "select previous cycle"),
(hotkey(Key::DownArrow), "select next cycle"),
(hotkey(Key::UpArrow), "select previous phase"),
(hotkey(Key::DownArrow), "select next phase"),
],
vec![(hotkey(Key::Escape), "quit")],
],
ctx,
),
diagram: TrafficSignalDiagram::new(i, idx, &ui.primary.map, ctx),
diagram: TrafficSignalDiagram::new(i, idx, ui, ctx),
})));
}
}
@ -144,13 +144,7 @@ impl State for ShowTrafficSignal {
&ui.primary.sim,
&ShowEverything::new(),
);
let ctx = DrawCtx {
cs: &ui.cs,
map: &ui.primary.map,
draw_map: &ui.primary.draw_map,
sim: &ui.primary.sim,
};
self.diagram.draw(g, &ctx);
self.diagram.draw(g, &ui.draw_ctx());
self.menu.draw(g);
}

View File

@ -6,7 +6,7 @@ use crate::debug::DebugMode;
use crate::game::{State, Transition, WizardState};
use crate::helpers::{ColorScheme, ID};
use crate::render::{
DrawCtx, DrawIntersection, DrawLane, DrawMap, DrawOptions, DrawRoad, DrawTurn, Renderable,
DrawIntersection, DrawLane, DrawMap, DrawOptions, DrawRoad, DrawTurn, Renderable,
MIN_ZOOM_FOR_DETAIL,
};
use crate::sandbox::SandboxMode;
@ -227,12 +227,7 @@ impl State for EditMode {
// just show diff relative to basemap.
let edits = ui.primary.map.get_edits();
let ctx = DrawCtx {
cs: &ui.cs,
map: &ui.primary.map,
draw_map: &ui.primary.draw_map,
sim: &ui.primary.sim,
};
let ctx = ui.draw_ctx();
let mut opts = DrawOptions::new();
// TODO Similar to drawing areas with traffic or not -- would be convenient to just

View File

@ -2,7 +2,7 @@ use crate::common::CommonState;
use crate::edit::apply_map_edits;
use crate::game::{State, Transition, WizardState};
use crate::helpers::ID;
use crate::render::{draw_signal_phase, DrawCtx, DrawOptions, DrawTurn, TrafficSignalDiagram};
use crate::render::{draw_signal_phase, DrawOptions, DrawTurn, TrafficSignalDiagram};
use crate::ui::{ShowEverything, UI};
use abstutil::Timer;
use ezgui::{hotkey, Choice, Color, EventCtx, GeomBatch, GfxCtx, Key, ModalMenu};
@ -49,7 +49,7 @@ impl TrafficSignalEditor {
TrafficSignalEditor {
menu,
icon_selected: None,
diagram: TrafficSignalDiagram::new(id, 0, &ui.primary.map, ctx),
diagram: TrafficSignalDiagram::new(id, 0, ui, ctx),
}
}
}
@ -132,7 +132,7 @@ impl State for TrafficSignalEditor {
.remove(0)
.1;
change_traffic_signal(signal, self.diagram.i, ui, ctx);
self.diagram = TrafficSignalDiagram::new(self.diagram.i, 0, &ui.primary.map, ctx);
self.diagram = TrafficSignalDiagram::new(self.diagram.i, 0, ui, ctx);
return Transition::Keep;
}
@ -148,15 +148,13 @@ impl State for TrafficSignalEditor {
if current_phase != 0 && self.menu.action("move current phase up") {
signal.phases.swap(current_phase, current_phase - 1);
change_traffic_signal(signal, self.diagram.i, ui, ctx);
self.diagram =
TrafficSignalDiagram::new(self.diagram.i, current_phase - 1, &ui.primary.map, ctx);
self.diagram = TrafficSignalDiagram::new(self.diagram.i, current_phase - 1, ui, ctx);
} else if current_phase != signal.phases.len() - 1
&& self.menu.action("move current phase down")
{
signal.phases.swap(current_phase, current_phase + 1);
change_traffic_signal(signal, self.diagram.i, ui, ctx);
self.diagram =
TrafficSignalDiagram::new(self.diagram.i, current_phase + 1, &ui.primary.map, ctx);
self.diagram = TrafficSignalDiagram::new(self.diagram.i, current_phase + 1, ui, ctx);
} else if signal.phases.len() > 1 && self.menu.action("delete current phase") {
signal.phases.remove(current_phase);
let num_phases = signal.phases.len();
@ -168,7 +166,7 @@ impl State for TrafficSignalEditor {
} else {
current_phase
},
&ui.primary.map,
ui,
ctx,
);
} else if self.menu.action("add a new empty phase") {
@ -176,8 +174,7 @@ impl State for TrafficSignalEditor {
.phases
.insert(current_phase, Phase::new(self.diagram.i));
change_traffic_signal(signal, self.diagram.i, ui, ctx);
self.diagram =
TrafficSignalDiagram::new(self.diagram.i, current_phase, &ui.primary.map, ctx);
self.diagram = TrafficSignalDiagram::new(self.diagram.i, current_phase, ui, ctx);
} else if has_sidewalks && self.menu.action("add a new pedestrian scramble phase") {
let mut phase = Phase::new(self.diagram.i);
for t in ui.primary.map.get_turns_in_intersection(self.diagram.i) {
@ -187,8 +184,7 @@ impl State for TrafficSignalEditor {
}
signal.phases.insert(current_phase, phase);
change_traffic_signal(signal, self.diagram.i, ui, ctx);
self.diagram =
TrafficSignalDiagram::new(self.diagram.i, current_phase, &ui.primary.map, ctx);
self.diagram = TrafficSignalDiagram::new(self.diagram.i, current_phase, ui, ctx);
} else if has_sidewalks
&& self
.menu
@ -196,7 +192,7 @@ impl State for TrafficSignalEditor {
{
signal.convert_to_ped_scramble(&ui.primary.map);
change_traffic_signal(signal, self.diagram.i, ui, ctx);
self.diagram = TrafficSignalDiagram::new(self.diagram.i, 0, &ui.primary.map, ctx);
self.diagram = TrafficSignalDiagram::new(self.diagram.i, 0, ui, ctx);
}
Transition::Keep
@ -210,12 +206,7 @@ impl State for TrafficSignalEditor {
}
let mut batch = GeomBatch::new();
let ctx = DrawCtx {
cs: &ui.cs,
map: &ui.primary.map,
draw_map: &ui.primary.draw_map,
sim: &ui.primary.sim,
};
let ctx = ui.draw_ctx();
let map = &ui.primary.map;
let phase = &map.get_traffic_signal(self.diagram.i).phases[self.diagram.current_phase()];
for t in &ui.primary.draw_map.get_turns(self.diagram.i, map) {
@ -285,7 +276,7 @@ fn make_change_phase_duration(current_duration: Duration) -> Box<dyn State> {
let idx = editor.diagram.current_phase();
signal.phases[idx].duration = Duration::seconds(new_duration as f64);
change_traffic_signal(signal, editor.diagram.i, ui, ctx);
editor.diagram = TrafficSignalDiagram::new(editor.diagram.i, idx, &ui.primary.map, ctx);
editor.diagram = TrafficSignalDiagram::new(editor.diagram.i, idx, ui, ctx);
})))
}))
}
@ -303,7 +294,7 @@ fn make_change_preset(i: IntersectionID) -> Box<dyn State> {
Some(Transition::PopWithData(Box::new(move |state, ui, ctx| {
let mut editor = state.downcast_mut::<TrafficSignalEditor>().unwrap();
change_traffic_signal(new_signal, editor.diagram.i, ui, ctx);
editor.diagram = TrafficSignalDiagram::new(editor.diagram.i, 0, &ui.primary.map, ctx);
editor.diagram = TrafficSignalDiagram::new(editor.diagram.i, 0, ui, ctx);
})))
}))
}

View File

@ -1,9 +1,11 @@
use crate::render::{DrawCtx, DrawTurn};
use crate::ui::UI;
use ezgui::{
Color, EventCtx, GeomBatch, GfxCtx, Line, ModalMenu, ScreenDims, ScreenPt, Scroller, Text,
Color, EventCtx, GeomBatch, GfxCtx, Line, ModalMenu, MultiText, NewScroller, ScreenDims,
ScreenPt, Scroller, Text,
};
use geom::{Circle, Distance, Duration, Line, Polygon, Pt2D};
use map_model::{IntersectionID, Map, Phase, TurnPriority, TurnType, LANE_THICKNESS};
use map_model::{IntersectionID, Phase, TurnPriority, TurnType, LANE_THICKNESS};
use ordered_float::NotNan;
// Only draws a box when time_left is present
@ -221,7 +223,7 @@ fn draw_signal_phase_with_icons(phase: &Phase, batch: &mut GeomBatch, ctx: &Draw
}
const PADDING: f64 = 5.0;
const ZOOM: f64 = 10.0;
const ZOOM: f64 = 15.0;
pub struct TrafficSignalDiagram {
pub i: IntersectionID,
@ -230,17 +232,19 @@ pub struct TrafficSignalDiagram {
intersection_width: f64, // TODO needed?
// The usizes are phase indices
scroller: Scroller<usize>,
new_scroller: NewScroller,
}
impl TrafficSignalDiagram {
pub fn new(
i: IntersectionID,
current_phase: usize,
map: &Map,
ui: &UI,
ctx: &EventCtx,
) -> TrafficSignalDiagram {
let (top_left, intersection_width, intersection_height) = {
let b = map.get_i(i).polygon.get_bounds();
let b = ui.primary.map.get_i(i).polygon.get_bounds();
(
Pt2D::new(b.min_x, b.min_y),
b.max_x - b.min_x,
@ -248,7 +252,7 @@ impl TrafficSignalDiagram {
b.max_y - b.min_y,
)
};
let phases = &map.get_traffic_signal(i).phases;
let phases = &ui.primary.map.get_traffic_signal(i).phases;
// Precalculate maximum text width.
let mut labels = Vec::new();
@ -285,6 +289,8 @@ impl TrafficSignalDiagram {
top_left,
intersection_width,
scroller,
new_scroller: make_new_scroller(i, &ui.draw_ctx(), ctx),
}
}
@ -301,6 +307,8 @@ impl TrafficSignalDiagram {
self.scroller.select_next(ctx.canvas);
return;
}
//self.new_scroller.event(ctx);
}
pub fn current_phase(&self) -> usize {
@ -324,5 +332,39 @@ impl TrafficSignalDiagram {
}
g.unfork();
//self.new_scroller.draw(g);
}
}
fn make_new_scroller(i: IntersectionID, draw_ctx: &DrawCtx, ctx: &EventCtx) -> NewScroller {
// TODO Nicer API would be passing in a list of (GeomBatch, MultiText)s each starting at the
// origin, then do the translation later.
let mut master_batch = GeomBatch::new();
let mut txt = MultiText::new();
// Slightly inaccurate -- the turn rendering may slightly exceed the intersection polygon --
// but this is close enough.
let bounds = draw_ctx.map.get_i(i).polygon.get_bounds();
let mut y_offset = 0.0;
for (idx, phase) in draw_ctx.map.get_traffic_signal(i).phases.iter().enumerate() {
let mut batch = GeomBatch::new();
draw_signal_phase(phase, None, &mut batch, draw_ctx);
for (color, poly) in batch.consume() {
master_batch.push(
color,
poly.translate(
Distance::meters(-bounds.min_x),
Distance::meters(y_offset - bounds.min_y),
),
);
}
txt.add(
Text::from(Line(format!("Phase {}: {}", idx + 1, phase.duration))),
ScreenPt::new(10.0 + (bounds.max_x - bounds.min_x) * ZOOM, y_offset * ZOOM),
);
y_offset += bounds.max_y - bounds.min_y;
}
NewScroller::new(master_batch, txt, ZOOM, ctx)
}

View File

@ -80,6 +80,15 @@ impl UI {
}
}
pub fn draw_ctx<'a>(&'a self) -> DrawCtx<'a> {
DrawCtx {
cs: &self.cs,
map: &self.primary.map,
draw_map: &self.primary.draw_map,
sim: &self.primary.sim,
}
}
pub fn draw(
&self,
g: &mut GfxCtx,
@ -87,12 +96,7 @@ impl UI {
source: &dyn GetDrawAgents,
show_objs: &dyn ShowObject,
) {
let ctx = DrawCtx {
cs: &self.cs,
map: &self.primary.map,
draw_map: &self.primary.draw_map,
sim: &self.primary.sim,
};
let ctx = self.draw_ctx();
let mut sample_intersection: Option<String> = None;
g.clear(self.cs.get_def("true background", Color::BLACK));