diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index c23ce7a07c..b571aea098 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -275,29 +275,26 @@ pub extern "C" fn set_view_end() { state.render_state.options.set_fast_mode(false); state.render_state.cancel_animation_frame(); - let zoom_changed = state.render_state.zoom_changed(); - // Only rebuild tile indices when zoom has changed. - // During pan-only operations, shapes stay in the same tiles - // because tile_size = 1/scale * TILE_SIZE (depends only on zoom). - if zoom_changed { - let _rebuild_start = performance::begin_timed_log!("rebuild_tiles"); - performance::begin_measure!("set_view_end::rebuild_tiles"); - if state.render_state.options.is_profile_rebuild_tiles() { - state.rebuild_tiles(); - } else { - state.rebuild_tiles_shallow(); - } - performance::end_measure!("set_view_end::rebuild_tiles"); - performance::end_timed_log!("rebuild_tiles", _rebuild_start); + // Update tile_viewbox first so that get_tiles_for_shape uses the correct interest area + // This is critical because we limit tiles to the interest area for optimization + let scale = state.render_state.get_scale(); + state + .render_state + .tile_viewbox + .update(state.render_state.viewbox, scale); + + // We rebuild the tile index on both pan and zoom because `get_tiles_for_shape` + // clips each shape to the current `TileViewbox::interest_rect` (viewport-dependent). + let _rebuild_start = performance::begin_timed_log!("rebuild_tiles"); + performance::begin_measure!("set_view_end::rebuild_tiles"); + if state.render_state.options.is_profile_rebuild_tiles() { + state.rebuild_tiles(); } else { - // During pan, we only clear the tile index without - // invalidating cached textures, which is more efficient. - let _clear_start = performance::begin_timed_log!("clear_tile_index"); - performance::begin_measure!("set_view_end::clear_tile_index"); - state.clear_tile_index(); - performance::end_measure!("set_view_end::clear_tile_index"); - performance::end_timed_log!("clear_tile_index", _clear_start); + state.rebuild_tiles_shallow(); } + performance::end_measure!("set_view_end::rebuild_tiles"); + performance::end_timed_log!("rebuild_tiles", _rebuild_start); + state.render_state.sync_cached_viewbox(); performance::end_measure!("set_view_end"); performance::end_timed_log!("set_view_end", _end_start); diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 480e510fe5..76eedf0288 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -1168,7 +1168,6 @@ impl RenderState { let scale = self.get_scale(); self.tile_viewbox.update(self.viewbox, scale); - self.focus_mode.reset(); performance::begin_measure!("render"); @@ -2111,13 +2110,44 @@ impl RenderState { } /* - * Given a shape returns the TileRect with the range of tiles that the shape is in + * Given a shape returns the TileRect with the range of tiles that the shape is in. + * This is always limited to the interest area to optimize performance and prevent + * processing unnecessary tiles outside the viewport. The interest area already + * includes a margin (VIEWPORT_INTEREST_AREA_THRESHOLD) calculated via + * get_tiles_for_viewbox_with_interest, ensuring smooth pan/zoom interactions. + * + * When the viewport changes (pan/zoom), the interest area is updated and shapes + * are dynamically added to the tile index via the fallback mechanism in + * render_shape_tree_partial_uncached, ensuring all shapes render correctly. */ pub fn get_tiles_for_shape(&mut self, shape: &Shape, tree: ShapesPoolRef) -> TileRect { let scale = self.get_scale(); let extrect = self.get_cached_extrect(shape, tree, scale); let tile_size = tiles::get_tile_size(scale); - tiles::get_tiles_for_rect(extrect, tile_size) + let shape_tiles = tiles::get_tiles_for_rect(extrect, tile_size); + let interest_rect = &self.tile_viewbox.interest_rect; + // Calculate the intersection of shape_tiles with interest_rect + // This returns only the tiles that are both in the shape and in the interest area + let intersection_x1 = shape_tiles.x1().max(interest_rect.x1()); + let intersection_y1 = shape_tiles.y1().max(interest_rect.y1()); + let intersection_x2 = shape_tiles.x2().min(interest_rect.x2()); + let intersection_y2 = shape_tiles.y2().min(interest_rect.y2()); + + // Return the intersection if valid (there is overlap), otherwise return empty rect + if intersection_x1 <= intersection_x2 && intersection_y1 <= intersection_y2 { + // Valid intersection: return the tiles that are in both shape_tiles and interest_rect + TileRect( + intersection_x1, + intersection_y1, + intersection_x2, + intersection_y2, + ) + } else { + // No intersection: shape is completely outside interest area + // The shape will be added dynamically via add_shape_tiles when it enters + // the interest area during pan/zoom operations + TileRect(0, 0, -1, -1) + } } /* @@ -2198,17 +2228,6 @@ impl RenderState { performance::end_measure!("rebuild_tiles_shallow"); } - /// Clears the tile index without invalidating cached tile textures. - /// This is useful when tile positions don't change (e.g., during pan operations) - /// but the tile index needs to be synchronized. The cached tile textures remain - /// valid since they don't depend on the current view position, only on zoom level. - /// This is much more efficient than clearing the entire cache surface. - pub fn clear_tile_index(&mut self) { - performance::begin_measure!("clear_tile_index"); - self.surfaces.clear_tiles(); - performance::end_measure!("clear_tile_index"); - } - pub fn rebuild_tiles_from(&mut self, tree: ShapesPoolRef, base_id: Option<&Uuid>) { performance::begin_measure!("rebuild_tiles"); diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index 385408d89f..7762d4b5aa 100644 --- a/render-wasm/src/state.rs +++ b/render-wasm/src/state.rs @@ -207,10 +207,6 @@ impl State { self.render_state.rebuild_tiles_shallow(&self.shapes); } - pub fn clear_tile_index(&mut self) { - self.render_state.clear_tile_index(); - } - pub fn rebuild_tiles(&mut self) { self.render_state.rebuild_tiles_from(&self.shapes, None); }