From 3f0bb632062b14b1ffcdc13fadb7f43ddaef0ac8 Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Thu, 5 Jun 2025 18:56:06 +0200 Subject: [PATCH 01/28] perf: use `push_repeat` for background nodes This diminishes the cost for this - one element of somewhat fixed size (no vec pushing) - one element given to gtk that can tile this as it pleases with less buffer pushes --- crates/rnote-compose/src/ext.rs | 21 ++++++++++ crates/rnote-engine/src/engine/mod.rs | 6 ++- crates/rnote-engine/src/engine/rendering.rs | 43 ++++++++++++--------- 3 files changed, 49 insertions(+), 21 deletions(-) diff --git a/crates/rnote-compose/src/ext.rs b/crates/rnote-compose/src/ext.rs index d41ba18f68..2244d2fc50 100644 --- a/crates/rnote-compose/src/ext.rs +++ b/crates/rnote-compose/src/ext.rs @@ -161,6 +161,8 @@ where /// splits a aabb into multiple which have a maximum of the given size. Their union is the given aabb. /// the split bounds are exactly fitted to not overlap, or extend the given bounds fn split(self, split_size: na::Vector2) -> Vec; + /// get the top patch for a given split size + fn get_origin(self, split_size: na::Vector2, split_order: SplitOrder) -> Self; /// splits a aabb into multiple of the given size. Their union contains the given aabb. /// The boxes on the edges most likely extend beyond the given aabb. fn split_extended(self, split_size: na::Vector2) -> Vec; @@ -394,6 +396,25 @@ impl AabbExt for Aabb { split_aabbs } + fn get_origin(self, split_size: na::Vector2, split_order: SplitOrder) -> Self { + let (outer_idx, inner_idx) = match split_order { + SplitOrder::RowMajor => (1, 0), + SplitOrder::ColumnMajor => (0, 1), + }; + + let offset_outer = + (self.mins[outer_idx] / split_size[outer_idx]).floor() * split_size[outer_idx]; + + let offset_inner = + (self.mins[inner_idx] / split_size[inner_idx]).floor() * split_size[inner_idx]; + + let mins = match split_order { + SplitOrder::RowMajor => na::point![offset_inner, offset_outer], + SplitOrder::ColumnMajor => na::point![offset_outer, offset_inner], + }; + Aabb::new(mins, mins + split_size) + } + fn split_extended_origin_aligned( self, split_size: na::Vector2, diff --git a/crates/rnote-engine/src/engine/mod.rs b/crates/rnote-engine/src/engine/mod.rs index 974ad0dd91..ce46087164 100644 --- a/crates/rnote-engine/src/engine/mod.rs +++ b/crates/rnote-engine/src/engine/mod.rs @@ -199,7 +199,8 @@ pub struct Engine { background_tile_image: Option, #[cfg(feature = "ui")] #[serde(skip)] - background_rendernodes: Vec, + background_rendernode: Option, + origin_background_rendernode: Option, // Origin indicator rendering #[serde(skip)] origin_indicator_image: Option, @@ -225,7 +226,8 @@ 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_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..0773b988b8 100644 --- a/crates/rnote-engine/src/engine/rendering.rs +++ b/crates/rnote-engine/src/engine/rendering.rs @@ -21,7 +21,6 @@ impl Engine { 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 +34,20 @@ impl Engine { } }; - for split_bounds in viewport.split_extended_origin_aligned( + let origin_aabb = viewport.get_origin( self.document.config.background.tile_size(), SplitOrder::default(), - ) { - rendernodes.push( - gsk::TextureNode::new( - &new_texture, - &graphene::Rect::from_p2d_aabb(split_bounds), - ) - .upcast(), - ); - } - } + ); - self.background_rendernodes = rendernodes; + self.background_rendernode = Some( + gsk::TextureNode::new( + &new_texture, + &graphene::Rect::from_p2d_aabb(origin_aabb), + ) + .upcast(), + ); + self.origin_background_rendernode = Some(origin_aabb); + } } #[cfg(feature = "ui")] @@ -117,7 +115,8 @@ impl Engine { self.origin_indicator_image.take(); #[cfg(feature = "ui")] { - self.background_rendernodes.clear(); + self.background_rendernode = None; + self.origin_background_rendernode = None; self.origin_indicator_rendernode.take(); } widget_flags.redraw = true; @@ -251,17 +250,23 @@ 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(bounds), Some(render_node)) = ( + self.origin_background_rendernode, + self.background_rendernode.clone(), + ) { + snapshot.push_repeat( + &graphene::Rect::from_p2d_aabb(doc_bounds), + Some(&graphene::Rect::from_p2d_aabb(bounds)), + ); + snapshot.append_node(render_node); + snapshot.pop(); } - - snapshot.pop(); Ok(()) } From 7da737fae8d3f16f87e8f64398c1de0ddb7bc40b Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Thu, 5 Jun 2025 19:45:01 +0200 Subject: [PATCH 02/28] visual debug : show rtree bounds --- crates/rnote-engine/src/store/keytree.rs | 5 +++++ crates/rnote-engine/src/store/render_comp.rs | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/crates/rnote-engine/src/store/keytree.rs b/crates/rnote-engine/src/store/keytree.rs index 72428c5e4c..a41427f64e 100644 --- a/crates/rnote-engine/src/store/keytree.rs +++ b/crates/rnote-engine/src/store/keytree.rs @@ -69,6 +69,11 @@ impl KeyTree { pub(crate) fn clear(&mut self) { *self = Self::default() } + + #[cfg(feature = "ui")] + pub(crate) fn get_tree(&self) -> &rstar::RTree { + &self.0 + } } fn new_keytree_object(key: StrokeKey, bounds: Aabb) -> KeyTreeObject { diff --git a/crates/rnote-engine/src/store/render_comp.rs b/crates/rnote-engine/src/store/render_comp.rs index 013c0d825a..bc3a3c8b0d 100644 --- a/crates/rnote-engine/src/store/render_comp.rs +++ b/crates/rnote-engine/src/store/render_comp.rs @@ -656,6 +656,18 @@ impl StrokeStore { } } + // draw the rtree root + let tree_bounds = self.key_tree.get_tree().root().envelope(); + visual_debug::draw_bounds_to_gtk_snapshot( + Aabb::new( + na::point![tree_bounds.lower()[0], tree_bounds.lower()[1]], + na::point![tree_bounds.upper()[0], tree_bounds.upper()[1]], + ), + rnote_compose::Color::new(1.0, 0.5, 0., 1.0), + snapshot, + border_widths, + ); + Ok(()) } } From 39bc7bb55856e29bf5103e788102b2b5f5b9b9d6 Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Thu, 5 Jun 2025 20:31:26 +0200 Subject: [PATCH 03/28] use rtree to speed up `regenerate_rendering_for_strokes_threaded` We use the rtree to find the element in the viewport, mark these ones using a hashmap, call the rendering function, then remove the rest by iterating on the keys a second time, filtering on keys not in the hashmap --- crates/rnote-engine/src/store/keytree.rs | 1 - crates/rnote-engine/src/store/render_comp.rs | 66 ++++++++++++++------ 2 files changed, 48 insertions(+), 19 deletions(-) diff --git a/crates/rnote-engine/src/store/keytree.rs b/crates/rnote-engine/src/store/keytree.rs index a41427f64e..9a09adec7f 100644 --- a/crates/rnote-engine/src/store/keytree.rs +++ b/crates/rnote-engine/src/store/keytree.rs @@ -70,7 +70,6 @@ impl KeyTree { *self = Self::default() } - #[cfg(feature = "ui")] pub(crate) fn get_tree(&self) -> &rstar::RTree { &self.0 } diff --git a/crates/rnote-engine/src/store/render_comp.rs b/crates/rnote-engine/src/store/render_comp.rs index bc3a3c8b0d..69df0b198d 100644 --- a/crates/rnote-engine/src/store/render_comp.rs +++ b/crates/rnote-engine/src/store/render_comp.rs @@ -6,9 +6,12 @@ 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 std::collections::HashMap; use tracing::error; +#[cfg(feature = "ui")] +use rnote_compose::shapes::Shapeable; + /// The tolerance where check between scale-factors are considered "equal". pub(crate) const RENDER_IMAGE_SCALE_TOLERANCE: f64 = 0.01; @@ -242,28 +245,39 @@ impl StrokeStore { viewport: Aabb, image_scale: f64, ) { - let keys = self.render_components.keys().collect::>(); - - for key in keys { + // use the rtree to reduce the number of keys to search through + // for now we are using directly the tree because we want to iter without actually + // collecting elements + 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 mut hashmap_in_viewport: HashMap = HashMap::new(); + + let keys_in_viewport = self + .key_tree + .get_tree() + .locate_in_envelope_intersecting(&rstar::AABB::from_corners( + [viewport_extended.mins[0], viewport_extended.mins[1]], + [viewport_extended.maxs[0], viewport_extended.maxs[1]], + )) + .map(|object| { + let key = object.data; + hashmap_in_viewport.insert(key, ()); + key + }) + .into_iter() + .collect::>(); + + for key in keys_in_viewport { 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 { @@ -314,6 +328,22 @@ impl StrokeStore { ); } } + + // iterate a second time on stroke keys that we know are not in + // the viewport + // This way we can skip calculting their bounds + for (_key, render_comp) in self + .render_components + .iter_mut() + .filter(|x| !hashmap_in_viewport.contains_key(&x.0)) + { + #[cfg(feature = "ui")] + { + render_comp.rendernodes = vec![]; + } + render_comp.images = vec![]; + render_comp.state = RenderCompState::Dirty; + } } /// Clear all rendering for all strokes. From 7f70361618f8fa3bd903913c46c7f28fc3979307 Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Thu, 5 Jun 2025 20:42:38 +0200 Subject: [PATCH 04/28] make the rtree only keep track of non-trashed strokes --- crates/rnote-engine/src/store/trash_comp.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/rnote-engine/src/store/trash_comp.rs b/crates/rnote-engine/src/store/trash_comp.rs index 9e9b24c58b..cf7e1bb4ba 100644 --- a/crates/rnote-engine/src/store/trash_comp.rs +++ b/crates/rnote-engine/src/store/trash_comp.rs @@ -49,6 +49,18 @@ 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) + if trash { + self.key_tree.remove_with_key(key); + } else { + if let Some(stroke) = Arc::make_mut(&mut self.stroke_components) + .get_mut(key) + .map(Arc::make_mut) + { + self.key_tree.update_with_key(key, stroke.bounds()); + } + } self.update_chrono_to_last(key); } } From 4eed9d2970f6d540bceed39a6dba790a04396965 Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Thu, 5 Jun 2025 20:44:07 +0200 Subject: [PATCH 05/28] fix typo --- crates/rnote-engine/src/store/stroke_comp.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rnote-engine/src/store/stroke_comp.rs b/crates/rnote-engine/src/store/stroke_comp.rs index 7acfa08093..eadaa297ce 100644 --- a/crates/rnote-engine/src/store/stroke_comp.rs +++ b/crates/rnote-engine/src/store/stroke_comp.rs @@ -67,7 +67,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() From e75519864ef25c03b0560c06f6044736ccae817e Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Thu, 5 Jun 2025 21:04:10 +0200 Subject: [PATCH 06/28] use rtree for `update_content_rendering_current_viewport` calculations --- crates/rnote-engine/src/document/mod.rs | 24 ++++++++++++++------ crates/rnote-engine/src/store/keytree.rs | 9 ++++++-- crates/rnote-engine/src/store/mod.rs | 2 +- crates/rnote-engine/src/store/stroke_comp.rs | 13 ++--------- 4 files changed, 27 insertions(+), 21 deletions(-) diff --git a/crates/rnote-engine/src/document/mod.rs b/crates/rnote-engine/src/document/mod.rs index 84b360e1ed..929b38c5e5 100644 --- a/crates/rnote-engine/src/document/mod.rs +++ b/crates/rnote-engine/src/document/mod.rs @@ -9,6 +9,7 @@ pub use background::Background; pub use config::DocumentConfig; pub use format::Format; pub use layout::Layout; +use rstar::Envelope; // Imports use crate::engine::EngineConfig; @@ -256,10 +257,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]) + let rendered_bounds = store.key_tree.get_bounds(); + + let content_bounds = if rendered_bounds.area() > 0.0 { + Aabb::new( + na::point![rendered_bounds.lower()[0], rendered_bounds.lower()[1]], + na::point![rendered_bounds.upper()[0], rendered_bounds.upper()[1]], + ) + .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()) @@ -301,9 +306,14 @@ 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.key_tree.get_bounds(); + + let content_bounds = if rendered_bounds.area() > 0.0 { + Aabb::new( + na::point![rendered_bounds.lower()[0], rendered_bounds.lower()[1]], + na::point![rendered_bounds.upper()[0], rendered_bounds.upper()[1]], + ) + .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/store/keytree.rs b/crates/rnote-engine/src/store/keytree.rs index 9a09adec7f..9df2f60bfd 100644 --- a/crates/rnote-engine/src/store/keytree.rs +++ b/crates/rnote-engine/src/store/keytree.rs @@ -1,16 +1,17 @@ // Imports use super::StrokeKey; use p2d::bounding_volume::Aabb; +use rstar::AABB; use rstar::primitives::GeomWithData; /// The rtree object that holds the bounds and [StrokeKey]. -type KeyTreeObject = GeomWithData, StrokeKey>; +pub(crate) type KeyTreeObject = GeomWithData, StrokeKey>; #[derive(Debug, Default)] /// A Rtree with [StrokeKey]'s as associated data. /// /// Used for faster spatial queries. -pub(super) struct KeyTree(rstar::RTree); +pub(crate) struct KeyTree(rstar::RTree); impl KeyTree { /// Insert a new tree object with the given [StrokeKey] and bounds. @@ -73,6 +74,10 @@ impl KeyTree { pub(crate) fn get_tree(&self) -> &rstar::RTree { &self.0 } + + pub fn get_bounds(&self) -> AABB<[f64; 2]> { + self.0.root().envelope() + } } fn new_keytree_object(key: StrokeKey, bounds: Aabb) -> KeyTreeObject { diff --git a/crates/rnote-engine/src/store/mod.rs b/crates/rnote-engine/src/store/mod.rs index e73fc2f7ef..72febba6fd 100644 --- a/crates/rnote-engine/src/store/mod.rs +++ b/crates/rnote-engine/src/store/mod.rs @@ -95,7 +95,7 @@ pub struct StrokeStore { /// /// Needs to be updated with `update_with_key()` when strokes changed their geometry or position! #[serde(skip)] - key_tree: KeyTree, + pub(crate) key_tree: KeyTree, } impl Default for StrokeStore { diff --git a/crates/rnote-engine/src/store/stroke_comp.rs b/crates/rnote-engine/src/store/stroke_comp.rs index eadaa297ce..7c7f50273a 100644 --- a/crates/rnote-engine/src/store/stroke_comp.rs +++ b/crates/rnote-engine/src/store/stroke_comp.rs @@ -127,17 +127,8 @@ impl StrokeStore { /// Calculate the height needed to fit all 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 + let bounds = self.key_tree.get_tree().root().envelope(); + bounds.upper()[1] - bounds.lower()[1] } /// Calculate the width needed to fit all strokes. From 675e31432660bdb157807a3b26b585417a2e9a7e Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Thu, 5 Jun 2025 21:19:04 +0200 Subject: [PATCH 07/28] rebuilt rtree for non trashed components only --- crates/rnote-engine/src/store/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/rnote-engine/src/store/mod.rs b/crates/rnote-engine/src/store/mod.rs index 72febba6fd..708fcb5348 100644 --- a/crates/rnote-engine/src/store/mod.rs +++ b/crates/rnote-engine/src/store/mod.rs @@ -147,6 +147,7 @@ impl StrokeStore { let tree_objects = self .stroke_components .iter() + .filter(|(key, _stroke)| self.trashed(*key).is_some_and(|x| !x)) .map(|(key, stroke)| (key, stroke.bounds())) .collect(); self.key_tree.rebuild_from_vec(tree_objects); From eaa09a6a9ebf581b2b565e9d1c14fe371505ce6c Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Fri, 6 Jun 2025 19:27:18 +0200 Subject: [PATCH 08/28] add a note on rendernode retention when trashing strokes --- crates/rnote-engine/src/store/trash_comp.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/rnote-engine/src/store/trash_comp.rs b/crates/rnote-engine/src/store/trash_comp.rs index cf7e1bb4ba..9976c5d162 100644 --- a/crates/rnote-engine/src/store/trash_comp.rs +++ b/crates/rnote-engine/src/store/trash_comp.rs @@ -51,6 +51,8 @@ impl StrokeStore { trash_comp.trashed = trash; // remove the key from the rtree (so that the rtree holds information // only for non trashed strokes) + // Remark : the corresponding stroke will hold onto its rendernodes and image + // until `regenerate_rendering_in_viewport_threaded` is called again if trash { self.key_tree.remove_with_key(key); } else { From 392f7997a9054c9bb5f3ec8dbe94160c2ddef156 Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Fri, 6 Jun 2025 19:48:29 +0200 Subject: [PATCH 09/28] clean up interfaces Do not leak key trees out of the store structure --- crates/rnote-engine/src/document/mod.rs | 4 ++-- crates/rnote-engine/src/store/keytree.rs | 4 ++-- crates/rnote-engine/src/store/mod.rs | 7 ++++++- crates/rnote-engine/src/store/render_comp.rs | 2 +- crates/rnote-engine/src/store/stroke_comp.rs | 2 +- 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/crates/rnote-engine/src/document/mod.rs b/crates/rnote-engine/src/document/mod.rs index 929b38c5e5..9253d32cff 100644 --- a/crates/rnote-engine/src/document/mod.rs +++ b/crates/rnote-engine/src/document/mod.rs @@ -257,7 +257,7 @@ impl Document { ); if include_content { - let rendered_bounds = store.key_tree.get_bounds(); + let rendered_bounds = store.get_bounds(); let content_bounds = if rendered_bounds.area() > 0.0 { Aabb::new( @@ -306,7 +306,7 @@ impl Document { .merged(&viewport.extend_by(na::vector![padding_horizontal, padding_vertical])); if include_content { - let rendered_bounds = store.key_tree.get_bounds(); + let rendered_bounds = store.get_bounds(); let content_bounds = if rendered_bounds.area() > 0.0 { Aabb::new( diff --git a/crates/rnote-engine/src/store/keytree.rs b/crates/rnote-engine/src/store/keytree.rs index 9df2f60bfd..e27a39a28e 100644 --- a/crates/rnote-engine/src/store/keytree.rs +++ b/crates/rnote-engine/src/store/keytree.rs @@ -5,13 +5,13 @@ use rstar::AABB; use rstar::primitives::GeomWithData; /// The rtree object that holds the bounds and [StrokeKey]. -pub(crate) type KeyTreeObject = GeomWithData, StrokeKey>; +type KeyTreeObject = GeomWithData, StrokeKey>; #[derive(Debug, Default)] /// A Rtree with [StrokeKey]'s as associated data. /// /// Used for faster spatial queries. -pub(crate) struct KeyTree(rstar::RTree); +pub(super) struct KeyTree(rstar::RTree); impl KeyTree { /// Insert a new tree object with the given [StrokeKey] and bounds. diff --git a/crates/rnote-engine/src/store/mod.rs b/crates/rnote-engine/src/store/mod.rs index 708fcb5348..cd2ecab96f 100644 --- a/crates/rnote-engine/src/store/mod.rs +++ b/crates/rnote-engine/src/store/mod.rs @@ -10,6 +10,7 @@ pub mod trash_comp; pub use chrono_comp::ChronoComponent; use keytree::KeyTree; pub use render_comp::RenderComponent; +use rstar::AABB; pub use selection_comp::SelectionComponent; pub use trash_comp::TrashComponent; @@ -95,7 +96,7 @@ pub struct StrokeStore { /// /// Needs to be updated with `update_with_key()` when strokes changed their geometry or position! #[serde(skip)] - pub(crate) key_tree: KeyTree, + key_tree: KeyTree, } impl Default for StrokeStore { @@ -369,4 +370,8 @@ impl StrokeStore { widget_flags } + + pub(super) fn get_bounds(&self) -> AABB<[f64; 2]> { + self.key_tree.get_bounds() + } } diff --git a/crates/rnote-engine/src/store/render_comp.rs b/crates/rnote-engine/src/store/render_comp.rs index 69df0b198d..37f02bd6fb 100644 --- a/crates/rnote-engine/src/store/render_comp.rs +++ b/crates/rnote-engine/src/store/render_comp.rs @@ -687,7 +687,7 @@ impl StrokeStore { } // draw the rtree root - let tree_bounds = self.key_tree.get_tree().root().envelope(); + let tree_bounds = self.key_tree.get_bounds(); visual_debug::draw_bounds_to_gtk_snapshot( Aabb::new( na::point![tree_bounds.lower()[0], tree_bounds.lower()[1]], diff --git a/crates/rnote-engine/src/store/stroke_comp.rs b/crates/rnote-engine/src/store/stroke_comp.rs index 7c7f50273a..cbad5177aa 100644 --- a/crates/rnote-engine/src/store/stroke_comp.rs +++ b/crates/rnote-engine/src/store/stroke_comp.rs @@ -127,7 +127,7 @@ impl StrokeStore { /// Calculate the height needed to fit all strokes. pub(crate) fn calc_height(&self) -> f64 { - let bounds = self.key_tree.get_tree().root().envelope(); + let bounds = self.key_tree.get_bounds(); bounds.upper()[1] - bounds.lower()[1] } From 9632dde9008b693dd423128940f13c1c5f0e4641 Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Sat, 7 Jun 2025 12:08:36 +0200 Subject: [PATCH 10/28] fix typo --- crates/rnote-engine/src/store/render_comp.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rnote-engine/src/store/render_comp.rs b/crates/rnote-engine/src/store/render_comp.rs index 37f02bd6fb..c1dae701fe 100644 --- a/crates/rnote-engine/src/store/render_comp.rs +++ b/crates/rnote-engine/src/store/render_comp.rs @@ -331,7 +331,7 @@ impl StrokeStore { // iterate a second time on stroke keys that we know are not in // the viewport - // This way we can skip calculting their bounds + // This way we can skip calculating their bounds for (_key, render_comp) in self .render_components .iter_mut() From 2e9a8730488212e4c802bc030ebdf11743817f82 Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Sat, 7 Jun 2025 12:24:33 +0200 Subject: [PATCH 11/28] handle correctly empty keytree for `resize_doc` methods --- crates/rnote-engine/src/document/mod.rs | 12 ++++++------ crates/rnote-engine/src/store/keytree.rs | 4 ++++ crates/rnote-engine/src/store/mod.rs | 4 ++++ crates/rnote-engine/src/store/stroke_comp.rs | 8 ++++++-- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/crates/rnote-engine/src/document/mod.rs b/crates/rnote-engine/src/document/mod.rs index 9253d32cff..caf14ccc31 100644 --- a/crates/rnote-engine/src/document/mod.rs +++ b/crates/rnote-engine/src/document/mod.rs @@ -257,18 +257,18 @@ impl Document { ); if include_content { - let rendered_bounds = store.get_bounds(); + 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 { + let rendered_bounds = store.get_bounds(); - let content_bounds = if rendered_bounds.area() > 0.0 { Aabb::new( na::point![rendered_bounds.lower()[0], rendered_bounds.lower()[1]], na::point![rendered_bounds.upper()[0], rendered_bounds.upper()[1]], ) .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()) - .extend_right_and_bottom_by(na::vector![padding_horizontal, padding_vertical]) }; new_bounds.merge(&content_bounds); } diff --git a/crates/rnote-engine/src/store/keytree.rs b/crates/rnote-engine/src/store/keytree.rs index e27a39a28e..97fef6bd06 100644 --- a/crates/rnote-engine/src/store/keytree.rs +++ b/crates/rnote-engine/src/store/keytree.rs @@ -78,6 +78,10 @@ impl KeyTree { pub fn get_bounds(&self) -> AABB<[f64; 2]> { self.0.root().envelope() } + + pub(crate) fn is_empty(&self) -> bool { + self.0.size() == 0 + } } fn new_keytree_object(key: StrokeKey, bounds: Aabb) -> KeyTreeObject { diff --git a/crates/rnote-engine/src/store/mod.rs b/crates/rnote-engine/src/store/mod.rs index cd2ecab96f..a996f32bca 100644 --- a/crates/rnote-engine/src/store/mod.rs +++ b/crates/rnote-engine/src/store/mod.rs @@ -374,4 +374,8 @@ impl StrokeStore { pub(super) fn get_bounds(&self) -> AABB<[f64; 2]> { 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/stroke_comp.rs b/crates/rnote-engine/src/store/stroke_comp.rs index cbad5177aa..cdd3ddd9cf 100644 --- a/crates/rnote-engine/src/store/stroke_comp.rs +++ b/crates/rnote-engine/src/store/stroke_comp.rs @@ -127,8 +127,12 @@ impl StrokeStore { /// Calculate the height needed to fit all strokes. pub(crate) fn calc_height(&self) -> f64 { - let bounds = self.key_tree.get_bounds(); - bounds.upper()[1] - bounds.lower()[1] + if self.keytree_is_empty() { + return 0.0; + } else { + let bounds = self.key_tree.get_bounds(); + bounds.upper()[1] - bounds.lower()[1] + } } /// Calculate the width needed to fit all strokes. From 4f08750234890235be91e042bb1071af4e6feb11 Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Sun, 29 Jun 2025 13:43:36 +0200 Subject: [PATCH 12/28] refactor: move color for rtree bounds to the `visual_debug` module --- crates/rnote-engine/src/engine/visual_debug.rs | 6 ++++++ crates/rnote-engine/src/store/render_comp.rs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/rnote-engine/src/engine/visual_debug.rs b/crates/rnote-engine/src/engine/visual_debug.rs index f50342d38c..544eeb610d 100644 --- a/crates/rnote-engine/src/engine/visual_debug.rs +++ b/crates/rnote-engine/src/engine/visual_debug.rs @@ -49,6 +49,12 @@ 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, +}; #[cfg(feature = "ui")] pub(crate) fn draw_bounds_to_gtk_snapshot( diff --git a/crates/rnote-engine/src/store/render_comp.rs b/crates/rnote-engine/src/store/render_comp.rs index c1dae701fe..54f1acc60b 100644 --- a/crates/rnote-engine/src/store/render_comp.rs +++ b/crates/rnote-engine/src/store/render_comp.rs @@ -693,7 +693,7 @@ impl StrokeStore { na::point![tree_bounds.lower()[0], tree_bounds.lower()[1]], na::point![tree_bounds.upper()[0], tree_bounds.upper()[1]], ), - rnote_compose::Color::new(1.0, 0.5, 0., 1.0), + visual_debug::COLOR_RTREE_BOUNDS, snapshot, border_widths, ); From 36aaed63ea8a4a9afa98c076bf55dbcc81b8fac6 Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Sun, 29 Jun 2025 14:10:30 +0200 Subject: [PATCH 13/28] refactor: remove `origin_background_rendernode` (using `None` for the `push_repeat` instead) `push_repeat(x,None)` will take the whole not and repeat it --- crates/rnote-engine/src/engine/mod.rs | 2 -- crates/rnote-engine/src/engine/rendering.rs | 12 ++---------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/crates/rnote-engine/src/engine/mod.rs b/crates/rnote-engine/src/engine/mod.rs index ce46087164..da8d8c2195 100644 --- a/crates/rnote-engine/src/engine/mod.rs +++ b/crates/rnote-engine/src/engine/mod.rs @@ -200,7 +200,6 @@ pub struct Engine { #[cfg(feature = "ui")] #[serde(skip)] background_rendernode: Option, - origin_background_rendernode: Option, // Origin indicator rendering #[serde(skip)] origin_indicator_image: Option, @@ -227,7 +226,6 @@ impl Default for Engine { background_tile_image: None, #[cfg(feature = "ui")] background_rendernode: None, - origin_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 0773b988b8..b67c97b8f6 100644 --- a/crates/rnote-engine/src/engine/rendering.rs +++ b/crates/rnote-engine/src/engine/rendering.rs @@ -46,7 +46,6 @@ impl Engine { ) .upcast(), ); - self.origin_background_rendernode = Some(origin_aabb); } } @@ -116,7 +115,6 @@ impl Engine { #[cfg(feature = "ui")] { self.background_rendernode = None; - self.origin_background_rendernode = None; self.origin_indicator_rendernode.take(); } widget_flags.redraw = true; @@ -256,14 +254,8 @@ impl Engine { ); snapshot.pop(); - if let (Some(bounds), Some(render_node)) = ( - self.origin_background_rendernode, - self.background_rendernode.clone(), - ) { - snapshot.push_repeat( - &graphene::Rect::from_p2d_aabb(doc_bounds), - Some(&graphene::Rect::from_p2d_aabb(bounds)), - ); + 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(); } From fda71990755858d3aaaf8b222543dee543b51cd8 Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Sun, 29 Jun 2025 14:32:12 +0200 Subject: [PATCH 14/28] refactor: rename `get_origin` method and remove unneeded arguments --- crates/rnote-compose/src/ext.rs | 25 ++++++--------------- crates/rnote-engine/src/engine/rendering.rs | 7 ++---- 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/crates/rnote-compose/src/ext.rs b/crates/rnote-compose/src/ext.rs index 2244d2fc50..e9516bf203 100644 --- a/crates/rnote-compose/src/ext.rs +++ b/crates/rnote-compose/src/ext.rs @@ -161,8 +161,6 @@ where /// splits a aabb into multiple which have a maximum of the given size. Their union is the given aabb. /// the split bounds are exactly fitted to not overlap, or extend the given bounds fn split(self, split_size: na::Vector2) -> Vec; - /// get the top patch for a given split size - fn get_origin(self, split_size: na::Vector2, split_order: SplitOrder) -> Self; /// splits a aabb into multiple of the given size. Their union contains the given aabb. /// The boxes on the edges most likely extend beyond the given aabb. fn split_extended(self, split_size: na::Vector2) -> Vec; @@ -174,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 @@ -396,22 +397,10 @@ impl AabbExt for Aabb { split_aabbs } - fn get_origin(self, split_size: na::Vector2, split_order: SplitOrder) -> Self { - let (outer_idx, inner_idx) = match split_order { - SplitOrder::RowMajor => (1, 0), - SplitOrder::ColumnMajor => (0, 1), - }; - - let offset_outer = - (self.mins[outer_idx] / split_size[outer_idx]).floor() * split_size[outer_idx]; - - let offset_inner = - (self.mins[inner_idx] / split_size[inner_idx]).floor() * split_size[inner_idx]; - - let mins = match split_order { - SplitOrder::RowMajor => na::point![offset_inner, offset_outer], - SplitOrder::ColumnMajor => na::point![offset_outer, offset_inner], - }; + 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) } diff --git a/crates/rnote-engine/src/engine/rendering.rs b/crates/rnote-engine/src/engine/rendering.rs index b67c97b8f6..c17f3a6453 100644 --- a/crates/rnote-engine/src/engine/rendering.rs +++ b/crates/rnote-engine/src/engine/rendering.rs @@ -17,7 +17,6 @@ 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(); @@ -34,10 +33,8 @@ impl Engine { } }; - let origin_aabb = viewport.get_origin( - self.document.config.background.tile_size(), - SplitOrder::default(), - ); + let origin_aabb = viewport + .split_first_origin_aligned(self.document.config.background.tile_size()); self.background_rendernode = Some( gsk::TextureNode::new( From 530e2eedf7c6b9c49cb5bd29d112764f9a73f60d Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Sun, 29 Jun 2025 15:05:54 +0200 Subject: [PATCH 15/28] add `keys_intersecting_bounds_hashset` for `render_comp` --- crates/rnote-engine/src/store/keytree.rs | 12 ++++ crates/rnote-engine/src/store/render_comp.rs | 63 +++++++++----------- crates/rnote-ui/src/main.rs | 1 + 3 files changed, 40 insertions(+), 36 deletions(-) diff --git a/crates/rnote-engine/src/store/keytree.rs b/crates/rnote-engine/src/store/keytree.rs index 97fef6bd06..4f8b966417 100644 --- a/crates/rnote-engine/src/store/keytree.rs +++ b/crates/rnote-engine/src/store/keytree.rs @@ -3,6 +3,7 @@ use super::StrokeKey; use p2d::bounding_volume::Aabb; use rstar::AABB; use rstar::primitives::GeomWithData; +use std::collections::HashMap; /// The rtree object that holds the bounds and [StrokeKey]. type KeyTreeObject = GeomWithData, StrokeKey>; @@ -45,6 +46,17 @@ impl KeyTree { .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 diff --git a/crates/rnote-engine/src/store/render_comp.rs b/crates/rnote-engine/src/store/render_comp.rs index 54f1acc60b..e3bb9c34a1 100644 --- a/crates/rnote-engine/src/store/render_comp.rs +++ b/crates/rnote-engine/src/store/render_comp.rs @@ -245,9 +245,6 @@ impl StrokeStore { viewport: Aabb, image_scale: f64, ) { - // use the rtree to reduce the number of keys to search through - // for now we are using directly the tree because we want to iter without actually - // collecting elements let viewport_extended = viewport.extend_by(viewport.extents() * render::VIEWPORT_EXTENTS_MARGIN_FACTOR); @@ -255,24 +252,34 @@ impl StrokeStore { // 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 mut hashmap_in_viewport: HashMap = HashMap::new(); - - let keys_in_viewport = self + let mut keys_in_viewport_hash = self .key_tree - .get_tree() - .locate_in_envelope_intersecting(&rstar::AABB::from_corners( - [viewport_extended.mins[0], viewport_extended.mins[1]], - [viewport_extended.maxs[0], viewport_extended.maxs[1]], - )) - .map(|object| { - let key = object.data; - hashmap_in_viewport.insert(key, ()); - key - }) - .into_iter() - .collect::>(); - - for key in keys_in_viewport { + .keys_intersecting_bounds_hashset(viewport_extended); + + // if we have a current key in the store, push it as well + if let Some(current_stroke_key) = self.current_key() { + dbg!("add current stroke key to the viewport"); + dbg!(current_stroke_key); + keys_in_viewport_hash.insert(current_stroke_key, ()); + } + + // 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_in_viewport_hash { if let (Some(stroke), Some(render_comp)) = ( self.stroke_components.get(key), self.render_components.get_mut(key), @@ -328,22 +335,6 @@ impl StrokeStore { ); } } - - // iterate a second time on 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| !hashmap_in_viewport.contains_key(&x.0)) - { - #[cfg(feature = "ui")] - { - render_comp.rendernodes = vec![]; - } - render_comp.images = vec![]; - render_comp.state = RenderCompState::Dirty; - } } /// Clear all rendering for all strokes. diff --git a/crates/rnote-ui/src/main.rs b/crates/rnote-ui/src/main.rs index 4eebe90dac..3907bab685 100644 --- a/crates/rnote-ui/src/main.rs +++ b/crates/rnote-ui/src/main.rs @@ -93,6 +93,7 @@ fn main() -> glib::ExitCode { eprintln!("failed to setup custom css for windows, Err: {e:?}"); } } + println!("git commit : fda71990"); app.run() } From 30261cc788b650fe120730e0351c5cc9c3f2108d Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Sun, 29 Jun 2025 15:09:47 +0200 Subject: [PATCH 16/28] refactor: remove `get_tree` method --- crates/rnote-engine/src/store/keytree.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/rnote-engine/src/store/keytree.rs b/crates/rnote-engine/src/store/keytree.rs index 4f8b966417..a01a97bbb0 100644 --- a/crates/rnote-engine/src/store/keytree.rs +++ b/crates/rnote-engine/src/store/keytree.rs @@ -83,10 +83,6 @@ impl KeyTree { *self = Self::default() } - pub(crate) fn get_tree(&self) -> &rstar::RTree { - &self.0 - } - pub fn get_bounds(&self) -> AABB<[f64; 2]> { self.0.root().envelope() } From e10608ff09bf2654db8199b369fee1b61b7bc221 Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Sun, 29 Jun 2025 15:17:06 +0200 Subject: [PATCH 17/28] refactor: do not leak `AABB` outside, use `Aabb` instead --- crates/rnote-engine/src/document/mod.rs | 17 ++++++----------- crates/rnote-engine/src/store/keytree.rs | 9 ++++++--- crates/rnote-engine/src/store/mod.rs | 4 ++-- crates/rnote-engine/src/store/render_comp.rs | 6 +----- crates/rnote-engine/src/store/stroke_comp.rs | 2 +- 5 files changed, 16 insertions(+), 22 deletions(-) diff --git a/crates/rnote-engine/src/document/mod.rs b/crates/rnote-engine/src/document/mod.rs index caf14ccc31..c028b52506 100644 --- a/crates/rnote-engine/src/document/mod.rs +++ b/crates/rnote-engine/src/document/mod.rs @@ -9,7 +9,6 @@ pub use background::Background; pub use config::DocumentConfig; pub use format::Format; pub use layout::Layout; -use rstar::Envelope; // Imports use crate::engine::EngineConfig; @@ -262,13 +261,9 @@ impl Document { 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 { - let rendered_bounds = store.get_bounds(); - - Aabb::new( - na::point![rendered_bounds.lower()[0], rendered_bounds.lower()[1]], - na::point![rendered_bounds.upper()[0], rendered_bounds.upper()[1]], - ) - .extend_right_and_bottom_by(na::vector![padding_horizontal, padding_vertical]) + store + .get_bounds() + .extend_right_and_bottom_by(na::vector![padding_horizontal, padding_vertical]) }; new_bounds.merge(&content_bounds); } @@ -308,10 +303,10 @@ impl Document { if include_content { let rendered_bounds = store.get_bounds(); - let content_bounds = if rendered_bounds.area() > 0.0 { + let content_bounds = if rendered_bounds.volume() > 0.0 { Aabb::new( - na::point![rendered_bounds.lower()[0], rendered_bounds.lower()[1]], - na::point![rendered_bounds.upper()[0], rendered_bounds.upper()[1]], + na::point![rendered_bounds.mins[0], rendered_bounds.mins[1]], + na::point![rendered_bounds.maxs[0], rendered_bounds.maxs[1]], ) .extend_right_and_bottom_by(na::vector![padding_horizontal, padding_vertical]) } else { diff --git a/crates/rnote-engine/src/store/keytree.rs b/crates/rnote-engine/src/store/keytree.rs index a01a97bbb0..bcdbad7012 100644 --- a/crates/rnote-engine/src/store/keytree.rs +++ b/crates/rnote-engine/src/store/keytree.rs @@ -1,7 +1,6 @@ // Imports use super::StrokeKey; use p2d::bounding_volume::Aabb; -use rstar::AABB; use rstar::primitives::GeomWithData; use std::collections::HashMap; @@ -83,8 +82,12 @@ impl KeyTree { *self = Self::default() } - pub fn get_bounds(&self) -> AABB<[f64; 2]> { - self.0.root().envelope() + 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 { diff --git a/crates/rnote-engine/src/store/mod.rs b/crates/rnote-engine/src/store/mod.rs index a996f32bca..5856233bcc 100644 --- a/crates/rnote-engine/src/store/mod.rs +++ b/crates/rnote-engine/src/store/mod.rs @@ -9,8 +9,8 @@ pub mod trash_comp; // Re-exports pub use chrono_comp::ChronoComponent; use keytree::KeyTree; +use p2d::bounding_volume::Aabb; pub use render_comp::RenderComponent; -use rstar::AABB; pub use selection_comp::SelectionComponent; pub use trash_comp::TrashComponent; @@ -371,7 +371,7 @@ impl StrokeStore { widget_flags } - pub(super) fn get_bounds(&self) -> AABB<[f64; 2]> { + pub(super) fn get_bounds(&self) -> Aabb { self.key_tree.get_bounds() } diff --git a/crates/rnote-engine/src/store/render_comp.rs b/crates/rnote-engine/src/store/render_comp.rs index e3bb9c34a1..9e872122c3 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 std::collections::HashMap; use tracing::error; #[cfg(feature = "ui")] @@ -680,10 +679,7 @@ impl StrokeStore { // draw the rtree root let tree_bounds = self.key_tree.get_bounds(); visual_debug::draw_bounds_to_gtk_snapshot( - Aabb::new( - na::point![tree_bounds.lower()[0], tree_bounds.lower()[1]], - na::point![tree_bounds.upper()[0], tree_bounds.upper()[1]], - ), + tree_bounds, visual_debug::COLOR_RTREE_BOUNDS, snapshot, border_widths, diff --git a/crates/rnote-engine/src/store/stroke_comp.rs b/crates/rnote-engine/src/store/stroke_comp.rs index cdd3ddd9cf..2a1b02fa83 100644 --- a/crates/rnote-engine/src/store/stroke_comp.rs +++ b/crates/rnote-engine/src/store/stroke_comp.rs @@ -131,7 +131,7 @@ impl StrokeStore { return 0.0; } else { let bounds = self.key_tree.get_bounds(); - bounds.upper()[1] - bounds.lower()[1] + bounds.maxs[1] - bounds.mins[1] } } From a2c4e5ba0ac30e1a8ce5495c29c0e4338b631dd9 Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Sun, 29 Jun 2025 18:30:31 +0200 Subject: [PATCH 18/28] import `rnote_compose::shapes::Shapeable` inside ui guarded functions --- crates/rnote-engine/src/store/render_comp.rs | 5 ++--- crates/rnote-ui/src/main.rs | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/rnote-engine/src/store/render_comp.rs b/crates/rnote-engine/src/store/render_comp.rs index 9e872122c3..e3cf2c0d6b 100644 --- a/crates/rnote-engine/src/store/render_comp.rs +++ b/crates/rnote-engine/src/store/render_comp.rs @@ -8,9 +8,6 @@ use p2d::bounding_volume::{Aabb, BoundingVolume}; use rnote_compose::ext::AabbExt; use tracing::error; -#[cfg(feature = "ui")] -use rnote_compose::shapes::Shapeable; - /// The tolerance where check between scale-factors are considered "equal". pub(crate) const RENDER_IMAGE_SCALE_TOLERANCE: f64 = 0.01; @@ -515,6 +512,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)); @@ -595,6 +593,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(); diff --git a/crates/rnote-ui/src/main.rs b/crates/rnote-ui/src/main.rs index 3907bab685..4eebe90dac 100644 --- a/crates/rnote-ui/src/main.rs +++ b/crates/rnote-ui/src/main.rs @@ -93,7 +93,6 @@ fn main() -> glib::ExitCode { eprintln!("failed to setup custom css for windows, Err: {e:?}"); } } - println!("git commit : fda71990"); app.run() } From 81c14340214393f9a43a0cc8ed7d8c3a34536a4d Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Sun, 29 Jun 2025 19:42:37 +0200 Subject: [PATCH 19/28] refactor: remove extraneous conversion from `Aabb` to itself --- crates/rnote-engine/src/document/mod.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/crates/rnote-engine/src/document/mod.rs b/crates/rnote-engine/src/document/mod.rs index c028b52506..f804f5003c 100644 --- a/crates/rnote-engine/src/document/mod.rs +++ b/crates/rnote-engine/src/document/mod.rs @@ -304,11 +304,8 @@ impl Document { let rendered_bounds = store.get_bounds(); let content_bounds = if rendered_bounds.volume() > 0.0 { - Aabb::new( - na::point![rendered_bounds.mins[0], rendered_bounds.mins[1]], - na::point![rendered_bounds.maxs[0], rendered_bounds.maxs[1]], - ) - .extend_right_and_bottom_by(na::vector![padding_horizontal, padding_vertical]) + 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()) From 043aa438f0375bfbd52676c650d79762d5f667d1 Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Thu, 17 Jul 2025 19:28:11 +0200 Subject: [PATCH 20/28] remove code from another feature that would push a 'current' stroke key to the rendering keys --- crates/rnote-engine/src/store/render_comp.rs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/crates/rnote-engine/src/store/render_comp.rs b/crates/rnote-engine/src/store/render_comp.rs index e3cf2c0d6b..e35efb3660 100644 --- a/crates/rnote-engine/src/store/render_comp.rs +++ b/crates/rnote-engine/src/store/render_comp.rs @@ -248,17 +248,10 @@ impl StrokeStore { // 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 mut keys_in_viewport_hash = self + let keys_in_viewport_hash = self .key_tree .keys_intersecting_bounds_hashset(viewport_extended); - // if we have a current key in the store, push it as well - if let Some(current_stroke_key) = self.current_key() { - dbg!("add current stroke key to the viewport"); - dbg!(current_stroke_key); - keys_in_viewport_hash.insert(current_stroke_key, ()); - } - // remove stroke keys that we know are not in // the viewport // This way we can skip calculating their bounds From ea1bcafed29b892ac97ba84733c5d426605396b8 Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Wed, 6 Aug 2025 22:38:10 +0200 Subject: [PATCH 21/28] duplicate the rtree for trashed and non trashed keys And remove now superfluous filters on the trash status for the outputs of the `key_tree` --- crates/rnote-engine/src/document/mod.rs | 4 +- crates/rnote-engine/src/store/keytree.rs | 16 ++++- crates/rnote-engine/src/store/mod.rs | 17 ++++-- crates/rnote-engine/src/store/stroke_comp.rs | 63 ++++++++++++-------- crates/rnote-engine/src/store/trash_comp.rs | 13 ++-- 5 files changed, 73 insertions(+), 40 deletions(-) diff --git a/crates/rnote-engine/src/document/mod.rs b/crates/rnote-engine/src/document/mod.rs index f804f5003c..41b4466369 100644 --- a/crates/rnote-engine/src/document/mod.rs +++ b/crates/rnote-engine/src/document/mod.rs @@ -262,7 +262,7 @@ impl Document { .extend_right_and_bottom_by(na::vector![padding_horizontal, padding_vertical]) } else { store - .get_bounds() + .get_bounds_non_trashed() .extend_right_and_bottom_by(na::vector![padding_horizontal, padding_vertical]) }; new_bounds.merge(&content_bounds); @@ -301,7 +301,7 @@ impl Document { .merged(&viewport.extend_by(na::vector![padding_horizontal, padding_vertical])); if include_content { - let rendered_bounds = store.get_bounds(); + let rendered_bounds = store.get_bounds_non_trashed(); let content_bounds = if rendered_bounds.volume() > 0.0 { rendered_bounds diff --git a/crates/rnote-engine/src/store/keytree.rs b/crates/rnote-engine/src/store/keytree.rs index bcdbad7012..ebb8455c25 100644 --- a/crates/rnote-engine/src/store/keytree.rs +++ b/crates/rnote-engine/src/store/keytree.rs @@ -20,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. @@ -104,3 +106,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 5856233bcc..7915ac5722 100644 --- a/crates/rnote-engine/src/store/mod.rs +++ b/crates/rnote-engine/src/store/mod.rs @@ -95,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 { @@ -113,6 +119,7 @@ impl Default for StrokeStore { live_index: 0, key_tree: KeyTree::default(), + trashed_key_tree: KeyTree::default(), chrono_counter: 0, } @@ -145,13 +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() - .filter(|(key, _stroke)| self.trashed(*key).is_some_and(|x| !x)) .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, @@ -350,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()) @@ -367,11 +375,12 @@ impl StrokeStore { self.render_components.clear(); self.key_tree.clear(); + self.trashed_key_tree.clear(); widget_flags } - pub(super) fn get_bounds(&self) -> Aabb { + pub(super) fn get_bounds_non_trashed(&self) -> Aabb { self.key_tree.get_bounds() } diff --git a/crates/rnote-engine/src/store/stroke_comp.rs b/crates/rnote-engine/src/store/stroke_comp.rs index 2a1b02fa83..0d3fc173a0 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. @@ -82,7 +84,6 @@ impl StrokeStore { ) -> Vec { self.keys_sorted_chrono_intersecting_bounds(bounds) .into_iter() - .filter(|&key| !(self.trashed(key).unwrap_or(false))) .collect::>() } @@ -90,7 +91,6 @@ impl StrokeStore { 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 +106,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,7 +131,7 @@ 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 { if self.keytree_is_empty() { return 0.0; @@ -189,6 +195,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) @@ -196,7 +203,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()); + } } } }); @@ -239,6 +250,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) @@ -246,7 +258,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()); + } } } }); @@ -394,6 +410,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) @@ -401,7 +418,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()); + } } } }); @@ -486,11 +507,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(); @@ -541,11 +557,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(); @@ -574,11 +585,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(); @@ -645,7 +651,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 @@ -666,7 +672,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 9976c5d162..b398503bba 100644 --- a/crates/rnote-engine/src/store/trash_comp.rs +++ b/crates/rnote-engine/src/store/trash_comp.rs @@ -50,17 +50,16 @@ impl StrokeStore { { trash_comp.trashed = trash; // remove the key from the rtree (so that the rtree holds information - // only for non trashed strokes) + // 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 { - self.key_tree.remove_with_key(key); + 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(stroke) = Arc::make_mut(&mut self.stroke_components) - .get_mut(key) - .map(Arc::make_mut) - { - self.key_tree.update_with_key(key, stroke.bounds()); + 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); From 5aee84d644f114053d01b8ad27e9ed99967990ff Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Wed, 6 Aug 2025 22:38:50 +0200 Subject: [PATCH 22/28] visual debug: show the `trashed_keytree` --- crates/rnote-engine/src/engine/visual_debug.rs | 6 ++++++ crates/rnote-engine/src/store/render_comp.rs | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/crates/rnote-engine/src/engine/visual_debug.rs b/crates/rnote-engine/src/engine/visual_debug.rs index 544eeb610d..38ac0c7739 100644 --- a/crates/rnote-engine/src/engine/visual_debug.rs +++ b/crates/rnote-engine/src/engine/visual_debug.rs @@ -55,6 +55,12 @@ pub const COLOR_RTREE_BOUNDS: Color = Color { 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/render_comp.rs b/crates/rnote-engine/src/store/render_comp.rs index e35efb3660..2fcdda96bb 100644 --- a/crates/rnote-engine/src/store/render_comp.rs +++ b/crates/rnote-engine/src/store/render_comp.rs @@ -677,6 +677,15 @@ impl StrokeStore { 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(()) } } From 5efa8e2b958e96ad6b362863ef0ca9fa8b382452 Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Wed, 6 Aug 2025 22:56:03 +0200 Subject: [PATCH 23/28] `trash_colliding_strokes`: get the stroke bounds from the keytree directly The information is there already, and it is probably more effective to get the value from the keytree rather than recalculating it on the fly on each event --- crates/rnote-engine/src/store/chrono_comp.rs | 27 ++++++++++++++++++++ crates/rnote-engine/src/store/keytree.rs | 15 +++++++++-- crates/rnote-engine/src/store/stroke_comp.rs | 10 ++++++++ crates/rnote-engine/src/store/trash_comp.rs | 6 ++--- 4 files changed, 53 insertions(+), 5 deletions(-) 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 ebb8455c25..0839a2f093 100644 --- a/crates/rnote-engine/src/store/keytree.rs +++ b/crates/rnote-engine/src/store/keytree.rs @@ -25,7 +25,7 @@ impl KeyTree { self.0 .remove(&object_to_remove) - .and_then(|key_object| Some(keytree_to_store(key_object))) + .and_then(|key_object| Some(keytree_to_store(&key_object))) } /// Update the Tree with new bounds for the given key. @@ -47,6 +47,17 @@ 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 @@ -107,7 +118,7 @@ fn new_keytree_object(key: StrokeKey, bounds: Aabb) -> KeyTreeObject { ) } -fn keytree_to_store(key_object: KeyTreeObject) -> (StrokeKey, Aabb) { +fn keytree_to_store(key_object: &KeyTreeObject) -> (StrokeKey, Aabb) { ( key_object.data, Aabb::new( diff --git a/crates/rnote-engine/src/store/stroke_comp.rs b/crates/rnote-engine/src/store/stroke_comp.rs index 0d3fc173a0..3956fcc6f4 100644 --- a/crates/rnote-engine/src/store/stroke_comp.rs +++ b/crates/rnote-engine/src/store/stroke_comp.rs @@ -87,6 +87,16 @@ impl StrokeStore { .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) diff --git a/crates/rnote-engine/src/store/trash_comp.rs b/crates/rnote-engine/src/store/trash_comp.rs index b398503bba..01018ee9da 100644 --- a/crates/rnote-engine/src/store/trash_comp.rs +++ b/crates/rnote-engine/src/store/trash_comp.rs @@ -98,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; From e81de42f4647ebe9767661133f1dfe79e5854fbe Mon Sep 17 00:00:00 2001 From: Doublonmousse Date: Sun, 4 Jan 2026 17:21:59 +0100 Subject: [PATCH 24/28] use let chain syntax --- crates/rnote-engine/src/store/render_comp.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/rnote-engine/src/store/render_comp.rs b/crates/rnote-engine/src/store/render_comp.rs index 2fcdda96bb..5b8c7f8acf 100644 --- a/crates/rnote-engine/src/store/render_comp.rs +++ b/crates/rnote-engine/src/store/render_comp.rs @@ -269,10 +269,9 @@ impl StrokeStore { } 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), - ) { + if let Some(stroke) = self.stroke_components.get(key) + && let Some(render_comp) = self.render_components.get_mut(key) + { let tasks_tx = tasks_tx.clone(); // only check if rerendering is not forced From a495e058afadbe9632b705ab93a2bcbf8166eb9a Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Wed, 28 Jan 2026 16:49:27 +0100 Subject: [PATCH 25/28] move the tree/rtree calls before the stroke-specific calls This seems to fix weird graphical glitches that would occur otherwise --- crates/rnote-engine/src/store/render_comp.rs | 36 ++++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/crates/rnote-engine/src/store/render_comp.rs b/crates/rnote-engine/src/store/render_comp.rs index ef39cc55af..381ad96a14 100644 --- a/crates/rnote-engine/src/store/render_comp.rs +++ b/crates/rnote-engine/src/store/render_comp.rs @@ -585,6 +585,24 @@ impl StrokeStore { let border_widths = 1.0 / engine.camera.total_zoom(); + // 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, + ); + for key in self.keys_sorted_chrono() { if let Some(stroke) = self.stroke_components.get(key) && let Some(trash_comp) = self.trash_components.get(key) @@ -663,24 +681,6 @@ 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(()) } } From f66a0e38001fbdf210c6963ff4cf18f9d720fc59 Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Wed, 28 Jan 2026 17:01:16 +0100 Subject: [PATCH 26/28] apply clippy lints --- crates/rnote-engine/src/store/keytree.rs | 4 ++-- crates/rnote-engine/src/store/stroke_comp.rs | 2 +- crates/rnote-engine/src/store/trash_comp.rs | 6 ++---- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/crates/rnote-engine/src/store/keytree.rs b/crates/rnote-engine/src/store/keytree.rs index 0839a2f093..89478369b7 100644 --- a/crates/rnote-engine/src/store/keytree.rs +++ b/crates/rnote-engine/src/store/keytree.rs @@ -25,7 +25,7 @@ impl KeyTree { self.0 .remove(&object_to_remove) - .and_then(|key_object| Some(keytree_to_store(&key_object))) + .map(|key_object| keytree_to_store(&key_object)) } /// Update the Tree with new bounds for the given key. @@ -54,7 +54,7 @@ impl KeyTree { [bounds.mins[0], bounds.mins[1]], [bounds.maxs[0], bounds.maxs[1]], )) - .map(|object| keytree_to_store(&object)) + .map(keytree_to_store) .collect() } diff --git a/crates/rnote-engine/src/store/stroke_comp.rs b/crates/rnote-engine/src/store/stroke_comp.rs index 3956fcc6f4..4e633b2278 100644 --- a/crates/rnote-engine/src/store/stroke_comp.rs +++ b/crates/rnote-engine/src/store/stroke_comp.rs @@ -144,7 +144,7 @@ impl StrokeStore { /// Calculate the height needed to fit all (non trashed) strokes. pub(crate) fn calc_height(&self) -> f64 { if self.keytree_is_empty() { - return 0.0; + 0.0 } else { let bounds = self.key_tree.get_bounds(); bounds.maxs[1] - bounds.mins[1] diff --git a/crates/rnote-engine/src/store/trash_comp.rs b/crates/rnote-engine/src/store/trash_comp.rs index 01018ee9da..894fab83c6 100644 --- a/crates/rnote-engine/src/store/trash_comp.rs +++ b/crates/rnote-engine/src/store/trash_comp.rs @@ -57,10 +57,8 @@ impl StrokeStore { 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); - } + } 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); } From c12b0fc0e03b8e696bf575a74926c59c1d06e85f Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Wed, 28 Jan 2026 18:42:11 +0100 Subject: [PATCH 27/28] remove drawing trashed and non trashed trees in visual debug To circumvent gtk4 snapshot issues ... --- .../rnote-engine/src/engine/visual_debug.rs | 12 ------------ crates/rnote-engine/src/store/render_comp.rs | 19 ------------------- 2 files changed, 31 deletions(-) diff --git a/crates/rnote-engine/src/engine/visual_debug.rs b/crates/rnote-engine/src/engine/visual_debug.rs index 38ac0c7739..f50342d38c 100644 --- a/crates/rnote-engine/src/engine/visual_debug.rs +++ b/crates/rnote-engine/src/engine/visual_debug.rs @@ -49,18 +49,6 @@ 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/render_comp.rs b/crates/rnote-engine/src/store/render_comp.rs index 381ad96a14..838abe0cc7 100644 --- a/crates/rnote-engine/src/store/render_comp.rs +++ b/crates/rnote-engine/src/store/render_comp.rs @@ -584,25 +584,6 @@ impl StrokeStore { use rnote_compose::shapes::Shapeable; let border_widths = 1.0 / engine.camera.total_zoom(); - - // 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, - ); - for key in self.keys_sorted_chrono() { if let Some(stroke) = self.stroke_components.get(key) && let Some(trash_comp) = self.trash_components.get(key) From 7c321f4ffa3332b972b3ad6fa2941fe424ca7753 Mon Sep 17 00:00:00 2001 From: Doublonmousse <115779707+Doublonmousse@users.noreply.github.com> Date: Wed, 28 Jan 2026 19:19:13 +0100 Subject: [PATCH 28/28] update calls post merge (for the render->image refactor) --- crates/rnote-engine/src/store/render_comp.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rnote-engine/src/store/render_comp.rs b/crates/rnote-engine/src/store/render_comp.rs index 4f04827632..0eddc84848 100644 --- a/crates/rnote-engine/src/store/render_comp.rs +++ b/crates/rnote-engine/src/store/render_comp.rs @@ -241,7 +241,7 @@ impl StrokeStore { image_scale: f64, ) { let viewport_extended = - viewport.extend_by(viewport.extents() * render::VIEWPORT_EXTENTS_MARGIN_FACTOR); + viewport.extend_by(viewport.extents() * image::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