Merge pull request #8205 from penpot/superalex-improve-huge-shapes-render

🎉 Improving huge shapes render
This commit is contained in:
Elena Torró 2026-01-27 13:08:25 +01:00 committed by GitHub
commit 9808b6ca57
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 99 additions and 69 deletions

View File

@ -124,33 +124,51 @@
(defn adjust-to-viewport (defn adjust-to-viewport
([viewport srect] (adjust-to-viewport viewport srect nil)) ([viewport srect] (adjust-to-viewport viewport srect nil))
([viewport srect {:keys [padding] :or {padding 0}}] ([viewport srect {:keys [padding min-zoom] :or {padding 0 min-zoom nil}}]
(let [gprop (/ (:width viewport) (let [gprop (/ (:width viewport)
(:height viewport)) (:height viewport))
srect (-> srect srect-padded (-> srect
(update :x #(- % padding)) (update :x #(- % padding))
(update :y #(- % padding)) (update :y #(- % padding))
(update :width #(+ % padding padding)) (update :width #(+ % padding padding))
(update :height #(+ % padding padding))) (update :height #(+ % padding padding)))
width (:width srect) width (:width srect-padded)
height (:height srect) height (:height srect-padded)
lprop (/ width height)] lprop (/ width height)
(cond adjusted-rect
(> gprop lprop) (cond
(let [width' (* (/ width lprop) gprop) (> gprop lprop)
padding (/ (- width' width) 2)] (let [width' (* (/ width lprop) gprop)
(-> srect padding (/ (- width' width) 2)]
(update :x #(- % padding)) (-> srect-padded
(assoc :width width') (update :x #(- % padding))
(grc/update-rect :position))) (assoc :width width')
(grc/update-rect :position)))
(< gprop lprop) (< gprop lprop)
(let [height' (/ (* height lprop) gprop) (let [height' (/ (* height lprop) gprop)
padding (/ (- height' height) 2)] padding (/ (- height' height) 2)]
(-> srect (-> srect-padded
(update :y #(- % padding)) (update :y #(- % padding))
(assoc :height height') (assoc :height height')
(grc/update-rect :position))) (grc/update-rect :position)))
:else :else
(grc/update-rect srect :position))))) (grc/update-rect srect-padded :position))]
;; If min-zoom is specified and the resulting zoom would be below it,
;; return a rect with the original top-left corner centered in the viewport
;; instead of using the aspect-ratio-adjusted rect (which can push coords
;; extremely far with extreme aspect ratios).
(if (and (some? min-zoom)
(< (/ (:width viewport) (:width adjusted-rect)) min-zoom))
(let [anchor-x (:x srect)
anchor-y (:y srect)
vbox-width (/ (:width viewport) min-zoom)
vbox-height (/ (:height viewport) min-zoom)]
(-> adjusted-rect
(assoc :x (- anchor-x (/ vbox-width 2))
:y (- anchor-y (/ vbox-height 2))
:width vbox-width
:height vbox-height)
(grc/update-rect :position)))
adjusted-rect))))

View File

@ -51,7 +51,7 @@
(or (> (:width srect) width) (or (> (:width srect) width)
(> (:height srect) height)) (> (:height srect) height))
(let [srect (gal/adjust-to-viewport size srect {:padding 40}) (let [srect (gal/adjust-to-viewport size srect {:padding 40 :min-zoom 0.01})
zoom (/ (:width size) (:width srect))] zoom (/ (:width size) (:width srect))]
(-> local (-> local

View File

@ -97,7 +97,7 @@
state state
(update state :workspace-local (update state :workspace-local
(fn [{:keys [vport] :as local}] (fn [{:keys [vport] :as local}]
(let [srect (gal/adjust-to-viewport vport srect {:padding 160}) (let [srect (gal/adjust-to-viewport vport srect {:padding 160 :min-zoom 0.01})
zoom (/ (:width vport) (:width srect))] zoom (/ (:width vport) (:width srect))]
(-> local (-> local
(assoc :zoom zoom) (assoc :zoom zoom)
@ -118,7 +118,7 @@
(gsh/shapes->rect))] (gsh/shapes->rect))]
(update state :workspace-local (update state :workspace-local
(fn [{:keys [vport] :as local}] (fn [{:keys [vport] :as local}]
(let [srect (gal/adjust-to-viewport vport srect {:padding 40}) (let [srect (gal/adjust-to-viewport vport srect {:padding 40 :min-zoom 0.01})
zoom (/ (:width vport) (:width srect))] zoom (/ (:width vport) (:width srect))]
(-> local (-> local
(assoc :zoom zoom) (assoc :zoom zoom)
@ -142,7 +142,7 @@
(fn [{:keys [vport] :as local}] (fn [{:keys [vport] :as local}]
(let [srect (gal/adjust-to-viewport (let [srect (gal/adjust-to-viewport
vport srect vport srect
{:padding 40}) {:padding 40 :min-zoom 0.01})
zoom (/ (:width vport) zoom (/ (:width vport)
(:width srect))] (:width srect))]
(-> local (-> local

View File

@ -275,29 +275,26 @@ pub extern "C" fn set_view_end() {
state.render_state.options.set_fast_mode(false); state.render_state.options.set_fast_mode(false);
state.render_state.cancel_animation_frame(); state.render_state.cancel_animation_frame();
let zoom_changed = state.render_state.zoom_changed(); // Update tile_viewbox first so that get_tiles_for_shape uses the correct interest area
// Only rebuild tile indices when zoom has changed. // This is critical because we limit tiles to the interest area for optimization
// During pan-only operations, shapes stay in the same tiles let scale = state.render_state.get_scale();
// because tile_size = 1/scale * TILE_SIZE (depends only on zoom). state
if zoom_changed { .render_state
let _rebuild_start = performance::begin_timed_log!("rebuild_tiles"); .tile_viewbox
performance::begin_measure!("set_view_end::rebuild_tiles"); .update(state.render_state.viewbox, scale);
if state.render_state.options.is_profile_rebuild_tiles() {
state.rebuild_tiles(); // We rebuild the tile index on both pan and zoom because `get_tiles_for_shape`
} else { // clips each shape to the current `TileViewbox::interest_rect` (viewport-dependent).
state.rebuild_tiles_shallow(); let _rebuild_start = performance::begin_timed_log!("rebuild_tiles");
} performance::begin_measure!("set_view_end::rebuild_tiles");
performance::end_measure!("set_view_end::rebuild_tiles"); if state.render_state.options.is_profile_rebuild_tiles() {
performance::end_timed_log!("rebuild_tiles", _rebuild_start); state.rebuild_tiles();
} else { } else {
// During pan, we only clear the tile index without state.rebuild_tiles_shallow();
// 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);
} }
performance::end_measure!("set_view_end::rebuild_tiles");
performance::end_timed_log!("rebuild_tiles", _rebuild_start);
state.render_state.sync_cached_viewbox(); state.render_state.sync_cached_viewbox();
performance::end_measure!("set_view_end"); performance::end_measure!("set_view_end");
performance::end_timed_log!("set_view_end", _end_start); performance::end_timed_log!("set_view_end", _end_start);

View File

@ -1168,7 +1168,6 @@ impl RenderState {
let scale = self.get_scale(); let scale = self.get_scale();
self.tile_viewbox.update(self.viewbox, scale); self.tile_viewbox.update(self.viewbox, scale);
self.focus_mode.reset(); self.focus_mode.reset();
performance::begin_measure!("render"); 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 { pub fn get_tiles_for_shape(&mut self, shape: &Shape, tree: ShapesPoolRef) -> TileRect {
let scale = self.get_scale(); let scale = self.get_scale();
let extrect = self.get_cached_extrect(shape, tree, scale); let extrect = self.get_cached_extrect(shape, tree, scale);
let tile_size = tiles::get_tile_size(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"); 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>) { pub fn rebuild_tiles_from(&mut self, tree: ShapesPoolRef, base_id: Option<&Uuid>) {
performance::begin_measure!("rebuild_tiles"); performance::begin_measure!("rebuild_tiles");

View File

@ -207,10 +207,6 @@ impl State {
self.render_state.rebuild_tiles_shallow(&self.shapes); 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) { pub fn rebuild_tiles(&mut self) {
self.render_state.rebuild_tiles_from(&self.shapes, None); self.render_state.rebuild_tiles_from(&self.shapes, None);
} }