mirror of
https://github.com/a-b-street/abstreet.git
synced 2024-12-01 02:33:54 +03:00
tiny start to a new multi-intersection traffic signal editor. just get
the left panel to display some kind of merged view
This commit is contained in:
parent
62828e1ffa
commit
c08e244d23
@ -85,7 +85,7 @@ impl GeomBatch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the bounds of all polygons in this batch.
|
/// Compute the bounds of all polygons in this batch.
|
||||||
fn get_bounds(&self) -> Bounds {
|
pub fn get_bounds(&self) -> Bounds {
|
||||||
let mut bounds = Bounds::new();
|
let mut bounds = Bounds::new();
|
||||||
for (_, poly) in &self.list {
|
for (_, poly) in &self.list {
|
||||||
bounds.union(poly.get_bounds());
|
bounds.union(poly.get_bounds());
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
mod bulk;
|
mod bulk;
|
||||||
mod cluster_traffic_signals;
|
mod cluster_traffic_signals;
|
||||||
mod lanes;
|
mod lanes;
|
||||||
|
mod new_traffic_signals;
|
||||||
mod routes;
|
mod routes;
|
||||||
mod select;
|
mod select;
|
||||||
mod stop_signs;
|
mod stop_signs;
|
||||||
@ -10,6 +11,7 @@ mod zones;
|
|||||||
|
|
||||||
pub use self::cluster_traffic_signals::ClusterTrafficSignalEditor;
|
pub use self::cluster_traffic_signals::ClusterTrafficSignalEditor;
|
||||||
pub use self::lanes::LaneEditor;
|
pub use self::lanes::LaneEditor;
|
||||||
|
pub use self::new_traffic_signals::NewTrafficSignalEditor;
|
||||||
pub use self::routes::RouteEditor;
|
pub use self::routes::RouteEditor;
|
||||||
pub use self::stop_signs::StopSignEditor;
|
pub use self::stop_signs::StopSignEditor;
|
||||||
pub use self::traffic_signals::TrafficSignalEditor;
|
pub use self::traffic_signals::TrafficSignalEditor;
|
||||||
@ -641,12 +643,7 @@ pub fn maybe_edit_intersection(
|
|||||||
if app.primary.map.maybe_get_traffic_signal(id).is_some()
|
if app.primary.map.maybe_get_traffic_signal(id).is_some()
|
||||||
&& app.per_obj.left_click(ctx, "edit traffic signal")
|
&& app.per_obj.left_click(ctx, "edit traffic signal")
|
||||||
{
|
{
|
||||||
return Some(Box::new(TrafficSignalEditor::new(
|
return Some(TrafficSignalEditor::new(ctx, app, id, mode.clone()));
|
||||||
ctx,
|
|
||||||
app,
|
|
||||||
id,
|
|
||||||
mode.clone(),
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if app.primary.map.get_i(id).is_closed()
|
if app.primary.map.get_i(id).is_closed()
|
||||||
|
246
game/src/edit/new_traffic_signals.rs
Normal file
246
game/src/edit/new_traffic_signals.rs
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
use crate::app::App;
|
||||||
|
use crate::edit::traffic_signals::make_top_panel;
|
||||||
|
use crate::game::{State, Transition};
|
||||||
|
use crate::options::TrafficSignalStyle;
|
||||||
|
use crate::render::draw_signal_phase;
|
||||||
|
use ezgui::{
|
||||||
|
hotkey, Btn, Color, Composite, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment, Key, Line,
|
||||||
|
Outcome, VerticalAlignment, Widget,
|
||||||
|
};
|
||||||
|
use geom::{Bounds, Distance, Polygon};
|
||||||
|
use map_model::{IntersectionID, Phase};
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
|
pub struct NewTrafficSignalEditor {
|
||||||
|
side_panel: Composite,
|
||||||
|
top_panel: Composite,
|
||||||
|
|
||||||
|
members: BTreeSet<IntersectionID>,
|
||||||
|
current_phase: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NewTrafficSignalEditor {
|
||||||
|
pub fn new(
|
||||||
|
ctx: &mut EventCtx,
|
||||||
|
app: &mut App,
|
||||||
|
members: BTreeSet<IntersectionID>,
|
||||||
|
) -> Box<dyn State> {
|
||||||
|
app.primary.current_selection = None;
|
||||||
|
|
||||||
|
Box::new(NewTrafficSignalEditor {
|
||||||
|
side_panel: make_side_panel(ctx, app, &members, 0),
|
||||||
|
top_panel: make_top_panel(ctx, app, false, false),
|
||||||
|
members,
|
||||||
|
current_phase: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn change_phase(&mut self, ctx: &mut EventCtx, app: &App, idx: usize) {
|
||||||
|
if self.current_phase == idx {
|
||||||
|
let mut new = make_side_panel(ctx, app, &self.members, self.current_phase);
|
||||||
|
new.restore(ctx, &self.side_panel);
|
||||||
|
self.side_panel = new;
|
||||||
|
} else {
|
||||||
|
self.current_phase = idx;
|
||||||
|
self.side_panel = make_side_panel(ctx, app, &self.members, self.current_phase);
|
||||||
|
// TODO Maybe center of previous member
|
||||||
|
self.side_panel
|
||||||
|
.scroll_to_member(ctx, format!("phase {}", idx + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State for NewTrafficSignalEditor {
|
||||||
|
fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
|
||||||
|
ctx.canvas_movement();
|
||||||
|
|
||||||
|
match self.side_panel.event(ctx) {
|
||||||
|
Outcome::Clicked(x) => {
|
||||||
|
if let Some(x) = x.strip_prefix("phase ") {
|
||||||
|
let idx = x.parse::<usize>().unwrap() - 1;
|
||||||
|
self.change_phase(ctx, app, idx);
|
||||||
|
return Transition::Keep;
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.top_panel.event(ctx) {
|
||||||
|
Outcome::Clicked(x) => match x.as_ref() {
|
||||||
|
"Finish" => {
|
||||||
|
return Transition::Pop;
|
||||||
|
}
|
||||||
|
// TODO Handle the other things
|
||||||
|
_ => unreachable!(),
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.current_phase != 0 && ctx.input.key_pressed(Key::UpArrow) {
|
||||||
|
self.change_phase(ctx, app, self.current_phase - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO When we enter this state, force all signals to have the same number of phases, so
|
||||||
|
// we can look up any of them.
|
||||||
|
let num_phases = self
|
||||||
|
.members
|
||||||
|
.iter()
|
||||||
|
.map(|i| app.primary.map.get_traffic_signal(*i).phases.len())
|
||||||
|
.max()
|
||||||
|
.unwrap();
|
||||||
|
if self.current_phase != num_phases - 1 && ctx.input.key_pressed(Key::DownArrow) {
|
||||||
|
self.change_phase(ctx, app, self.current_phase + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Transition::Keep
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&self, g: &mut GfxCtx, _: &App) {
|
||||||
|
self.top_panel.draw(g);
|
||||||
|
self.side_panel.draw(g);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_side_panel(
|
||||||
|
ctx: &mut EventCtx,
|
||||||
|
app: &App,
|
||||||
|
members: &BTreeSet<IntersectionID>,
|
||||||
|
selected: usize,
|
||||||
|
) -> Composite {
|
||||||
|
let map = &app.primary.map;
|
||||||
|
let num_phases = members
|
||||||
|
.iter()
|
||||||
|
.map(|i| map.get_traffic_signal(*i).phases.len())
|
||||||
|
.max()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut col = Vec::new();
|
||||||
|
|
||||||
|
for idx in 0..num_phases {
|
||||||
|
// Separator
|
||||||
|
col.push(
|
||||||
|
Widget::draw_batch(
|
||||||
|
ctx,
|
||||||
|
GeomBatch::from(vec![(
|
||||||
|
Color::WHITE,
|
||||||
|
// TODO draw_batch will scale up, but that's inappropriate here, since we're
|
||||||
|
// depending on window width, which already factors in scale
|
||||||
|
Polygon::rectangle(0.2 * ctx.canvas.window_width / ctx.get_scale_factor(), 2.0),
|
||||||
|
)]),
|
||||||
|
)
|
||||||
|
.centered_horiz(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let unselected_btn = draw_multiple_signals(ctx, app, members, idx);
|
||||||
|
let mut selected_btn = unselected_btn.clone();
|
||||||
|
let bbox = unselected_btn.get_bounds().get_rectangle();
|
||||||
|
selected_btn.push(Color::RED, bbox.to_outline(Distance::meters(5.0)).unwrap());
|
||||||
|
let phase_btn = Btn::custom(unselected_btn, selected_btn, bbox).build(
|
||||||
|
ctx,
|
||||||
|
format!("phase {}", idx + 1),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let phase_col = Widget::col(vec![
|
||||||
|
Widget::row(vec![
|
||||||
|
// TODO Print duration
|
||||||
|
Line(format!("Phase {}", idx + 1)).small_heading().draw(ctx),
|
||||||
|
Btn::svg_def("system/assets/tools/edit.svg").build(
|
||||||
|
ctx,
|
||||||
|
format!("change duration of phase {}", idx + 1),
|
||||||
|
if selected == idx {
|
||||||
|
hotkey(Key::X)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if num_phases > 1 {
|
||||||
|
Btn::svg_def("system/assets/tools/delete.svg")
|
||||||
|
.build(ctx, format!("delete phase {}", idx + 1), None)
|
||||||
|
.align_right()
|
||||||
|
} else {
|
||||||
|
Widget::nothing()
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
Widget::row(vec![
|
||||||
|
phase_btn,
|
||||||
|
Widget::col(vec![
|
||||||
|
if idx == 0 {
|
||||||
|
Btn::text_fg("↑").inactive(ctx)
|
||||||
|
} else {
|
||||||
|
Btn::text_fg("↑").build(ctx, format!("move up phase {}", idx + 1), None)
|
||||||
|
},
|
||||||
|
if idx == num_phases - 1 {
|
||||||
|
Btn::text_fg("↓").inactive(ctx)
|
||||||
|
} else {
|
||||||
|
Btn::text_fg("↓").build(ctx, format!("move down phase {}", idx + 1), None)
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.centered_vert()
|
||||||
|
.align_right(),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
.padding(10);
|
||||||
|
|
||||||
|
if idx == selected {
|
||||||
|
col.push(phase_col.bg(Color::hex("#2A2A2A")));
|
||||||
|
} else {
|
||||||
|
col.push(phase_col);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Composite::new(Widget::col(col))
|
||||||
|
.aligned(HorizontalAlignment::Left, VerticalAlignment::Top)
|
||||||
|
.exact_size_percent(30, 85)
|
||||||
|
.build(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_multiple_signals(
|
||||||
|
ctx: &mut EventCtx,
|
||||||
|
app: &App,
|
||||||
|
members: &BTreeSet<IntersectionID>,
|
||||||
|
idx: usize,
|
||||||
|
) -> GeomBatch {
|
||||||
|
let mut batch = GeomBatch::new();
|
||||||
|
for i in members {
|
||||||
|
batch.push(
|
||||||
|
app.cs.normal_intersection,
|
||||||
|
app.primary.map.get_i(*i).polygon.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
draw_signal_phase(
|
||||||
|
ctx.prerender,
|
||||||
|
app.primary
|
||||||
|
.map
|
||||||
|
.get_traffic_signal(*i)
|
||||||
|
.phases
|
||||||
|
.get(idx)
|
||||||
|
.unwrap_or(&Phase::new()),
|
||||||
|
*i,
|
||||||
|
None,
|
||||||
|
&mut batch,
|
||||||
|
app,
|
||||||
|
TrafficSignalStyle::Sidewalks,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform to a screen-space icon. How much should we scale things down?
|
||||||
|
batch = batch.autocrop();
|
||||||
|
let mut zoom: f64 = 1.0;
|
||||||
|
if true {
|
||||||
|
// Make the whole thing fit a fixed width
|
||||||
|
let mut bounds = Bounds::new();
|
||||||
|
for i in members {
|
||||||
|
bounds.union(app.primary.map.get_i(*i).polygon.get_bounds());
|
||||||
|
}
|
||||||
|
zoom = 300.0 / bounds.width();
|
||||||
|
} else {
|
||||||
|
// Don't let any intersection get too small
|
||||||
|
for i in members {
|
||||||
|
zoom = zoom.max(150.0 / app.primary.map.get_i(*i).polygon.get_bounds().width());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
batch.scale(zoom)
|
||||||
|
}
|
@ -169,12 +169,12 @@ impl State for StopSignEditor {
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
apply_map_edits(ctx, app, edits);
|
apply_map_edits(ctx, app, edits);
|
||||||
return Transition::Replace(Box::new(TrafficSignalEditor::new(
|
return Transition::Replace(TrafficSignalEditor::new(
|
||||||
ctx,
|
ctx,
|
||||||
app,
|
app,
|
||||||
self.id,
|
self.id,
|
||||||
self.mode.clone(),
|
self.mode.clone(),
|
||||||
)));
|
));
|
||||||
}
|
}
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
},
|
},
|
||||||
|
@ -20,10 +20,10 @@ use std::collections::BTreeSet;
|
|||||||
|
|
||||||
// TODO Warn if there are empty phases or if some turn is completely absent from the signal.
|
// TODO Warn if there are empty phases or if some turn is completely absent from the signal.
|
||||||
pub struct TrafficSignalEditor {
|
pub struct TrafficSignalEditor {
|
||||||
pub i: IntersectionID,
|
i: IntersectionID,
|
||||||
current_phase: usize,
|
current_phase: usize,
|
||||||
composite: Composite,
|
composite: Composite,
|
||||||
pub top_panel: Composite,
|
top_panel: Composite,
|
||||||
mode: GameplayMode,
|
mode: GameplayMode,
|
||||||
|
|
||||||
groups: Vec<DrawTurnGroup>,
|
groups: Vec<DrawTurnGroup>,
|
||||||
@ -31,8 +31,8 @@ pub struct TrafficSignalEditor {
|
|||||||
group_selected: Option<(TurnGroupID, Option<TurnPriority>)>,
|
group_selected: Option<(TurnGroupID, Option<TurnPriority>)>,
|
||||||
|
|
||||||
// The first ControlTrafficSignal is the original
|
// The first ControlTrafficSignal is the original
|
||||||
pub command_stack: Vec<ControlTrafficSignal>,
|
command_stack: Vec<ControlTrafficSignal>,
|
||||||
pub redo_stack: Vec<ControlTrafficSignal>,
|
redo_stack: Vec<ControlTrafficSignal>,
|
||||||
|
|
||||||
fade_irrelevant: Drawable,
|
fade_irrelevant: Drawable,
|
||||||
}
|
}
|
||||||
@ -43,7 +43,7 @@ impl TrafficSignalEditor {
|
|||||||
app: &mut App,
|
app: &mut App,
|
||||||
id: IntersectionID,
|
id: IntersectionID,
|
||||||
mode: GameplayMode,
|
mode: GameplayMode,
|
||||||
) -> TrafficSignalEditor {
|
) -> Box<dyn State> {
|
||||||
app.primary.current_selection = None;
|
app.primary.current_selection = None;
|
||||||
|
|
||||||
let map = &app.primary.map;
|
let map = &app.primary.map;
|
||||||
@ -57,7 +57,7 @@ impl TrafficSignalEditor {
|
|||||||
vec![Polygon::convex_hull(holes).into_ring()],
|
vec![Polygon::convex_hull(holes).into_ring()],
|
||||||
);
|
);
|
||||||
|
|
||||||
TrafficSignalEditor {
|
Box::new(TrafficSignalEditor {
|
||||||
i: id,
|
i: id,
|
||||||
current_phase: 0,
|
current_phase: 0,
|
||||||
composite: make_signal_diagram(ctx, app, id, 0),
|
composite: make_signal_diagram(ctx, app, id, 0),
|
||||||
@ -68,7 +68,7 @@ impl TrafficSignalEditor {
|
|||||||
command_stack: Vec::new(),
|
command_stack: Vec::new(),
|
||||||
redo_stack: Vec::new(),
|
redo_stack: Vec::new(),
|
||||||
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),
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn change_phase(&mut self, idx: usize, ctx: &mut EventCtx, app: &App) {
|
fn change_phase(&mut self, idx: usize, ctx: &mut EventCtx, app: &App) {
|
||||||
|
@ -442,7 +442,7 @@ impl ContextualActions for Actions {
|
|||||||
match (id, action.as_ref()) {
|
match (id, action.as_ref()) {
|
||||||
(ID::Intersection(i), "edit traffic signal") => Transition::PushTwice(
|
(ID::Intersection(i), "edit traffic signal") => Transition::PushTwice(
|
||||||
EditMode::new(ctx, app, self.gameplay.clone()),
|
EditMode::new(ctx, app, self.gameplay.clone()),
|
||||||
Box::new(TrafficSignalEditor::new(ctx, app, i, self.gameplay.clone())),
|
TrafficSignalEditor::new(ctx, app, i, self.gameplay.clone()),
|
||||||
),
|
),
|
||||||
(ID::Intersection(i), "edit stop sign") => Transition::PushTwice(
|
(ID::Intersection(i), "edit stop sign") => Transition::PushTwice(
|
||||||
EditMode::new(ctx, app, self.gameplay.clone()),
|
EditMode::new(ctx, app, self.gameplay.clone()),
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use crate::app::{App, ShowEverything};
|
use crate::app::{App, ShowEverything};
|
||||||
use crate::common::CommonState;
|
use crate::common::CommonState;
|
||||||
use crate::edit::ClusterTrafficSignalEditor;
|
use crate::edit::{ClusterTrafficSignalEditor, NewTrafficSignalEditor};
|
||||||
use crate::game::{DrawBaselayer, PopupMsg, State, Transition};
|
use crate::game::{DrawBaselayer, PopupMsg, State, Transition};
|
||||||
use crate::helpers::ID;
|
use crate::helpers::ID;
|
||||||
use crate::render::{DrawOptions, BIG_ARROW_THICKNESS};
|
use crate::render::{DrawOptions, BIG_ARROW_THICKNESS};
|
||||||
@ -39,7 +39,8 @@ impl UberTurnPicker {
|
|||||||
.align_right(),
|
.align_right(),
|
||||||
]),
|
]),
|
||||||
Btn::text_fg("View uber-turns").build_def(ctx, hotkey(Key::Enter)),
|
Btn::text_fg("View uber-turns").build_def(ctx, hotkey(Key::Enter)),
|
||||||
Btn::text_fg("Edit").build_def(ctx, hotkey(Key::E)),
|
Btn::text_fg("Edit (old attempt)").build_def(ctx, None),
|
||||||
|
Btn::text_fg("Edit (new attempt)").build_def(ctx, hotkey(Key::E)),
|
||||||
Btn::text_fg("Detect all clusters").build_def(ctx, hotkey(Key::D)),
|
Btn::text_fg("Detect all clusters").build_def(ctx, hotkey(Key::D)),
|
||||||
]))
|
]))
|
||||||
.aligned(HorizontalAlignment::Center, VerticalAlignment::Top)
|
.aligned(HorizontalAlignment::Center, VerticalAlignment::Top)
|
||||||
@ -85,7 +86,7 @@ impl State for UberTurnPicker {
|
|||||||
true,
|
true,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
"Edit" => {
|
"Edit (old attempt)" => {
|
||||||
if self.members.len() < 2 {
|
if self.members.len() < 2 {
|
||||||
return Transition::Push(PopupMsg::new(
|
return Transition::Push(PopupMsg::new(
|
||||||
ctx,
|
ctx,
|
||||||
@ -99,6 +100,20 @@ impl State for UberTurnPicker {
|
|||||||
&IntersectionCluster::new(self.members.clone(), &app.primary.map).0,
|
&IntersectionCluster::new(self.members.clone(), &app.primary.map).0,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
"Edit (new attempt)" => {
|
||||||
|
if self.members.len() < 2 {
|
||||||
|
return Transition::Push(PopupMsg::new(
|
||||||
|
ctx,
|
||||||
|
"Error",
|
||||||
|
vec!["Select at least two intersections"],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return Transition::Replace(NewTrafficSignalEditor::new(
|
||||||
|
ctx,
|
||||||
|
app,
|
||||||
|
self.members.clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
"Detect all clusters" => {
|
"Detect all clusters" => {
|
||||||
self.members.clear();
|
self.members.clear();
|
||||||
for ic in IntersectionCluster::find_all(&app.primary.map) {
|
for ic in IntersectionCluster::find_all(&app.primary.map) {
|
||||||
|
Loading…
Reference in New Issue
Block a user