Lift the freehand filter tool as a main edit mode.

We need a way to reset EditMode when we leave the state, no matter how
it happens. There are some bugs now with fiddling with proposals in the
middle of drawing!
This commit is contained in:
Dustin Carlino 2022-07-19 11:35:20 +02:00
parent ad5ce6c088
commit 8580485a36
8 changed files with 112 additions and 129 deletions

View File

@ -31,6 +31,9 @@ impl BrowseNeighbourhoods {
if let EditMode::Shortcuts(ref mut maybe_focus) = app.session.edit_mode {
*maybe_focus = None;
}
if let EditMode::FreehandFilters(_) = app.session.edit_mode {
app.session.edit_mode = EditMode::Filters;
}
let (world, draw_over_roads) =
ctx.loading_screen("calculate neighbourhoods", |ctx, timer| {

View File

@ -1,95 +0,0 @@
use std::collections::BTreeSet;
use geom::PolyLine;
use map_model::{IntersectionID, Perimeter};
use widgetry::tools::PolyLineLasso;
use widgetry::{DrawBaselayer, EventCtx, GfxCtx, Key, Line, ScreenPt, State, Text, Widget};
use crate::{after_edit, App, DiagonalFilter, Neighbourhood, Transition};
pub struct FreehandFilters {
lasso: PolyLineLasso,
perimeter: Perimeter,
interior_intersections: BTreeSet<IntersectionID>,
instructions: Text,
instructions_at: ScreenPt,
}
impl FreehandFilters {
pub fn new_state(
ctx: &EventCtx,
neighbourhood: &Neighbourhood,
instructions_at: ScreenPt,
) -> Box<dyn State<App>> {
Box::new(Self {
lasso: PolyLineLasso::new(),
perimeter: neighbourhood.orig_perimeter.clone(),
interior_intersections: neighbourhood.interior_intersections.clone(),
instructions_at,
instructions: Text::from_all(vec![
Line("Click and drag").fg(ctx.style().text_hotkey_color),
Line(" across the roads you want to filter"),
]),
})
}
pub fn button(ctx: &EventCtx) -> Widget {
ctx.style()
.btn_outline
.icon_text(
"system/assets/tools/select.svg",
"Create filters along a shape",
)
.hotkey(Key::F)
.build_def(ctx)
}
fn make_filters_along_path(&self, ctx: &mut EventCtx, app: &mut App, path: PolyLine) {
app.session.modal_filters.before_edit();
for r in &self.perimeter.interior {
if app.session.modal_filters.roads.contains_key(r) {
continue;
}
let road = app.map.get_r(*r);
// Don't show error messages
if road.oneway_for_driving().is_some() || road.is_deadend_for_driving(&app.map) {
continue;
}
if let Some((pt, _)) = road.center_pts.intersection(&path) {
let dist = road
.center_pts
.dist_along_of_point(pt)
.map(|pair| pair.0)
.unwrap_or(road.center_pts.length() / 2.0);
app.session.modal_filters.roads.insert(*r, dist);
}
}
for i in &self.interior_intersections {
if app.map.get_i(*i).polygon.intersects_polyline(&path) {
// We probably won't guess the right one, but make an attempt
DiagonalFilter::cycle_through_alternatives(app, *i);
}
}
after_edit(ctx, app);
}
}
impl State<App> for FreehandFilters {
fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
if let Some(pl) = self.lasso.event(ctx) {
self.make_filters_along_path(ctx, app, pl);
return Transition::Multi(vec![Transition::Pop, Transition::Recreate]);
}
Transition::Keep
}
fn draw(&self, g: &mut GfxCtx, _: &App) {
self.lasso.draw(g);
// Hacky, but just draw instructions over the other panel
g.draw_tooltip_at(self.instructions.clone(), self.instructions_at);
}
fn draw_baselayer(&self) -> DrawBaselayer {
DrawBaselayer::PreviousState
}
}

View File

@ -1,8 +1,6 @@
mod about;
mod freehand_filters;
mod left_panel;
mod top_panel;
pub use freehand_filters::FreehandFilters;
pub use left_panel::LeftPanel;
pub use top_panel::TopPanel;

View File

@ -144,13 +144,10 @@ impl State<App> for Viewer {
return Transition::Keep;
}
match self.edit.handle_panel_action(
ctx,
app,
x.as_ref(),
&self.neighbourhood,
&self.left_panel,
) {
match self
.edit
.handle_panel_action(ctx, app, x.as_ref(), &self.neighbourhood)
{
// Fall through to AltProposals
EditOutcome::Nothing => {}
EditOutcome::UpdatePanelAndWorld => {
@ -235,6 +232,10 @@ impl State<App> for Viewer {
if self.left_panel.currently_hovering() == Some(&"warning".to_string()) {
g.redraw(&self.show_error);
}
if let EditMode::FreehandFilters(ref lasso) = app.session.edit_mode {
lasso.draw(g);
}
}
fn recreate(&mut self, ctx: &mut EventCtx, app: &mut App) -> Box<dyn State<App>> {

View File

@ -7,19 +7,16 @@ use super::{EditOutcome, Obj};
use crate::{after_edit, colors, App, DiagonalFilter, Neighbourhood};
pub fn widget(ctx: &mut EventCtx) -> Widget {
Widget::col(vec![
Widget::row(vec![
Image::from_path("system/assets/tools/pencil.svg")
.into_widget(ctx)
.centered_vert(),
Text::from(Line(
"Click a road or intersection to add or remove a modal filter",
))
.wrap_to_pct(ctx, 15)
.into_widget(ctx),
]),
crate::components::FreehandFilters::button(ctx),
])
Widget::col(vec![Widget::row(vec![
Image::from_path("system/assets/tools/pencil.svg")
.into_widget(ctx)
.centered_vert(),
Text::from(Line(
"Click a road or intersection to add or remove a modal filter",
))
.wrap_to_pct(ctx, 15)
.into_widget(ctx),
])])
}
/// Creates clickable objects for managing filters on roads and intersections. Everything is

View File

@ -0,0 +1,63 @@
use geom::PolyLine;
use widgetry::{EventCtx, Line, Text, Widget};
use crate::edit::{EditMode, EditOutcome};
use crate::{after_edit, App, DiagonalFilter, Neighbourhood, Transition};
pub fn widget(ctx: &mut EventCtx) -> Widget {
Text::from_all(vec![
Line("Click and drag").fg(ctx.style().text_hotkey_color),
Line(" across the roads you want to filter"),
])
.into_widget(ctx)
}
pub fn event(ctx: &mut EventCtx, app: &mut App, neighbourhood: &Neighbourhood) -> EditOutcome {
if let EditMode::FreehandFilters(ref mut lasso) = app.session.edit_mode {
if let Some(pl) = lasso.event(ctx) {
make_filters_along_path(ctx, app, neighbourhood, pl);
// Reset the tool
app.session.edit_mode = EditMode::Filters;
EditOutcome::Transition(Transition::Recreate)
} else {
// Do this instead of EditOutcome::Nothing to interrupt other processing
EditOutcome::Transition(Transition::Keep)
}
} else {
unreachable!()
}
}
fn make_filters_along_path(
ctx: &mut EventCtx,
app: &mut App,
neighbourhood: &Neighbourhood,
path: PolyLine,
) {
app.session.modal_filters.before_edit();
for r in &neighbourhood.orig_perimeter.interior {
if app.session.modal_filters.roads.contains_key(r) {
continue;
}
let road = app.map.get_r(*r);
// Don't show error messages
if road.oneway_for_driving().is_some() || road.is_deadend_for_driving(&app.map) {
continue;
}
if let Some((pt, _)) = road.center_pts.intersection(&path) {
let dist = road
.center_pts
.dist_along_of_point(pt)
.map(|pair| pair.0)
.unwrap_or(road.center_pts.length() / 2.0);
app.session.modal_filters.roads.insert(*r, dist);
}
}
for i in &neighbourhood.interior_intersections {
if app.map.get_i(*i).polygon.intersects_polyline(&path) {
// We probably won't guess the right one, but make an attempt
DiagonalFilter::cycle_through_alternatives(app, *i);
}
}
after_edit(ctx, app);
}

View File

@ -1,16 +1,18 @@
mod filters;
mod freehand_filters;
mod one_ways;
mod shortcuts;
use map_model::{IntersectionID, RoadID};
use widgetry::mapspace::{ObjectID, World};
use widgetry::tools::PopupMsg;
use widgetry::tools::{PolyLineLasso, PopupMsg};
use widgetry::{lctrl, EventCtx, Key, Line, Panel, PanelBuilder, TextExt, Widget};
use crate::{after_edit, App, BrowseNeighbourhoods, Neighbourhood, Transition};
pub enum EditMode {
Filters,
FreehandFilters(PolyLineLasso),
Oneways,
// Is a road clicked on right now?
Shortcuts(Option<shortcuts::FocusedRoad>),
@ -58,6 +60,7 @@ impl EditNeighbourhood {
Self {
world: match &app.session.edit_mode {
EditMode::Filters => filters::make_world(ctx, app, neighbourhood),
EditMode::FreehandFilters(_) => World::unbounded(),
EditMode::Oneways => one_ways::make_world(ctx, app, neighbourhood),
EditMode::Shortcuts(focus) => shortcuts::make_world(ctx, app, neighbourhood, focus),
},
@ -81,6 +84,7 @@ impl EditNeighbourhood {
edit_mode(ctx, &app.session.edit_mode),
match app.session.edit_mode {
EditMode::Filters => filters::widget(ctx),
EditMode::FreehandFilters(_) => freehand_filters::widget(ctx),
EditMode::Oneways => one_ways::widget(ctx),
EditMode::Shortcuts(ref focus) => shortcuts::widget(ctx, app, focus.as_ref()),
},
@ -126,9 +130,14 @@ impl EditNeighbourhood {
app: &mut App,
neighbourhood: &Neighbourhood,
) -> EditOutcome {
if let EditMode::FreehandFilters(_) = app.session.edit_mode {
return freehand_filters::event(ctx, app, neighbourhood);
}
let outcome = self.world.event(ctx);
let outcome = match app.session.edit_mode {
EditMode::Filters => filters::handle_world_outcome(ctx, app, outcome),
EditMode::FreehandFilters(_) => unreachable!(),
EditMode::Oneways => one_ways::handle_world_outcome(ctx, app, outcome),
EditMode::Shortcuts(_) => shortcuts::handle_world_outcome(app, outcome, neighbourhood),
};
@ -144,7 +153,6 @@ impl EditNeighbourhood {
app: &mut App,
action: &str,
neighbourhood: &Neighbourhood,
panel: &Panel,
) -> EditOutcome {
let id = neighbourhood.id;
match action {
@ -157,14 +165,6 @@ impl EditNeighbourhood {
"Adjust boundary" => EditOutcome::Transition(Transition::Replace(
crate::select_boundary::SelectBoundary::new_state(ctx, app, id),
)),
// Overkill to force all mode-specific code into the module
"Create filters along a shape" => EditOutcome::Transition(Transition::Push(
crate::components::FreehandFilters::new_state(
ctx,
neighbourhood,
panel.center_of("Create filters along a shape"),
),
)),
"undo" => {
let prev = app.session.modal_filters.previous_version.take().unwrap();
app.session.modal_filters = prev;
@ -173,6 +173,9 @@ impl EditNeighbourhood {
if let EditMode::Shortcuts(ref mut maybe_focus) = app.session.edit_mode {
*maybe_focus = None;
}
if let EditMode::FreehandFilters(_) = app.session.edit_mode {
app.session.edit_mode = EditMode::Filters;
}
EditOutcome::Transition(Transition::Recreate)
}
"Plan a route" => EditOutcome::Transition(Transition::Push(
@ -182,6 +185,10 @@ impl EditNeighbourhood {
app.session.edit_mode = EditMode::Filters;
EditOutcome::UpdatePanelAndWorld
}
"Freehand filters" => {
app.session.edit_mode = EditMode::FreehandFilters(PolyLineLasso::new());
EditOutcome::UpdatePanelAndWorld
}
"One-ways" => {
app.session.edit_mode = EditMode::Oneways;
EditOutcome::UpdatePanelAndWorld
@ -211,10 +218,16 @@ fn edit_mode(ctx: &mut EventCtx, edit_mode: &EditMode) -> Widget {
let mut row = Vec::new();
for (label, key, is_current) in [
("Filters", Key::F1, matches!(edit_mode, EditMode::Filters)),
("One-ways", Key::F2, matches!(edit_mode, EditMode::Oneways)),
// system/assets/tools/select.svg when we have icons
(
"Freehand filters",
Key::F2,
matches!(edit_mode, EditMode::FreehandFilters(_)),
),
("One-ways", Key::F3, matches!(edit_mode, EditMode::Oneways)),
(
"Shortcuts",
Key::F3,
Key::F4,
matches!(edit_mode, EditMode::Shortcuts(_)),
),
] {

View File

@ -45,6 +45,9 @@ impl SelectBoundary {
if let EditMode::Shortcuts(ref mut maybe_focus) = app.session.edit_mode {
*maybe_focus = None;
}
if let EditMode::FreehandFilters(_) = app.session.edit_mode {
app.session.edit_mode = EditMode::Filters;
}
let top_panel = crate::components::TopPanel::panel(ctx, app);
let left_panel = make_panel(ctx, app, id, &top_panel);