From c08e244d231bb1eb90d109e3f85c49816741aa37 Mon Sep 17 00:00:00 2001 From: Dustin Carlino Date: Tue, 11 Aug 2020 13:55:21 -0700 Subject: [PATCH] tiny start to a new multi-intersection traffic signal editor. just get the left panel to display some kind of merged view --- ezgui/src/geom.rs | 2 +- game/src/edit/mod.rs | 9 +- game/src/edit/new_traffic_signals.rs | 246 +++++++++++++++++++++++++++ game/src/edit/stop_signs.rs | 4 +- game/src/edit/traffic_signals.rs | 14 +- game/src/sandbox/mod.rs | 2 +- game/src/sandbox/uber_turns.rs | 21 ++- 7 files changed, 278 insertions(+), 20 deletions(-) create mode 100644 game/src/edit/new_traffic_signals.rs diff --git a/ezgui/src/geom.rs b/ezgui/src/geom.rs index a698586d69..e91dd2c969 100644 --- a/ezgui/src/geom.rs +++ b/ezgui/src/geom.rs @@ -85,7 +85,7 @@ impl GeomBatch { } /// 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(); for (_, poly) in &self.list { bounds.union(poly.get_bounds()); diff --git a/game/src/edit/mod.rs b/game/src/edit/mod.rs index 191377744c..7938d6f140 100644 --- a/game/src/edit/mod.rs +++ b/game/src/edit/mod.rs @@ -1,6 +1,7 @@ mod bulk; mod cluster_traffic_signals; mod lanes; +mod new_traffic_signals; mod routes; mod select; mod stop_signs; @@ -10,6 +11,7 @@ mod zones; pub use self::cluster_traffic_signals::ClusterTrafficSignalEditor; pub use self::lanes::LaneEditor; +pub use self::new_traffic_signals::NewTrafficSignalEditor; pub use self::routes::RouteEditor; pub use self::stop_signs::StopSignEditor; 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() && app.per_obj.left_click(ctx, "edit traffic signal") { - return Some(Box::new(TrafficSignalEditor::new( - ctx, - app, - id, - mode.clone(), - ))); + return Some(TrafficSignalEditor::new(ctx, app, id, mode.clone())); } if app.primary.map.get_i(id).is_closed() diff --git a/game/src/edit/new_traffic_signals.rs b/game/src/edit/new_traffic_signals.rs new file mode 100644 index 0000000000..1aeb68b0d0 --- /dev/null +++ b/game/src/edit/new_traffic_signals.rs @@ -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, + current_phase: usize, +} + +impl NewTrafficSignalEditor { + pub fn new( + ctx: &mut EventCtx, + app: &mut App, + members: BTreeSet, + ) -> Box { + 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::().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, + 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, + 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) +} diff --git a/game/src/edit/stop_signs.rs b/game/src/edit/stop_signs.rs index 3ba809bfb0..6937e1f51c 100644 --- a/game/src/edit/stop_signs.rs +++ b/game/src/edit/stop_signs.rs @@ -169,12 +169,12 @@ impl State for StopSignEditor { ), }); apply_map_edits(ctx, app, edits); - return Transition::Replace(Box::new(TrafficSignalEditor::new( + return Transition::Replace(TrafficSignalEditor::new( ctx, app, self.id, self.mode.clone(), - ))); + )); } _ => unreachable!(), }, diff --git a/game/src/edit/traffic_signals.rs b/game/src/edit/traffic_signals.rs index c4c4eb4b17..a6a879c14a 100644 --- a/game/src/edit/traffic_signals.rs +++ b/game/src/edit/traffic_signals.rs @@ -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. pub struct TrafficSignalEditor { - pub i: IntersectionID, + i: IntersectionID, current_phase: usize, composite: Composite, - pub top_panel: Composite, + top_panel: Composite, mode: GameplayMode, groups: Vec, @@ -31,8 +31,8 @@ pub struct TrafficSignalEditor { group_selected: Option<(TurnGroupID, Option)>, // The first ControlTrafficSignal is the original - pub command_stack: Vec, - pub redo_stack: Vec, + command_stack: Vec, + redo_stack: Vec, fade_irrelevant: Drawable, } @@ -43,7 +43,7 @@ impl TrafficSignalEditor { app: &mut App, id: IntersectionID, mode: GameplayMode, - ) -> TrafficSignalEditor { + ) -> Box { app.primary.current_selection = None; let map = &app.primary.map; @@ -57,7 +57,7 @@ impl TrafficSignalEditor { vec![Polygon::convex_hull(holes).into_ring()], ); - TrafficSignalEditor { + Box::new(TrafficSignalEditor { i: id, current_phase: 0, composite: make_signal_diagram(ctx, app, id, 0), @@ -68,7 +68,7 @@ impl TrafficSignalEditor { command_stack: Vec::new(), redo_stack: Vec::new(), 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) { diff --git a/game/src/sandbox/mod.rs b/game/src/sandbox/mod.rs index dc12ad1a1a..6d630c461f 100644 --- a/game/src/sandbox/mod.rs +++ b/game/src/sandbox/mod.rs @@ -442,7 +442,7 @@ impl ContextualActions for Actions { match (id, action.as_ref()) { (ID::Intersection(i), "edit traffic signal") => Transition::PushTwice( 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( EditMode::new(ctx, app, self.gameplay.clone()), diff --git a/game/src/sandbox/uber_turns.rs b/game/src/sandbox/uber_turns.rs index 5ec49bb39b..97a4c86a1f 100644 --- a/game/src/sandbox/uber_turns.rs +++ b/game/src/sandbox/uber_turns.rs @@ -1,6 +1,6 @@ use crate::app::{App, ShowEverything}; use crate::common::CommonState; -use crate::edit::ClusterTrafficSignalEditor; +use crate::edit::{ClusterTrafficSignalEditor, NewTrafficSignalEditor}; use crate::game::{DrawBaselayer, PopupMsg, State, Transition}; use crate::helpers::ID; use crate::render::{DrawOptions, BIG_ARROW_THICKNESS}; @@ -39,7 +39,8 @@ impl UberTurnPicker { .align_right(), ]), 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)), ])) .aligned(HorizontalAlignment::Center, VerticalAlignment::Top) @@ -85,7 +86,7 @@ impl State for UberTurnPicker { true, )); } - "Edit" => { + "Edit (old attempt)" => { if self.members.len() < 2 { return Transition::Push(PopupMsg::new( ctx, @@ -99,6 +100,20 @@ impl State for UberTurnPicker { &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" => { self.members.clear(); for ic in IntersectionCluster::find_all(&app.primary.map) {