diff --git a/apps/game/src/edit/buildings.rs b/apps/game/src/edit/buildings.rs new file mode 100644 index 0000000000..0d5756edca --- /dev/null +++ b/apps/game/src/edit/buildings.rs @@ -0,0 +1,329 @@ +use std::vec; + +use map_model::{BuildingID, EditCmd, MapEdits, OffstreetParking}; +use widgetry::{ + lctrl, Choice, EventCtx, HorizontalAlignment, Key, Line, Outcome, Panel, Spinner, State, + TextBox, VerticalAlignment, Widget, +}; + +use crate::{ + app::{App, Transition}, + common::Warping, + id::ID, +}; + +use super::apply_map_edits; + +pub struct BuildingEditor { + b: BuildingID, + + top_panel: Panel, + main_panel: Panel, + // TODO: fade_irrelevant to make things look nicer + + // Undo/redo management + num_edit_cmds_originally: usize, + redo_stack: Vec, +} + +impl BuildingEditor { + pub fn new_state(ctx: &mut EventCtx, app: &mut App, b: BuildingID) -> Box> { + BuildingEditor::create(ctx, app, b) + } + + fn create(ctx: &mut EventCtx, app: &mut App, b: BuildingID) -> Box> { + app.primary.current_selection = None; + + let mut editor = BuildingEditor { + b, + top_panel: Panel::empty(ctx), + main_panel: Panel::empty(ctx), + + num_edit_cmds_originally: app.primary.map.get_edits().commands.len(), + redo_stack: Vec::new(), + }; + editor.recalc_all_panels(ctx, app); + Box::new(editor) + } + + fn recalc_all_panels(&mut self, ctx: &mut EventCtx, app: &App) { + self.main_panel = make_main_panel(ctx, app, self.b); + + self.top_panel = make_top_panel( + ctx, + app, + self.num_edit_cmds_originally, + self.redo_stack.is_empty(), + self.b, + ); + } + + fn compress_edits(&self, app: &App) -> Option { + // 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::ChangeBuilding { new, .. } => new, + _ => unreachable!(), + }; + edits.commands.truncate(self.num_edit_cmds_originally + 1); + match edits.commands.last_mut().unwrap() { + EditCmd::ChangeBuilding { ref mut new, .. } => { + *new = last_edit; + } + _ => unreachable!(), + } + return Some(edits); + } + None + } +} + +impl State for BuildingEditor { + fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition { + ctx.canvas_movement(); + + let mut panels_need_recalc = false; + + if let Outcome::Clicked(x) = self.top_panel.event(ctx) { + match x.as_ref() { + "Finish" => { + if let Some(edits) = self.compress_edits(app) { + apply_map_edits(ctx, app, edits); + } + return Transition::Pop; + } + "Cancel" => { + let mut edits = app.primary.map.get_edits().clone(); + if edits.commands.len() != self.num_edit_cmds_originally { + edits.commands.truncate(self.num_edit_cmds_originally); + apply_map_edits(ctx, app, edits); + } + return Transition::Pop; + } + "undo" => { + let mut edits = app.primary.map.get_edits().clone(); + self.redo_stack.push(edits.commands.pop().unwrap()); + apply_map_edits(ctx, app, edits); + + panels_need_recalc = true; + } + "redo" => { + let mut edits = app.primary.map.get_edits().clone(); + edits.commands.push(self.redo_stack.pop().unwrap()); + apply_map_edits(ctx, app, edits); + + panels_need_recalc = true; + } + "jump to building" => { + return Transition::Push(Warping::new_state( + ctx, + app.primary.canonical_point(ID::Building(self.b)).unwrap(), + Some(10.0), + Some(ID::Building(self.b)), + &mut app.primary, + )) + } + _ => unreachable!("received unknown clicked key: {}", x), + } + } + + match self.main_panel.event(ctx) { + Outcome::Changed(x) => match x.as_ref() { + "parking_type" => { + let parking_type = self.main_panel.dropdown_value("parking_type"); + let parking_capacity: usize = self.main_panel.spinner("parking_capacity"); + + let mut edits = app.primary.map.get_edits().clone(); + let old = app.primary.map.get_b_edit(self.b); + let mut new = old.clone(); + + new.parking = match parking_type { + "public" => OffstreetParking::PublicGarage( + "untitled public garage".to_string(), + parking_capacity, + ), + "private" => { + OffstreetParking::Private(parking_capacity, parking_capacity > 0) + } + _ => unreachable!("unknown parking type received: {}", parking_type), + }; + + edits.commands.push(EditCmd::ChangeBuilding { + b: self.b, + old, + new, + }); + apply_map_edits(ctx, app, edits); + self.redo_stack.clear(); + + panels_need_recalc = true; + } + "parking_capacity" => { + let parking_capacity: usize = self.main_panel.spinner("parking_capacity"); + + let mut edits = app.primary.map.get_edits().clone(); + let old = app.primary.map.get_b_edit(self.b); + let mut new = old.clone(); + new.parking = match old.parking { + OffstreetParking::Private(_, has_parking) => { + OffstreetParking::Private(parking_capacity, has_parking) + } + OffstreetParking::PublicGarage(ref name, _) => { + OffstreetParking::PublicGarage(name.to_string(), parking_capacity) + } + }; + edits.commands.push(EditCmd::ChangeBuilding { + b: self.b, + old, + new, + }); + apply_map_edits(ctx, app, edits); + self.redo_stack.clear(); + + panels_need_recalc = true; + } + "new_garage_name" => { + let new_garage_name = self.main_panel.text_box("new_garage_name"); + + let mut edits = app.primary.map.get_edits().clone(); + let old = app.primary.map.get_b_edit(self.b); + let mut new = old.clone(); + + new.parking = match old.parking { + OffstreetParking::Private(_, _) => { + unreachable!("Garage name can only be edited if it is public"); + } + OffstreetParking::PublicGarage(_, size) => { + OffstreetParking::PublicGarage(new_garage_name, size) + } + }; + edits.commands.push(EditCmd::ChangeBuilding { + b: self.b, + old, + new, + }) + } + _ => unreachable!("received unknown change key: {}", x), + }, + _ => debug!("main_panel had unhandled outcome"), + } + + if panels_need_recalc { + self.recalc_all_panels(ctx, app); + } + + Transition::Keep + } + + fn draw(&self, g: &mut widgetry::GfxCtx, _: &App) { + self.top_panel.draw(g); + self.main_panel.draw(g); + } +} + +fn make_top_panel( + ctx: &mut EventCtx, + app: &App, + num_edit_cmds_originally: usize, + no_redo_cmds: bool, + b: BuildingID, +) -> Panel { + let map = &app.primary.map; + + Panel::new_builder(Widget::col(vec![ + Widget::row(vec![ + Line(format!("Edit {}", b)).small_heading().into_widget(ctx), + ctx.style() + .btn_plain + .icon("system/assets/tools/location.svg") + .build_widget(ctx, "jump to building"), + ]), + 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(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) +} + +fn make_main_panel(ctx: &mut EventCtx, app: &App, b: BuildingID) -> Panel { + let map = &app.primary.map; + let current_state = map.get_b_edit(b); + let current_parking_capacity = match current_state.parking { + OffstreetParking::Private(count, true) | OffstreetParking::PublicGarage(_, count) => count, + OffstreetParking::Private(_, false) => 0, + }; + + let mut fields = vec![ + Widget::row(vec![ + Line("Parking type") + .secondary() + .into_widget(ctx) + .centered_vert(), + Widget::dropdown( + ctx, + "parking_type", + current_state.parking.get_variant_name(), + parking_type_choices(), + ), + ]), + Widget::row(vec![ + Line("Parking capacity") + .secondary() + .into_widget(ctx) + .centered_vert(), + Spinner::widget( + ctx, + "parking_capacity", + (0, 999_999), + current_parking_capacity, + 1, + ), + ]), + ]; + + if let OffstreetParking::PublicGarage(name, _) = current_state.parking { + fields.push(Widget::row(vec![ + Line("Garage Name") + .secondary() + .into_widget(ctx) + .centered_vert(), + TextBox::widget(ctx, "new_garage_name", name, false, 100), + ])); + } + + Panel::new_builder(Widget::col(fields)) + .aligned(HorizontalAlignment::Left, VerticalAlignment::Center) + .build(ctx) +} + +fn parking_type_choices() -> Vec> { + let choices = vec!["public", "private"]; + choices + .into_iter() + .map(|choice| Choice::new(choice.to_string(), choice.to_string())) + .collect() +} diff --git a/apps/game/src/edit/mod.rs b/apps/game/src/edit/mod.rs index 33b1824cfc..5dcc76f7fc 100644 --- a/apps/game/src/edit/mod.rs +++ b/apps/game/src/edit/mod.rs @@ -14,6 +14,7 @@ use widgetry::{ Menu, Outcome, Panel, State, Text, TextBox, TextExt, VerticalAlignment, Widget, }; +pub use self::buildings::BuildingEditor; pub use self::roads::RoadEditor; pub use self::routes::RouteEditor; pub use self::stop_signs::StopSignEditor; @@ -24,6 +25,7 @@ use crate::common::{tool_panel, CommonState, Warping}; use crate::debug::DebugMode; use crate::sandbox::{GameplayMode, SandboxMode, TimeWarpScreen}; +mod buildings; mod crosswalks; mod multiple_roads; mod roads; @@ -784,6 +786,10 @@ pub fn apply_map_edits(ctx: &mut EventCtx, app: &mut App, edits: MapEdits) { app.primary.draw_map.get_pl(pl).clear_rendering(); } + for b in effects.changed_buildings { + app.primary.draw_map.recreate_building(b, &app.primary.map); + } + if app.primary.layer.as_ref().and_then(|l| l.name()) == Some("map edits") { app.primary.layer = Some(Box::new(crate::layer::map::Static::edits(ctx, app))); } @@ -940,6 +946,7 @@ fn cmd_to_id(cmd: &EditCmd) -> Option { EditCmd::ChangeRoad { r, .. } => Some(ID::Road(*r)), EditCmd::ChangeIntersection { i, .. } => Some(ID::Intersection(*i)), EditCmd::ChangeRouteSchedule { .. } => None, + EditCmd::ChangeBuilding { b, .. } => Some(ID::Building(*b)), } } diff --git a/apps/game/src/sandbox/gameplay/mod.rs b/apps/game/src/sandbox/gameplay/mod.rs index 8c909c793a..a32d93d469 100644 --- a/apps/game/src/sandbox/gameplay/mod.rs +++ b/apps/game/src/sandbox/gameplay/mod.rs @@ -200,6 +200,7 @@ impl GameplayMode { } } EditCmd::ChangeRouteSchedule { .. } => {} + EditCmd::ChangeBuilding { .. } => {} } } true diff --git a/apps/game/src/sandbox/mod.rs b/apps/game/src/sandbox/mod.rs index fa0fb788cb..27ac7ef274 100644 --- a/apps/game/src/sandbox/mod.rs +++ b/apps/game/src/sandbox/mod.rs @@ -21,7 +21,8 @@ use crate::app::{App, Transition}; use crate::common::{tool_panel, CommonState}; use crate::debug::DebugMode; use crate::edit::{ - can_edit_lane, EditMode, RoadEditor, SaveEdits, StopSignEditor, TrafficSignalEditor, + can_edit_lane, BuildingEditor, EditMode, RoadEditor, SaveEdits, StopSignEditor, + TrafficSignalEditor, }; use crate::info::ContextualActions; use crate::layer::favorites::{Favorites, ShowFavorites}; @@ -338,6 +339,7 @@ impl ContextualActions for Actions { } else { actions.push((Key::F, "add this building to favorites".to_string())); } + actions.push((Key::E, "edit the parking of this building".to_string())); } _ => {} } @@ -396,6 +398,10 @@ impl ContextualActions for Actions { app.primary.layer = Some(Box::new(ShowFavorites::new(ctx, app))); Transition::Keep } + (ID::Building(b), "edit the parking of this building") => Transition::Multi(vec![ + Transition::Push(EditMode::new_state(ctx, app, self.gameplay.clone())), + Transition::Push(BuildingEditor::new_state(ctx, app, b)), + ]), (_, "follow (run the simulation)") => { *close_panel = false; Transition::ModifyState(Box::new(|state, ctx, app| { diff --git a/map_gui/src/render/building.rs b/map_gui/src/render/building.rs index dfcd83ac99..e5418d9611 100644 --- a/map_gui/src/render/building.rs +++ b/map_gui/src/render/building.rs @@ -206,6 +206,13 @@ impl DrawBuilding { } } + pub fn new_empty(id: BuildingID) -> DrawBuilding { + DrawBuilding { + id, + label: RefCell::new(None), + } + } + pub fn clear_rendering(&mut self) { *self.label.borrow_mut() = None; } diff --git a/map_gui/src/render/map.rs b/map_gui/src/render/map.rs index 7ba30c9fb7..ed96303a9d 100644 --- a/map_gui/src/render/map.rs +++ b/map_gui/src/render/map.rs @@ -514,6 +514,15 @@ impl DrawMap { self.roads[road.id.0] = draw; } + pub fn recreate_building(&mut self, b: BuildingID, map: &Map) { + self.quadtree.remove(ID::Building(b)).unwrap(); + + let draw = DrawBuilding::new_empty(b); + self.quadtree + .insert_with_box(draw.get_id(), draw.get_bounds(map)); + self.buildings[b.0] = draw; + } + pub fn free_memory(&mut self) { // Clear the lazily evaluated zoomed-in details for r in &mut self.roads { @@ -528,5 +537,8 @@ impl DrawMap { for pl in &mut self.parking_lots { pl.clear_rendering(); } + for b in &mut self.buildings { + b.clear_rendering(); + } } } diff --git a/map_model/src/edits/apply.rs b/map_model/src/edits/apply.rs index 2ff1dab566..25b825d3ad 100644 --- a/map_model/src/edits/apply.rs +++ b/map_model/src/edits/apply.rs @@ -45,6 +45,7 @@ impl Map { deleted_turns: BTreeSet::new(), changed_parking_lots: BTreeSet::new(), modified_lanes: BTreeSet::new(), + changed_buildings: BTreeSet::new(), }; // Short-circuit to avoid marking pathfinder_dirty @@ -273,6 +274,13 @@ impl EditCmd { EditCmd::ChangeRouteSchedule { id, new, .. } => { map.transit_routes[id.0].spawn_times = new.clone(); } + EditCmd::ChangeBuilding { b, ref new, .. } => { + let old_state = map.get_b_edit(*b); + if old_state == new.clone() { + return; + } + map.buildings[b.0].parking = new.parking.clone() + } } } @@ -293,6 +301,11 @@ impl EditCmd { old: new, new: old, }, + EditCmd::ChangeBuilding { b, old, new } => EditCmd::ChangeBuilding { + b, + old: new, + new: old, + }, } } } diff --git a/map_model/src/edits/mod.rs b/map_model/src/edits/mod.rs index 1c0b04a337..58e28efa9e 100644 --- a/map_model/src/edits/mod.rs +++ b/map_model/src/edits/mod.rs @@ -13,9 +13,9 @@ use osm2streets::{get_lane_specs_ltr, RestrictionType}; pub use self::perma::PermanentMapEdits; use crate::{ - AccessRestrictions, ControlStopSign, ControlTrafficSignal, Crossing, DiagonalFilter, - IntersectionControl, IntersectionID, LaneID, LaneSpec, Map, MapConfig, ParkingLotID, Road, - RoadFilter, RoadID, TransitRouteID, TurnID, TurnType, + AccessRestrictions, BuildingID, ControlStopSign, ControlTrafficSignal, Crossing, + DiagonalFilter, IntersectionControl, IntersectionID, LaneID, LaneSpec, Map, MapConfig, + OffstreetParking, ParkingLotID, Road, RoadFilter, RoadID, TransitRouteID, TurnID, TurnType, }; mod apply; @@ -36,6 +36,7 @@ pub struct MapEdits { pub original_roads: BTreeMap, pub original_intersections: BTreeMap, pub changed_routes: BTreeSet, + pub original_buildings: BTreeMap, /// Some edits are included in the game by default, in data/system/proposals, as "community /// proposals." They require a description and may have a link to a write-up. @@ -60,12 +61,18 @@ pub enum EditCmd { old: Vec