Skip to content

Commit

Permalink
Create a UI to apply edits from one road to others with matching conf…
Browse files Browse the repository at this point in the history
…igurations. #597
  • Loading branch information
dabreegster committed May 19, 2021
1 parent 28afbe1 commit a528409
Show file tree
Hide file tree
Showing 3 changed files with 265 additions and 40 deletions.
1 change: 1 addition & 0 deletions game/src/edit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use crate::sandbox::{GameplayMode, SandboxMode, TimeWarpScreen};

mod bulk;
mod lanes;
mod multiple_roads;
mod roads;
mod routes;
mod select;
Expand Down
186 changes: 186 additions & 0 deletions game/src/edit/multiple_roads.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
use std::collections::HashSet;

use map_gui::tools::{ColorLegend, PopupMsg};
use map_gui::ID;
use map_model::{EditRoad, MapEdits, RoadID};
use widgetry::{
Color, Drawable, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment, Key, Line, Panel,
SimpleState, State, Text, VerticalAlignment, Widget,
};

use crate::app::App;
use crate::app::Transition;
use crate::common::CommonState;
use crate::edit::apply_map_edits;

pub struct SelectSegments {
new_state: EditRoad,
candidates: HashSet<RoadID>,
base_road: RoadID,
base_edits: MapEdits,

current: HashSet<RoadID>,
draw: Drawable,
}

impl SelectSegments {
pub fn new_state(
ctx: &mut EventCtx,
app: &App,
base_road: RoadID,
orig_state: EditRoad,
new_state: EditRoad,
base_edits: MapEdits,
) -> Box<dyn State<App>> {
// Find all roads matching the original state. base_road has already changed to new_state,
// so no need to exclude it.
// Start out only applying the change to segments with the same name -- a reasonable proxy
// for "the same road".
let map = &app.primary.map;
let base_name = map.get_r(base_road).get_name(None);
let mut candidates = HashSet::new();
let mut current = HashSet::new();
for r in map.all_roads() {
if map.get_r_edit(r.id) == orig_state {
candidates.insert(r.id);
if r.get_name(None) == base_name {
current.insert(r.id);
}
}
}

if candidates.is_empty() {
return PopupMsg::new_state(
ctx,
"Error",
vec!["No other roads resemble the one you changed"],
);
}

let panel = Panel::new_builder(Widget::col(vec![
Line("Apply changes to multiple roads")
.small_heading()
.into_widget(ctx),
Text::from_multiline(vec![
Line("All roads with the same number of lanes have been selected."),
Line("Click a road segment to select/deselect it."),
])
.into_widget(ctx),
ColorLegend::row(ctx, Color::RED, "road you've changed"),
ColorLegend::row(ctx, Color::PURPLE, "also apply changes to this road"),
ColorLegend::row(ctx, Color::PINK, "candidate road"),
Widget::row(vec![
ctx.style()
.btn_solid_primary
.text("Apply")
.hotkey(Key::Enter)
.build_def(ctx),
ctx.style()
.btn_plain
.text("Cancel")
.hotkey(Key::Escape)
.build_def(ctx),
]),
]))
.aligned(HorizontalAlignment::Center, VerticalAlignment::Top)
.build(ctx);

let mut state = SelectSegments {
new_state,
candidates,
base_road,
base_edits,

current,
draw: Drawable::empty(ctx),
};
state.recalc_draw(ctx, app);
<dyn SimpleState<_>>::new_state(panel, Box::new(state))
}

fn recalc_draw(&mut self, ctx: &mut EventCtx, app: &App) {
let mut batch = GeomBatch::new();
let map = &app.primary.map;
batch.push(Color::RED, map.get_r(self.base_road).get_thick_polygon(map));
for r in &self.candidates {
let color = if self.current.contains(r) {
Color::PURPLE
} else {
Color::PINK
};
batch.push(color.alpha(0.8), map.get_r(*r).get_thick_polygon(map));
}
self.draw = ctx.upload(batch);
}
}

impl SimpleState<App> for SelectSegments {
fn on_click(&mut self, ctx: &mut EventCtx, app: &mut App, x: &str, _: &Panel) -> Transition {
match x {
"Apply" => {
app.primary.current_selection = None;
let mut edits = std::mem::take(&mut self.base_edits);
for r in &self.current {
edits
.commands
.push(app.primary.map.edit_road_cmd(*r, |new| {
*new = self.new_state.clone();
}));
}
apply_map_edits(ctx, app, edits);
Transition::Multi(vec![
Transition::Pop,
Transition::Replace(PopupMsg::new_state(
ctx,
"Success",
vec![format!(
"Changed {} other road segments to match",
self.current.len()
)],
)),
])
}
"Cancel" => {
app.primary.current_selection = None;
Transition::Pop
}
_ => unreachable!(),
}
}

fn on_mouseover(&mut self, ctx: &mut EventCtx, app: &mut App) {
app.primary.current_selection = None;
if let Some(r) = match app.mouseover_unzoomed_roads_and_intersections(ctx) {
Some(ID::Road(r)) => Some(r),
Some(ID::Lane(l)) => Some(app.primary.map.get_l(l).parent),
_ => None,
} {
if self.candidates.contains(&r) {
app.primary.current_selection = Some(ID::Road(r));
}
}
}

fn other_event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
ctx.canvas_movement();

if let Some(ID::Road(r)) = app.primary.current_selection {
if self.current.contains(&r) && app.per_obj.left_click(ctx, "exclude road segment") {
self.current.remove(&r);
self.recalc_draw(ctx, app);
} else if !self.current.contains(&r)
&& app.per_obj.left_click(ctx, "include road segment")
{
self.current.insert(r);
self.recalc_draw(ctx, app);
}
}

Transition::Keep
}

fn draw(&self, g: &mut GfxCtx, app: &App) {
g.redraw(&self.draw);
CommonState::draw_osd(g, app);
}
}
118 changes: 78 additions & 40 deletions game/src/edit/roads.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use geom::{CornerRadii, Distance};
use map_gui::render::{Renderable, OUTLINE_THICKNESS};
use map_gui::tools::PopupMsg;
use map_gui::ID;
use map_model::{
Direction, EditCmd, EditRoad, LaneID, LaneSpec, LaneType, Road, RoadID, NORMAL_LANE_THICKNESS,
Direction, EditCmd, EditRoad, LaneID, LaneSpec, LaneType, MapEdits, Road, RoadID,
NORMAL_LANE_THICKNESS,
};
use widgetry::{
lctrl, Choice, Color, ControlState, Drawable, EventCtx, GeomBatch, GeomBatchStack, GfxCtx,
Expand All @@ -23,6 +25,7 @@ pub struct RoadEditor {
// Undo/redo management
num_edit_cmds_originally: usize,
redo_stack: Vec<EditCmd>,
orig_road_state: EditRoad,
}

impl RoadEditor {
Expand All @@ -38,6 +41,7 @@ impl RoadEditor {

num_edit_cmds_originally: app.primary.map.get_edits().commands.len(),
redo_stack: Vec::new(),
orig_road_state: app.primary.map.get_r_edit(r),
};
editor.recalc_all_panels(ctx, app);
Box::new(editor)
Expand Down Expand Up @@ -83,6 +87,26 @@ impl RoadEditor {
self.redo_stack.is_empty(),
);
}

fn compress_edits(&self, app: &App) -> Option<MapEdits> {
// Compress all of the edits, unless there were 0 or 1 changes
if app.primary.map.get_edits().commands.len() > self.num_edit_cmds_originally + 2 {
let mut edits = app.primary.map.get_edits().clone();
let last_edit = match edits.commands.pop().unwrap() {
EditCmd::ChangeRoad { new, .. } => new,
_ => unreachable!(),
};
edits.commands.truncate(self.num_edit_cmds_originally + 1);
match edits.commands.last_mut().unwrap() {
EditCmd::ChangeRoad { ref mut new, .. } => {
*new = last_edit;
}
_ => unreachable!(),
}
return Some(edits);
}
None
}
}

impl State<App> for RoadEditor {
Expand All @@ -92,23 +116,9 @@ impl State<App> for RoadEditor {
if let Outcome::Clicked(x) = self.top_panel.event(ctx) {
match x.as_ref() {
"Finish" => {
// Compress all of the edits, unless there were 0 or 1 changes
let mut edits = app.primary.map.get_edits().clone();
if edits.commands.len() > self.num_edit_cmds_originally + 2 {
let last_edit = match edits.commands.pop().unwrap() {
EditCmd::ChangeRoad { new, .. } => new,
_ => unreachable!(),
};
edits.commands.truncate(self.num_edit_cmds_originally + 1);
match edits.commands.last_mut().unwrap() {
EditCmd::ChangeRoad { ref mut new, .. } => {
*new = last_edit;
}
_ => unreachable!(),
}
if let Some(edits) = self.compress_edits(app) {
apply_map_edits(ctx, app, edits);
}

return Transition::Pop;
}
"Cancel" => {
Expand All @@ -135,6 +145,23 @@ impl State<App> for RoadEditor {
self.current_lane = None;
self.recalc_all_panels(ctx, app);
}
"Edit multiple roads" => {
let current_state = app.primary.map.get_r_edit(self.r);
if current_state == self.orig_road_state {
return Transition::Push(PopupMsg::new_state(ctx, "Error", vec!["Change this road first, then you can apply the edits to more segments"]));
}
return Transition::Push(
crate::edit::multiple_roads::SelectSegments::new_state(
ctx,
app,
self.r,
self.orig_road_state.clone(),
current_state,
self.compress_edits(app)
.unwrap_or_else(|| app.primary.map.get_edits().clone()),
),
);
}
_ => unreachable!(),
}
}
Expand Down Expand Up @@ -264,30 +291,41 @@ fn make_top_panel(
num_edit_cmds_originally: usize,
no_redo_cmds: bool,
) -> Panel {
Panel::new_builder(Widget::row(vec![
ctx.style()
.btn_solid_primary
.text("Finish")
.hotkey(Key::Enter)
.build_def(ctx),
ctx.style()
.btn_plain
.icon("system/assets/tools/undo.svg")
.disabled(app.primary.map.get_edits().commands.len() == num_edit_cmds_originally)
.hotkey(lctrl(Key::Z))
.build_widget(ctx, "undo"),
ctx.style()
.btn_plain
.icon("system/assets/tools/redo.svg")
.disabled(no_redo_cmds)
// TODO ctrl+shift+Z!
.hotkey(lctrl(Key::Y))
.build_widget(ctx, "redo"),
ctx.style()
.btn_plain
.text("Cancel")
.hotkey(Key::Escape)
.build_def(ctx),
Panel::new_builder(Widget::col(vec![
Widget::row(vec![
Line("Editing road").small_heading().into_widget(ctx),
ctx.style()
.btn_plain
.text("+ Edit multiple")
.label_color(Color::hex("#4CA7E9"), ControlState::Default)
.hotkey(Key::M)
.build_widget(ctx, "Edit multiple roads"),
]),
Widget::row(vec![
ctx.style()
.btn_solid_primary
.text("Finish")
.hotkey(Key::Enter)
.build_def(ctx),
ctx.style()
.btn_plain
.icon("system/assets/tools/undo.svg")
.disabled(app.primary.map.get_edits().commands.len() == num_edit_cmds_originally)
.hotkey(lctrl(Key::Z))
.build_widget(ctx, "undo"),
ctx.style()
.btn_plain
.icon("system/assets/tools/redo.svg")
.disabled(no_redo_cmds)
// TODO ctrl+shift+Z!
.hotkey(lctrl(Key::Y))
.build_widget(ctx, "redo"),
ctx.style()
.btn_plain
.text("Cancel")
.hotkey(Key::Escape)
.build_def(ctx),
]),
]))
.aligned(HorizontalAlignment::Center, VerticalAlignment::Top)
.build(ctx)
Expand Down

0 comments on commit a528409

Please sign in to comment.