867 lines
28 KiB
Markdown
867 lines
28 KiB
Markdown
# Map Renderer Rewrite Plan
|
|
|
|
**Date:** 2026-07-26
|
|
**Status:** Draft
|
|
**Scope:** `makepad/widgets/src/map/` (8 files, ~9,000 lines)
|
|
**Constraint:** Preserve Makepad Widget/Script API, all 184+ existing tests, downstream book.rs DSL
|
|
|
|
---
|
|
|
|
## Problem Statement
|
|
|
|
The renderer is a monolithic `MapView` struct (1,882 lines) that owns viewport, scheduling,
|
|
caching, rendering, labels, interaction, and style in one object. The review identified:
|
|
|
|
1. **Modular architecture** — 8.5/10: Good ideas, but everything lives in one struct
|
|
2. **Performance** — 8/10: CPU-bound draw, unnecessary HashMap lookups, per-frame recomputation
|
|
3. **Responsibility boundaries** — Scheduler knows about rendering, rendering knows about workers
|
|
4. **Hidden state machine** — `TileLoadState` scattered as `if loading/if ready/if cached/if dirty`
|
|
5. **Cache ownership** — Who evicts? Who marks stale? Who retries? Not obvious
|
|
6. **Mutable state** — Dozens of fields mutated from multiple code paths
|
|
7. **No render graph** — Everything funnels into one draw path
|
|
8. **Long functions** — 200-500 line methods with deep nesting
|
|
|
|
---
|
|
|
|
## Target Architecture
|
|
|
|
### Current Module Structure
|
|
|
|
```
|
|
map/
|
|
mod.rs (13 lines) — pub exports
|
|
view.rs (1882 lines) — MapView widget: EVERYTHING
|
|
tile.rs (2163 lines) — Tile types + loading + tessellation + disk cache
|
|
geometry.rs (1968 lines) — Projection, DP, tessellation (well-factored)
|
|
label.rs (1098 lines) — Label extraction, scoring, collision grid
|
|
style.rs (496 lines) — Theme system
|
|
style_json.rs(3227 lines) — JSON parser + tests (behind map_style feature)
|
|
asset_loader.rs (124 lines) — Sprite/glyph caches
|
|
sprite.rs (433 lines) — POI icon classification
|
|
```
|
|
|
|
### Proposed Module Structure
|
|
|
|
```
|
|
map/
|
|
mod.rs — pub exports (MapView + subsystems for testing)
|
|
view.rs — THIN Widget shell (~400 lines): DSL, #[live] props, delegates to subsystems
|
|
viewport.rs — NEW: ViewportState (center, zoom, screen<->world, wrap/clamp)
|
|
scheduler.rs — NEW: TileScheduler (request queue, retry, prioritization, cancellation)
|
|
cache.rs — NEW: TileCache (HashMap, eviction, dedup, generation, GC)
|
|
renderer.rs — NEW: RenderPass (fill, stroke, POI, label passes with explicit ordering)
|
|
label_state.rs — NEW: LabelState (all scratch buffers, collision grid, placement)
|
|
tile.rs — SLIMMED: Tile types only (TileKey, TileEntry, TileLoadState enum, message types)
|
|
tile_decode.rs — NEW: MVT parsing, tessellation, Overpass body processing (extracted from tile.rs)
|
|
tile_disk.rs — NEW: Disk cache read/write, mbtiles batch loading (extracted from tile.rs)
|
|
geometry.rs — UNCHANGED
|
|
label.rs — UNCHANGED (scoring/extraction only)
|
|
style.rs — UNCHANGED
|
|
style_json.rs — UNCHANGED
|
|
asset_loader.rs — UNCHANGED
|
|
sprite.rs — UNCHANGED
|
|
```
|
|
|
|
**Total new files:** 6
|
|
**Total modified files:** 3 (view.rs, tile.rs, mod.rs)
|
|
**Total unchanged files:** 6 (geometry, label, style, style_json, asset_loader, sprite)
|
|
|
|
---
|
|
|
|
## New Types — Detailed Design
|
|
|
|
### 1. `ViewportState` (viewport.rs, ~150 lines)
|
|
|
|
**Addresses:** Review points #1, #4 (mutable state, coupling)
|
|
|
|
Extracts all viewport/coordinate logic from `view.rs`. No dependency on tiles, workers, or rendering.
|
|
|
|
```rust
|
|
pub struct ViewportState {
|
|
center_norm: Vec2d, // [0,1] Web Mercator
|
|
zoom: f64,
|
|
min_zoom: f64,
|
|
max_zoom: f64,
|
|
view_rect: Rect, // pixel rect of widget area
|
|
}
|
|
|
|
// Pure functions, no Cx dependency
|
|
impl ViewportState {
|
|
pub fn view_zoom(&self) -> f64;
|
|
pub fn request_zoom_level(&self, use_local: bool) -> u32;
|
|
pub fn world_size(&self) -> f64; // 256 * 2^zoom
|
|
pub fn center_world(&self) -> Vec2d; // center_norm * world_size
|
|
pub fn map_offset(&self) -> Vec2f; // screen offset for GPU uniforms
|
|
pub fn wrap_and_clamp(&mut self);
|
|
pub fn set_rect(&mut self, rect: Rect);
|
|
|
|
// Interaction
|
|
pub fn apply_drag(&mut self, delta_pixels: Vec2d);
|
|
pub fn apply_pinch(&mut self, initial_zoom: f64, initial_center: Vec2d,
|
|
initial_distance: f64, new_distance: f64, center: Vec2d);
|
|
pub fn apply_scroll(&mut self, scroll: f64, anchor_abs: Vec2d);
|
|
|
|
// Coordinate transforms
|
|
pub fn screen_to_world(&self, screen_pos: Vec2d) -> Vec2d;
|
|
pub fn world_to_screen(&self, world_pos: Vec2d) -> Vec2d;
|
|
pub fn tile_world_size_zoom(zoom: f64) -> f64; // static
|
|
|
|
// Visible tile computation
|
|
pub fn visible_tile_keys(&self) -> Vec<TileKey>;
|
|
|
|
// Property sync
|
|
pub fn sync_from_properties(&mut self, center_lon: f64, center_lat: f64, zoom: f64);
|
|
}
|
|
```
|
|
|
|
**Current code moving here:**
|
|
- `view_zoom()`, `request_zoom_level()` (view.rs:1851-1863)
|
|
- `wrap_and_clamp_center()` (view.rs:1064-1067)
|
|
- `zoom_with_anchor()` (view.rs:1069-1100)
|
|
- `visible_tile_keys()` (view.rs:1185-1226)
|
|
- Part of `on_after_apply` that sets center_norm, zoom
|
|
- Part of `draw_walk` that computes map_offset
|
|
- Part of `handle_event` FingerMove/FingerDown/FingerUp interaction
|
|
|
|
---
|
|
|
|
### 2. `TileCache` (cache.rs, ~200 lines)
|
|
|
|
**Addresses:** Review points #3, #4, #19 (cache ownership, mutable state, memory growth)
|
|
|
|
Single owner of tile lifetime. Every mutation goes through `TileCache` methods.
|
|
|
|
```rust
|
|
pub struct TileCache {
|
|
tiles: HashMap<TileKey, TileEntry>,
|
|
frame_counter: u64,
|
|
style_epoch: u64,
|
|
max_tiles: usize, // default 640
|
|
stale_frame_threshold: u64, // 240 frames
|
|
}
|
|
```
|
|
|
|
**Explicit `TileLoadState` enum** (review point #5 — replace hidden state machine):
|
|
|
|
```rust
|
|
pub enum TileLoadState {
|
|
Missing,
|
|
LoadingLocal,
|
|
LoadingNetwork,
|
|
Ready {
|
|
fill_geometry: Option<Geometry>,
|
|
stroke_geometry: Option<Geometry>,
|
|
feature_count: usize,
|
|
labels: Vec<Label>,
|
|
pois: Vec<PoiFeature>,
|
|
},
|
|
Failed {
|
|
retry_after: u64,
|
|
attempts: u8,
|
|
},
|
|
}
|
|
```
|
|
|
|
**Methods:**
|
|
|
|
```rust
|
|
impl TileCache {
|
|
// Query
|
|
pub fn get(&self, key: TileKey) -> Option<&TileEntry>;
|
|
pub fn get_mut(&mut self, key: TileKey) -> Option<&mut TileEntry>;
|
|
pub fn is_ready(&self, key: TileKey) -> bool;
|
|
pub fn is_loading(&self, key: TileKey) -> bool;
|
|
pub fn is_failed(&self, key: TileKey) -> bool;
|
|
pub fn find_ready_ancestor(&self, key: TileKey) -> Option<TileKey>;
|
|
pub fn find_ready_descendants(&self, key: TileKey) -> Vec<TileKey>;
|
|
pub fn ready_count(&self, visible: &[TileKey]) -> usize;
|
|
pub fn loading_count(&self) -> usize;
|
|
|
|
// Mutation
|
|
pub fn insert_loading(&mut self, key: TileKey, state: TileLoadState);
|
|
pub fn insert_ready(&mut self, cx: &mut Cx, key: TileKey, buffers: TileBuffers);
|
|
pub fn mark_failed(&mut self, key: TileKey, reason: &str);
|
|
pub fn tick(&mut self); // advance frame_counter
|
|
|
|
// Eviction
|
|
pub fn evict(&mut self, visible: &HashSet<TileKey>, target_zoom: u32);
|
|
|
|
// Theme change
|
|
pub fn clear_all(&mut self); // bumps style_epoch, clears everything
|
|
pub fn style_epoch(&self) -> u64;
|
|
|
|
// Status
|
|
pub fn status_counts(&self, visible: &[TileKey]) -> TileStatusCounts;
|
|
}
|
|
```
|
|
|
|
**Current code moving here:**
|
|
- All `self.tiles.*` operations from view.rs
|
|
- `insert_ready_tile()` (view.rs:813-846)
|
|
- `mark_tile_failed()` (view.rs:1039-1062)
|
|
- `tile_is_ready()`, `find_ready_ancestor()`, `fill_ready_descendants()` (view.rs:1256-1296)
|
|
- Eviction block (view.rs:1163-1181)
|
|
- `apply_theme_change()` tile clearing (view.rs:788-797)
|
|
|
|
---
|
|
|
|
### 3. `TileScheduler` (scheduler.rs, ~250 lines)
|
|
|
|
**Addresses:** Review points #1, #2, #16, #17, #18 (scheduler/renderer coupling, race conditions, worker starvation)
|
|
|
|
Owns the request queue, retry logic, priority, and cancellation. Does NOT know about rendering.
|
|
|
|
```rust
|
|
pub struct TileScheduler {
|
|
visible_tiles: Vec<TileKey>,
|
|
local_requested: HashSet<TileKey>,
|
|
local_missing: HashSet<TileKey>,
|
|
request_to_tile: HashMap<LiveId, PendingTileRequest>,
|
|
next_request_id: u64,
|
|
pending_count: usize,
|
|
local_source_missing_logged: bool,
|
|
}
|
|
```
|
|
|
|
**Generation tracking** (review point #16 — prevent old tiles overwriting new):
|
|
|
|
Every request carries a `generation: u64` that is checked when the result arrives.
|
|
The generation is incremented on zoom change and theme change.
|
|
|
|
```rust
|
|
pub struct PendingTileRequest {
|
|
pub tile_key: TileKey,
|
|
pub endpoint: &'static str,
|
|
pub generation: u64,
|
|
}
|
|
```
|
|
|
|
```rust
|
|
pub enum TileAction {
|
|
// Caller should dispatch these
|
|
LoadLocalBatch {
|
|
mbtiles_path: PathBuf,
|
|
cache_dir: String,
|
|
requested: Vec<TileKey>,
|
|
theme: CompiledMapTheme,
|
|
style_epoch: u64,
|
|
generation: u64,
|
|
},
|
|
LoadFromDiskCache {
|
|
tile_key: TileKey,
|
|
cache_path: PathBuf,
|
|
theme: CompiledMapTheme,
|
|
style_epoch: u64,
|
|
generation: u64,
|
|
},
|
|
LoadFromNetwork {
|
|
request_id: LiveId,
|
|
http_request: HttpRequest,
|
|
tile_key: TileKey,
|
|
generation: u64,
|
|
},
|
|
Nothing,
|
|
}
|
|
```
|
|
|
|
**Methods:**
|
|
|
|
```rust
|
|
impl TileScheduler {
|
|
// Core scheduling
|
|
pub fn update(&mut self, cache: &TileCache, viewport: &ViewportState,
|
|
config: &SchedulerConfig) -> Vec<TileAction>;
|
|
pub fn on_batch_loaded(&mut self, requested: Vec<TileKey>, loaded: Vec<TileKey>);
|
|
pub fn on_batch_failed(&mut self, requested: Vec<TileKey>);
|
|
pub fn on_tile_loaded(&mut self, tile_key: TileKey);
|
|
pub fn on_tile_failed(&mut self, tile_key: TileKey);
|
|
|
|
// HTTP integration
|
|
pub fn register_http_request(&mut self, request_id: LiveId, tile_key: TileKey, generation: u64);
|
|
pub fn on_http_response(&mut self, request_id: LiveId) -> Option<(TileKey, u64)>;
|
|
pub fn on_http_error(&mut self, request_id: LiveId) -> Option<(TileKey, u64)>;
|
|
|
|
// Theme change
|
|
pub fn reset_generation(&mut self); // on theme change
|
|
|
|
// Visible tile management
|
|
pub fn visible_tiles(&self) -> &[TileKey];
|
|
}
|
|
|
|
pub struct SchedulerConfig {
|
|
pub use_network: bool,
|
|
pub use_local_mbtiles: bool,
|
|
pub max_pending_requests: usize, // default 2
|
|
pub max_local_tile_batch: usize, // default 10
|
|
pub max_tile_retries: u8, // default 6
|
|
pub local_mbtiles_path: String,
|
|
pub local_tile_cache_dir: String,
|
|
}
|
|
```
|
|
|
|
**Current code moving here:**
|
|
- `request_visible_tiles_from_local_source()` (view.rs:961-1037)
|
|
- `request_tile()` (view.rs:1298-1375)
|
|
- Part of `ensure_visible_tiles()` that does retry/request logic (view.rs:1109-1181)
|
|
- `handle_tile_worker_messages()` dispatch logic (view.rs:848-936)
|
|
- `handle_http_response()` dispatch logic (view.rs:658-727)
|
|
- `handle_http_request_error()` dispatch logic (view.rs:729-748)
|
|
|
|
---
|
|
|
|
### 4. `RenderPass` (renderer.rs, ~200 lines)
|
|
|
|
**Addresses:** Review points #11 (render graph), #8 (CPU bound)
|
|
|
|
Explicit ordered passes with clear separation.
|
|
|
|
```rust
|
|
pub struct RenderPass;
|
|
|
|
impl RenderPass {
|
|
pub fn draw_fill(
|
|
cx: &mut Cx2d,
|
|
draw_map: &mut DrawMapVector,
|
|
cache: &TileCache,
|
|
draw_keys: &[TileKey],
|
|
view_zoom: f64,
|
|
map_offset: Vec2f,
|
|
);
|
|
|
|
pub fn draw_stroke(
|
|
cx: &mut Cx2d,
|
|
draw_map: &mut DrawMapVector,
|
|
cache: &TileCache,
|
|
draw_keys: &[TileKey],
|
|
view_zoom: f64,
|
|
map_offset: Vec2f,
|
|
);
|
|
|
|
pub fn draw_pois(
|
|
cx: &mut Cx2d,
|
|
draw_poi: &mut DrawColor,
|
|
cache: &TileCache,
|
|
draw_keys: &[TileKey],
|
|
view_zoom: f64,
|
|
map_offset: Vec2f,
|
|
min_zoom: f64,
|
|
);
|
|
|
|
// Labels handled by LabelState::draw()
|
|
|
|
/// Build the list of tile keys to actually draw, with ancestor/descendant fallback
|
|
pub fn build_draw_keys(
|
|
cache: &TileCache,
|
|
visible_tiles: &[TileKey],
|
|
draw_seen: &mut HashSet<TileKey>,
|
|
draw_tiles: &mut Vec<TileKey>,
|
|
descendant_scratch: &mut Vec<TileKey>,
|
|
);
|
|
}
|
|
```
|
|
|
|
**Current code moving here:**
|
|
- Fill pass loop (view.rs:569-586)
|
|
- Stroke pass loop (view.rs:588-608)
|
|
- POI pass loop (view.rs:610-640)
|
|
- `fill_draw_tile_keys()` (view.rs:1228-1254)
|
|
|
|
---
|
|
|
|
### 5. `LabelState` (label_state.rs, ~300 lines)
|
|
|
|
**Addresses:** Review points #4, #7 (mutable state, too many responsibilities)
|
|
|
|
All scratch buffers for label placement in one struct. Keeps `label.rs` unchanged as
|
|
the pure extraction/scoring module.
|
|
|
|
```rust
|
|
pub struct LabelState {
|
|
// Scratch buffers (reuse across frames)
|
|
scratch_candidates: Vec<LabelCandidate>,
|
|
scratch_accepted_centers: HashMap<String, Vec<Vec2d>>,
|
|
scratch_accepted_bounds: Vec<Rect>,
|
|
scratch_accepted_plans: Vec<(f64, usize, usize)>,
|
|
scratch_collision_grid: HashMap<(i32, i32), Vec<usize>>,
|
|
scratch_screen_path: Vec<Vec2d>,
|
|
scratch_cumulative: Vec<f64>,
|
|
scratch_smooth_a: Vec<Vec2d>,
|
|
scratch_smooth_b: Vec<Vec2d>,
|
|
path_glyphs: Vec<PathGlyphInstance>,
|
|
|
|
// Performance tracking
|
|
perf: LabelPerfStats,
|
|
prev_perf: LabelPerfStats,
|
|
}
|
|
```
|
|
|
|
**Methods:**
|
|
|
|
```rust
|
|
impl LabelState {
|
|
pub fn place_and_draw(
|
|
&mut self,
|
|
cx: &mut Cx2d,
|
|
cache: &TileCache,
|
|
draw_label: &mut DrawRotatedText,
|
|
draw_keys: &[TileKey],
|
|
view_zoom: f64,
|
|
map_offset: Vec2f,
|
|
rect: Rect,
|
|
);
|
|
|
|
// Internally:
|
|
fn collect_candidates(/* ... */);
|
|
fn build_placement(/* ... */);
|
|
}
|
|
```
|
|
|
|
**Current code moving here:**
|
|
- `place_and_draw_labels()` (view.rs:1377-1533)
|
|
- `collect_label_candidates()` (view.rs:1535-1659)
|
|
- `build_label_placement()` (view.rs:1661-1759)
|
|
|
|
---
|
|
|
|
### 6. Tile module split
|
|
|
|
**Addresses:** Review point #7 (too many responsibilities per file)
|
|
|
|
`tile.rs` currently handles types, MVT parsing, tessellation, Overpass queries, disk caching,
|
|
mbtiles batch loading — all in 2,163 lines. Split into:
|
|
|
|
**`tile.rs`** (~300 lines) — Types and tile lifecycle only:
|
|
- `TileKey`, `TileEntry`, `TileLoadState` (the explicit enum)
|
|
- `TileBuffers`, `TileWorkerMessage`
|
|
- `PendingTileRequest`
|
|
- `TileStatusCounts`
|
|
- `is_descendant_tile()`
|
|
- `overpass_query()`, `overpass_endpoint()`
|
|
|
|
**`tile_decode.rs`** (~800 lines) — Parsing and tessellation:
|
|
- `build_tile_buffers_from_body()`
|
|
- `build_tile_buffers_from_response()`
|
|
- `build_tile_buffers_from_response_owned()`
|
|
- `tessellate_tile_buffers()`
|
|
- MVT protobuf decode functions
|
|
- `mbtiles_tile_to_overpass_response()`
|
|
- Node projection, way grouping, polygon tessellation
|
|
|
|
**`tile_disk.rs`** (~300 lines) — Disk I/O:
|
|
- `tile_data_cache_path_for()`
|
|
- `store_tile_data_cache_on_disk()`
|
|
- `load_local_tile_batch()`
|
|
- Path resolution helpers
|
|
|
|
---
|
|
|
|
## Revised `view.rs` (~400 lines)
|
|
|
|
The widget becomes a thin coordinator:
|
|
|
|
```rust
|
|
pub struct MapView {
|
|
// Framework
|
|
uid: WidgetUid,
|
|
source: ScriptObjectRef,
|
|
walk: Walk,
|
|
layout: Layout,
|
|
|
|
// Draw objects (#[redraw] #[live])
|
|
draw_bg: DrawColor,
|
|
draw_map: DrawMapVector,
|
|
draw_label: DrawRotatedText,
|
|
draw_poi: DrawColor,
|
|
draw_text: DrawText,
|
|
|
|
// DSL-configurable properties (#[live])
|
|
center_lon: f64,
|
|
center_lat: f64,
|
|
zoom: f64,
|
|
min_zoom: f64,
|
|
max_zoom: f64,
|
|
dark_theme: bool,
|
|
style_light: MapThemeStyle,
|
|
style_dark: MapThemeStyle,
|
|
use_network: bool,
|
|
use_local_mbtiles: bool,
|
|
local_mbtiles_path: String,
|
|
local_tile_cache_dir: String,
|
|
|
|
// Subsystems (#[rust])
|
|
viewport: ViewportState,
|
|
cache: TileCache,
|
|
scheduler: TileScheduler,
|
|
label_state: LabelState,
|
|
renderer: RenderPass,
|
|
|
|
// Style (#[rust])
|
|
compiled_style_light: CompiledMapTheme,
|
|
compiled_style_dark: CompiledMapTheme,
|
|
applied_dark_theme: Option<bool>,
|
|
#[cfg(feature = "map_style")]
|
|
style_json_light: Option<StyleJson>,
|
|
|
|
// Worker thread (#[rust])
|
|
tile_worker_rx: ToUIReceiver<TileWorkerMessage>,
|
|
tile_thread_pool: Option<TagThreadPool<TileKey>>,
|
|
|
|
// Interaction (#[rust]) — kept minimal, delegates to viewport
|
|
drag_start_abs: Option<Vec2d>,
|
|
drag_start_center_norm: Vec2d,
|
|
active_fingers: HashMap<DigitId, Vec2d>,
|
|
pinch_initial_distance: Option<f64>,
|
|
pinch_initial_zoom: f64,
|
|
pinch_initial_center_norm: Vec2d,
|
|
}
|
|
```
|
|
|
|
**Method count reduction:**
|
|
|
|
| Before | After | Change |
|
|
|--------|-------|--------|
|
|
| ~30 methods on MapView | ~15 methods on MapView | -50% |
|
|
| 1,882 lines | ~400 lines | -79% |
|
|
|
|
**`draw_walk` becomes:**
|
|
|
|
```rust
|
|
fn draw_walk(&mut self, cx: &mut Cx2d, _scope: &mut Scope, walk: Walk) -> DrawStep {
|
|
let rect = cx.walk_turtle(walk);
|
|
self.viewport.set_rect(rect);
|
|
self.draw_bg.draw_abs(cx, rect);
|
|
|
|
// Schedule tiles (this was previously mixed into the draw path)
|
|
let actions = self.scheduler.update(&self.cache, &self.viewport, &self.config());
|
|
self.execute_actions(cx, actions);
|
|
|
|
// Render
|
|
let map_offset = self.viewport.map_offset();
|
|
let view_zoom = self.viewport.view_zoom();
|
|
|
|
RenderPass::build_draw_keys(&self.cache, self.scheduler.visible_tiles(), ...);
|
|
RenderPass::draw_fill(cx, &mut self.draw_map, &self.cache, &draw_tiles, view_zoom, map_offset);
|
|
RenderPass::draw_stroke(cx, &mut self.draw_map, &self.cache, &draw_tiles, view_zoom, map_offset);
|
|
RenderPass::draw_pois(cx, &mut self.draw_poi, &self.cache, &draw_tiles, view_zoom, map_offset, 13.0);
|
|
|
|
if view_zoom >= 13.0 {
|
|
self.label_state.place_and_draw(
|
|
cx, &self.cache, &mut self.draw_label,
|
|
&draw_tiles, view_zoom, map_offset, rect
|
|
);
|
|
}
|
|
|
|
DrawStep::done()
|
|
}
|
|
```
|
|
|
|
**`handle_event` becomes:**
|
|
|
|
```rust
|
|
fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
|
|
// 1. Worker messages → cache
|
|
self.handle_tile_worker_messages(cx);
|
|
|
|
// 2. HTTP → scheduler
|
|
self.widget_match_event(cx, event, scope);
|
|
|
|
// 3. Keyboard
|
|
if let Event::KeyDown(KeyDownEvent { key_code: KeyCode::KeyT, .. }) = event {
|
|
self.set_dark_theme(cx, !self.dark_theme);
|
|
}
|
|
|
|
// 4. Touch interaction → viewport
|
|
for actions in cx.read_hit_actions(&self.draw_bg.area, hit::Event::Hit(e)) {
|
|
match actions {
|
|
Hit::FingerDown(pe) => self.handle_finger_down(pe),
|
|
Hit::FingerMove(pe) => self.handle_finger_move(pe),
|
|
Hit::FingerUp(pe) => self.handle_finger_up(pe),
|
|
Hit::FingerScroll(fe) => self.viewport.apply_scroll(fe.scroll.y, fe.abs),
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Phase Plan
|
|
|
|
### Phase 1: Extract types (no behavior change)
|
|
|
|
**Goal:** Move types into new files without changing any behavior. Tests must pass.
|
|
|
|
**Steps:**
|
|
|
|
1. Create `tile.rs` slim version — move `TileKey`, `TileEntry`, `TileLoadState`, `TileBuffers`, `TileWorkerMessage`, `PendingTileRequest` into type-only definitions. Keep existing `tile.rs` as `tile_decode.rs` + `tile_disk.rs` temporarily.
|
|
|
|
2. Create `viewport.rs` — extract `ViewportState` with all viewport methods. Wire it into `MapView` as a field. MapView methods delegate to viewport.
|
|
|
|
3. Create `cache.rs` — extract `TileCache` wrapping the `HashMap<TileKey, TileEntry>` and all methods that operate on it. Wire into MapView.
|
|
|
|
4. Create `label_state.rs` — extract `LabelState` with all scratch buffers and label methods. Wire into MapView.
|
|
|
|
5. Create `renderer.rs` — extract `RenderPass` with fill/stroke/POI draw loops. Wire into MapView.
|
|
|
|
**Verification:** `cargo test -p makepad-widgets --features maps --lib` — all 184+ tests pass.
|
|
|
|
**Risk:** Makepad `#[derive(Widget)]` macro may have constraints on field types.
|
|
Mitigation: subsystems are plain Rust structs, no derive macros needed.
|
|
|
|
---
|
|
|
|
### Phase 2: Extract scheduler
|
|
|
|
**Goal:** Separate tile scheduling from rendering.
|
|
|
|
**Steps:**
|
|
|
|
1. Create `scheduler.rs` — extract `TileScheduler` with request/retry/priority logic.
|
|
2. Add generation tracking to `PendingTileRequest` and `TileWorkerMessage`.
|
|
3. Make `scheduler.update()` return `Vec<TileAction>` instead of directly issuing requests.
|
|
4. MapView `execute_actions()` dispatches actions to thread pool / HTTP.
|
|
5. Add `SchedulerConfig` to decouple scheduler from live properties.
|
|
|
|
**Verification:** All tests pass. Manual test: pan around Nairobi, verify tiles load correctly.
|
|
|
|
---
|
|
|
|
### Phase 3: Split tile.rs
|
|
|
|
**Goal:** Break the 2,163-line tile.rs into focused modules.
|
|
|
|
**Steps:**
|
|
|
|
1. Move MVT parsing + tessellation to `tile_decode.rs`.
|
|
2. Move disk cache + mbtiles batch to `tile_disk.rs`.
|
|
3. Keep `tile.rs` as type definitions + Overpass query generation.
|
|
|
|
**Verification:** All tests pass (tile.rs has 49 tests — they move with the functions).
|
|
|
|
---
|
|
|
|
### Phase 4: Optimize
|
|
|
|
**Address:** Review points #6, #7, #8, #9, #10, #19.
|
|
|
|
**Steps:**
|
|
|
|
1. **Cache visible tile set** — Don't recompute `visible_tile_keys()` every frame.
|
|
Store result in `ViewportState`, only recompute when viewport actually changes.
|
|
Add a `dirty` flag.
|
|
|
|
2. **Reduce HashMap traffic in draw loop** — `RenderPass` methods take `&TileCache`
|
|
and do single lookups per tile. The current code does `self.tiles.get(key)`
|
|
multiple times per tile in the draw loop. Cache the entry reference.
|
|
|
|
3. **Frame allocation audit** — Add debug assertions that no allocation happens
|
|
during normal frame rendering (all scratch buffers pre-allocated).
|
|
|
|
4. **Generation-based stale detection** — Worker messages carry generation.
|
|
Cache checks generation before accepting results. Prevents zoom-10 results
|
|
overwriting zoom-11 requests.
|
|
|
|
5. **Worker priority** — Visible tiles get higher priority in the thread pool.
|
|
Use `TagThreadPool` with priority tags or a priority queue wrapper.
|
|
|
|
6. **Memory stabilization** — TileCache eviction runs every frame if count > threshold.
|
|
Verify memory stabilizes during continuous panning. Add a `memory_bytes()`
|
|
estimation method for monitoring.
|
|
|
|
---
|
|
|
|
### Phase 5: Render pass ordering (optional, future)
|
|
|
|
**Address:** Review point #11 (render graph).
|
|
|
|
Currently fill/stroke/POI/label are hardcoded loops. Future improvement:
|
|
|
|
```rust
|
|
enum Pass {
|
|
Background,
|
|
WaterFill,
|
|
LanduseFill,
|
|
BuildingFill,
|
|
RoadCasing,
|
|
RoadCenter,
|
|
Railway,
|
|
Waterway,
|
|
Label,
|
|
Poi,
|
|
Selection,
|
|
Debug,
|
|
}
|
|
```
|
|
|
|
Each pass has a `draw()` method and a `z_order` for sorting. This enables:
|
|
- Easy insertion of new passes (3D buildings, terrain, traffic)
|
|
- Per-pass culling
|
|
- Debug visualization (show only one pass)
|
|
|
|
**Not implementing now** — the current fill/stroke/POI/label ordering works.
|
|
This is documented for future reference.
|
|
|
|
---
|
|
|
|
## File-by-File Changes
|
|
|
|
### `view.rs` (1,882 → ~400 lines)
|
|
|
|
**Removed:**
|
|
- `ViewportState` fields and methods → `viewport.rs`
|
|
- `TileCache` fields and methods → `cache.rs`
|
|
- `TileScheduler` fields and methods → `scheduler.rs`
|
|
- `LabelState` fields and methods → `label_state.rs`
|
|
- `RenderPass` methods → `renderer.rs`
|
|
- Tile type definitions → `tile.rs`
|
|
|
|
**Kept:**
|
|
- `#[derive(Script, Widget)]` struct with subsystem fields
|
|
- `ScriptHook::on_after_apply` (thin: sync properties → viewport, rebuild styles)
|
|
- `Widget::handle_event` (thin: delegates to subsystems)
|
|
- `Widget::draw_walk` (thin: delegates to RenderPass + LabelState)
|
|
- `WidgetMatchEvent` (thin: delegates HTTP to scheduler)
|
|
- `set_dark_theme`, `apply_theme_change`, `apply_theme_palette`
|
|
- `load_style_json`, `recompile_style_for_zoom`
|
|
- `resolve_mbtiles_path` (could move to SchedulerConfig)
|
|
|
|
### `tile.rs` (2,163 → ~300 lines + 2 new files)
|
|
|
|
**Keeps:** Type definitions, `overpass_query`, `overpass_endpoint`, `is_descendant_tile`, constants
|
|
|
|
**Moves to `tile_decode.rs`:** MVT protobuf, tessellation, body processing
|
|
**Moves to `tile_disk.rs`:** Disk cache, mbtiles batch loading
|
|
|
|
### New files (6 files, ~1,450 lines total)
|
|
|
|
| File | Lines | Responsibility |
|
|
|------|-------|----------------|
|
|
| `viewport.rs` | ~150 | Coordinate math, interaction, visible tiles |
|
|
| `cache.rs` | ~200 | Tile storage, lifecycle, eviction |
|
|
| `scheduler.rs` | ~250 | Request queue, retry, priority, generation |
|
|
| `renderer.rs` | ~200 | Fill/stroke/POI draw passes |
|
|
| `label_state.rs` | ~300 | Label scratch buffers, placement, drawing |
|
|
| `tile_decode.rs` | ~800 | MVT parsing, tessellation |
|
|
| `tile_disk.rs` | ~300 | Disk I/O, mbtiles batch |
|
|
|
|
### Unchanged files (6)
|
|
|
|
- `geometry.rs` (1,968 lines)
|
|
- `label.rs` (1,098 lines)
|
|
- `style.rs` (496 lines)
|
|
- `style_json.rs` (3,227 lines)
|
|
- `asset_loader.rs` (124 lines)
|
|
- `sprite.rs` (433 lines)
|
|
|
|
---
|
|
|
|
## Downstream Compatibility
|
|
|
|
### Public API (must not change)
|
|
|
|
```rust
|
|
// Widget trait — framework calls these
|
|
fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope);
|
|
fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep;
|
|
|
|
// ScriptHook — DSL properties flow in here
|
|
fn on_after_apply(&mut self, vm: &mut ScriptVm, apply: &Apply, scope: &mut Scope, value: ScriptValue);
|
|
|
|
// WidgetMatchEvent — HTTP responses
|
|
fn handle_http_response(&mut self, cx: &mut Cx, request_id: LiveId, response: &HttpResponse, scope: &mut Scope);
|
|
fn handle_http_request_error(&mut self, cx: &mut Cx, request_id: LiveId, err: &HttpError, scope: &mut Scope);
|
|
|
|
// Public methods
|
|
fn load_style_json(&mut self, json_str: &str) -> Result<(), String>;
|
|
fn recompile_style_for_zoom(&mut self, zoom: f64);
|
|
```
|
|
|
|
### DSL (book.rs must not change)
|
|
|
|
```rust
|
|
map_layer := MapView {
|
|
width: Fill
|
|
height: Fill
|
|
center_lon: 36.8219
|
|
center_lat: -1.2921
|
|
zoom: 14.0
|
|
min_zoom: 10.0
|
|
max_zoom: 18.0
|
|
use_local_mbtiles: true
|
|
use_network: false
|
|
local_mbtiles_path: "kenya-shortbread-1.0.mbtiles"
|
|
}
|
|
```
|
|
|
|
### mbtile_reader crate (must not change)
|
|
|
|
Separate crate at `makepad/libs/mbtile_reader/`. API:
|
|
- `MbtileReader::open()`
|
|
- `get_tiles_at_zoom()`
|
|
- `get_tiles_filtered()`
|
|
- `Value`, `Error` types
|
|
|
|
---
|
|
|
|
## Test Strategy
|
|
|
|
### Existing tests (184+)
|
|
|
|
| Module | Tests | Status |
|
|
|--------|-------|--------|
|
|
| geometry.rs | 54 | UNCHANGED — no code movement |
|
|
| tile.rs | 49 | MOVE with functions to tile_decode.rs / tile_disk.rs |
|
|
| label.rs | 69 | UNCHANGED — no code movement |
|
|
| style_json.rs | 101 | UNCHANGED — no code movement |
|
|
| sprite.rs | 25 | UNCHANGED — no code movement |
|
|
| mbtile_reader | 61 | SEPARATE CRATE — no change |
|
|
|
|
**Zero test breakage** — all tests move with their functions, module paths update in `#[cfg(test)]` blocks.
|
|
|
|
### New tests per phase
|
|
|
|
**Phase 1 (types):**
|
|
- `viewport.rs`: screen<->world transforms, wrap/clamp, visible tile keys
|
|
- `cache.rs`: insert/evict/mark-failed, generation tracking
|
|
- `renderer.rs`: build_draw_keys with ancestor/descendant fallback
|
|
- `label_state.rs`: (already covered by label.rs tests)
|
|
|
|
**Phase 2 (scheduler):**
|
|
- `scheduler.rs`: request prioritization, retry timing, generation staleness
|
|
- Action generation for various cache states
|
|
|
|
**Phase 3 (tile split):**
|
|
- Verify all existing tile.rs tests pass with new module paths
|
|
|
|
**Phase 4 (optimize):**
|
|
- Visible tile cache: verify no recomputation when viewport unchanged
|
|
- Frame allocation: debug assertion that scratch buffers don't reallocate
|
|
|
|
---
|
|
|
|
## Risk Assessment
|
|
|
|
| Risk | Likelihood | Impact | Mitigation |
|
|
|------|-----------|--------|------------|
|
|
| Makepad derive macro incompatibility | Low | High | Subsystems are plain structs, not widgets |
|
|
| Thread pool API changes | Low | Medium | Pin to current `TagThreadPool<TileKey>` API |
|
|
| Performance regression from indirection | Medium | Low | Profile after each phase; subsystems are `#[inline]` candidates |
|
|
| Test coverage gaps in view.rs | High | Medium | No tests exist for view.rs today — add integration tests in Phase 1 |
|
|
| merge conflicts with upstream makepad | Medium | Medium | Minimize changes to files outside `map/` directory |
|
|
|
|
---
|
|
|
|
## Decision Log
|
|
|
|
| Decision | Rationale |
|
|
|----------|-----------|
|
|
| Keep `geometry.rs`, `label.rs`, `style.rs` unchanged | Already well-factored; 297 tests pass |
|
|
| ViewportState has no Cx dependency | Enables unit testing without Makepad context |
|
|
| TileScheduler returns actions, doesn't execute | Decouples scheduling from I/O execution |
|
|
| TileCache owns the HashMap | Single clear owner for tile lifecycle |
|
|
| RenderPass takes &TileCache, doesn't own tiles | Read-only during draw, no mutation in render path |
|
|
| LabelState keeps scratch buffers | Avoids per-frame allocation; keeps label.rs pure |
|
|
| Generation tracking via u64 counter | Simple, no ABA problem at realistic rates |
|
|
| SchedulerConfig struct for constants | Makes scheduling testable without live properties |
|
|
| Split tile.rs into 3 files | Each file has one responsibility |
|
|
| Phase 1 is purely mechanical extraction | Zero risk of behavior change; builds confidence |
|