Overhaul traffic signal editor. Movements can be directly clicked now;

the turn icons (now circles) are just for currently banned turns. #331
[rebuild]

Still a little work left (tuning arrow styles and using special icons
for crosswalks), but this mostly seems to match Yuwen's design.
This commit is contained in:
Dustin Carlino 2020-09-17 17:21:36 -07:00
parent 60cb96bc91
commit 69622bb86d
4 changed files with 178 additions and 118 deletions

View File

@ -7,7 +7,7 @@ use crate::common::CommonState;
use crate::edit::{apply_map_edits, ConfirmDiscard}; use crate::edit::{apply_map_edits, ConfirmDiscard};
use crate::game::{DrawBaselayer, PopupMsg, State, Transition}; use crate::game::{DrawBaselayer, PopupMsg, State, Transition};
use crate::options::TrafficSignalStyle; use crate::options::TrafficSignalStyle;
use crate::render::{draw_signal_stage, DrawMovement, DrawOptions}; use crate::render::{draw_signal_stage, draw_stage_number, DrawMovement, DrawOptions};
use crate::sandbox::GameplayMode; use crate::sandbox::GameplayMode;
use abstutil::Timer; use abstutil::Timer;
use geom::{Distance, Duration, Line, Polygon, Pt2D}; use geom::{Distance, Duration, Line, Polygon, Pt2D};
@ -77,11 +77,6 @@ impl TrafficSignalEditor {
) )
}; };
let mut movements = Vec::new();
for i in &members {
movements.extend(DrawMovement::for_i(*i, &app.primary.map));
}
let original = BundleEdits::get_current(app, &members); let original = BundleEdits::get_current(app, &members);
let synced = BundleEdits::synchronize(app, &members); let synced = BundleEdits::synchronize(app, &members);
let warn_changed = original != synced; let warn_changed = original != synced;
@ -93,7 +88,7 @@ impl TrafficSignalEditor {
mode, mode,
members, members,
current_stage: 0, current_stage: 0,
movements, movements: Vec::new(),
movement_selected: None, movement_selected: None,
draw_current: ctx.upload(GeomBatch::new()), draw_current: ctx.upload(GeomBatch::new()),
command_stack: Vec::new(), command_stack: Vec::new(),
@ -102,7 +97,7 @@ impl TrafficSignalEditor {
original, original,
fade_irrelevant: GeomBatch::from(vec![(app.cs.fade_map_dark, fade_area)]).upload(ctx), fade_irrelevant: GeomBatch::from(vec![(app.cs.fade_map_dark, fade_area)]).upload(ctx),
}; };
editor.draw_current = editor.recalc_draw_current(ctx, app); editor.recalc_draw_current(ctx, app);
Box::new(editor) Box::new(editor)
} }
@ -122,7 +117,7 @@ impl TrafficSignalEditor {
.scroll_to_member(ctx, format!("stage {}", idx + 1)); .scroll_to_member(ctx, format!("stage {}", idx + 1));
} }
self.draw_current = self.recalc_draw_current(ctx, app); self.recalc_draw_current(ctx, app);
} }
fn add_new_edit<F: Fn(&mut ControlTrafficSignal)>( fn add_new_edit<F: Fn(&mut ControlTrafficSignal)>(
@ -144,55 +139,45 @@ impl TrafficSignalEditor {
self.change_stage(ctx, app, idx); self.change_stage(ctx, app, idx);
} }
fn recalc_draw_current(&self, ctx: &mut EventCtx, app: &App) -> Drawable { fn recalc_draw_current(&mut self, ctx: &mut EventCtx, app: &App) {
let mut batch = GeomBatch::new(); let mut batch = GeomBatch::new();
let mut movements = Vec::new();
for i in &self.members { for i in &self.members {
let signal = app.primary.map.get_traffic_signal(*i); let stage = &app.primary.map.get_traffic_signal(*i).stages[self.current_stage];
let mut stage = signal.stages[self.current_stage].clone(); for (m, draw) in DrawMovement::for_i(&app.primary.map, &app.cs, *i, self.current_stage)
if let Some((id, _)) = self.movement_selected { {
if id.parent == signal.id { if self
stage.edit_movement(&signal.movements[&id], TurnPriority::Banned); .movement_selected
.map(|(x, _)| x != m.id)
.unwrap_or(true)
{
batch.append(draw);
} else if !stage.protected_movements.contains(&m.id)
&& !stage.yield_movements.contains(&m.id)
{
// Still draw the icon, but highlight it
batch.append(draw.color(RewriteColor::Change(
Color::hex("#7C7C7C"),
Color::hex("#72CE36"),
)));
}
movements.push(m);
}
draw_stage_number(app, ctx.prerender, *i, self.current_stage, &mut batch);
}
// Draw the selected thing on top of everything else
if let Some((selected, next_priority)) = self.movement_selected {
for m in &movements {
if m.id == selected {
m.draw_selected_movement(app, &mut batch, next_priority);
break;
} }
} }
draw_signal_stage(
ctx.prerender,
&stage,
self.current_stage,
signal.id,
None,
&mut batch,
app,
app.opts.traffic_signal_style.clone(),
);
} }
for m in &self.movements { self.draw_current = ctx.upload(batch);
let signal = app.primary.map.get_traffic_signal(m.id.parent); self.movements = movements;
if self
.movement_selected
.as_ref()
.map(|(id, _)| *id == m.id)
.unwrap_or(false)
{
m.draw_selected_movement(
app,
&mut batch,
&signal.movements[&m.id],
self.movement_selected.unwrap().1,
);
} else {
batch.push(app.cs.signal_turn_block_bg, m.block.clone());
let stage = &signal.stages[self.current_stage];
let arrow_color = match stage.get_priority_of_movement(m.id) {
TurnPriority::Protected => app.cs.signal_protected_turn,
TurnPriority::Yield => app.cs.signal_permitted_turn,
TurnPriority::Banned => app.cs.signal_banned_turn,
};
batch.push(arrow_color, m.arrow.clone());
}
}
ctx.upload(batch)
} }
} }
@ -410,7 +395,7 @@ impl State for TrafficSignalEditor {
if let Some(pt) = ctx.canvas.get_cursor_in_map_space() { if let Some(pt) = ctx.canvas.get_cursor_in_map_space() {
for m in &self.movements { for m in &self.movements {
let signal = app.primary.map.get_traffic_signal(m.id.parent); let signal = app.primary.map.get_traffic_signal(m.id.parent);
if m.block.contains_pt(pt) { if m.hitbox.contains_pt(pt) {
let stage = &signal.stages[self.current_stage]; let stage = &signal.stages[self.current_stage];
let next_priority = match stage.get_priority_of_movement(m.id) { let next_priority = match stage.get_priority_of_movement(m.id) {
TurnPriority::Banned => { TurnPriority::Banned => {
@ -438,7 +423,6 @@ impl State for TrafficSignalEditor {
} }
if self.movement_selected != old { if self.movement_selected != old {
self.draw_current = self.recalc_draw_current(ctx, app);
self.change_stage(ctx, app, self.current_stage); self.change_stage(ctx, app, self.current_stage);
} }
} }

View File

@ -22,7 +22,7 @@ use crate::render::car::DrawCar;
pub use crate::render::intersection::{calculate_corners, DrawIntersection}; pub use crate::render::intersection::{calculate_corners, DrawIntersection};
pub use crate::render::map::{AgentCache, DrawMap, UnzoomedAgents}; pub use crate::render::map::{AgentCache, DrawMap, UnzoomedAgents};
pub use crate::render::pedestrian::{DrawPedCrowd, DrawPedestrian}; pub use crate::render::pedestrian::{DrawPedCrowd, DrawPedestrian};
pub use crate::render::traffic_signal::draw_signal_stage; pub use crate::render::traffic_signal::{draw_signal_stage, draw_stage_number};
pub use crate::render::turn::{DrawMovement, DrawUberTurnGroup}; pub use crate::render::turn::{DrawMovement, DrawUberTurnGroup};
use geom::{Distance, Polygon, Pt2D}; use geom::{Distance, Polygon, Pt2D};
use map_model::{IntersectionID, Map}; use map_model::{IntersectionID, Map};

View File

@ -119,19 +119,7 @@ pub fn draw_signal_stage(
), ),
); );
} }
draw_stage_number(app, prerender, i, idx, batch);
let radius = Distance::meters(1.0);
let center = app.primary.map.get_i(i).polygon.polylabel();
batch.push(
Color::hex("#5B5B5B"),
Circle::new(center, radius).to_polygon(),
);
batch.append(
Text::from(Line(format!("{}", idx + 1)))
.render_to_batch(prerender)
.scale(0.075)
.centered_on(center),
);
} }
TrafficSignalStyle::Yuwen => { TrafficSignalStyle::Yuwen => {
for m in &stage.yield_movements { for m in &stage.yield_movements {
@ -200,6 +188,27 @@ pub fn draw_signal_stage(
} }
} }
pub fn draw_stage_number(
app: &App,
prerender: &Prerender,
i: IntersectionID,
idx: usize,
batch: &mut GeomBatch,
) {
let radius = Distance::meters(1.0);
let center = app.primary.map.get_i(i).polygon.polylabel();
batch.push(
Color::hex("#5B5B5B"),
Circle::new(center, radius).to_polygon(),
);
batch.append(
Text::from(Line(format!("{}", idx + 1)))
.render_to_batch(prerender)
.scale(0.075)
.centered_on(center),
);
}
fn draw_time_left( fn draw_time_left(
app: &App, app: &App,
prerender: &Prerender, prerender: &Prerender,

View File

@ -1,9 +1,10 @@
use crate::app::App; use crate::app::App;
use crate::colors::ColorScheme;
use crate::render::BIG_ARROW_THICKNESS; use crate::render::BIG_ARROW_THICKNESS;
use geom::{Angle, ArrowCap, Distance, PolyLine, Polygon}; use geom::{Angle, ArrowCap, Circle, Distance, PolyLine, Polygon};
use map_model::{ use map_model::{
IntersectionCluster, IntersectionID, LaneID, Map, Movement, MovementID, TurnPriority, IntersectionCluster, IntersectionID, LaneID, Map, MovementID, TurnPriority, UberTurnGroup,
UberTurnGroup, SIDEWALK_THICKNESS,
}; };
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use widgetry::{Color, GeomBatch}; use widgetry::{Color, GeomBatch};
@ -12,66 +13,117 @@ const TURN_ICON_ARROW_LENGTH: Distance = Distance::const_meters(1.5);
pub struct DrawMovement { pub struct DrawMovement {
pub id: MovementID, pub id: MovementID,
pub block: Polygon, pub hitbox: Polygon,
pub arrow: Polygon,
} }
impl DrawMovement { impl DrawMovement {
pub fn for_i(i: IntersectionID, map: &Map) -> Vec<DrawMovement> { // Also returns the stuff to draw each movement
pub fn for_i(
map: &Map,
cs: &ColorScheme,
i: IntersectionID,
idx: usize,
) -> Vec<(DrawMovement, GeomBatch)> {
let signal = map.get_traffic_signal(i);
let stage = &signal.stages[idx];
// TODO Sort by angle here if we want some consistency // TODO Sort by angle here if we want some consistency
let mut offset_per_lane: HashMap<LaneID, usize> = HashMap::new(); let mut offset_per_lane: HashMap<LaneID, usize> = HashMap::new();
let mut draw = Vec::new(); let mut results = Vec::new();
for movement in map.get_traffic_signal(i).movements.values() { for movement in signal.movements.values() {
let offset = movement let mut batch = GeomBatch::new();
.members // TODO Refactor the slice_start/slice_end stuff from draw_signal_stage.
.iter() let hitbox = if stage.protected_movements.contains(&movement.id) {
.map(|t| *offset_per_lane.entry(t.src).or_insert(0)) let arrow = movement
.max() .geom
.unwrap(); .make_arrow(BIG_ARROW_THICKNESS, ArrowCap::Triangle);
let (pl, width) = movement.src_center_and_width(map); batch.push(cs.signal_protected_turn, arrow.clone());
let (block, arrow) = make_geom(offset as f64, pl, width, movement.angle); arrow
let mut seen_lanes = HashSet::new(); } else if stage.yield_movements.contains(&movement.id) {
for t in &movement.members { let pl = &movement.geom;
if !seen_lanes.contains(&t.src) { batch.extend(
*offset_per_lane.get_mut(&t.src).unwrap() = offset + 1; Color::BLACK,
seen_lanes.insert(t.src); pl.exact_slice(
SIDEWALK_THICKNESS - Distance::meters(0.1),
pl.length() - SIDEWALK_THICKNESS + Distance::meters(0.1),
)
.dashed_arrow(
BIG_ARROW_THICKNESS,
Distance::meters(1.2),
Distance::meters(0.3),
ArrowCap::Triangle,
),
);
let arrow = pl
.exact_slice(SIDEWALK_THICKNESS, pl.length() - SIDEWALK_THICKNESS)
.dashed_arrow(
BIG_ARROW_THICKNESS / 2.0,
Distance::meters(1.0),
Distance::meters(0.5),
ArrowCap::Triangle,
);
batch.extend(cs.signal_protected_turn, arrow.clone());
// Bit weird, but don't use the dashed arrow as the hitbox. The gaps in between
// should still be clickable.
movement
.geom
.make_arrow(BIG_ARROW_THICKNESS, ArrowCap::Triangle)
} else {
// Use circular icons for banned turns
let offset = movement
.members
.iter()
.map(|t| *offset_per_lane.entry(t.src).or_insert(0))
.max()
.unwrap();
let (pl, _) = movement.src_center_and_width(map);
let (circle, arrow) = make_circle_geom(offset as f64, pl, movement.angle);
let mut seen_lanes = HashSet::new();
for t in &movement.members {
if !seen_lanes.contains(&t.src) {
*offset_per_lane.get_mut(&t.src).unwrap() = offset + 1;
seen_lanes.insert(t.src);
}
} }
} batch.push(Color::hex("#7C7C7C"), circle.clone());
batch.push(Color::WHITE, arrow);
draw.push(DrawMovement { circle
id: movement.id, };
block, results.push((
arrow, DrawMovement {
}); id: movement.id,
hitbox,
},
batch,
));
} }
draw results
} }
pub fn draw_selected_movement( pub fn draw_selected_movement(
&self, &self,
app: &App, app: &App,
batch: &mut GeomBatch, batch: &mut GeomBatch,
m: &Movement,
next_priority: Option<TurnPriority>, next_priority: Option<TurnPriority>,
) { ) {
// TODO Refactor this mess. Maybe after things like "dashed with outline" can be expressed let movement = &app.primary.map.get_traffic_signal(self.id.parent).movements[&self.id];
// more composably like SVG, using lyon. let pl = &movement.geom;
let block_color = match next_priority {
match next_priority {
Some(TurnPriority::Protected) => { Some(TurnPriority::Protected) => {
let green = Color::hex("#72CE36"); let green = Color::hex("#72CE36");
let arrow = m.geom.make_arrow(BIG_ARROW_THICKNESS, ArrowCap::Triangle); let arrow = pl.make_arrow(BIG_ARROW_THICKNESS, ArrowCap::Triangle);
batch.push(green.alpha(0.5), arrow.clone()); batch.push(green.alpha(0.5), arrow.clone());
if let Ok(p) = arrow.to_outline(Distance::meters(0.1)) { if let Ok(p) = arrow.to_outline(Distance::meters(0.1)) {
batch.push(green, p); batch.push(green, p);
} }
green
} }
Some(TurnPriority::Yield) => { Some(TurnPriority::Yield) => {
batch.extend( batch.extend(
// TODO Ideally the inner part would be the lower opacity blue, but can't yet // TODO Ideally the inner part would be the lower opacity blue, but can't yet
// express that it should cover up the thicker solid blue beneath it // express that it should cover up the thicker solid blue beneath it
Color::BLACK.alpha(0.8), Color::BLACK.alpha(0.8),
m.geom.dashed_arrow( pl.dashed_arrow(
BIG_ARROW_THICKNESS, BIG_ARROW_THICKNESS,
Distance::meters(1.2), Distance::meters(1.2),
Distance::meters(0.3), Distance::meters(0.3),
@ -80,11 +132,7 @@ impl DrawMovement {
); );
batch.extend( batch.extend(
app.cs.signal_permitted_turn.alpha(0.8), app.cs.signal_permitted_turn.alpha(0.8),
m.geom pl.exact_slice(Distance::meters(0.1), pl.length() - Distance::meters(0.1))
.exact_slice(
Distance::meters(0.1),
m.geom.length() - Distance::meters(0.1),
)
.dashed_arrow( .dashed_arrow(
BIG_ARROW_THICKNESS / 2.0, BIG_ARROW_THICKNESS / 2.0,
Distance::meters(1.0), Distance::meters(1.0),
@ -92,21 +140,17 @@ impl DrawMovement {
ArrowCap::Triangle, ArrowCap::Triangle,
), ),
); );
app.cs.signal_permitted_turn
} }
Some(TurnPriority::Banned) => { Some(TurnPriority::Banned) => {
let red = Color::hex("#EB3223"); let red = Color::hex("#EB3223");
let arrow = m.geom.make_arrow(BIG_ARROW_THICKNESS, ArrowCap::Triangle); let arrow = pl.make_arrow(BIG_ARROW_THICKNESS, ArrowCap::Triangle);
batch.push(red.alpha(0.5), arrow.clone()); batch.push(red.alpha(0.5), arrow.clone());
if let Ok(p) = arrow.to_outline(Distance::meters(0.1)) { if let Ok(p) = arrow.to_outline(Distance::meters(0.1)) {
batch.push(red, p); batch.push(red, p);
} }
red
} }
None => app.cs.signal_turn_block_bg, None => {}
}; }
batch.push(block_color, self.block.clone());
batch.push(Color::WHITE, self.arrow.clone());
} }
} }
@ -128,7 +172,7 @@ impl DrawUberTurnGroup {
.max() .max()
.unwrap(); .unwrap();
let (pl, width) = group.src_center_and_width(map); let (pl, width) = group.src_center_and_width(map);
let (block, arrow) = make_geom(offset as f64, pl, width, group.angle()); let (block, arrow) = make_block_geom(offset as f64, pl, width, group.angle());
let mut seen_lanes = HashSet::new(); let mut seen_lanes = HashSet::new();
for ut in &group.members { for ut in &group.members {
if !seen_lanes.contains(&ut.entry()) { if !seen_lanes.contains(&ut.entry()) {
@ -148,7 +192,7 @@ impl DrawUberTurnGroup {
} }
// Produces (block, arrow) // Produces (block, arrow)
fn make_geom(offset: f64, pl: PolyLine, width: Distance, angle: Angle) -> (Polygon, Polygon) { fn make_block_geom(offset: f64, pl: PolyLine, width: Distance, angle: Angle) -> (Polygon, Polygon) {
let height = TURN_ICON_ARROW_LENGTH; let height = TURN_ICON_ARROW_LENGTH;
// Always extend the pl first to handle short entry lanes // Always extend the pl first to handle short entry lanes
let extension = PolyLine::must_new(vec![ let extension = PolyLine::must_new(vec![
@ -171,3 +215,26 @@ fn make_geom(offset: f64, pl: PolyLine, width: Distance, angle: Angle) -> (Polyg
(block, arrow) (block, arrow)
} }
// Produces (circle, arrow)
fn make_circle_geom(offset: f64, pl: PolyLine, angle: Angle) -> (Polygon, Polygon) {
let height = 2.0 * TURN_ICON_ARROW_LENGTH;
// Always extend the pl first to handle short entry lanes
let extension = PolyLine::must_new(vec![
pl.last_pt(),
pl.last_pt()
.project_away(Distance::meters(500.0), pl.last_line().angle()),
]);
let pl = pl.must_extend(extension);
let slice = pl.exact_slice(offset * height, (offset + 1.0) * height);
let center = slice.middle();
let block = Circle::new(center, TURN_ICON_ARROW_LENGTH).to_polygon();
let arrow = PolyLine::must_new(vec![
center.project_away(TURN_ICON_ARROW_LENGTH / 2.0, angle.opposite()),
center.project_away(TURN_ICON_ARROW_LENGTH / 2.0, angle),
])
.make_arrow(Distance::meters(0.5), ArrowCap::Triangle);
(block, arrow)
}