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::game::{DrawBaselayer, PopupMsg, State, Transition};
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 abstutil::Timer;
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 synced = BundleEdits::synchronize(app, &members);
let warn_changed = original != synced;
@ -93,7 +88,7 @@ impl TrafficSignalEditor {
mode,
members,
current_stage: 0,
movements,
movements: Vec::new(),
movement_selected: None,
draw_current: ctx.upload(GeomBatch::new()),
command_stack: Vec::new(),
@ -102,7 +97,7 @@ impl TrafficSignalEditor {
original,
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)
}
@ -122,7 +117,7 @@ impl TrafficSignalEditor {
.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)>(
@ -144,55 +139,45 @@ impl TrafficSignalEditor {
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 movements = Vec::new();
for i in &self.members {
let signal = app.primary.map.get_traffic_signal(*i);
let mut stage = signal.stages[self.current_stage].clone();
if let Some((id, _)) = self.movement_selected {
if id.parent == signal.id {
stage.edit_movement(&signal.movements[&id], TurnPriority::Banned);
}
}
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 {
let signal = app.primary.map.get_traffic_signal(m.id.parent);
let stage = &app.primary.map.get_traffic_signal(*i).stages[self.current_stage];
for (m, draw) in DrawMovement::for_i(&app.primary.map, &app.cs, *i, self.current_stage)
{
if self
.movement_selected
.as_ref()
.map(|(id, _)| *id == m.id)
.unwrap_or(false)
.map(|(x, _)| x != m.id)
.unwrap_or(true)
{
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());
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;
}
}
ctx.upload(batch)
}
self.draw_current = ctx.upload(batch);
self.movements = movements;
}
}
@ -410,7 +395,7 @@ impl State for TrafficSignalEditor {
if let Some(pt) = ctx.canvas.get_cursor_in_map_space() {
for m in &self.movements {
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 next_priority = match stage.get_priority_of_movement(m.id) {
TurnPriority::Banned => {
@ -438,7 +423,6 @@ impl State for TrafficSignalEditor {
}
if self.movement_selected != old {
self.draw_current = self.recalc_draw_current(ctx, app);
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::map::{AgentCache, DrawMap, UnzoomedAgents};
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};
use geom::{Distance, Polygon, Pt2D};
use map_model::{IntersectionID, Map};

View File

@ -119,19 +119,7 @@ pub fn draw_signal_stage(
),
);
}
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),
);
draw_stage_number(app, prerender, i, idx, batch);
}
TrafficSignalStyle::Yuwen => {
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(
app: &App,
prerender: &Prerender,

View File

@ -1,9 +1,10 @@
use crate::app::App;
use crate::colors::ColorScheme;
use crate::render::BIG_ARROW_THICKNESS;
use geom::{Angle, ArrowCap, Distance, PolyLine, Polygon};
use geom::{Angle, ArrowCap, Circle, Distance, PolyLine, Polygon};
use map_model::{
IntersectionCluster, IntersectionID, LaneID, Map, Movement, MovementID, TurnPriority,
UberTurnGroup,
IntersectionCluster, IntersectionID, LaneID, Map, MovementID, TurnPriority, UberTurnGroup,
SIDEWALK_THICKNESS,
};
use std::collections::{HashMap, HashSet};
use widgetry::{Color, GeomBatch};
@ -12,24 +13,71 @@ const TURN_ICON_ARROW_LENGTH: Distance = Distance::const_meters(1.5);
pub struct DrawMovement {
pub id: MovementID,
pub block: Polygon,
pub arrow: Polygon,
pub hitbox: Polygon,
}
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
let mut offset_per_lane: HashMap<LaneID, usize> = HashMap::new();
let mut draw = Vec::new();
for movement in map.get_traffic_signal(i).movements.values() {
let mut results = Vec::new();
for movement in signal.movements.values() {
let mut batch = GeomBatch::new();
// TODO Refactor the slice_start/slice_end stuff from draw_signal_stage.
let hitbox = if stage.protected_movements.contains(&movement.id) {
let arrow = movement
.geom
.make_arrow(BIG_ARROW_THICKNESS, ArrowCap::Triangle);
batch.push(cs.signal_protected_turn, arrow.clone());
arrow
} else if stage.yield_movements.contains(&movement.id) {
let pl = &movement.geom;
batch.extend(
Color::BLACK,
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, width) = movement.src_center_and_width(map);
let (block, arrow) = make_geom(offset as f64, pl, width, movement.angle);
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) {
@ -37,41 +85,45 @@ impl DrawMovement {
seen_lanes.insert(t.src);
}
}
draw.push(DrawMovement {
batch.push(Color::hex("#7C7C7C"), circle.clone());
batch.push(Color::WHITE, arrow);
circle
};
results.push((
DrawMovement {
id: movement.id,
block,
arrow,
});
hitbox,
},
batch,
));
}
draw
results
}
pub fn draw_selected_movement(
&self,
app: &App,
batch: &mut GeomBatch,
m: &Movement,
next_priority: Option<TurnPriority>,
) {
// TODO Refactor this mess. Maybe after things like "dashed with outline" can be expressed
// more composably like SVG, using lyon.
let block_color = match next_priority {
let movement = &app.primary.map.get_traffic_signal(self.id.parent).movements[&self.id];
let pl = &movement.geom;
match next_priority {
Some(TurnPriority::Protected) => {
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());
if let Ok(p) = arrow.to_outline(Distance::meters(0.1)) {
batch.push(green, p);
}
green
}
Some(TurnPriority::Yield) => {
batch.extend(
// 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
Color::BLACK.alpha(0.8),
m.geom.dashed_arrow(
pl.dashed_arrow(
BIG_ARROW_THICKNESS,
Distance::meters(1.2),
Distance::meters(0.3),
@ -80,11 +132,7 @@ impl DrawMovement {
);
batch.extend(
app.cs.signal_permitted_turn.alpha(0.8),
m.geom
.exact_slice(
Distance::meters(0.1),
m.geom.length() - Distance::meters(0.1),
)
pl.exact_slice(Distance::meters(0.1), pl.length() - Distance::meters(0.1))
.dashed_arrow(
BIG_ARROW_THICKNESS / 2.0,
Distance::meters(1.0),
@ -92,21 +140,17 @@ impl DrawMovement {
ArrowCap::Triangle,
),
);
app.cs.signal_permitted_turn
}
Some(TurnPriority::Banned) => {
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());
if let Ok(p) = arrow.to_outline(Distance::meters(0.1)) {
batch.push(red, p);
}
red
}
None => app.cs.signal_turn_block_bg,
};
batch.push(block_color, self.block.clone());
batch.push(Color::WHITE, self.arrow.clone());
None => {}
}
}
}
@ -128,7 +172,7 @@ impl DrawUberTurnGroup {
.max()
.unwrap();
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();
for ut in &group.members {
if !seen_lanes.contains(&ut.entry()) {
@ -148,7 +192,7 @@ impl DrawUberTurnGroup {
}
// 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;
// Always extend the pl first to handle short entry lanes
let extension = PolyLine::must_new(vec![
@ -171,3 +215,26 @@ fn make_geom(offset: f64, pl: PolyLine, width: Distance, angle: Angle) -> (Polyg
(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)
}