Skip to content
Draft
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3f0bb63
perf: use `push_repeat` for background nodes
Doublonmousse Jun 5, 2025
7da737f
visual debug : show rtree bounds
Doublonmousse Jun 5, 2025
39bc7bb
use rtree to speed up `regenerate_rendering_for_strokes_threaded`
Doublonmousse Jun 5, 2025
7f70361
make the rtree only keep track of non-trashed strokes
Doublonmousse Jun 5, 2025
4eed9d2
fix typo
Doublonmousse Jun 5, 2025
e755198
use rtree for `update_content_rendering_current_viewport` calculations
Doublonmousse Jun 5, 2025
675e314
rebuilt rtree for non trashed components only
Doublonmousse Jun 5, 2025
eaa09a6
add a note on rendernode retention when trashing strokes
Doublonmousse Jun 6, 2025
392f799
clean up interfaces
Doublonmousse Jun 6, 2025
9632dde
fix typo
Doublonmousse Jun 7, 2025
2e9a873
handle correctly empty keytree for `resize_doc` methods
Doublonmousse Jun 7, 2025
4f08750
refactor: move color for rtree bounds to the `visual_debug` module
Doublonmousse Jun 29, 2025
36aaed6
refactor: remove `origin_background_rendernode` (using `None` for the…
Doublonmousse Jun 29, 2025
fda7199
refactor: rename `get_origin` method and remove unneeded arguments
Doublonmousse Jun 29, 2025
530e2ee
add `keys_intersecting_bounds_hashset` for `render_comp`
Doublonmousse Jun 29, 2025
30261cc
refactor: remove `get_tree` method
Doublonmousse Jun 29, 2025
e10608f
refactor: do not leak `AABB` outside, use `Aabb` instead
Doublonmousse Jun 29, 2025
a2c4e5b
import `rnote_compose::shapes::Shapeable` inside ui guarded functions
Doublonmousse Jun 29, 2025
81c1434
refactor: remove extraneous conversion from `Aabb` to itself
Doublonmousse Jun 29, 2025
043aa43
remove code from another feature
Doublonmousse Jul 17, 2025
ea1bcaf
duplicate the rtree for trashed and non trashed keys
Doublonmousse Aug 6, 2025
5aee84d
visual debug: show the `trashed_keytree`
Doublonmousse Aug 6, 2025
5efa8e2
`trash_colliding_strokes`: get the stroke bounds from the keytree dir…
Doublonmousse Aug 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions crates/rnote-compose/src/ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<f64>) -> Vec<Self>;
/// get the top patch for a given split size
fn get_origin(self, split_size: na::Vector2<f64>, 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<f64>) -> Vec<Self>;
Expand Down Expand Up @@ -394,6 +396,25 @@ impl AabbExt for Aabb {
split_aabbs
}

fn get_origin(self, split_size: na::Vector2<f64>, 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<f64>,
Expand Down
26 changes: 18 additions & 8 deletions crates/rnote-engine/src/document/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -256,14 +257,18 @@ 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 {
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])
};
new_bounds.merge(&content_bounds);
}
Expand Down Expand Up @@ -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.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())
Expand Down
6 changes: 4 additions & 2 deletions crates/rnote-engine/src/engine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,8 @@ pub struct Engine {
background_tile_image: Option<render::Image>,
#[cfg(feature = "ui")]
#[serde(skip)]
background_rendernodes: Vec<gtk4::gsk::RenderNode>,
background_rendernode: Option<gtk4::gsk::RenderNode>,
origin_background_rendernode: Option<Aabb>,
// Origin indicator rendering
#[serde(skip)]
origin_indicator_image: Option<render::Image>,
Expand All @@ -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,
Expand Down
43 changes: 24 additions & 19 deletions crates/rnote-engine/src/engine/rendering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ impl Engine {
use rnote_compose::ext::AabbExt;

let viewport = self.camera.viewport();
let mut rendernodes: Vec<gsk::RenderNode> = vec![];

if let Some(image) = &self.background_tile_image {
// Only create the texture once, it is expensive
Expand All @@ -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")]
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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(())
}

Expand Down
13 changes: 13 additions & 0 deletions crates/rnote-engine/src/store/keytree.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// 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].
Expand Down Expand Up @@ -69,6 +70,18 @@ impl KeyTree {
pub(crate) fn clear(&mut self) {
*self = Self::default()
}

pub(crate) fn get_tree(&self) -> &rstar::RTree<KeyTreeObject, rstar::DefaultParams> {
&self.0
}

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 {
Expand Down
10 changes: 10 additions & 0 deletions crates/rnote-engine/src/store/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -147,6 +148,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);
Expand Down Expand Up @@ -368,4 +370,12 @@ impl StrokeStore {

widget_flags
}

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()
}
}
78 changes: 60 additions & 18 deletions crates/rnote-engine/src/store/render_comp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -242,28 +245,39 @@ impl StrokeStore {
viewport: Aabb,
image_scale: f64,
) {
let keys = self.render_components.keys().collect::<Vec<StrokeKey>>();

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<StrokeKey, ()> = 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::<Vec<StrokeKey>>();

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 {
Expand Down Expand Up @@ -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 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.
Expand Down Expand Up @@ -656,6 +686,18 @@ 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]],
),
rnote_compose::Color::new(1.0, 0.5, 0., 1.0),
snapshot,
border_widths,
);

Ok(())
}
}
19 changes: 7 additions & 12 deletions crates/rnote-engine/src/store/stroke_comp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<StrokeKey> {
self.keys_sorted_chrono()
.into_iter()
Expand Down Expand Up @@ -127,17 +127,12 @@ 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
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.
Expand Down
Loading