diff --git a/src/handwriting/canvas_rasterizer.rs b/src/handwriting/canvas_rasterizer.rs index d1907ae..af9f17e 100644 --- a/src/handwriting/canvas_rasterizer.rs +++ b/src/handwriting/canvas_rasterizer.rs @@ -2,80 +2,169 @@ use std::sync::Arc; use egui::{ Color32, ColorImage, CornerRadius, Painter, Pos2, Rect, Stroke, StrokeKind, TextureHandle, + Vec2, + ahash::HashMap, emath::TSTransform, epaint::{Brush, RectShape, Vertex}, load::SizedTexture, }; -use crate::rasterizer::rasterize_onto; +use crate::rasterizer::{PxBoundingBox, rasterize_onto, triangle_bounding_box}; use super::StrokeBlendMode; +const CHUNK_SIZE: usize = 64; + /// Rasterize onto a resizeable canvas. #[derive(Default)] pub struct CanvasRasterizer { + tiles: HashMap<[usize; 2], Tile>, +} + +struct Tile { + bounding_box: PxBoundingBox, image: ColorImage, texture: Option, texture_is_dirty: bool, } +impl Tile { + fn new(xi: usize, yi: usize) -> Self { + let x_from = xi * CHUNK_SIZE; + let y_from = yi * CHUNK_SIZE; + let bounding_box = PxBoundingBox { + x_from, + y_from, + x_to: x_from + CHUNK_SIZE, + y_to: y_from + CHUNK_SIZE, + }; + + Self { + bounding_box, + image: ColorImage::new([CHUNK_SIZE, CHUNK_SIZE], Color32::TRANSPARENT), + texture: None, + texture_is_dirty: false, + } + } +} + impl CanvasRasterizer { //pub fn clear(&mut self) { // self.image = ColorImage::new(self.image.size, Color32::TRANSPARENT); // self.texture_is_dirty = true; //} - //pub fn set_size(&mut self, width: usize, height: usize) { - // if self.image.size != [width, height] { - // self.image = ColorImage::new([width, height], Color32::TRANSPARENT); - // self.texture_is_dirty = true; - // } - //} + pub fn set_size(&mut self, width: usize, height: usize) { + let chunk = |max: usize| { + (0..) + .step_by(CHUNK_SIZE) + .take_while(move |n| n <= &max) + .enumerate() + }; + + for (xi, _x) in chunk(width) { + for (yi, _y) in chunk(height) { + self.tiles + .entry([xi, yi]) + .or_insert_with(|| Tile::new(xi, yi)); + } + } + } pub fn clear_and_set_size(&mut self, width: usize, height: usize) { - self.image = ColorImage::new([width, height], Color32::TRANSPARENT); - self.texture_is_dirty = true; + log::error!("clearing all tiles"); + self.tiles.clear(); + + self.set_size(width, height); } pub fn rasterize<'a>( &mut self, point_to_pixel: TSTransform, - triangles: impl Iterator, + triangles: impl Iterator + Clone, ) { - rasterize_onto::(&mut self.image, point_to_pixel, triangles); - self.texture_is_dirty = true; + // calculate smallest bounding box that encompasses all triangles + let triangles_bounding_box = triangles + .clone() + .map(|t| triangle_bounding_box(&t, point_to_pixel)) + .reduce(|a, b| a.union(&b)); + + let Some(triangles_bounding_box) = triangles_bounding_box else { + return; // no triangles + }; + + // get all tiles that are encompassed by the bounding box + // TODO: smarter iteration: don't iterate over all tiles and filter + let mut n = 0; + self.tiles + .values_mut() + // skip tiles which won't be touched + .filter(|tile| tile.bounding_box.overlaps_with(&triangles_bounding_box)) + // calculate TSTransform for tile + .map(|tile| { + let mut point_to_tile_pixel = point_to_pixel; + point_to_tile_pixel.translation -= Vec2::new( + tile.bounding_box.x_from as f32, + tile.bounding_box.y_from as f32, + ); + (point_to_tile_pixel, tile) + }) + // rasterize triangles onto the tile + .for_each(|(tile_point_to_pixel, tile)| { + tile.texture_is_dirty = true; + rasterize_onto::( + &mut tile.image, + tile_point_to_pixel, + triangles.clone(), + ); + n += 1; + }); + + log::error!("rasterizing touched {n} tiles"); } + /// `at` defines the location in screen-coordinates where the canvas should be drawn. pub fn show(&mut self, ctx: &egui::Context, painter: &Painter, at: Rect) { - if self.texture_is_dirty { - self.texture_is_dirty = false; - let new_image = || { - let image = ColorImage::new(self.image.size, Color32::TRANSPARENT); - ctx.load_texture("handwriting", image, Default::default()) - }; - let texture = self.texture.get_or_insert_with(new_image); - texture.set(self.image.clone(), Default::default()); - } + let pixels_per_point = ctx.pixels_per_point(); + let chunk_vec = Vec2::splat(CHUNK_SIZE as f32) / pixels_per_point; - if let Some(texture) = &mut self.texture { - let texture = SizedTexture::new(texture.id(), texture.size_vec2()); - let shape = RectShape { - rect: at, - corner_radius: CornerRadius::ZERO, - fill: Color32::WHITE, - stroke: Stroke::NONE, - stroke_kind: StrokeKind::Inside, - round_to_pixels: None, - blur_width: 0.0, - brush: Some(Arc::new(Brush { - fill_texture_id: texture.id, - uv: Rect { - min: Pos2::ZERO, - max: Pos2::new(1.0, 1.0), - }, - })), - }; - painter.add(shape); + for ([xi, yi], tile) in &mut self.tiles { + if tile.texture_is_dirty { + tile.texture_is_dirty = false; + if let Some(texture) = &mut tile.texture { + texture.set(tile.image.clone(), Default::default()); + } else { + tile.texture = Some(ctx.load_texture( + "handwriting", + tile.image.clone(), + Default::default(), + )); + } + } + + if let Some(texture) = &mut tile.texture { + let texture = SizedTexture::new(texture.id(), texture.size_vec2()); + let shape = RectShape { + rect: Rect::from_min_size( + at.min + Vec2::new(*xi as f32, *yi as f32) * chunk_vec, + chunk_vec, + ), + corner_radius: CornerRadius::ZERO, + fill: Color32::WHITE, + stroke: Stroke::NONE, + stroke_kind: StrokeKind::Inside, + round_to_pixels: None, + blur_width: 0.0, + brush: Some(Arc::new(Brush { + fill_texture_id: texture.id, + uv: Rect { + min: Pos2::ZERO, + max: Pos2::new(1.0, 1.0), + }, + })), + }; + painter.add(shape); + } } } } diff --git a/src/handwriting/mod.rs b/src/handwriting/mod.rs index 53203fd..b97bb3e 100644 --- a/src/handwriting/mod.rs +++ b/src/handwriting/mod.rs @@ -721,8 +721,10 @@ impl HandwritingStyle { } } -fn mesh_triangles(mesh: &Mesh) -> impl Iterator { - mesh.triangles() +fn mesh_triangles(mesh: &Mesh) -> impl Iterator + Clone { + mesh.indices + .chunks_exact(3) + .map(|chunk| [chunk[0], chunk[1], chunk[2]]) .map(|indices| indices.map(|i| &mesh.vertices[i as usize])) } diff --git a/src/rasterizer.rs b/src/rasterizer.rs index 623224e..076a125 100644 --- a/src/rasterizer.rs +++ b/src/rasterizer.rs @@ -1,5 +1,6 @@ use core::f32; use egui::{Color32, ColorImage, Pos2, Rect, Vec2, emath::TSTransform, epaint::Vertex}; +use std::ops::Range; pub trait BlendFn { fn blend(a: Color32, b: Color32) -> Color32; @@ -88,7 +89,7 @@ pub fn rasterize_onto<'a, Blend: BlendFn>( /// A bounding box, measured in pixels. #[derive(Debug, PartialEq, Eq)] -struct PxBoundingBox { +pub struct PxBoundingBox { pub x_from: usize, pub y_from: usize, pub x_to: usize, @@ -104,9 +105,42 @@ impl PxBoundingBox { y_to: self.y_to.min(other.y_to), } } + + pub fn union(&self, other: &PxBoundingBox) -> PxBoundingBox { + PxBoundingBox { + x_from: self.x_from.min(other.x_from), + y_from: self.y_from.min(other.y_from), + x_to: self.x_to.max(other.x_to), + y_to: self.y_to.max(other.y_to), + } + } + + pub fn x_range(&self) -> Range { + self.x_from..self.x_to + } + + pub fn y_range(&self) -> Range { + self.y_from..self.y_to + } + + /// Test whether two boxes do NOT overlap + pub fn overlaps_with(&self, other: &PxBoundingBox) -> bool { + !self.is_disjoint_from(other) + } + + pub fn is_disjoint_from(&self, other: &PxBoundingBox) -> bool { + false + || self.x_from > other.x_to + || self.y_from > other.y_to + || other.x_from > self.x_to + || other.y_from > self.y_to + } } -fn triangle_bounding_box(triangle: &[&Vertex; 3], point_to_pixel: TSTransform) -> PxBoundingBox { +pub fn triangle_bounding_box( + triangle: &[&Vertex; 3], + point_to_pixel: TSTransform, +) -> PxBoundingBox { // calculate bounding box in point coords let mut rect = Rect::NOTHING; for vertex in triangle {