a-b-street / a-b-street/abstreet

Refactor map-space objects

Aperta
#763 1 commento 1 reazione 0 assegnatari Vedi su GitHub
Lingua principale
Rust
Stelle
8.2k
Fork
380
Metriche di merge delle PR
Nessuna PR unita negli ultimi 30g

Descrizione

widgetry now has pretty good abstractions for handling most "screen-space" interactions, with nice APIs like the button builders. But the world of "map-space" objects is the wild west -- just ad-hoc code for figuring out what exists, hovering, selecting, dragging, tooltips, z-ordering. Can we do better?

# Use cases

All of these have some kind of hovering behavior.

- KML viewer (needs quadtree, can select multiple overlapping things)
- stop sign editor (underlying basemap renders enabled stop signs)
- traffic signal editor (both icons and movement arrows, complex hover state calculation)
- the devtools polygon editor (dragging, clicking to add new points. dragging the polygon itself would be nice!)
- map_editor (dragging, has a `World` abstraction)
- collision viewer (has `MapspaceTooltips`)
- story map editor (dragging and currently awkward way of placing new stuff)
- commuter patterns (has a hovering vs locked/selected state)
- road editor (also hovering vs locked/selected)
- traffic signal demand viewer (hierarchical -- first look for intersections. but if we trust the quadtree, is that necessary?)
- bike tool's quick sketch (dragging, snapping)
- bike tool's routing (both `InputWaypoints` and now alternate routes, snapping)

And I'm sure I've missed some.

An extreme take: Almost no app code should call `ctx.redo_mouseover()`, but there are... >50 callers right now!

# What makes this stuff hard?

Caching and maintaining state -- when you hover or select something different, we usually want to draw something, maybe add a tooltip. Ad-hoc right now.

When hovering or actively dragging a draggable object, we still want to allow zooming, but not panning.

Dragging is complicated and buggy. In the route tool, if you drag a waypoint close to an alternate path, stuff breaks. Z-ordering there is implicit in the code, has to match in `event` and `draw`. Conflicts between waypoints and alt paths.

# Ideas

Extend `map_editor`'s `World` abstraction. There'll be something for a `State` that knows about all of the map-space objects. Objects are:

- drawable -- always in a big batch?
- hoverable -- so they have a hitbox, and we can use a quadtree to prune
- "on hover" -- draw extra stuff and maybe a mouse tooltip
- sometimes clickable
- sometimes draggable
- z-orderable... waypoints and alternate routes are an example where it's hard to get it right ad-hoc
- sometimes different behavior in unzoomed and zoomed modes... for drawing, selecting, etc

## What's in a world?

In most of the use cases, the map-space stuff is "extra" objects on top of the basemap. The basemap stuff is mostly managed by `map_gui`, which has its own system for managing these kinds of concerns. Except there are also some places that "subset" the basemap objects:

- the picker for editing multiple traffic signals
- the LTN "pick a neighborhood" screen
- the LTN "edit a neighborhood" screen
- and everything calling things like `mouseover_unzoomed_buildings` or doing further filtering (like no editing service roads or light rail)

Hot take: `app.primary.current_selection` is a bad idea? I count around 30 places that reset it to `None` when entering/leaving the state. I think the world of selectable objects shouldn't be shared App-wide. Most states should control this more directly.

## How to drag "correctly"

1) Bail out if the cursor isn't in map-space
2) Go through all the objects, maybe start a drag
3) Only then allow normal canvas movement (not while hovering on something that's draggable)

## Where's the state live?

The `World` approach is to be generic over some ID type. Shoving in custom data for the objects is an option, or letting the `State` manage it and map things. (Hey, we're inventing parts of ECS...)

Similar to `Panel`s, could we make do with strings, or do we need IDs? Could the world container thing assign its own opaque IDs and make the caller do the mapping?

## Should we subsume chunks of `map_gui`?

Stepping back, `DrawMap` solves lots of these problems too. Should we try to subsume it?

```
pub trait Renderable {
fn get_id(&self) -> ID;
fn draw(&self, g: &mut GfxCtx, app: &dyn AppLike, opts: &DrawOptions);
fn get_zorder(&self) -> isize;
fn get_outline(&self, map: &Map) -> Polygon;
fn contains_pt(&self, pt: Pt2D, map: &Map) -> bool;
}
```

Things that're maybe different (but maybe not):
- the data ownership is a bit weird
- lazy rendering of zoomed-in stuff
- more complex batching for some stuff

## Delegation

We have a few places in the code where different logical components all shove their widgets in a single `Panel`, so we do scary dances to try to delegate outcomes. Will we have the same problems? Think about the route tool -- `InputWaypoints` is a common thing, living alongside the one-off alt route clickables.

## Prior art

Any good ideas from Leaflet, mapbox, other game engine APIs?

Guida per i contributori

Nessuna guida per i contributori indicizzata per questo repository

Valutazione

Questa issue non è ancora stata valutata.

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.