mirror of
https://github.com/a-b-street/abstreet.git
synced 2024-12-24 23:15:24 +03:00
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:
parent
60cb96bc91
commit
69622bb86d
@ -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);
|
||||
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
|
||||
.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 {
|
||||
let signal = app.primary.map.get_traffic_signal(m.id.parent);
|
||||
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)
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -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};
|
||||
|
@ -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,
|
||||
|
@ -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,66 +13,117 @@ 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 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 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);
|
||||
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, _) = 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
draw.push(DrawMovement {
|
||||
id: movement.id,
|
||||
block,
|
||||
arrow,
|
||||
});
|
||||
batch.push(Color::hex("#7C7C7C"), circle.clone());
|
||||
batch.push(Color::WHITE, arrow);
|
||||
circle
|
||||
};
|
||||
results.push((
|
||||
DrawMovement {
|
||||
id: movement.id,
|
||||
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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user