From 43afb9dfd315573a840c73cbefa43a4f8f7aee5d Mon Sep 17 00:00:00 2001 From: Joakim Hulthe Date: Sat, 21 Jun 2025 17:20:00 +0200 Subject: [PATCH] Re-use tessellator between frames --- src/file_editor.rs | 4 +- src/handwriting/mod.rs | 174 ++++++++++++++++++++++++----------------- 2 files changed, 103 insertions(+), 75 deletions(-) diff --git a/src/file_editor.rs b/src/file_editor.rs index 9c3b829..a776f9a 100644 --- a/src/file_editor.rs +++ b/src/file_editor.rs @@ -30,7 +30,7 @@ pub struct FileEditor { #[derive(serde::Deserialize, serde::Serialize)] pub enum BufferItem { Text(MdTextEdit), - Handwriting(Handwriting), + Handwriting(Box), } impl FileEditor { @@ -297,7 +297,7 @@ impl From<&str> for FileEditor { } } }; - buffer.push(BufferItem::Handwriting(handwriting)) + buffer.push(BufferItem::Handwriting(Box::new(handwriting))) } Err(e) => { log::error!("Failed to decode handwriting {content:?}: {e}"); diff --git a/src/handwriting/mod.rs b/src/handwriting/mod.rs index 5790184..4e387ee 100644 --- a/src/handwriting/mod.rs +++ b/src/handwriting/mod.rs @@ -36,7 +36,7 @@ pub const CODE_BLOCK_KEY: &str = "handwriting"; type StrokeBlendMode = rasterizer::blend::Normal; -const TESSELATION_OPTIONS: TessellationOptions = TessellationOptions { +const TESSELLATION_OPTIONS: TessellationOptions = TessellationOptions { feathering: true, feathering_size_in_pixels: 1.0, coarse_tessellation_culling: true, @@ -63,37 +63,37 @@ pub struct HandwritingStyle { #[derive(serde::Deserialize, serde::Serialize)] #[serde(default)] pub struct Handwriting { - #[serde(skip, default = "random_id")] - id: Id, - strokes: Vec>, - /// The stroke that is currently being drawed. - #[serde(skip)] - current_stroke: Vec, - - /// The lines that have not been blitted to `texture` yet. - #[serde(skip)] - unblitted_lines: Vec<[Pos2; 2]>, - height: f32, desired_height: f32, - /// Tesselated mesh of all strokes #[serde(skip)] + e: Ephemeral, +} + +/// Handwriting data that isn't persisted across restarts. +struct Ephemeral { + id: Id, + + /// The stroke that is currently being drawed. + current_stroke: Vec, + + /// The lines that have not been blitted to `texture` yet. + unblitted_lines: Vec<[Pos2; 2]>, + + tessellator: Option, + + /// Tessellated mesh of all strokes mesh: Arc, - #[serde(skip)] texture: Option, - #[serde(skip)] image: ColorImage, - #[serde(skip)] refresh_texture: bool, /// Context of the last mesh render. - #[serde(skip)] last_mesh_ctx: Option, } @@ -151,11 +151,20 @@ impl MeshContext { impl Default for Handwriting { fn default() -> Self { Self { - id: random_id(), strokes: Default::default(), - current_stroke: Default::default(), height: HANDWRITING_MIN_HEIGHT, desired_height: HANDWRITING_MIN_HEIGHT, + e: Default::default(), + } + } +} + +impl Default for Ephemeral { + fn default() -> Self { + Self { + id: random_id(), + current_stroke: Default::default(), + tessellator: None, mesh: Default::default(), texture: None, image: ColorImage::new([0, 0], Color32::WHITE), @@ -180,32 +189,26 @@ impl Handwriting { ui.separator(); } - if ui.button("Clear Painting").clicked() { + if ui.button("clear").clicked() { self.strokes.clear(); - self.refresh_texture = true; + self.e.refresh_texture = true; response.changed = true; } ui.add_enabled_ui(!self.strokes.is_empty(), |ui| { - if ui.button("Undo").clicked() { + if ui.button("undo").clicked() { self.strokes.pop(); - self.refresh_texture = true; + self.e.refresh_texture = true; response.changed = true; } }); - let vertex_count: usize = self.mesh.indices.len() / 3; + let vertex_count: usize = self.e.mesh.indices.len() / 3; ui.label(format!("vertices: {vertex_count}")); }) .response } - fn commit_current_line(&mut self, response: &mut HandwritingResponse) { - debug_assert!(!self.current_stroke.is_empty()); - self.strokes.push(mem::take(&mut self.current_stroke)); - response.changed = true; - } - pub fn ui_content( &mut self, style: &HandwritingStyle, @@ -214,7 +217,7 @@ impl Handwriting { ) -> egui::Response { if style.animate { self.height = ui.ctx().animate_value_with_time( - self.id.with("height animation"), + self.e.id.with("height animation"), self.desired_height, 0.4, ); @@ -241,7 +244,7 @@ impl Handwriting { let from_screen = to_screen.inverse(); // Was the user in the process of drawing a stroke last frame? - let was_drawing = !self.current_stroke.is_empty(); + let was_drawing = !self.e.current_stroke.is_empty(); // Is the user in the process of drawing a stroke now? let is_drawing = response.interact_pointer_pos().is_some(); @@ -299,7 +302,7 @@ impl Handwriting { // Process input events and turn them into strokes for event in events { - let last_canvas_pos = self.current_stroke.last(); + let last_canvas_pos = self.e.current_stroke.last(); match event { Event::PointerMoved(new_position) => { @@ -341,7 +344,7 @@ impl Handwriting { } => match (button, pressed) { (PointerButton::Primary, true) => { if last_canvas_pos.is_none() { - self.current_stroke.push(from_screen * pos); + self.e.current_stroke.push(from_screen * pos); } } (PointerButton::Primary, false) => { @@ -363,7 +366,7 @@ impl Handwriting { // TODO: In theory, we can get multiple press->draw->release series // in the same frame. Should handle this. Event::PointerGone | Event::WindowFocused(false) => { - if !self.current_stroke.is_empty() { + if !self.e.current_stroke.is_empty() { self.commit_current_line(hw_response); break; } @@ -411,25 +414,25 @@ impl Handwriting { }; // Figure out if we need to re-rasterize the mesh. - if Some(&new_context) != self.last_mesh_ctx.as_ref() { - self.refresh_texture = true; + if Some(&new_context) != self.e.last_mesh_ctx.as_ref() { + self.e.refresh_texture = true; } - if self.refresh_texture { + if self.e.refresh_texture { // ...if we do, rasterize the entire texture from scratch self.refresh_texture(style, new_context, ui); - self.unblitted_lines.clear(); - } else if !self.unblitted_lines.is_empty() { + self.e.unblitted_lines.clear(); + } else if !self.e.unblitted_lines.is_empty() { // ...if we don't, we can get away with only rasterizing the *new* lines onto the // existing texture. - for [from, to] in std::mem::take(&mut self.unblitted_lines) { + for [from, to] in std::mem::take(&mut self.e.unblitted_lines) { self.draw_line_to_texture(from, to, &new_context, ui); } - self.unblitted_lines.clear(); + self.e.unblitted_lines.clear(); } // Draw the texture - if let Some(texture) = &self.texture { + if let Some(texture) = &self.e.texture { let texture = SizedTexture::new(texture.id(), texture.size_vec2()); let shape = RectShape { rect: mesh_rect, @@ -453,57 +456,68 @@ impl Handwriting { response } + fn commit_current_line(&mut self, response: &mut HandwritingResponse) { + debug_assert!(!self.e.current_stroke.is_empty()); + self.strokes.push(mem::take(&mut self.e.current_stroke)); + response.changed = true; + } + + /// Tessellate and rasterize the strokes into a new texture. fn refresh_texture( &mut self, style: &HandwritingStyle, mesh_context: MeshContext, ui: &mut Ui, ) { - // TODO: don't tesselate and rasterize on the GUI thread + let Ephemeral { + current_stroke, + tessellator, + mesh, + refresh_texture, + last_mesh_ctx, + .. + } = &mut self.e; + // TODO: don't tessellate and rasterize on the GUI thread - self.last_mesh_ctx = Some(mesh_context); + *last_mesh_ctx = Some(mesh_context); - self.refresh_texture = false; + *refresh_texture = false; #[cfg(not(target_arch = "wasm32"))] let start_time = std::time::Instant::now(); - let mut tesselator = Tessellator::new( - mesh_context.pixels_per_point, - TESSELATION_OPTIONS, - Default::default(), // we don't tesselate fonts - vec![], - ); - - let mesh = Arc::make_mut(&mut self.mesh); + let mesh = Arc::make_mut(mesh); mesh.clear(); + // TODO: re-use tessellator if pixels_per_point hasn't changed + let tessellator = tessellator.insert(new_tessellator(mesh_context.pixels_per_point)); + self.strokes .iter() - .chain([&self.current_stroke]) + .chain([&*current_stroke]) .filter(|stroke| stroke.len() >= 2) .map(|stroke| { //let points: Vec = stroke.iter().map(|&p| to_screen * p).collect(); egui::Shape::line(stroke.clone(), style.stroke) }) .for_each(|shape| { - tesselator.tessellate_shape(shape, mesh); + tessellator.tessellate_shape(shape, mesh); }); - // sanity-check that tesselation did not produce any NaNs. + // sanity-check that tessellation did not produce any NaNs. // this can happen if the line contains duplicated consecutive positions //for vertex in &mesh.vertices { // debug_assert!(vertex.pos.x.is_finite(), "{} must be finite", vertex.pos.x); // debug_assert!(vertex.pos.y.is_finite(), "{} must be finite", vertex.pos.y); //} - let texture = texture!(self, ui, &mesh_context); - let triangles = mesh_triangles(&self.mesh); + let texture = texture!(self.e, ui, &mesh_context); + let triangles = mesh_triangles(&self.e.mesh); let [px_x, px_y] = mesh_context.pixel_size(); let point_to_pixel = TSTransform::from_scaling(mesh_context.pixels_per_point); - self.image = rasterize::(px_x, px_y, point_to_pixel, triangles); - texture.set(self.image.clone(), Default::default()); + self.e.image = rasterize::(px_x, px_y, point_to_pixel, triangles); + texture.set(self.e.image.clone(), Default::default()); #[cfg(not(target_arch = "wasm32"))] { @@ -531,16 +545,21 @@ impl Handwriting { response } + /// Append a new [Pos2] to [Self::current_stroke]. + /// + /// Queue a new line to be drawn onto [Self::texture]. fn push_to_stroke(&mut self, new_canvas_pos: Pos2) { - if let Some(&last_canvas_pos) = self.current_stroke.last() { + if let Some(&last_canvas_pos) = self.e.current_stroke.last() { if last_canvas_pos == new_canvas_pos { return; } - self.unblitted_lines.push([last_canvas_pos, new_canvas_pos]); + self.e + .unblitted_lines + .push([last_canvas_pos, new_canvas_pos]); } - self.current_stroke.push(new_canvas_pos); + self.e.current_stroke.push(new_canvas_pos); } /// Draw a single line onto the existing texture. @@ -551,16 +570,16 @@ impl Handwriting { mesh_context: &MeshContext, ui: &mut Ui, ) { - let mut tesselator = Tessellator::new( - mesh_context.pixels_per_point, - TESSELATION_OPTIONS, - Default::default(), // we don't tesselate fonts - vec![], - ); + // INVARIANT: if this function was called, then pixels_per_point is the same as last frame, + // so there's no need to create a new tessellator. + let tessellator = self + .e + .tessellator + .get_or_insert_with(|| new_tessellator(mesh_context.pixels_per_point)); let mut mesh = Mesh::default(); let line = egui::Shape::line_segment([from, to], mesh_context.stroke); - tesselator.tessellate_shape(line, &mut mesh); + tessellator.tessellate_shape(line, &mut mesh); self.draw_mesh_to_texture(&mesh, mesh_context, ui); } @@ -569,8 +588,8 @@ impl Handwriting { fn draw_mesh_to_texture(&mut self, mesh: &Mesh, mesh_context: &MeshContext, ui: &mut Ui) { let triangles = mesh_triangles(mesh); let point_to_pixel = TSTransform::from_scaling(mesh_context.pixels_per_point); - rasterize_onto::(&mut self.image, point_to_pixel, triangles); - texture!(self, ui, mesh_context).set(self.image.clone(), Default::default()); + rasterize_onto::(&mut self.e.image, point_to_pixel, triangles); + texture!(self.e, ui, mesh_context).set(self.e.image.clone(), Default::default()); } pub fn strokes(&self) -> &[Vec] { @@ -629,6 +648,15 @@ impl Handwriting { } } +fn new_tessellator(pixels_per_point: f32) -> Tessellator { + Tessellator::new( + pixels_per_point, + TESSELLATION_OPTIONS, + Default::default(), // we don't tessellate fonts + vec![], + ) +} + impl Display for Handwriting { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let raw = self.encode_as_disk_format();