diff --git a/crates/rnote-compose/src/ext.rs b/crates/rnote-compose/src/ext.rs index d41ba18f68..e9516bf203 100644 --- a/crates/rnote-compose/src/ext.rs +++ b/crates/rnote-compose/src/ext.rs @@ -172,6 +172,9 @@ where split_size: na::Vector2, split_order: SplitOrder, ) -> Vec; + /// Gives an aabb of size split_size, snapped to a multiple of split_size + /// This returns the first element of a call to `split_extended_origin_aligned` + fn split_first_origin_aligned(self, split_size: na::Vector2) -> Self; /// Converts a Aabb to a kurbo Rectangle fn to_kurbo_rect(&self) -> kurbo::Rect; /// Converts a kurbo Rectangle to Aabb @@ -394,6 +397,13 @@ impl AabbExt for Aabb { split_aabbs } + fn split_first_origin_aligned(self, split_size: na::Vector2) -> Self { + let offset_outer = (self.mins[1] / split_size[1]).floor() * split_size[1]; + let offset_inner = (self.mins[0] / split_size[0]).floor() * split_size[0]; + let mins = na::point![offset_inner, offset_outer]; + Aabb::new(mins, mins + split_size) + } + fn split_extended_origin_aligned( self, split_size: na::Vector2, diff --git a/crates/rnote-engine/src/document/mod.rs b/crates/rnote-engine/src/document/mod.rs index 84b360e1ed..41b4466369 100644 --- a/crates/rnote-engine/src/document/mod.rs +++ b/crates/rnote-engine/src/document/mod.rs @@ -256,14 +256,14 @@ impl Document { ); if include_content { - let keys = store.stroke_keys_as_rendered(); - let content_bounds = if let Some(content_bounds) = store.bounds_for_strokes(&keys) { - content_bounds - .extend_right_and_bottom_by(na::vector![padding_horizontal, padding_vertical]) - } else { + let content_bounds = if store.keytree_is_empty() { // If doc is empty, resize to one page with the format size Aabb::new(na::point![0.0, 0.0], self.config.format.size().into()) .extend_right_and_bottom_by(na::vector![padding_horizontal, padding_vertical]) + } else { + store + .get_bounds_non_trashed() + .extend_right_and_bottom_by(na::vector![padding_horizontal, padding_vertical]) }; new_bounds.merge(&content_bounds); } @@ -301,9 +301,11 @@ impl Document { .merged(&viewport.extend_by(na::vector![padding_horizontal, padding_vertical])); if include_content { - let keys = store.stroke_keys_as_rendered(); - let content_bounds = if let Some(content_bounds) = store.bounds_for_strokes(&keys) { - content_bounds.extend_by(na::vector![padding_horizontal, padding_vertical]) + let rendered_bounds = store.get_bounds_non_trashed(); + + let content_bounds = if rendered_bounds.volume() > 0.0 { + rendered_bounds + .extend_right_and_bottom_by(na::vector![padding_horizontal, padding_vertical]) } else { // If doc is empty, resize to one page with the format size Aabb::new(na::point![0.0, 0.0], self.config.format.size().into()) diff --git a/crates/rnote-engine/src/engine/mod.rs b/crates/rnote-engine/src/engine/mod.rs index 974ad0dd91..da8d8c2195 100644 --- a/crates/rnote-engine/src/engine/mod.rs +++ b/crates/rnote-engine/src/engine/mod.rs @@ -199,7 +199,7 @@ pub struct Engine { background_tile_image: Option, #[cfg(feature = "ui")] #[serde(skip)] - background_rendernodes: Vec, + background_rendernode: Option, // Origin indicator rendering #[serde(skip)] origin_indicator_image: Option, @@ -225,7 +225,7 @@ impl Default for Engine { tasks_rx: Some(EngineTaskReceiver(tasks_rx)), background_tile_image: None, #[cfg(feature = "ui")] - background_rendernodes: Vec::default(), + background_rendernode: None, origin_indicator_image: None, #[cfg(feature = "ui")] origin_indicator_rendernode: None, diff --git a/crates/rnote-engine/src/engine/rendering.rs b/crates/rnote-engine/src/engine/rendering.rs index 55066bbec1..c17f3a6453 100644 --- a/crates/rnote-engine/src/engine/rendering.rs +++ b/crates/rnote-engine/src/engine/rendering.rs @@ -17,11 +17,9 @@ impl Engine { { use crate::ext::GrapheneRectExt; use gtk4::{graphene, gsk, prelude::*}; - use rnote_compose::SplitOrder; use rnote_compose::ext::AabbExt; let viewport = self.camera.viewport(); - let mut rendernodes: Vec = vec![]; if let Some(image) = &self.background_tile_image { // Only create the texture once, it is expensive @@ -35,21 +33,17 @@ impl Engine { } }; - for split_bounds in viewport.split_extended_origin_aligned( - self.document.config.background.tile_size(), - SplitOrder::default(), - ) { - rendernodes.push( - gsk::TextureNode::new( - &new_texture, - &graphene::Rect::from_p2d_aabb(split_bounds), - ) - .upcast(), - ); - } - } + let origin_aabb = viewport + .split_first_origin_aligned(self.document.config.background.tile_size()); - self.background_rendernodes = rendernodes; + self.background_rendernode = Some( + gsk::TextureNode::new( + &new_texture, + &graphene::Rect::from_p2d_aabb(origin_aabb), + ) + .upcast(), + ); + } } #[cfg(feature = "ui")] @@ -117,7 +111,7 @@ impl Engine { self.origin_indicator_image.take(); #[cfg(feature = "ui")] { - self.background_rendernodes.clear(); + self.background_rendernode = None; self.origin_indicator_rendernode.take(); } widget_flags.redraw = true; @@ -251,17 +245,17 @@ impl Engine { snapshot.append_node( gsk::ColorNode::new( &gdk::RGBA::from_compose_color(self.document.config.background.color), - //&gdk::RGBA::RED, &graphene::Rect::from_p2d_aabb(doc_bounds), ) .upcast(), ); + snapshot.pop(); - for r in self.background_rendernodes.iter() { - snapshot.append_node(r); + if let Some(render_node) = &self.background_rendernode { + snapshot.push_repeat(&graphene::Rect::from_p2d_aabb(doc_bounds), None); + snapshot.append_node(render_node); + snapshot.pop(); } - - snapshot.pop(); Ok(()) } diff --git a/crates/rnote-engine/src/engine/visual_debug.rs b/crates/rnote-engine/src/engine/visual_debug.rs index f50342d38c..38ac0c7739 100644 --- a/crates/rnote-engine/src/engine/visual_debug.rs +++ b/crates/rnote-engine/src/engine/visual_debug.rs @@ -49,6 +49,18 @@ pub const COLOR_DOC_BOUNDS: Color = Color { b: 0.8, a: 1.0, }; +pub const COLOR_RTREE_BOUNDS: Color = Color { + r: 1.0, + g: 0.5, + b: 0.0, + a: 1.0, +}; +pub const COLOR_TRASH_RTREE_BOUNDS: Color = Color { + r: 0.0, + g: 0.5, + b: 1.0, + a: 1.0, +}; #[cfg(feature = "ui")] pub(crate) fn draw_bounds_to_gtk_snapshot( diff --git a/crates/rnote-engine/src/store/chrono_comp.rs b/crates/rnote-engine/src/store/chrono_comp.rs index a06c984825..75c273e387 100644 --- a/crates/rnote-engine/src/store/chrono_comp.rs +++ b/crates/rnote-engine/src/store/chrono_comp.rs @@ -143,6 +143,33 @@ impl StrokeStore { keys } + pub(crate) fn keys_bounds_sorted_chrono_intersecting_bounds( + &self, + bounds: Aabb, + ) -> Vec<(StrokeKey, Aabb)> { + let chrono_components = &self.chrono_components; + + let mut keys = self.key_tree.keys_bounds_intersecting_bounds(bounds); + + keys.par_sort_unstable_by(|&(first, ..), &(second, ..)| { + if let (Some(first_chrono), Some(second_chrono)) = + (chrono_components.get(first), chrono_components.get(second)) + { + let layer_order = first_chrono.layer.cmp(&second_chrono.layer); + + if layer_order != std::cmp::Ordering::Equal { + layer_order + } else { + first_chrono.t.cmp(&second_chrono.t) + } + } else { + std::cmp::Ordering::Equal + } + }); + + keys + } + pub(crate) fn keys_sorted_chrono_in_bounds(&self, bounds: Aabb) -> Vec { let chrono_components = &self.chrono_components; diff --git a/crates/rnote-engine/src/store/keytree.rs b/crates/rnote-engine/src/store/keytree.rs index 72428c5e4c..0839a2f093 100644 --- a/crates/rnote-engine/src/store/keytree.rs +++ b/crates/rnote-engine/src/store/keytree.rs @@ -2,6 +2,7 @@ use super::StrokeKey; use p2d::bounding_volume::Aabb; use rstar::primitives::GeomWithData; +use std::collections::HashMap; /// The rtree object that holds the bounds and [StrokeKey]. type KeyTreeObject = GeomWithData, StrokeKey>; @@ -19,10 +20,12 @@ impl KeyTree { } /// Removes the [KeyTreeObject] for the given key. - pub(crate) fn remove_with_key(&mut self, key: StrokeKey) -> Option { + pub(crate) fn remove_with_key(&mut self, key: StrokeKey) -> Option<(StrokeKey, Aabb)> { let object_to_remove = self.0.iter().find(|&object| object.data == key)?.to_owned(); - self.0.remove(&object_to_remove) + self.0 + .remove(&object_to_remove) + .and_then(|key_object| Some(keytree_to_store(&key_object))) } /// Update the Tree with new bounds for the given key. @@ -44,6 +47,28 @@ impl KeyTree { .collect() } + /// Return the keys + bounds that intersect with the given bounds. + pub(crate) fn keys_bounds_intersecting_bounds(&self, bounds: Aabb) -> Vec<(StrokeKey, Aabb)> { + self.0 + .locate_in_envelope_intersecting(&rstar::AABB::from_corners( + [bounds.mins[0], bounds.mins[1]], + [bounds.maxs[0], bounds.maxs[1]], + )) + .map(|object| keytree_to_store(&object)) + .collect() + } + + /// Return the keys that intersect with the given bounds. + pub(crate) fn keys_intersecting_bounds_hashset(&self, bounds: Aabb) -> HashMap { + self.0 + .locate_in_envelope_intersecting(&rstar::AABB::from_corners( + [bounds.mins[0], bounds.mins[1]], + [bounds.maxs[0], bounds.maxs[1]], + )) + .map(|object| (object.data, ())) + .collect() + } + /// Return the keys that are completely contained in the given bounds. pub(crate) fn keys_in_bounds(&self, bounds: Aabb) -> Vec { self.0 @@ -69,6 +94,18 @@ impl KeyTree { pub(crate) fn clear(&mut self) { *self = Self::default() } + + pub fn get_bounds(&self) -> Aabb { + let aabb_enveloppe = self.0.root().envelope(); + Aabb::new( + na::point![aabb_enveloppe.lower()[0], aabb_enveloppe.lower()[1]], + na::point![aabb_enveloppe.upper()[0], aabb_enveloppe.upper()[1]], + ) + } + + pub(crate) fn is_empty(&self) -> bool { + self.0.size() == 0 + } } fn new_keytree_object(key: StrokeKey, bounds: Aabb) -> KeyTreeObject { @@ -80,3 +117,13 @@ fn new_keytree_object(key: StrokeKey, bounds: Aabb) -> KeyTreeObject { key, ) } + +fn keytree_to_store(key_object: &KeyTreeObject) -> (StrokeKey, Aabb) { + ( + key_object.data, + Aabb::new( + na::point![key_object.geom().lower()[0], key_object.geom().lower()[1]], + na::point![key_object.geom().upper()[0], key_object.geom().upper()[1]], + ), + ) +} diff --git a/crates/rnote-engine/src/store/mod.rs b/crates/rnote-engine/src/store/mod.rs index e73fc2f7ef..7915ac5722 100644 --- a/crates/rnote-engine/src/store/mod.rs +++ b/crates/rnote-engine/src/store/mod.rs @@ -9,6 +9,7 @@ pub mod trash_comp; // Re-exports pub use chrono_comp::ChronoComponent; use keytree::KeyTree; +use p2d::bounding_volume::Aabb; pub use render_comp::RenderComponent; pub use selection_comp::SelectionComponent; pub use trash_comp::TrashComponent; @@ -94,8 +95,14 @@ pub struct StrokeStore { /// An rtree backed by the slotmap store, for faster spatial queries. /// /// Needs to be updated with `update_with_key()` when strokes changed their geometry or position! + /// Only holds non trashed keys #[serde(skip)] key_tree: KeyTree, + /// Same principle but only for trashed keys + /// This allows operations on non trashed strokes to + /// be faster (as they don't incur filtering after the fact) + #[serde(skip)] + trashed_key_tree: KeyTree, } impl Default for StrokeStore { @@ -112,6 +119,7 @@ impl Default for StrokeStore { live_index: 0, key_tree: KeyTree::default(), + trashed_key_tree: KeyTree::default(), chrono_counter: 0, } @@ -144,12 +152,13 @@ impl StrokeStore { /// Rebuild the rtree with the current stored strokes keys and bounds. fn rebuild_rtree(&mut self) { - let tree_objects = self + let (tree_objects, trashed_tree_objects) = self .stroke_components .iter() .map(|(key, stroke)| (key, stroke.bounds())) - .collect(); + .partition(|(key, _bounds)| self.trashed(*key).is_some_and(|x| !x)); self.key_tree.rebuild_from_vec(tree_objects); + self.trashed_key_tree.rebuild_from_vec(trashed_tree_objects); } /// Checks the equality of current state to all fields of the given history entry, @@ -348,6 +357,7 @@ impl StrokeStore { self.render_components.remove(key); self.key_tree.remove_with_key(key); + self.trashed_key_tree.remove_with_key(key); Arc::make_mut(&mut self.stroke_components) .remove(key) .map(|stroke| (*stroke).clone()) @@ -365,7 +375,16 @@ impl StrokeStore { self.render_components.clear(); self.key_tree.clear(); + self.trashed_key_tree.clear(); widget_flags } + + pub(super) fn get_bounds_non_trashed(&self) -> Aabb { + self.key_tree.get_bounds() + } + + pub(super) fn keytree_is_empty(&self) -> bool { + self.key_tree.is_empty() + } } diff --git a/crates/rnote-engine/src/store/render_comp.rs b/crates/rnote-engine/src/store/render_comp.rs index 013c0d825a..2fcdda96bb 100644 --- a/crates/rnote-engine/src/store/render_comp.rs +++ b/crates/rnote-engine/src/store/render_comp.rs @@ -6,7 +6,6 @@ use crate::strokes::content::GeneratedContentImages; use crate::{Drawable, render}; use p2d::bounding_volume::{Aabb, BoundingVolume}; use rnote_compose::ext::AabbExt; -use rnote_compose::shapes::Shapeable; use tracing::error; /// The tolerance where check between scale-factors are considered "equal". @@ -242,28 +241,39 @@ impl StrokeStore { viewport: Aabb, image_scale: f64, ) { - let keys = self.render_components.keys().collect::>(); + let viewport_extended = + viewport.extend_by(viewport.extents() * render::VIEWPORT_EXTENTS_MARGIN_FACTOR); + + // we want to iterate on the keys that are in the viewport using the + // rtree but also get from this the keys that are not in here + // for that also create a slotmap of keys that are in the viewport + // so that we can iterate a second time on keys and filter on elements not in the slotmap + let keys_in_viewport_hash = self + .key_tree + .keys_intersecting_bounds_hashset(viewport_extended); + + // remove stroke keys that we know are not in + // the viewport + // This way we can skip calculating their bounds + for (_key, render_comp) in self + .render_components + .iter_mut() + .filter(|x| !keys_in_viewport_hash.contains_key(&x.0)) + { + #[cfg(feature = "ui")] + { + render_comp.rendernodes = vec![]; + } + render_comp.images = vec![]; + render_comp.state = RenderCompState::Dirty; + } - for key in keys { + for (key, _) in keys_in_viewport_hash { if let (Some(stroke), Some(render_comp)) = ( self.stroke_components.get(key), self.render_components.get_mut(key), ) { let tasks_tx = tasks_tx.clone(); - let stroke_bounds = stroke.bounds(); - let viewport_extended = - viewport.extend_by(viewport.extents() * render::VIEWPORT_EXTENTS_MARGIN_FACTOR); - - // skip and clear image buffer if stroke is not in viewport - if !viewport_extended.intersects(&stroke_bounds) { - #[cfg(feature = "ui")] - { - render_comp.rendernodes = vec![]; - } - render_comp.images = vec![]; - render_comp.state = RenderCompState::Dirty; - continue; - } // only check if rerendering is not forced if !force_regenerate { @@ -495,6 +505,7 @@ impl StrokeStore { use crate::ext::{GdkRGBAExt, GrapheneRectExt}; use gtk4::{gdk, graphene, prelude::*}; use rnote_compose::color; + use rnote_compose::shapes::Shapeable; snapshot.push_clip(&graphene::Rect::from_p2d_aabb(doc_bounds)); @@ -575,6 +586,7 @@ impl StrokeStore { ) -> anyhow::Result<()> { use crate::engine::visual_debug; use gtk4::prelude::*; + use rnote_compose::shapes::Shapeable; let border_widths = 1.0 / engine.camera.total_zoom(); @@ -656,6 +668,24 @@ impl StrokeStore { } } + // draw the rtree root + let tree_bounds = self.key_tree.get_bounds(); + visual_debug::draw_bounds_to_gtk_snapshot( + tree_bounds, + visual_debug::COLOR_RTREE_BOUNDS, + snapshot, + border_widths, + ); + + // draw the trashed rtree root + let trashed_tree_bounds = self.trashed_key_tree.get_bounds(); + visual_debug::draw_bounds_to_gtk_snapshot( + trashed_tree_bounds, + visual_debug::COLOR_TRASH_RTREE_BOUNDS, + snapshot, + border_widths, + ); + Ok(()) } } diff --git a/crates/rnote-engine/src/store/stroke_comp.rs b/crates/rnote-engine/src/store/stroke_comp.rs index 7acfa08093..3956fcc6f4 100644 --- a/crates/rnote-engine/src/store/stroke_comp.rs +++ b/crates/rnote-engine/src/store/stroke_comp.rs @@ -56,7 +56,9 @@ impl StrokeStore { #[allow(unused)] pub(crate) fn keys_unordered_intersecting_bounds(&self, bounds: Aabb) -> Vec { - self.key_tree.keys_intersecting_bounds(bounds) + let mut keys_collect = self.key_tree.keys_intersecting_bounds(bounds); + keys_collect.extend_from_slice(&self.trashed_key_tree.keys_intersecting_bounds(bounds)); + keys_collect } /// All stroke keys that are not trashed, unordered. @@ -67,7 +69,7 @@ impl StrokeStore { .collect() } - /// Storke keys in the order that they should be rendered. + /// Stroke keys in the order that they should be rendered. pub(crate) fn stroke_keys_as_rendered(&self) -> Vec { self.keys_sorted_chrono() .into_iter() @@ -82,15 +84,23 @@ impl StrokeStore { ) -> Vec { self.keys_sorted_chrono_intersecting_bounds(bounds) .into_iter() - .filter(|&key| !(self.trashed(key).unwrap_or(false))) .collect::>() } + /// Stroke keys + bounds intersecting the given bounds, in the order that they should be rendered + pub(crate) fn stroke_keys_and_bounds_as_rendered_intersecting_bounds( + &self, + bounds: Aabb, + ) -> Vec<(StrokeKey, Aabb)> { + self.keys_bounds_sorted_chrono_intersecting_bounds(bounds) + .into_iter() + .collect() + } + /// Stroke keys contained in the given bounds, in the order that they should be rendered. pub(crate) fn stroke_keys_as_rendered_in_bounds(&self, bounds: Aabb) -> Vec { self.keys_sorted_chrono_in_bounds(bounds) .into_iter() - .filter(|&key| !(self.trashed(key).unwrap_or(false))) .collect::>() } @@ -106,12 +116,18 @@ impl StrokeStore { /// /// The stroke then needs to update its rendering. pub(crate) fn update_geometry_for_stroke(&mut self, key: StrokeKey) { + let is_trashed = self.trashed(key).is_some_and(|x| x); + if let Some(stroke) = Arc::make_mut(&mut self.stroke_components) .get_mut(key) .map(Arc::make_mut) { stroke.update_geometry(); - self.key_tree.update_with_key(key, stroke.bounds()); + if is_trashed { + self.trashed_key_tree.update_with_key(key, stroke.bounds()); + } else { + self.key_tree.update_with_key(key, stroke.bounds()); + } self.set_rendering_dirty(key); } } @@ -125,19 +141,14 @@ impl StrokeStore { }); } - /// Calculate the height needed to fit all strokes. + /// Calculate the height needed to fit all (non trashed) strokes. pub(crate) fn calc_height(&self) -> f64 { - let strokes_iter = self - .stroke_keys_unordered() - .into_iter() - .filter_map(|key| self.stroke_components.get(key)); - - let strokes_min_y = strokes_iter - .clone() - .fold(0.0, |acc, stroke| stroke.bounds().mins[1].min(acc)); - let strokes_max_y = strokes_iter.fold(0.0, |acc, stroke| stroke.bounds().maxs[1].max(acc)); - - strokes_max_y - strokes_min_y + if self.keytree_is_empty() { + return 0.0; + } else { + let bounds = self.key_tree.get_bounds(); + bounds.maxs[1] - bounds.mins[1] + } } /// Calculate the width needed to fit all strokes. @@ -194,6 +205,7 @@ impl StrokeStore { /// The strokes then need to update their geometry and rendering. pub(crate) fn translate_strokes(&mut self, keys: &[StrokeKey], offset: na::Vector2) { keys.iter().for_each(|&key| { + let is_trashed = self.trashed(key).is_some_and(|x| x); if let Some(stroke) = Arc::make_mut(&mut self.stroke_components) .get_mut(key) .map(Arc::make_mut) @@ -201,7 +213,11 @@ impl StrokeStore { { // translate the stroke geometry stroke.translate(offset); - self.key_tree.update_with_key(key, stroke.bounds()); + if is_trashed { + self.trashed_key_tree.update_with_key(key, stroke.bounds()); + } else { + self.key_tree.update_with_key(key, stroke.bounds()); + } } } }); @@ -244,6 +260,7 @@ impl StrokeStore { center: na::Point2, ) { keys.iter().for_each(|&key| { + let is_trashed = self.trashed(key).is_some_and(|x| x); if let Some(stroke) = Arc::make_mut(&mut self.stroke_components) .get_mut(key) .map(Arc::make_mut) @@ -251,7 +268,11 @@ impl StrokeStore { { // rotate the stroke geometry stroke.rotate(angle, center); - self.key_tree.update_with_key(key, stroke.bounds()); + if is_trashed { + self.trashed_key_tree.update_with_key(key, stroke.bounds()); + } else { + self.key_tree.update_with_key(key, stroke.bounds()); + } } } }); @@ -399,6 +420,7 @@ impl StrokeStore { /// The strokes then need to update their rendering. pub(crate) fn scale_strokes(&mut self, keys: &[StrokeKey], scale: na::Vector2) { keys.iter().for_each(|&key| { + let is_trashed = self.trashed(key).is_some_and(|x| x); if let Some(stroke) = Arc::make_mut(&mut self.stroke_components) .get_mut(key) .map(Arc::make_mut) @@ -406,7 +428,11 @@ impl StrokeStore { { // rotate the stroke geometry stroke.scale(scale); - self.key_tree.update_with_key(key, stroke.bounds()); + if is_trashed { + self.trashed_key_tree.update_with_key(key, stroke.bounds()); + } else { + self.key_tree.update_with_key(key, stroke.bounds()); + } } } }); @@ -491,11 +517,6 @@ impl StrokeStore { self.keys_sorted_chrono_intersecting_bounds(bounds) .into_iter() .filter_map(|key| { - // skip if stroke is trashed - if self.trashed(key)? { - return None; - } - let stroke = self.stroke_components.get(key)?; let stroke_bounds = stroke.bounds(); @@ -546,11 +567,6 @@ impl StrokeStore { self.keys_sorted_chrono_intersecting_bounds(bounds) .into_iter() .filter_map(|key| { - // skip if stroke is trashed - if self.trashed(key)? { - return None; - } - let stroke = self.stroke_components.get(key)?; let stroke_bounds = stroke.bounds(); @@ -579,11 +595,6 @@ impl StrokeStore { self.keys_sorted_chrono_intersecting_bounds(viewport.merged(&aabb)) .into_iter() .filter_map(|key| { - // skip if stroke is trashed - if self.trashed(key)? { - return None; - } - let stroke = self.stroke_components.get(key)?; let stroke_bounds = stroke.bounds(); @@ -650,7 +661,7 @@ impl StrokeStore { limit_movement_vertical_border: bool, limit_movement_horizontal_border: bool, ) -> Vec { - self.key_tree.keys_intersecting_bounds(Aabb::new( + let bounds = Aabb::new( na::point![ if limit_movement_vertical_border { x_lims.0 @@ -671,7 +682,14 @@ impl StrokeStore { f64::INFINITY } ], - )) + ); + let mut collector = self.key_tree.keys_intersecting_bounds(bounds); + collector.extend( + self.trashed_key_tree + .keys_intersecting_bounds(bounds) + .iter(), + ); + collector } pub(crate) fn filter_keys_intersecting_bounds<'a, I: IntoIterator>( diff --git a/crates/rnote-engine/src/store/trash_comp.rs b/crates/rnote-engine/src/store/trash_comp.rs index 9e9b24c58b..01018ee9da 100644 --- a/crates/rnote-engine/src/store/trash_comp.rs +++ b/crates/rnote-engine/src/store/trash_comp.rs @@ -49,6 +49,19 @@ impl StrokeStore { .map(Arc::make_mut) { trash_comp.trashed = trash; + // remove the key from the rtree (so that the rtree holds information + // only for non trashed strokes) and move to the trashed_keytree instead + // Remark : the corresponding stroke will hold onto its rendernodes and image + // until `regenerate_rendering_in_viewport_threaded` is called again + if trash { + if let Some((key, bounds)) = self.key_tree.remove_with_key(key) { + self.trashed_key_tree.insert_with_key(key, bounds); + } + } else { + if let Some((key, bounds)) = self.trashed_key_tree.remove_with_key(key) { + self.key_tree.insert_with_key(key, bounds); + } + } self.update_chrono_to_last(key); } } @@ -85,16 +98,16 @@ impl StrokeStore { ) -> WidgetFlags { let mut widget_flags = WidgetFlags::default(); - self.stroke_keys_as_rendered_intersecting_bounds(viewport) + self.stroke_keys_and_bounds_as_rendered_intersecting_bounds(viewport) .into_iter() - .for_each(|key| { + .for_each(|(key, bounds)| { let mut trash_current_stroke = false; if let Some(stroke) = self.stroke_components.get(key) { match stroke.as_ref() { Stroke::BrushStroke(_) | Stroke::ShapeStroke(_) => { // First check if eraser even intersects stroke bounds, avoiding unnecessary work - if eraser_bounds.intersects(&stroke.bounds()) { + if eraser_bounds.intersects(&bounds) { for hitbox in stroke.hitboxes().into_iter() { if eraser_bounds.intersects(&hitbox) { trash_current_stroke = true;