mirror of
https://github.com/a-b-street/abstreet.git
synced 2025-01-04 12:36:46 +03:00
turn edit mode's lane brushes into a proper Composite
This commit is contained in:
parent
6bdd557c80
commit
4327322ea4
@ -2,166 +2,75 @@ use crate::edit::apply_map_edits;
|
|||||||
use crate::game::{msg, State, Transition, WizardState};
|
use crate::game::{msg, State, Transition, WizardState};
|
||||||
use crate::helpers::ID;
|
use crate::helpers::ID;
|
||||||
use crate::ui::UI;
|
use crate::ui::UI;
|
||||||
use ezgui::{hotkey, Button, Choice, Color, EventCtx, GfxCtx, Key, ScreenPt};
|
use ezgui::{
|
||||||
|
hotkey, Button, Choice, Color, Composite, EventCtx, GfxCtx, HorizontalAlignment, Key,
|
||||||
|
ManagedWidget, Outcome, VerticalAlignment,
|
||||||
|
};
|
||||||
use map_model::{
|
use map_model::{
|
||||||
connectivity, EditCmd, IntersectionType, LaneID, LaneType, Map, PathConstraints, RoadID,
|
connectivity, EditCmd, IntersectionType, LaneID, LaneType, Map, PathConstraints, RoadID,
|
||||||
};
|
};
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
pub struct LaneEditor {
|
pub struct LaneEditor {
|
||||||
brushes: Vec<Paintbrush>,
|
pub brush: Brush,
|
||||||
pub active_idx: Option<usize>,
|
composite: Composite,
|
||||||
pub construction_idx: usize,
|
|
||||||
reverse_idx: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Paintbrush {
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
btn: Button,
|
pub enum Brush {
|
||||||
enabled_btn: Button,
|
Inactive,
|
||||||
label: String,
|
Driving,
|
||||||
// If this returns a string error message, the edit didn't work. If it returns Ok(None), then
|
Bike,
|
||||||
// it's a no-op.
|
Bus,
|
||||||
apply: Box<dyn Fn(&Map, LaneID) -> Result<Option<EditCmd>, String>>,
|
Parking,
|
||||||
|
Construction,
|
||||||
|
Reverse,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LaneEditor {
|
impl LaneEditor {
|
||||||
pub fn setup(ctx: &EventCtx) -> LaneEditor {
|
pub fn new(ctx: &mut EventCtx) -> LaneEditor {
|
||||||
// TODO This won't handle resizing well
|
|
||||||
let mut x1 = 0.5 * ctx.canvas.window_width;
|
|
||||||
let mut make_brush =
|
|
||||||
|icon: &str,
|
|
||||||
label: &str,
|
|
||||||
key: Key,
|
|
||||||
apply: Box<dyn Fn(&Map, LaneID) -> Result<Option<EditCmd>, String>>| {
|
|
||||||
let btn = Button::rectangle_svg_bg(
|
|
||||||
&format!("assets/edit/{}.svg", icon),
|
|
||||||
label,
|
|
||||||
hotkey(key),
|
|
||||||
Color::grey(0.4),
|
|
||||||
Color::ORANGE,
|
|
||||||
ctx,
|
|
||||||
)
|
|
||||||
.at(ScreenPt::new(x1, 0.0));
|
|
||||||
let enabled_btn = Button::rectangle_svg_bg(
|
|
||||||
&format!("assets/edit/{}.svg", icon),
|
|
||||||
label,
|
|
||||||
hotkey(key),
|
|
||||||
Color::RED,
|
|
||||||
Color::ORANGE,
|
|
||||||
ctx,
|
|
||||||
)
|
|
||||||
.at(ScreenPt::new(x1, 0.0));
|
|
||||||
|
|
||||||
x1 += 70.0;
|
|
||||||
Paintbrush {
|
|
||||||
btn,
|
|
||||||
enabled_btn,
|
|
||||||
label: label.to_string(),
|
|
||||||
apply,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let brushes = vec![
|
|
||||||
make_brush(
|
|
||||||
"driving",
|
|
||||||
"driving lane",
|
|
||||||
Key::D,
|
|
||||||
Box::new(|map, l| try_change_lane_type(l, LaneType::Driving, map)),
|
|
||||||
),
|
|
||||||
make_brush(
|
|
||||||
"bike",
|
|
||||||
"protected bike lane",
|
|
||||||
Key::B,
|
|
||||||
Box::new(|map, l| try_change_lane_type(l, LaneType::Biking, map)),
|
|
||||||
),
|
|
||||||
make_brush(
|
|
||||||
"bus",
|
|
||||||
"bus-only lane",
|
|
||||||
Key::T,
|
|
||||||
Box::new(|map, l| try_change_lane_type(l, LaneType::Bus, map)),
|
|
||||||
),
|
|
||||||
make_brush(
|
|
||||||
"parking",
|
|
||||||
"on-street parking lane",
|
|
||||||
Key::P,
|
|
||||||
Box::new(|map, l| try_change_lane_type(l, LaneType::Parking, map)),
|
|
||||||
),
|
|
||||||
make_brush(
|
|
||||||
"construction",
|
|
||||||
"closed for construction",
|
|
||||||
Key::C,
|
|
||||||
Box::new(|map, l| try_change_lane_type(l, LaneType::Construction, map)),
|
|
||||||
),
|
|
||||||
make_brush(
|
|
||||||
"contraflow",
|
|
||||||
"reverse lane direction",
|
|
||||||
Key::F,
|
|
||||||
Box::new(|map, l| {
|
|
||||||
let lane = map.get_l(l);
|
|
||||||
if !lane.lane_type.is_for_moving_vehicles() {
|
|
||||||
return Err(format!("You can't reverse a {:?} lane", lane.lane_type));
|
|
||||||
}
|
|
||||||
if map.get_r(lane.parent).dir_and_offset(l).1 != 0 {
|
|
||||||
return Err(format!(
|
|
||||||
"You can only reverse the lanes next to the road's yellow center line"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Ok(Some(EditCmd::ReverseLane {
|
|
||||||
l,
|
|
||||||
dst_i: lane.src_i,
|
|
||||||
}))
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
let construction_idx = brushes
|
|
||||||
.iter()
|
|
||||||
.position(|b| b.label == "closed for construction")
|
|
||||||
.unwrap();
|
|
||||||
let reverse_idx = brushes
|
|
||||||
.iter()
|
|
||||||
.position(|b| b.label == "reverse lane direction")
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
LaneEditor {
|
LaneEditor {
|
||||||
brushes,
|
brush: Brush::Inactive,
|
||||||
active_idx: None,
|
composite: make_brush_panel(ctx, Brush::Inactive),
|
||||||
construction_idx,
|
|
||||||
reverse_idx,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn event(&mut self, ui: &mut UI, ctx: &mut EventCtx) -> Option<Transition> {
|
pub fn event(&mut self, ctx: &mut EventCtx, ui: &mut UI) -> Option<Transition> {
|
||||||
// TODO This is some awkward way to express mutual exclusion. :(
|
// Change brush
|
||||||
let mut undo_old = None;
|
match self.composite.event(ctx) {
|
||||||
for (idx, p) in self.brushes.iter_mut().enumerate() {
|
Some(Outcome::Clicked(x)) => {
|
||||||
if Some(idx) == undo_old {
|
let b = match x.as_ref() {
|
||||||
p.btn.just_replaced(ctx);
|
"driving lane" => Brush::Driving,
|
||||||
undo_old = None;
|
"protected bike lane" => Brush::Bike,
|
||||||
}
|
"bus-only lane" => Brush::Bus,
|
||||||
|
"on-street parking lane" => Brush::Parking,
|
||||||
if self.active_idx == Some(idx) {
|
"closed for construction" => Brush::Construction,
|
||||||
p.enabled_btn.event(ctx);
|
"reverse lane direction" => Brush::Reverse,
|
||||||
if p.enabled_btn.clicked() {
|
_ => unreachable!(),
|
||||||
self.active_idx = None;
|
};
|
||||||
p.btn.just_replaced(ctx);
|
if self.brush == b {
|
||||||
}
|
self.brush = Brush::Inactive;
|
||||||
} else {
|
} else {
|
||||||
p.btn.event(ctx);
|
self.brush = b;
|
||||||
if p.btn.clicked() {
|
|
||||||
undo_old = self.active_idx;
|
|
||||||
self.active_idx = Some(idx);
|
|
||||||
p.enabled_btn.just_replaced(ctx);
|
|
||||||
}
|
}
|
||||||
|
self.composite = make_brush_panel(ctx, self.brush);
|
||||||
}
|
}
|
||||||
}
|
None => {}
|
||||||
// Have to do this outside the loop where brushes are all mutably borrowed
|
|
||||||
if let Some(idx) = undo_old {
|
|
||||||
self.brushes[idx].btn.just_replaced(ctx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ID::Lane(l)) = ui.primary.current_selection {
|
if let Some(ID::Lane(l)) = ui.primary.current_selection {
|
||||||
if let Some(idx) = self.active_idx {
|
// TODO Refactor all of these mappings!
|
||||||
if ui.per_obj.action(ctx, Key::Space, &self.brushes[idx].label) {
|
if self.brush != Brush::Inactive {
|
||||||
|
let label = match self.brush {
|
||||||
|
Brush::Inactive => unreachable!(),
|
||||||
|
Brush::Driving => "driving lane",
|
||||||
|
Brush::Bike => "protected bike lane",
|
||||||
|
Brush::Bus => "bus-only lane",
|
||||||
|
Brush::Parking => "on-street parking lane",
|
||||||
|
Brush::Construction => "closed for construction",
|
||||||
|
Brush::Reverse => "reverse lane direction",
|
||||||
|
};
|
||||||
|
if ui.per_obj.action(ctx, Key::Space, label) {
|
||||||
// These errors are universal.
|
// These errors are universal.
|
||||||
if ui.primary.map.get_l(l).is_sidewalk() {
|
if ui.primary.map.get_l(l).is_sidewalk() {
|
||||||
return Some(Transition::Push(msg(
|
return Some(Transition::Push(msg(
|
||||||
@ -176,7 +85,7 @@ impl LaneEditor {
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
match (self.brushes[idx].apply)(&ui.primary.map, l) {
|
match apply_brush(self.brush, &ui.primary.map, l) {
|
||||||
Ok(Some(cmd)) => {
|
Ok(Some(cmd)) => {
|
||||||
let mut edits = ui.primary.map.get_edits().clone();
|
let mut edits = ui.primary.map.get_edits().clone();
|
||||||
edits.commands.push(cmd);
|
edits.commands.push(cmd);
|
||||||
@ -213,7 +122,7 @@ impl LaneEditor {
|
|||||||
}
|
}
|
||||||
} else if ui.primary.map.get_edits().reversed_lanes.contains(&l) {
|
} else if ui.primary.map.get_edits().reversed_lanes.contains(&l) {
|
||||||
if ui.per_obj.action(ctx, Key::R, "revert") {
|
if ui.per_obj.action(ctx, Key::R, "revert") {
|
||||||
match (self.brushes[self.reverse_idx].apply)(&ui.primary.map, l) {
|
match apply_brush(Brush::Reverse, &ui.primary.map, l) {
|
||||||
Ok(Some(cmd)) => {
|
Ok(Some(cmd)) => {
|
||||||
let mut edits = ui.primary.map.get_edits().clone();
|
let mut edits = ui.primary.map.get_edits().clone();
|
||||||
edits.commands.push(cmd);
|
edits.commands.push(cmd);
|
||||||
@ -227,12 +136,13 @@ impl LaneEditor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Woo, a special case! The construction tool also applies to intersections.
|
// Woo, a special case! The construction tool also applies to intersections.
|
||||||
if let Some(ID::Intersection(i)) = ui.primary.current_selection {
|
if let Some(ID::Intersection(i)) = ui.primary.current_selection {
|
||||||
if self.active_idx == Some(self.construction_idx)
|
if self.brush == Brush::Construction
|
||||||
&& ui
|
&& ui
|
||||||
.per_obj
|
.per_obj
|
||||||
.action(ctx, Key::Space, &self.brushes[self.construction_idx].label)
|
.action(ctx, Key::Space, "closed for construction")
|
||||||
{
|
{
|
||||||
let it = ui.primary.map.get_i(i).intersection_type;
|
let it = ui.primary.map.get_i(i).intersection_type;
|
||||||
if it != IntersectionType::Construction && it != IntersectionType::Border {
|
if it != IntersectionType::Construction && it != IntersectionType::Border {
|
||||||
@ -267,16 +177,55 @@ impl LaneEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw(&self, g: &mut GfxCtx) {
|
pub fn draw(&self, g: &mut GfxCtx) {
|
||||||
for (idx, p) in self.brushes.iter().enumerate() {
|
self.composite.draw(g);
|
||||||
if self.active_idx == Some(idx) {
|
|
||||||
p.enabled_btn.draw(g);
|
|
||||||
} else {
|
|
||||||
p.btn.draw(g);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn make_brush_panel(ctx: &mut EventCtx, brush: Brush) -> Composite {
|
||||||
|
let mut row = Vec::new();
|
||||||
|
for (b, icon, label, key) in vec![
|
||||||
|
(Brush::Driving, "driving", "driving lane", Key::D),
|
||||||
|
(Brush::Bike, "bike", "protected bike lane", Key::B),
|
||||||
|
(Brush::Bus, "bus", "bus-only lane", Key::T),
|
||||||
|
(Brush::Parking, "parking", "on-street parking lane", Key::P),
|
||||||
|
(
|
||||||
|
Brush::Construction,
|
||||||
|
"construction",
|
||||||
|
"closed for construction",
|
||||||
|
Key::C,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Brush::Reverse,
|
||||||
|
"contraflow",
|
||||||
|
"reverse lane direction",
|
||||||
|
Key::F,
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
row.push(
|
||||||
|
ManagedWidget::col(vec![ManagedWidget::btn(Button::rectangle_svg_bg(
|
||||||
|
&format!("assets/edit/{}.svg", icon),
|
||||||
|
label,
|
||||||
|
hotkey(key),
|
||||||
|
if brush == b {
|
||||||
|
Color::RED
|
||||||
|
} else {
|
||||||
|
Color::grey(0.4)
|
||||||
|
},
|
||||||
|
Color::ORANGE,
|
||||||
|
ctx,
|
||||||
|
))])
|
||||||
|
.padding(5),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Composite::new(
|
||||||
|
ManagedWidget::row(row)
|
||||||
|
.bg(Color::hex("#4C4C4C"))
|
||||||
|
.padding(10),
|
||||||
|
)
|
||||||
|
.aligned(HorizontalAlignment::Center, VerticalAlignment::Top)
|
||||||
|
.build(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
fn can_change_lane_type(l: LaneID, new_lt: LaneType, map: &Map) -> Option<String> {
|
fn can_change_lane_type(l: LaneID, new_lt: LaneType, map: &Map) -> Option<String> {
|
||||||
let r = map.get_parent(l);
|
let r = map.get_parent(l);
|
||||||
let (fwds, idx) = r.dir_and_offset(l);
|
let (fwds, idx) = r.dir_and_offset(l);
|
||||||
@ -405,3 +354,31 @@ fn make_bulk_edit_lanes(road: RoadID) -> Box<dyn State> {
|
|||||||
)))
|
)))
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If this returns a string error message, the edit didn't work. If it returns Ok(None), then
|
||||||
|
// it's a no-op.
|
||||||
|
fn apply_brush(brush: Brush, map: &Map, l: LaneID) -> Result<Option<EditCmd>, String> {
|
||||||
|
match brush {
|
||||||
|
Brush::Inactive => unreachable!(),
|
||||||
|
Brush::Driving => try_change_lane_type(l, LaneType::Driving, map),
|
||||||
|
Brush::Bike => try_change_lane_type(l, LaneType::Biking, map),
|
||||||
|
Brush::Bus => try_change_lane_type(l, LaneType::Bus, map),
|
||||||
|
Brush::Parking => try_change_lane_type(l, LaneType::Parking, map),
|
||||||
|
Brush::Construction => try_change_lane_type(l, LaneType::Construction, map),
|
||||||
|
Brush::Reverse => {
|
||||||
|
let lane = map.get_l(l);
|
||||||
|
if !lane.lane_type.is_for_moving_vehicles() {
|
||||||
|
return Err(format!("You can't reverse a {:?} lane", lane.lane_type));
|
||||||
|
}
|
||||||
|
if map.get_r(lane.parent).dir_and_offset(l).1 != 0 {
|
||||||
|
return Err(format!(
|
||||||
|
"You can only reverse the lanes next to the road's yellow center line"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(Some(EditCmd::ReverseLane {
|
||||||
|
l,
|
||||||
|
dst_i: lane.src_i,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -2,6 +2,7 @@ mod lanes;
|
|||||||
mod stop_signs;
|
mod stop_signs;
|
||||||
mod traffic_signals;
|
mod traffic_signals;
|
||||||
|
|
||||||
|
use self::lanes::{Brush, LaneEditor};
|
||||||
pub use self::traffic_signals::TrafficSignalEditor;
|
pub use self::traffic_signals::TrafficSignalEditor;
|
||||||
use crate::common::{tool_panel, CommonState, Warping};
|
use crate::common::{tool_panel, CommonState, Warping};
|
||||||
use crate::debug::DebugMode;
|
use crate::debug::DebugMode;
|
||||||
@ -31,7 +32,7 @@ pub struct EditMode {
|
|||||||
mode: GameplayMode,
|
mode: GameplayMode,
|
||||||
pub suspended_sim: Sim,
|
pub suspended_sim: Sim,
|
||||||
|
|
||||||
lane_editor: lanes::LaneEditor,
|
lane_editor: LaneEditor,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EditMode {
|
impl EditMode {
|
||||||
@ -53,7 +54,7 @@ impl EditMode {
|
|||||||
),
|
),
|
||||||
mode,
|
mode,
|
||||||
suspended_sim,
|
suspended_sim,
|
||||||
lane_editor: lanes::LaneEditor::setup(ctx),
|
lane_editor: LaneEditor::new(ctx),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -85,7 +86,7 @@ impl State for EditMode {
|
|||||||
self.menu.event(ctx);
|
self.menu.event(ctx);
|
||||||
|
|
||||||
if self.mode.can_edit_lanes() {
|
if self.mode.can_edit_lanes() {
|
||||||
if let Some(t) = self.lane_editor.event(ui, ctx) {
|
if let Some(t) = self.lane_editor.event(ctx, ui) {
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -95,13 +96,13 @@ impl State for EditMode {
|
|||||||
ui.recalculate_current_selection(ctx);
|
ui.recalculate_current_selection(ctx);
|
||||||
if let Some(ID::Lane(_)) = ui.primary.current_selection {
|
if let Some(ID::Lane(_)) = ui.primary.current_selection {
|
||||||
} else if let Some(ID::Intersection(_)) = ui.primary.current_selection {
|
} else if let Some(ID::Intersection(_)) = ui.primary.current_selection {
|
||||||
if self.lane_editor.active_idx != Some(self.lane_editor.construction_idx)
|
if self.lane_editor.brush != Brush::Construction
|
||||||
&& self.lane_editor.active_idx.is_some()
|
&& self.lane_editor.brush != Brush::Inactive
|
||||||
{
|
{
|
||||||
ui.primary.current_selection = None;
|
ui.primary.current_selection = None;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if self.lane_editor.active_idx.is_some() {
|
if self.lane_editor.brush != Brush::Inactive {
|
||||||
ui.primary.current_selection = None;
|
ui.primary.current_selection = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user