From f2556f712595ce358e24810324966c65adc18e8c Mon Sep 17 00:00:00 2001 From: Joakim Hulthe Date: Mon, 23 Jun 2025 20:34:02 +0200 Subject: [PATCH] Split Image handling from handwriting/mod.rs --- src/handwriting/canvas_rasterizer.rs | 81 +++++++++++++++++++ src/handwriting/mod.rs | 114 +++++++-------------------- 2 files changed, 109 insertions(+), 86 deletions(-) create mode 100644 src/handwriting/canvas_rasterizer.rs diff --git a/src/handwriting/canvas_rasterizer.rs b/src/handwriting/canvas_rasterizer.rs new file mode 100644 index 0000000..d1907ae --- /dev/null +++ b/src/handwriting/canvas_rasterizer.rs @@ -0,0 +1,81 @@ +use std::sync::Arc; + +use egui::{ + Color32, ColorImage, CornerRadius, Painter, Pos2, Rect, Stroke, StrokeKind, TextureHandle, + emath::TSTransform, + epaint::{Brush, RectShape, Vertex}, + load::SizedTexture, +}; + +use crate::rasterizer::rasterize_onto; + +use super::StrokeBlendMode; + +/// Rasterize onto a resizeable canvas. +#[derive(Default)] +pub struct CanvasRasterizer { + image: ColorImage, + texture: Option, + texture_is_dirty: bool, +} + +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 clear_and_set_size(&mut self, width: usize, height: usize) { + self.image = ColorImage::new([width, height], Color32::TRANSPARENT); + self.texture_is_dirty = true; + } + + pub fn rasterize<'a>( + &mut self, + point_to_pixel: TSTransform, + triangles: impl Iterator, + ) { + rasterize_onto::(&mut self.image, point_to_pixel, triangles); + self.texture_is_dirty = true; + } + + 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()); + } + + 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); + } + } +} diff --git a/src/handwriting/mod.rs b/src/handwriting/mod.rs index 4e387ee..53203fd 100644 --- a/src/handwriting/mod.rs +++ b/src/handwriting/mod.rs @@ -1,3 +1,6 @@ +mod canvas_rasterizer; +mod disk_format; + use std::{ fmt::{self, Display}, iter, mem, @@ -6,27 +9,22 @@ use std::{ }; use base64::{Engine, prelude::BASE64_STANDARD}; +use canvas_rasterizer::CanvasRasterizer; use disk_format::{DiskFormat, RawStroke, RawStrokeHeader, f16_le}; use egui::{ - Color32, ColorImage, CornerRadius, Event, Frame, Id, Mesh, PointerButton, Pos2, Rect, Sense, - Shape, Stroke, TextureHandle, Theme, Ui, Vec2, + Color32, Event, Frame, Id, Mesh, PointerButton, Pos2, Rect, Sense, Shape, Stroke, Theme, Ui, + Vec2, emath::{self, TSTransform}, - epaint::{Brush, RectShape, TessellationOptions, Tessellator, Vertex}, - load::SizedTexture, + epaint::{TessellationOptions, Tessellator, Vertex}, }; use eyre::{Context, bail}; use eyre::{OptionExt, eyre}; use half::f16; use zerocopy::{FromBytes, IntoBytes}; -use crate::{ - custom_code_block::try_from_custom_code_block, - rasterizer::{self, rasterize, rasterize_onto}, -}; +use crate::{custom_code_block::try_from_custom_code_block, rasterizer}; use crate::{custom_code_block::write_custom_code_block, util::random_id}; -mod disk_format; - const HANDWRITING_MIN_HEIGHT: f32 = 100.0; const HANDWRITING_BOTTOM_PADDING: f32 = 80.0; const HANDWRITING_MARGIN: f32 = 0.05; @@ -76,6 +74,8 @@ pub struct Handwriting { struct Ephemeral { id: Id, + canvas_rasterizer: CanvasRasterizer, + /// The stroke that is currently being drawed. current_stroke: Vec, @@ -87,10 +87,6 @@ struct Ephemeral { /// Tessellated mesh of all strokes mesh: Arc, - texture: Option, - - image: ColorImage, - refresh_texture: bool, /// Context of the last mesh render. @@ -115,31 +111,6 @@ struct MeshContext { pub stroke: Stroke, } -/// Get [Painting::texture], initializing it if necessary. -macro_rules! texture { - ($self_:expr, $ui:expr, $mesh_context:expr) => {{ - let ui: &Ui = $ui; - let mesh_context: &MeshContext = $mesh_context; - let image_size = mesh_context.pixel_size(); - - let new_image = || { - let image = ColorImage::new(image_size, Color32::TRANSPARENT); - ui.ctx() - .load_texture("handwriting", image, Default::default()) - }; - - let texture = $self_.texture.get_or_insert_with(new_image); - - if texture.size() != image_size { - $self_.refresh_texture = true; - // TODO: don't redraw the entire mesh, just blit the old texture onto the new one - *texture = new_image() - }; - - texture - }}; -} - impl MeshContext { /// Calculate canvas size in pixels pub fn pixel_size(&self) -> [usize; 2] { @@ -163,11 +134,10 @@ impl Default for Ephemeral { fn default() -> Self { Self { id: random_id(), + canvas_rasterizer: Default::default(), current_stroke: Default::default(), tessellator: None, mesh: Default::default(), - texture: None, - image: ColorImage::new([0, 0], Color32::WHITE), refresh_texture: true, last_mesh_ctx: None, unblitted_lines: Default::default(), @@ -400,7 +370,7 @@ impl Handwriting { painter.add(shape); }); - // Get the dimensions of the image + // Get the position and dimensions of the image let mesh_rect = response .rect .with_max_y(response.rect.min.y + self.desired_height); @@ -420,38 +390,19 @@ impl Handwriting { if self.e.refresh_texture { // ...if we do, rasterize the entire texture from scratch - self.refresh_texture(style, new_context, ui); + self.refresh_texture(style, new_context); 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.e.unblitted_lines) { - self.draw_line_to_texture(from, to, &new_context, ui); + self.draw_line_to_texture(from, to, &new_context); } self.e.unblitted_lines.clear(); } // Draw the texture - if let Some(texture) = &self.e.texture { - let texture = SizedTexture::new(texture.id(), texture.size_vec2()); - let shape = RectShape { - rect: mesh_rect, - corner_radius: CornerRadius::ZERO, - fill: Color32::WHITE, - stroke: Stroke::NONE, - stroke_kind: egui::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); - } + self.e.canvas_rasterizer.show(ui.ctx(), &painter, mesh_rect); response } @@ -463,12 +414,7 @@ impl Handwriting { } /// Tessellate and rasterize the strokes into a new texture. - fn refresh_texture( - &mut self, - style: &HandwritingStyle, - mesh_context: MeshContext, - ui: &mut Ui, - ) { + fn refresh_texture(&mut self, style: &HandwritingStyle, mesh_context: MeshContext) { let Ephemeral { current_stroke, tessellator, @@ -511,13 +457,14 @@ impl Handwriting { // debug_assert!(vertex.pos.y.is_finite(), "{} must be finite", vertex.pos.y); //} - 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.e.image = rasterize::(px_x, px_y, point_to_pixel, triangles); - texture.set(self.e.image.clone(), Default::default()); + let triangles = mesh_triangles(&self.e.mesh); + + self.e.canvas_rasterizer.clear_and_set_size(px_x, px_y); + self.e + .canvas_rasterizer + .rasterize(point_to_pixel, triangles); #[cfg(not(target_arch = "wasm32"))] { @@ -563,13 +510,7 @@ impl Handwriting { } /// Draw a single line onto the existing texture. - fn draw_line_to_texture( - &mut self, - from: Pos2, - to: Pos2, - mesh_context: &MeshContext, - ui: &mut Ui, - ) { + fn draw_line_to_texture(&mut self, from: Pos2, to: Pos2, mesh_context: &MeshContext) { // 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 @@ -581,15 +522,16 @@ impl Handwriting { let line = egui::Shape::line_segment([from, to], mesh_context.stroke); tessellator.tessellate_shape(line, &mut mesh); - self.draw_mesh_to_texture(&mesh, mesh_context, ui); + self.draw_mesh_to_texture(&mesh, mesh_context); } /// Draw a single mesh onto the existing texture. - fn draw_mesh_to_texture(&mut self, mesh: &Mesh, mesh_context: &MeshContext, ui: &mut Ui) { + fn draw_mesh_to_texture(&mut self, mesh: &Mesh, mesh_context: &MeshContext) { let triangles = mesh_triangles(mesh); let point_to_pixel = TSTransform::from_scaling(mesh_context.pixels_per_point); - rasterize_onto::(&mut self.e.image, point_to_pixel, triangles); - texture!(self.e, ui, mesh_context).set(self.e.image.clone(), Default::default()); + self.e + .canvas_rasterizer + .rasterize(point_to_pixel, triangles); } pub fn strokes(&self) -> &[Vec] {