Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 39 additions & 41 deletions game/src/common/route_sketcher.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
use geom::{Circle, Distance, FindClosest};
use map_model::{IntersectionID, Map, PathConstraints, RoadID};
use widgetry::{Color, Drawable, EventCtx, GeomBatch, GfxCtx, Line, TextExt, Widget};
use widgetry::mapspace::DrawUnzoomedShapes;
use widgetry::{Color, EventCtx, GfxCtx, TextExt, Widget};

use crate::app::App;

const INTERSECTON_RADIUS: Distance = Distance::const_meters(10.0);

// TODO Supercede RoadSelector, probably..
pub struct RouteSketcher {
snap_to_intersections: FindClosest<IntersectionID>,
route: Route,
mode: Mode,
preview: Drawable,
preview: DrawUnzoomedShapes,
}

impl RouteSketcher {
pub fn new(ctx: &mut EventCtx, app: &App) -> RouteSketcher {
pub fn new(app: &App) -> RouteSketcher {
let mut snap_to_intersections = FindClosest::new(app.primary.map.get_bounds());
for i in app.primary.map.all_intersections() {
snap_to_intersections.add(i.id, i.polygon.points());
Expand All @@ -23,14 +26,15 @@ impl RouteSketcher {
snap_to_intersections,
route: Route::new(),
mode: Mode::Neutral,
preview: Drawable::empty(ctx),
preview: DrawUnzoomedShapes::empty(),
}
}

fn mouseover_i(&self, ctx: &EventCtx) -> Option<IntersectionID> {
let pt = ctx.canvas.get_cursor_in_map_space()?;
// When zoomed really far out, it's harder to click small intersections, so snap more
// aggressively.
// aggressively. Note this should always be a larger hitbox than how the waypoint circles
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still want to convert this code to use World instead of handling dragging directly, but not urgent.

// are drawn.
let threshold = Distance::meters(30.0) / ctx.canvas.cam_zoom;
let (i, _) = self.snap_to_intersections.closest_pt(pt, threshold)?;
// After we have a path started, only snap to points on the path to drag them
Expand Down Expand Up @@ -97,68 +101,62 @@ impl RouteSketcher {
}
}

fn update_preview(&mut self, ctx: &mut EventCtx, app: &App) {
fn update_preview(&mut self, app: &App) {
let map = &app.primary.map;
let mut batch = GeomBatch::new();
let mut shapes = DrawUnzoomedShapes::builder();

// Draw the confirmed route
for pair in self.route.full_path.windows(2) {
// TODO Inefficient!
let r = map.find_road_between(pair[0], pair[1]).unwrap();
batch.push(Color::RED.alpha(0.5), map.get_r(r).get_thick_polygon());
let r = map.get_r(map.find_road_between(pair[0], pair[1]).unwrap());
shapes.add_line(r.center_pts.clone(), r.get_width(), Color::RED.alpha(0.5));
}
for i in &self.route.full_path {
batch.push(
shapes.add_circle(
map.get_i(*i).polygon.center(),
INTERSECTON_RADIUS,
Color::BLUE.alpha(0.5),
Circle::new(map.get_i(*i).polygon.center(), Distance::meters(10.0)).to_polygon(),
);
}

// Debugging
if false {
let mut cnt = 0;
for i in &self.route.waypoints {
cnt += 1;
batch.push(
Color::RED,
Circle::new(map.get_i(*i).polygon.center(), Distance::meters(10.0))
.to_polygon(),
);
batch.append(
widgetry::Text::from(Line(format!("{}", cnt)))
.render(ctx)
.centered_on(map.get_i(*i).polygon.center()),
);
}
}

// Draw the current operation
if let Mode::Hovering(i) = self.mode {
batch.push(
shapes.add_circle(
map.get_i(i).polygon.center(),
INTERSECTON_RADIUS,
Color::BLUE,
Circle::new(map.get_i(i).polygon.center(), Distance::meters(10.0)).to_polygon(),
);
if self.route.waypoints.len() == 1 {
if let Some((roads, intersections)) =
map.simple_path_btwn_v2(self.route.waypoints[0], i, PathConstraints::Car)
{
for r in roads {
batch.push(Color::BLUE.alpha(0.5), map.get_r(r).get_thick_polygon());
let r = map.get_r(r);
shapes.add_line(
r.center_pts.clone(),
r.get_width(),
Color::BLUE.alpha(0.5),
);
}
for i in intersections {
batch.push(Color::BLUE.alpha(0.5), map.get_i(i).polygon.clone());
shapes.add_circle(
map.get_i(i).polygon.center(),
INTERSECTON_RADIUS,
Color::BLUE.alpha(0.5),
);
}
}
}
}
if let Mode::Dragging { at, .. } = self.mode {
batch.push(
shapes.add_circle(
map.get_i(at).polygon.center(),
INTERSECTON_RADIUS,
Color::BLUE,
Circle::new(map.get_i(at).polygon.center(), Distance::meters(10.0)).to_polygon(),
);
}

self.preview = batch.upload(ctx);
self.preview = shapes.build();
}

pub fn get_widget_to_describe(&self, ctx: &mut EventCtx) -> Widget {
Expand Down Expand Up @@ -197,31 +195,31 @@ impl RouteSketcher {
let orig_mode = self.mode.clone();
self.update_mode(ctx, app);
if self.route != orig_route || self.mode != orig_mode {
self.update_preview(ctx, app);
self.update_preview(app);
true
} else {
false
}
}

/// True if something changed. False if this component doesn't even handle that kind of click.
pub fn on_click(&mut self, ctx: &EventCtx, x: &str) -> bool {
pub fn on_click(&mut self, x: &str) -> bool {
if x == "Start over" {
self.route = Route::new();
self.mode = Mode::Neutral;
self.preview = Drawable::empty(ctx);
self.preview = DrawUnzoomedShapes::empty();
return true;
}
false
}

pub fn draw(&self, g: &mut GfxCtx) {
g.redraw(&self.preview);
self.preview.draw(g);
if matches!(self.mode, Mode::Dragging { .. }) {
if let Some(pt) = g.canvas.get_cursor_in_map_space() {
g.draw_polygon(
Color::BLUE.alpha(0.5),
Circle::new(pt, Distance::meters(10.0)).to_polygon(),
Circle::new(pt, INTERSECTON_RADIUS).to_polygon(),
);
}
}
Expand Down
66 changes: 20 additions & 46 deletions game/src/ungap/bike_network.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use std::cell::RefCell;
use std::collections::HashMap;

use map_model::{LaneType, PathConstraints, Road};
use widgetry::{Color, Drawable, GeomBatch, GfxCtx};
use widgetry::mapspace::DrawUnzoomedShapes;
use widgetry::{Color, Drawable, EventCtx, GeomBatch, GfxCtx};

use crate::app::App;

Expand All @@ -15,50 +15,15 @@ lazy_static::lazy_static! {

/// Shows the bike network while unzoomed. Handles thickening the roads at low zoom levels.
pub struct DrawNetworkLayer {
per_zoom: RefCell<[Option<Drawable>; 11]>,
draw_roads: DrawUnzoomedShapes,
draw_intersections: Drawable,
}

impl DrawNetworkLayer {
pub fn new() -> DrawNetworkLayer {
DrawNetworkLayer {
per_zoom: Default::default(),
}
}

pub fn draw(&self, g: &mut GfxCtx, app: &App) {
let (zoom, idx) = DrawNetworkLayer::discretize_zoom(g.canvas.cam_zoom);
let value = &mut self.per_zoom.borrow_mut()[idx];
if value.is_none() {
*value = Some(DrawNetworkLayer::render_network_layer(g, app, zoom));
}
g.redraw(value.as_ref().unwrap());
}

// Continuously changing road width as we zoom looks great, but it's terribly slow. We'd have
// to move line thickening into the shader to do it better. So recalculate with less
// granularity.
fn discretize_zoom(zoom: f64) -> (f64, usize) {
if zoom >= 1.0 {
return (1.0, 10);
}
let rounded = (zoom * 10.0).round();
let idx = rounded as usize;
(rounded / 10.0, idx)
}

fn render_network_layer(g: &mut GfxCtx, app: &App, zoom: f64) -> Drawable {
let mut batch = GeomBatch::new();
let map = &app.primary.map;

// Thicker lines as we zoom out. Scale up to 5x. Never shrink past the road's actual width.
let mut thickness = (0.5 / zoom).max(1.0);
// And on gigantic maps, zoom may approach 0, so avoid NaNs.
if !thickness.is_finite() {
thickness = 5.0;
}

pub fn new(ctx: &EventCtx, app: &App) -> DrawNetworkLayer {
let mut lines = DrawUnzoomedShapes::builder();
let mut intersections = HashMap::new();
for r in map.all_roads() {
for r in app.primary.map.all_roads() {
let mut bike_lane = false;
let mut buffer = false;
for l in &r.lanes {
Expand All @@ -69,7 +34,7 @@ impl DrawNetworkLayer {
}
}

let color = if map.get_edits().changed_roads.contains(&r.id) {
let color = if app.primary.map.get_edits().changed_roads.contains(&r.id) {
Color::CYAN
} else if r.is_cycleway() {
*DEDICATED_TRAIL
Expand All @@ -83,19 +48,28 @@ impl DrawNetworkLayer {
continue;
};

batch.push(color, r.center_pts.make_polygons(thickness * r.get_width()));
lines.add_line(r.center_pts.clone(), r.get_width(), color);

// Arbitrarily pick a color when two different types of roads meet
intersections.insert(r.src_i, color);
intersections.insert(r.dst_i, color);
}

let mut batch = GeomBatch::new();
for (i, color) in intersections {
// No clear way to thicken the intersection at different zoom levels
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot from 2021-10-18 15-49-07
The "choppiness" is annoying. The nice thing would be to merge contiguous chunk of a bike path along multiple road segments into one PolyLine and thicken the whole thing.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯 agree!

batch.push(color, map.get_i(i).polygon.clone());
batch.push(color, app.primary.map.get_i(i).polygon.clone());
}

g.upload(batch)
DrawNetworkLayer {
draw_roads: lines.build(),
draw_intersections: ctx.upload(batch),
}
}

pub fn draw(&self, g: &mut GfxCtx) {
g.redraw(&self.draw_intersections);
self.draw_roads.draw(g);
}
}

Expand Down
8 changes: 4 additions & 4 deletions game/src/ungap/layers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ impl Layers {
let mut l = Layers {
panel: Panel::empty(ctx),
minimized: true,
bike_network: Some(DrawNetworkLayer::new()),
bike_network: Some(DrawNetworkLayer::new(ctx, app)),
labels: Some(DrawRoadLabels::new()),
elevation: false,
steep_streets: None,
Expand All @@ -59,7 +59,7 @@ impl Layers {
if self.map_edit_key != key {
self.map_edit_key = key;
if self.bike_network.is_some() {
self.bike_network = Some(DrawNetworkLayer::new());
self.bike_network = Some(DrawNetworkLayer::new(ctx, app));
}
self.road_types.clear();
}
Expand Down Expand Up @@ -127,7 +127,7 @@ impl Layers {
Outcome::Changed(x) => match x.as_ref() {
"bike network" => {
if self.panel.is_checked("bike network") {
self.bike_network = Some(DrawNetworkLayer::new());
self.bike_network = Some(DrawNetworkLayer::new(ctx, app));
} else {
self.bike_network = None;
}
Expand Down Expand Up @@ -212,7 +212,7 @@ impl Layers {
}
if draw_bike_layer {
if let Some(ref n) = self.bike_network {
n.draw(g, app);
n.draw(g);
}
}

Expand Down
6 changes: 3 additions & 3 deletions game/src/ungap/quick_sketch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ impl QuickSketch {
let mut qs = QuickSketch {
top_panel: Panel::empty(ctx),
layers,
route_sketcher: RouteSketcher::new(ctx, app),
route_sketcher: RouteSketcher::new(app),
};
qs.update_top_panel(ctx, app);
Box::new(qs)
Expand Down Expand Up @@ -88,13 +88,13 @@ impl State<App> for QuickSketch {
self.route_sketcher.all_roads(app),
self.top_panel.dropdown_value("buffer type"),
);
self.route_sketcher = RouteSketcher::new(ctx, app);
self.route_sketcher = RouteSketcher::new(app);
self.update_top_panel(ctx, app);
return Transition::Push(PopupMsg::new_state(ctx, "Changes made", messages));
}
x => {
// TODO More brittle routing of outcomes.
if self.route_sketcher.on_click(ctx, x) {
if self.route_sketcher.on_click(x) {
self.update_top_panel(ctx, app);
return Transition::Keep;
}
Expand Down
2 changes: 2 additions & 0 deletions widgetry/src/mapspace/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
mod unzoomed;
mod world;

use crate::{Drawable, EventCtx, GeomBatch, GfxCtx, RewriteColor};
pub use unzoomed::DrawUnzoomedShapes;
pub use world::{DummyID, ObjectID, World, WorldOutcome};

/// Draws one of two versions of something, based on whether the canvas is zoomed in past a threshold.
Expand Down
Loading