Render A/B Street's lanes and traffic simulation on top of Mapbox GL (#788)

[rebuild] [release]
This commit is contained in:
Dustin Carlino 2021-10-31 13:52:58 -07:00 committed by GitHub
parent 04b54b08cd
commit 00df96f173
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 751 additions and 91 deletions

18
Cargo.lock generated
View File

@ -2889,6 +2889,24 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468"
[[package]]
name = "piggyback"
version = "0.1.0"
dependencies = [
"abstio",
"abstutil",
"geom",
"getrandom",
"js-sys",
"log",
"map_gui",
"map_model",
"sim",
"wasm-bindgen",
"web-sys",
"widgetry",
]
[[package]]
name = "pin-project-lite"
version = "0.2.6"

View File

@ -17,6 +17,7 @@ members = [
"map_model",
"osm_viewer",
"parking_mapper",
"piggyback",
"popdat",
"santa",
"sim",

View File

@ -163,7 +163,13 @@ impl App {
}
let mut cache = self.primary.agents.borrow_mut();
cache.draw_unzoomed_agents(g, self);
cache.draw_unzoomed_agents(
g,
&self.primary.map,
&self.primary.sim,
&self.cs,
&self.opts,
);
if let Some(a) = self
.primary
@ -419,10 +425,8 @@ impl App {
borrows.extend(bus_stops);
// Expand all of the Traversables into agents, populating the cache if needed.
{
for on in &agents_on {
agents.populate_if_needed(*on, map, &self.primary.sim, &self.cs, prerender);
}
for on in &agents_on {
agents.populate_if_needed(*on, map, &self.primary.sim, &self.cs, prerender);
}
for on in agents_on {
@ -666,7 +670,7 @@ impl PerMap {
map,
draw_map,
sim,
agents: RefCell::new(AgentCache::new_state()),
agents: RefCell::new(AgentCache::new()),
current_selection: None,
current_flags: flags,
last_warped_from: None,

View File

@ -26,7 +26,7 @@ impl MinimapControls<App> for MinimapController {
}
let mut cache = app.primary.agents.borrow_mut();
cache.draw_unzoomed_agents(g, app);
cache.draw_unzoomed_agents(g, &app.primary.map, &app.primary.sim, &app.cs, &app.opts);
}
fn make_unzoomed_panel(&self, ctx: &mut EventCtx, app: &App) -> Panel {

View File

@ -647,7 +647,7 @@ fn mouseover_unzoomed_agent_circle(ctx: &mut EventCtx, app: &mut App) {
.primary
.agents
.borrow_mut()
.calculate_unzoomed_agents(ctx, app)
.calculate_unzoomed_agents(ctx, &app.primary.map, &app.primary.sim, &app.cs)
.query(
Circle::new(cursor, Distance::meters(3.0))
.get_bounds()

View File

@ -52,6 +52,11 @@ impl Duration {
Duration::seconds(mins * 60.0)
}
/// Creates a duration in milliseconds.
pub fn milliseconds(value: f64) -> Duration {
Duration::seconds(value / 1000.0)
}
pub const fn const_seconds(value: f64) -> Duration {
Duration(value)
}

View File

@ -12,10 +12,10 @@ use sim::{AgentID, Sim, UnzoomedAgent, VehicleType};
use widgetry::{Color, Drawable, GeomBatch, GfxCtx, Panel, Prerender};
use crate::colors::ColorScheme;
use crate::options::Options;
use crate::render::{
draw_vehicle, unzoomed_agent_radius, DrawPedCrowd, DrawPedestrian, Renderable,
};
use crate::AppLike;
pub struct AgentCache {
/// This is controlled almost entirely by the minimap panel. It has no meaning in edit mode.
@ -30,7 +30,7 @@ pub struct AgentCache {
}
impl AgentCache {
pub fn new_state() -> AgentCache {
pub fn new() -> AgentCache {
AgentCache {
unzoomed_agents: UnzoomedAgents::new(),
time: None,
@ -87,9 +87,11 @@ impl AgentCache {
pub fn calculate_unzoomed_agents<P: AsRef<Prerender>>(
&mut self,
prerender: &mut P,
app: &dyn AppLike,
map: &Map,
sim: &Sim,
cs: &ColorScheme,
) -> &QuadTree<AgentID> {
let now = app.sim().time();
let now = sim.time();
let mut recalc = true;
if let Some((time, ref orig_agents, _, _)) = self.unzoomed {
if now == time && self.unzoomed_agents == orig_agents.clone() {
@ -98,10 +100,10 @@ impl AgentCache {
}
if recalc {
let highlighted = app.sim().get_highlighted_people();
let highlighted = sim.get_highlighted_people();
let mut batch = GeomBatch::new();
let mut quadtree = QuadTree::default(app.map().get_bounds().as_bbox());
let mut quadtree = QuadTree::default(map.get_bounds().as_bbox());
// It's quite silly to produce triangles for the same circle over and over again. ;)
let car_circle = Circle::new(
Pt2D::new(0.0, 0.0),
@ -111,8 +113,8 @@ impl AgentCache {
let ped_circle =
Circle::new(Pt2D::new(0.0, 0.0), unzoomed_agent_radius(None)).to_polygon();
for agent in app.sim().get_unzoomed_agents(app.map()) {
if let Some(mut color) = self.unzoomed_agents.color(&agent, app.cs()) {
for agent in sim.get_unzoomed_agents(map) {
if let Some(mut color) = self.unzoomed_agents.color(&agent, cs) {
// If the sim has highlighted people, then fade all others out.
if highlighted
.as_ref()
@ -141,19 +143,26 @@ impl AgentCache {
&self.unzoomed.as_ref().unwrap().2
}
pub fn draw_unzoomed_agents(&mut self, g: &mut GfxCtx, app: &dyn AppLike) {
self.calculate_unzoomed_agents(g, app);
pub fn draw_unzoomed_agents(
&mut self,
g: &mut GfxCtx,
map: &Map,
sim: &Sim,
cs: &ColorScheme,
opts: &Options,
) {
self.calculate_unzoomed_agents(g, map, sim, cs);
g.redraw(&self.unzoomed.as_ref().unwrap().3);
if app.opts().debug_all_agents {
if opts.debug_all_agents {
let mut cnt = 0;
for input in app.sim().get_all_draw_cars(app.map()) {
for input in sim.get_all_draw_cars(map) {
cnt += 1;
draw_vehicle(input, app.map(), app.sim(), g.prerender, app.cs());
draw_vehicle(input, map, sim, g.prerender, cs);
}
println!(
"At {}, debugged {} cars",
app.sim().time(),
sim.time(),
abstutil::prettyprint_usize(cnt)
);
// Pedestrians aren't the ones crashing

View File

@ -252,7 +252,7 @@ impl State<App> for Viewer {
// get_obj must succeed, because we can only click static map elements.
let outline = app
.draw_map
.get_obj(ctx, id, app, &mut map_gui::render::AgentCache::new_state())
.get_obj(ctx, id, app, &mut map_gui::render::AgentCache::new())
.unwrap()
.get_outline(&app.map);
let mut batch = GeomBatch::from(vec![(app.cs.perma_selected_object, outline)]);

25
piggyback/Cargo.toml Normal file
View File

@ -0,0 +1,25 @@
[package]
name = "piggyback"
version = "0.1.0"
authors = ["Dustin Carlino <dabreegster@gmail.com>"]
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
[features]
wasm = ["getrandom/js", "js-sys", "map_gui/wasm", "wasm-bindgen", "web-sys", "widgetry/wasm-backend"]
[dependencies]
abstio = { path = "../abstio" }
abstutil = { path = "../abstutil" }
geom = { path = "../geom" }
getrandom = { version = "0.2.3", optional = true }
js-sys = { version = "0.3.51", optional = true }
log = "0.4.14"
map_gui= { path = "../map_gui" }
map_model = { path = "../map_model" }
sim = { path = "../sim" }
wasm-bindgen = { version = "0.2.70", optional = true }
web-sys = { version = "0.3.47", optional = true }
widgetry = { path = "../widgetry" }

43
piggyback/README.md Normal file
View File

@ -0,0 +1,43 @@
# A/B Street + Mapbox demo
This is an example of integrating parts of A/B Street with Mapbox GL. It's a
normal web app using Mapbox, but it includes a layer rendering streets and
moving agents from A/B Street.
The goal is to increase interoperability and meet developers where they're at.
Parts of the A/B Street code-base are intended as
[a platform](https://a-b-street.github.io/docs/tech/map/platform.html) to build
other transportation-related things, using unique features like the detailed
street rendering. But Rust and our unusual UI library are a huge barrier.
Treating A/B Street as a layer that can be added to Mapbox and as a library with
a simple API for controlling a traffic simulation should be an easier start.
Another goal is to take advantage of all the great stuff that exists in the web
ecosystem. Instead of implementing satellite layers, multi-line text entry
(seriously!), and story mapping ourselves, we can just use stuff that's built
aready.
## How to run
You'll need `wasm-pack` and `python3` setup. You'll also need the `data/system/`
directory to contain some maps.
Quick development:
`wasm-pack build --dev --target web -- --features wasm && ./serve_locally.py`
To build the WASM in release mode:
`wasm-pack build --release --target web -- --features wasm && ./serve_locally.py`
Maps can be specified by URL:
- http://localhost:8000/?map=/data/system/us/seattle/maps/arboretum.bin
No deployment instructions yet.
## How it works
The `PiggybackDemo` struct is a thin layer written in Rust to hook up to the
rest of the A/B Street codebase. After beng initialized with a WebGL context and
a map file, it can render streets and agents and control a traffic simulation.
It serves as a public API, exposed via WASM. Then a regular Mapbox GL app treats
it as a library and adds a custom WebGL rendering layer that calls this API.

View File

@ -0,0 +1,22 @@
#!/bin/bash
# This creates a .zip with all of the files needed to serve a copy of the Mapbox demo.
set -x
set -e
wasm-pack build --release --target web -- --features wasm
mkdir mapbox_demo
cp -Rv index.html serve_locally.py pkg mapbox_demo
mkdir -p mapbox_demo/data/system/us/seattle/maps
mkdir -p mapbox_demo/data/system/de/berlin/maps
# Just include a few maps
cp ../data/system/us/seattle/maps/montlake.bin mapbox_demo/data/system/us/seattle/maps
cp ../data/system/de/berlin/maps/neukolln.bin mapbox_demo/data/system/de/berlin/maps
# Uncomment with caution!
# Note this embeds a tiny slice of the data/ directory underneath mapbox_demo.
# The S3 bucket has gzipped map files, but the JS / Rust layers don't handle
# reading both yet.
#aws s3 sync mapbox_demo s3://abstreet/dev/mapbox_demo

1
piggyback/data Symbolic link
View File

@ -0,0 +1 @@
../data/

180
piggyback/index.html Normal file
View File

@ -0,0 +1,180 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>MVP</title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">
<link href="https://api.mapbox.com/mapbox-gl-js/v2.5.1/mapbox-gl.css" rel="stylesheet">
<script src="https://api.mapbox.com/mapbox-gl-js/v2.5.1/mapbox-gl.js"></script>
<style>
body {
margin: 0px;
border: 0px;
padding: 0px;
}
#map {
/* TODO Fill remaining space. Can't get flexbox working... */
height: 800px;
width: 90%;
}
</style>
</head>
<body>
<input type="checkbox" id="show_roads" checked />
<label for="show_roads">Show A/B Street roads</label>
<span id="traffic_controls"></span>
<div id="map"></div>
<script type="module">
import init, { PiggybackDemo } from './pkg/piggyback.js';
async function setup() {
// Initialize the WASM library.
await init();
// What map should we load?
var loadPath = new URL(window.location).searchParams.get('map');
if (loadPath == null) {
loadPath = 'data/system/us/seattle/maps/montlake.bin';
}
// TODO Ideally we'd load this in the background and let Mapbox run first, but I'm not sure how to make the map.on('load') callback async.
console.log(`Fetching ${loadPath}`);
const resp = await fetch(loadPath);
const mapBytes = await resp.arrayBuffer();
// TODO I definitely copied this from example code somewhere. Generate my own and restrict its use once we decide how to deploy this demo.
mapboxgl.accessToken = 'pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw';
// Create the Mapbox map
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/satellite-streets-v11',
center: [-122.3037, 47.6427],
zoom: 16,
antialias: true,
// TODO This appears to duplicate the parameter as soon as we move...
//hash: `map=${loadPath}`
hash: true
});
var piggyback = null;
const abstLayer = {
id: 'abst',
type: 'custom',
onAdd: function (map, gl) {
piggyback = PiggybackDemo.create_with_map_bytes(gl, mapBytes);
// TODO If the URL didn't specify an initial location, warp to the center of this map?
sync_canvas();
},
render: function (gl, matrix) {
if (piggyback == null) {
return;
}
if (map.getZoom() >= 16) {
piggyback.draw_zoomed(document.getElementById('show_roads').checked);
} else {
piggyback.draw_unzoomed();
}
}
};
map.on('load', () => {
map.addLayer(abstLayer);
});
map.on('move', sync_canvas);
map.on('click', (e) => {
const debug = piggyback.debug_object_at(e.lngLat.lng, e.lngLat.lat);
if (debug != null) {
new mapboxgl.Popup()
.setLngLat(e.lngLat)
.setHTML(`<pre style="overflow-x: scroll; overflow-y: scroll; max-width: 240px; max-height: 300px;">${debug}</pre>`)
.addTo(map);
}
});
// We don't have map until here, so set up the handler now
document.getElementById('show_roads').onclick = function () {
map.triggerRepaint();
}
function sync_canvas() {
// If things broke during initialization, don't spam the console trying to call methods on a null object
if (piggyback != null) {
const bounds = map.getBounds();
const ne = bounds.getNorthEast();
const sw = bounds.getSouthWest();
piggyback.move_canvas(ne.lng, ne.lat, sw.lng, sw.lat);
}
}
function traffic_controls_inactive() {
const span = document.getElementById('traffic_controls');
span.innerHTML = `
<button type="button" onclick="start_traffic_sim()">Start traffic simulation</button>
`;
}
function traffic_controls_active() {
const span = document.getElementById('traffic_controls');
span.innerHTML = `
<button type="button" onclick="pause_or_resume()">Pause / resume</button>
<button type="button" onclick="clear_traffic_sim()">Clear simulation</button>
`;
}
var lastTime = performance.now();
var animationRequest = null;
function start_traffic_sim() {
traffic_controls_active();
lastTime = performance.now();
animationRequest = requestAnimationFrame(runSimulation);
piggyback.spawn_traffic();
}
function pause_or_resume() {
if (animationRequest == null) {
lastTime = performance.now();
animationRequest = requestAnimationFrame(runSimulation);
} else {
cancelAnimationFrame(animationRequest);
animationRequest = null;
}
}
function clear_traffic_sim() {
traffic_controls_inactive();
cancelAnimationFrame(animationRequest);
animationRequest = null;
piggyback.clear_traffic();
map.triggerRepaint();
}
function runSimulation(timestamp) {
const now = performance.now();
const dt = now - lastTime;
// This is called over 60 times per second or so! Throttle to about 10fps
if (dt >= 100) {
lastTime = now;
piggyback.advance_sim_time(dt);
map.triggerRepaint();
}
animationRequest = requestAnimationFrame(runSimulation);
}
traffic_controls_inactive();
// Let the onclick handlers reach into this scope. Alternatively, grab the buttons here and set onclick handlers.
window.start_traffic_sim = start_traffic_sim;
window.pause_or_resume = pause_or_resume;
window.clear_traffic_sim = clear_traffic_sim;
}
setup();
</script>
</body>
</html>

14
piggyback/serve_locally.py Executable file
View File

@ -0,0 +1,14 @@
#!/usr/bin/env python3
# This serves the current directory over HTTP. We need something more than
# `python3 -m http.server 8000` to inject the CORS header.
from http.server import HTTPServer, SimpleHTTPRequestHandler, test
import sys
class CORSRequestHandler (SimpleHTTPRequestHandler):
def end_headers (self):
self.send_header('Access-Control-Allow-Origin', '*')
SimpleHTTPRequestHandler.end_headers(self)
if __name__ == '__main__':
test(CORSRequestHandler, HTTPServer, port=int(sys.argv[1]) if len(sys.argv) > 1 else 8000)

13
piggyback/src/lib.rs Normal file
View File

@ -0,0 +1,13 @@
#[macro_use]
extern crate log;
#[cfg(target_arch = "wasm32")]
mod piggyback;
#[cfg(target_arch = "wasm32")]
pub use piggyback::*;
#[cfg(not(target_arch = "wasm32"))]
pub fn dummy() {
info!("Just avoiding an unused warning");
}

236
piggyback/src/piggyback.rs Normal file
View File

@ -0,0 +1,236 @@
use wasm_bindgen::prelude::*;
use abstutil::{prettyprint_usize, Timer};
use geom::{Circle, Distance, Duration, LonLat, Pt2D, Time};
use map_gui::colors::ColorScheme;
use map_gui::options::Options;
use map_gui::render::{AgentCache, DrawMap, DrawOptions};
use map_gui::{AppLike, ID};
use map_model::{Map, Traversable};
use sim::Sim;
use widgetry::{EventCtx, GfxCtx, RenderOnly, Settings, State};
/// This allows part of A/B Street to "piggyback" onto a WebGL canvas managed by something else,
/// such as Mapbox GL.
#[wasm_bindgen]
pub struct PiggybackDemo {
render_only: RenderOnly,
map: Map,
sim: Sim,
draw_map: DrawMap,
agents: AgentCache,
cs: ColorScheme,
options: Options,
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
impl PiggybackDemo {
/// Initializes the piggyback mode with a WebGL context and raw bytes representing a map file
/// to manage. (The map file shouldn't be gzipped.)
pub fn create_with_map_bytes(
gl: web_sys::WebGlRenderingContext,
map_bytes: js_sys::ArrayBuffer,
) -> PiggybackDemo {
abstutil::logger::setup();
let mut render_only = RenderOnly::new(
gl,
Settings::new("Piggyback demo").read_svg(Box::new(abstio::slurp_bytes)),
);
let mut timer = Timer::new("loading map");
let array = js_sys::Uint8Array::new(&map_bytes);
info!(
"Parsing {} map bytes",
prettyprint_usize(map_bytes.byte_length() as usize)
);
let mut map: Map = abstutil::from_binary(&array.to_vec()).unwrap();
map.map_loaded_directly(&mut timer);
info!("Loaded {:?}", map.get_name());
let sim = Sim::new(&map, sim::SimOptions::default());
let mut ctx = render_only.event_ctx();
let cs = ColorScheme::new(&mut ctx, map_gui::colors::ColorSchemeChoice::DayMode);
let options = map_gui::options::Options::load_or_default();
info!("Creating draw map");
let draw_map = DrawMap::new(&mut ctx, &map, &options, &cs, &mut timer);
PiggybackDemo {
render_only,
map,
sim,
draw_map,
agents: AgentCache::new(),
cs,
options,
}
}
/// Set the camera to match a northeast and southwest corner, given by lon/lat.
pub fn move_canvas(&mut self, ne_lon: f64, ne_lat: f64, sw_lon: f64, sw_lat: f64) {
let gps_bounds = self.map.get_gps_bounds();
let top_left = LonLat::new(ne_lon, ne_lat).to_pt(gps_bounds);
let bottom_right = LonLat::new(sw_lon, sw_lat).to_pt(gps_bounds);
let center =
LonLat::new((ne_lon + sw_lon) / 2.0, (ne_lat + sw_lat) / 2.0).to_pt(gps_bounds);
let mut ctx = self.render_only.event_ctx();
// This is quite a strange way of calculating zoom, but it works
let want_diagonal_dist = top_left.dist_to(bottom_right);
let b = ctx.canvas.get_screen_bounds();
let current_diagonal_dist =
Pt2D::new(b.min_x, b.min_y).dist_to(Pt2D::new(b.max_x, b.max_y));
// We can do this calculation before changing the center, because we're working in mercator
// already; distances shouldn't change based on where we are.
ctx.canvas.cam_zoom *= current_diagonal_dist / want_diagonal_dist;
ctx.canvas.center_on_map_pt(center);
}
/// Advances the traffic simulation.
pub fn advance_sim_time(&mut self, delta_milliseconds: f64) {
let dt = Duration::milliseconds(delta_milliseconds);
// Use the real time passed as the deadline
self.sim.time_limited_step(&self.map, dt, dt, &mut None);
}
/// Spawn random, unrealistic traffic.
pub fn spawn_traffic(&mut self) {
let mut rng = sim::SimFlags::for_test("spawn_traffic").make_rng();
let mut timer = Timer::new("spawn traffic");
sim::ScenarioGenerator::small_run(&self.map)
.generate(&self.map, &mut rng, &mut timer)
.instantiate(&mut self.sim, &self.map, &mut rng, &mut timer);
}
/// Reset the traffic simulation.
pub fn clear_traffic(&mut self) {
self.sim = Sim::new(&self.map, sim::SimOptions::default());
}
/// Draw the zoomed-in view. If `show_roads` is true, render roads and intersections in detail.
/// Always draw agents in their zoomed-in view. Don't draw anything else -- it's assumed that
/// the web app otherwise renders areas, buildings, and such already.
// Note this is &mut to conveniently work with AgentCache. Other code uses RefCell.
pub fn draw_zoomed(&mut self, show_roads: bool) {
// Short-circuit if there's nothing to do
if !show_roads && self.sim.is_empty() {
return;
}
let g = &mut self.render_only.gfx_ctx();
let objects = self
.draw_map
.get_renderables_back_to_front(g.get_screen_bounds(), &self.map);
let opts = DrawOptions::new();
// As we draw the static map elements, track where we need to draw live agents.
let mut agents_on = Vec::new();
for obj in objects {
if show_roads {
if let ID::Lane(_) | ID::Intersection(_) | ID::Road(_) = obj.get_id() {
obj.draw(g, self, &opts);
}
}
if let ID::Lane(l) = obj.get_id() {
agents_on.push(Traversable::Lane(l));
}
if let ID::Intersection(i) = obj.get_id() {
for t in &self.map.get_i(i).turns {
agents_on.push(Traversable::Turn(t.id));
}
}
}
for on in agents_on {
self.agents
.populate_if_needed(on, &self.map, &self.sim, &self.cs, g.prerender);
for obj in self.agents.get(on) {
obj.draw(g, self, &opts);
}
}
}
/// Draw unzoomed agents.
pub fn draw_unzoomed(&mut self) {
if self.sim.is_empty() {
return;
}
let g = &mut self.render_only.gfx_ctx();
self.agents
.draw_unzoomed_agents(g, &self.map, &self.sim, &self.cs, &self.options);
}
/// If there's a road, intersection, or area at the specififed coordinates, return a JSON
/// string with debug info.
pub fn debug_object_at(&self, lon: f64, lat: f64) -> Option<String> {
let pt = LonLat::new(lon, lat).to_pt(self.map.get_gps_bounds());
let mut objects = self.draw_map.get_renderables_back_to_front(
Circle::new(pt, Distance::meters(3.0)).get_bounds(),
&self.map,
);
objects.reverse();
for obj in objects {
if obj.contains_pt(pt, &self.map) {
let json = match obj.get_id() {
ID::Road(r) => abstutil::to_json(self.map.get_r(r)),
ID::Intersection(i) => abstutil::to_json(self.map.get_i(i)),
ID::Area(a) => abstutil::to_json(self.map.get_a(a)),
_ => continue,
};
return Some(json);
}
}
None
}
}
// Drawing some of the objects requires this interface
impl AppLike for PiggybackDemo {
fn map(&self) -> &Map {
&self.map
}
fn sim(&self) -> &Sim {
&self.sim
}
fn cs(&self) -> &ColorScheme {
&self.cs
}
fn mut_cs(&mut self) -> &mut ColorScheme {
&mut self.cs
}
fn draw_map(&self) -> &DrawMap {
&self.draw_map
}
fn mut_draw_map(&mut self) -> &mut DrawMap {
&mut self.draw_map
}
fn opts(&self) -> &Options {
&self.options
}
fn mut_opts(&mut self) -> &mut Options {
&mut self.options
}
fn map_switched(&mut self, _: &mut EventCtx, _: map_model::Map, _: &mut abstutil::Timer) {
unreachable!()
}
fn draw_with_opts(&self, _: &mut GfxCtx, _: map_gui::render::DrawOptions) {
unreachable!()
}
fn make_warper(
&mut self,
_: &EventCtx,
_: Pt2D,
_: Option<f64>,
_: Option<map_gui::ID>,
) -> Box<dyn State<PiggybackDemo>> {
unreachable!()
}
fn sim_time(&self) -> Time {
self.sim.time()
}
}

View File

@ -260,7 +260,7 @@ type WindowAdapter = crate::backend_glow_native::WindowAdapter;
pub struct PrerenderInnards {
gl: Rc<glow::Context>,
window_adapter: WindowAdapter,
window_adapter: Option<WindowAdapter>,
program: <glow::Context as glow::HasContext>::Program,
// TODO Prerender doesn't know what things are temporary and permanent. Could make the API more
@ -272,7 +272,7 @@ impl PrerenderInnards {
pub fn new(
gl: glow::Context,
program: <glow::Context as glow::HasContext>::Program,
window_adapter: WindowAdapter,
window_adapter: Option<WindowAdapter>,
) -> PrerenderInnards {
PrerenderInnards {
gl: Rc::new(gl),
@ -378,7 +378,7 @@ impl PrerenderInnards {
}
pub(crate) fn window(&self) -> &winit::window::Window {
self.window_adapter.window()
self.window_adapter.as_ref().expect("no window").window()
}
pub fn request_redraw(&self) {
@ -399,7 +399,10 @@ impl PrerenderInnards {
pub fn window_resized(&self, new_size: ScreenDims, scale_factor: f64) {
let physical_size = winit::dpi::LogicalSize::from(new_size).to_physical(scale_factor);
self.window_adapter.window_resized(new_size, scale_factor);
self.window_adapter
.as_ref()
.expect("no window")
.window_resized(new_size, scale_factor);
unsafe {
self.gl
.viewport(0, 0, physical_size.width, physical_size.height);
@ -421,8 +424,11 @@ impl PrerenderInnards {
self.window().scale_factor()
}
pub fn draw_finished(&self, gfc_ctx_innards: GfxCtxInnards) {
self.window_adapter.draw_finished(gfc_ctx_innards)
pub fn draw_finished(&self, gfx_ctx_innards: GfxCtxInnards) {
self.window_adapter
.as_ref()
.expect("no window")
.draw_finished(gfx_ctx_innards)
}
pub(crate) fn screencap(&self, dims: ScreenDims, filename: String) -> anyhow::Result<()> {
@ -455,6 +461,13 @@ impl PrerenderInnards {
)?;
Ok(())
}
#[allow(unused)]
pub fn use_program_for_renderonly(&self) {
unsafe {
self.gl.use_program(Some(self.program));
}
}
}
/// Uploads a sprite sheet of textures to the GPU so they can be used by Fill::Texture and

View File

@ -73,7 +73,7 @@ pub fn setup(
timer.stop("load textures");
(
PrerenderInnards::new(gl, program, WindowAdapter(windowed_context)),
PrerenderInnards::new(gl, program, Some(WindowAdapter(windowed_context))),
event_loop,
)
}

View File

@ -1,11 +1,15 @@
use std::cell::Cell;
use std::rc::Rc;
use anyhow::Result;
use wasm_bindgen::JsCast;
use winit::platform::web::WindowExtWebSys;
use abstutil::Timer;
use crate::assets::Assets;
use crate::backend_glow::{build_program, GfxCtxInnards, PrerenderInnards, SpriteTexture};
use crate::{ScreenDims, Settings};
use crate::{Canvas, Event, EventCtx, GfxCtx, Prerender, ScreenDims, Settings, Style, UserInput};
pub fn setup(
settings: &Settings,
@ -63,22 +67,18 @@ pub fn setup(
// First try WebGL 2.0 context.
// WebGL 2.0 isn't supported by default on macOS Safari, or any iOS browser (which are all just
// Safari wrappers).
let (program, gl) = webgl2_program_context(&canvas, timer)
let (gl, program) = webgl2_glow_context(&canvas)
.and_then(|gl| webgl2_program(gl, timer))
.or_else(|err| {
warn!(
"failed to build WebGL 2.0 context with error: \"{}\". Trying WebGL 1.0 instead...",
err
);
webgl1_program_context(&canvas, timer)
webgl1_glow_context(&canvas).and_then(|gl| webgl1_program(gl, timer))
})
.unwrap();
debug!("built WebGL context");
fn webgl2_program_context(
canvas: &web_sys::HtmlCanvasElement,
timer: &mut Timer,
) -> anyhow::Result<(glow::Program, glow::Context)> {
fn webgl2_glow_context(canvas: &web_sys::HtmlCanvasElement) -> Result<glow::Context> {
let maybe_context: Option<_> = canvas
.get_context("webgl2")
.map_err(|err| anyhow!("error getting context for WebGL 2.0: {:?}", err))?;
@ -87,34 +87,10 @@ pub fn setup(
let webgl2_context = js_webgl2_context
.dyn_into::<web_sys::WebGl2RenderingContext>()
.map_err(|err| anyhow!("unable to cast to WebGl2RenderingContext. error: {:?}", err))?;
let gl = glow::Context::from_webgl2_context(webgl2_context);
let program = unsafe {
build_program(
&gl,
include_str!("../shaders/vertex_300.glsl"),
include_str!("../shaders/fragment_300.glsl"),
)?
};
timer.start("load textures");
let sprite_texture = SpriteTexture::new(
include_bytes!("../textures/spritesheet.png").to_vec(),
64,
64,
)
.expect("failed to format texture sprite sheet");
sprite_texture
.upload_gl2(&gl)
.expect("failed to upload textures");
timer.stop("load textures");
Ok((program, gl))
Ok(glow::Context::from_webgl2_context(webgl2_context))
}
fn webgl1_program_context(
canvas: &web_sys::HtmlCanvasElement,
timer: &mut Timer,
) -> anyhow::Result<(glow::Program, glow::Context)> {
fn webgl1_glow_context(canvas: &web_sys::HtmlCanvasElement) -> Result<glow::Context> {
let maybe_context: Option<_> = canvas
.get_context("webgl")
.map_err(|err| anyhow!("error getting context for WebGL 1.0: {:?}", err))?;
@ -123,36 +99,63 @@ pub fn setup(
let webgl1_context = js_webgl1_context
.dyn_into::<web_sys::WebGlRenderingContext>()
.map_err(|err| anyhow!("unable to cast to WebGlRenderingContext. error: {:?}", err))?;
let gl = glow::Context::from_webgl1_context(webgl1_context);
let program = unsafe {
build_program(
&gl,
include_str!("../shaders/vertex_webgl1.glsl"),
include_str!("../shaders/fragment_webgl1.glsl"),
)?
};
timer.start("load textures");
let sprite_texture = SpriteTexture::new(
include_bytes!("../textures/spritesheet.png").to_vec(),
64,
64,
)
.expect("failed to format texture sprite sheet");
sprite_texture
.upload_webgl1(&gl)
.expect("failed to upload textures");
timer.stop("load textures");
Ok((program, gl))
Ok(glow::Context::from_webgl1_context(webgl1_context))
}
(
PrerenderInnards::new(gl, program, WindowAdapter(winit_window)),
PrerenderInnards::new(gl, program, Some(WindowAdapter(winit_window))),
event_loop,
)
}
fn webgl2_program(gl: glow::Context, timer: &mut Timer) -> Result<(glow::Context, glow::Program)> {
let program = unsafe {
build_program(
&gl,
include_str!("../shaders/vertex_300.glsl"),
include_str!("../shaders/fragment_300.glsl"),
)?
};
timer.start("load textures");
let sprite_texture = SpriteTexture::new(
include_bytes!("../textures/spritesheet.png").to_vec(),
64,
64,
)
.expect("failed to format texture sprite sheet");
sprite_texture
.upload_gl2(&gl)
.expect("failed to upload textures");
timer.stop("load textures");
Ok((gl, program))
}
fn webgl1_program(gl: glow::Context, timer: &mut Timer) -> Result<(glow::Context, glow::Program)> {
let program = unsafe {
build_program(
&gl,
include_str!("../shaders/vertex_webgl1.glsl"),
include_str!("../shaders/fragment_webgl1.glsl"),
)?
};
timer.start("load textures");
let sprite_texture = SpriteTexture::new(
include_bytes!("../textures/spritesheet.png").to_vec(),
64,
64,
)
.expect("failed to format texture sprite sheet");
sprite_texture
.upload_webgl1(&gl)
.expect("failed to upload textures");
timer.stop("load textures");
Ok((gl, program))
}
pub struct WindowAdapter(Rc<winit::window::Window>);
impl WindowAdapter {
@ -169,3 +172,73 @@ impl WindowAdapter {
pub fn draw_finished(&self, _gfc_ctx_innards: GfxCtxInnards) {}
}
/// Sets up widgetry in a mode where it just draws to a WebGL context and doesn't handle events or
/// interactions at all.
pub struct RenderOnly {
prerender: Prerender,
style: Style,
canvas: Canvas,
}
impl RenderOnly {
pub fn new(raw_gl: web_sys::WebGlRenderingContext, settings: Settings) -> RenderOnly {
std::panic::set_hook(Box::new(|info| {
error!("Panicked: {}", info);
}));
info!("Setting up widgetry in render-only mode");
let mut timer = Timer::new("setup render-only");
let initial_size = ScreenDims::new(
raw_gl.drawing_buffer_width().into(),
raw_gl.drawing_buffer_height().into(),
);
// Mapbox always seems to hand us WebGL1
let (gl, program) =
webgl1_program(glow::Context::from_webgl1_context(raw_gl), &mut timer).unwrap();
let prerender_innards = PrerenderInnards::new(gl, program, None);
let style = Style::light_bg();
let prerender = Prerender {
assets: Assets::new(
style.clone(),
settings.assets_base_url,
settings.assets_are_gzipped,
settings.read_svg,
),
num_uploads: Cell::new(0),
inner: prerender_innards,
scale_factor: settings.scale_factor.unwrap_or(1.0),
};
let canvas = Canvas::new(initial_size, settings.canvas_settings);
RenderOnly {
prerender,
style,
canvas,
}
}
/// Creates a no-op `EventCtx`, just for client code that needs this interface to upload
/// geometry. There's no actual event.
pub fn event_ctx(&mut self) -> EventCtx {
EventCtx {
fake_mouseover: true,
input: UserInput::new(Event::NoOp, &self.canvas),
canvas: &mut self.canvas,
prerender: &self.prerender,
style: &mut self.style,
updates_requested: vec![],
canvas_movement_called: false,
focus_owned_by: None,
next_focus_owned_by: None,
}
}
/// Creates a `GfxCtx`, allowing things to be drawn.
pub fn gfx_ctx(&self) -> GfxCtx {
self.prerender.inner.use_program_for_renderonly();
let screenshot = false;
GfxCtx::new(&self.prerender, &self.canvas, &self.style, screenshot)
}
}

View File

@ -107,6 +107,9 @@ mod backend {
pub use crate::backend_glow::*;
}
#[cfg(feature = "wasm-backend")]
pub use crate::backend_glow_wasm::RenderOnly;
/// Like [`std::include_bytes!`], but also returns its argument, the relative path to the bytes
///
/// returns a `(path, bytes): (&str, &[u8])` tuple

View File

@ -196,12 +196,12 @@ pub struct Settings {
pub(crate) assets_base_url: Option<String>,
pub(crate) assets_are_gzipped: bool,
dump_raw_events: bool,
scale_factor: Option<f64>,
pub(crate) scale_factor: Option<f64>,
require_minimum_width: Option<f64>,
window_icon: Option<String>,
loading_tips: Option<Text>,
read_svg: Box<dyn Fn(&str) -> Vec<u8>>,
canvas_settings: CanvasSettings,
pub(crate) read_svg: Box<dyn Fn(&str) -> Vec<u8>>,
pub(crate) canvas_settings: CanvasSettings,
}
impl Settings {