Re-use tessellator between frames

This commit is contained in:
2025-06-21 17:20:00 +02:00
parent 6b5bbfbc54
commit 43afb9dfd3
2 changed files with 103 additions and 75 deletions

View File

@ -30,7 +30,7 @@ pub struct FileEditor {
#[derive(serde::Deserialize, serde::Serialize)] #[derive(serde::Deserialize, serde::Serialize)]
pub enum BufferItem { pub enum BufferItem {
Text(MdTextEdit), Text(MdTextEdit),
Handwriting(Handwriting), Handwriting(Box<Handwriting>),
} }
impl FileEditor { 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) => { Err(e) => {
log::error!("Failed to decode handwriting {content:?}: {e}"); log::error!("Failed to decode handwriting {content:?}: {e}");

View File

@ -36,7 +36,7 @@ pub const CODE_BLOCK_KEY: &str = "handwriting";
type StrokeBlendMode = rasterizer::blend::Normal; type StrokeBlendMode = rasterizer::blend::Normal;
const TESSELATION_OPTIONS: TessellationOptions = TessellationOptions { const TESSELLATION_OPTIONS: TessellationOptions = TessellationOptions {
feathering: true, feathering: true,
feathering_size_in_pixels: 1.0, feathering_size_in_pixels: 1.0,
coarse_tessellation_culling: true, coarse_tessellation_culling: true,
@ -63,37 +63,37 @@ pub struct HandwritingStyle {
#[derive(serde::Deserialize, serde::Serialize)] #[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)] #[serde(default)]
pub struct Handwriting { pub struct Handwriting {
#[serde(skip, default = "random_id")]
id: Id,
strokes: Vec<Vec<Pos2>>, strokes: Vec<Vec<Pos2>>,
/// The stroke that is currently being drawed.
#[serde(skip)]
current_stroke: Vec<Pos2>,
/// The lines that have not been blitted to `texture` yet.
#[serde(skip)]
unblitted_lines: Vec<[Pos2; 2]>,
height: f32, height: f32,
desired_height: f32, desired_height: f32,
/// Tesselated mesh of all strokes
#[serde(skip)] #[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<Pos2>,
/// The lines that have not been blitted to `texture` yet.
unblitted_lines: Vec<[Pos2; 2]>,
tessellator: Option<Tessellator>,
/// Tessellated mesh of all strokes
mesh: Arc<Mesh>, mesh: Arc<Mesh>,
#[serde(skip)]
texture: Option<TextureHandle>, texture: Option<TextureHandle>,
#[serde(skip)]
image: ColorImage, image: ColorImage,
#[serde(skip)]
refresh_texture: bool, refresh_texture: bool,
/// Context of the last mesh render. /// Context of the last mesh render.
#[serde(skip)]
last_mesh_ctx: Option<MeshContext>, last_mesh_ctx: Option<MeshContext>,
} }
@ -151,11 +151,20 @@ impl MeshContext {
impl Default for Handwriting { impl Default for Handwriting {
fn default() -> Self { fn default() -> Self {
Self { Self {
id: random_id(),
strokes: Default::default(), strokes: Default::default(),
current_stroke: Default::default(),
height: HANDWRITING_MIN_HEIGHT, height: HANDWRITING_MIN_HEIGHT,
desired_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(), mesh: Default::default(),
texture: None, texture: None,
image: ColorImage::new([0, 0], Color32::WHITE), image: ColorImage::new([0, 0], Color32::WHITE),
@ -180,32 +189,26 @@ impl Handwriting {
ui.separator(); ui.separator();
} }
if ui.button("Clear Painting").clicked() { if ui.button("clear").clicked() {
self.strokes.clear(); self.strokes.clear();
self.refresh_texture = true; self.e.refresh_texture = true;
response.changed = true; response.changed = true;
} }
ui.add_enabled_ui(!self.strokes.is_empty(), |ui| { ui.add_enabled_ui(!self.strokes.is_empty(), |ui| {
if ui.button("Undo").clicked() { if ui.button("undo").clicked() {
self.strokes.pop(); self.strokes.pop();
self.refresh_texture = true; self.e.refresh_texture = true;
response.changed = 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}")); ui.label(format!("vertices: {vertex_count}"));
}) })
.response .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( pub fn ui_content(
&mut self, &mut self,
style: &HandwritingStyle, style: &HandwritingStyle,
@ -214,7 +217,7 @@ impl Handwriting {
) -> egui::Response { ) -> egui::Response {
if style.animate { if style.animate {
self.height = ui.ctx().animate_value_with_time( self.height = ui.ctx().animate_value_with_time(
self.id.with("height animation"), self.e.id.with("height animation"),
self.desired_height, self.desired_height,
0.4, 0.4,
); );
@ -241,7 +244,7 @@ impl Handwriting {
let from_screen = to_screen.inverse(); let from_screen = to_screen.inverse();
// Was the user in the process of drawing a stroke last frame? // 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? // Is the user in the process of drawing a stroke now?
let is_drawing = response.interact_pointer_pos().is_some(); let is_drawing = response.interact_pointer_pos().is_some();
@ -299,7 +302,7 @@ impl Handwriting {
// Process input events and turn them into strokes // Process input events and turn them into strokes
for event in events { for event in events {
let last_canvas_pos = self.current_stroke.last(); let last_canvas_pos = self.e.current_stroke.last();
match event { match event {
Event::PointerMoved(new_position) => { Event::PointerMoved(new_position) => {
@ -341,7 +344,7 @@ impl Handwriting {
} => match (button, pressed) { } => match (button, pressed) {
(PointerButton::Primary, true) => { (PointerButton::Primary, true) => {
if last_canvas_pos.is_none() { if last_canvas_pos.is_none() {
self.current_stroke.push(from_screen * pos); self.e.current_stroke.push(from_screen * pos);
} }
} }
(PointerButton::Primary, false) => { (PointerButton::Primary, false) => {
@ -363,7 +366,7 @@ impl Handwriting {
// TODO: In theory, we can get multiple press->draw->release series // TODO: In theory, we can get multiple press->draw->release series
// in the same frame. Should handle this. // in the same frame. Should handle this.
Event::PointerGone | Event::WindowFocused(false) => { Event::PointerGone | Event::WindowFocused(false) => {
if !self.current_stroke.is_empty() { if !self.e.current_stroke.is_empty() {
self.commit_current_line(hw_response); self.commit_current_line(hw_response);
break; break;
} }
@ -411,25 +414,25 @@ impl Handwriting {
}; };
// Figure out if we need to re-rasterize the mesh. // Figure out if we need to re-rasterize the mesh.
if Some(&new_context) != self.last_mesh_ctx.as_ref() { if Some(&new_context) != self.e.last_mesh_ctx.as_ref() {
self.refresh_texture = true; self.e.refresh_texture = true;
} }
if self.refresh_texture { if self.e.refresh_texture {
// ...if we do, rasterize the entire texture from scratch // ...if we do, rasterize the entire texture from scratch
self.refresh_texture(style, new_context, ui); self.refresh_texture(style, new_context, ui);
self.unblitted_lines.clear(); self.e.unblitted_lines.clear();
} else if !self.unblitted_lines.is_empty() { } else if !self.e.unblitted_lines.is_empty() {
// ...if we don't, we can get away with only rasterizing the *new* lines onto the // ...if we don't, we can get away with only rasterizing the *new* lines onto the
// existing texture. // 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.draw_line_to_texture(from, to, &new_context, ui);
} }
self.unblitted_lines.clear(); self.e.unblitted_lines.clear();
} }
// Draw the texture // 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 texture = SizedTexture::new(texture.id(), texture.size_vec2());
let shape = RectShape { let shape = RectShape {
rect: mesh_rect, rect: mesh_rect,
@ -453,57 +456,68 @@ impl Handwriting {
response 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( fn refresh_texture(
&mut self, &mut self,
style: &HandwritingStyle, style: &HandwritingStyle,
mesh_context: MeshContext, mesh_context: MeshContext,
ui: &mut Ui, 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"))] #[cfg(not(target_arch = "wasm32"))]
let start_time = std::time::Instant::now(); let start_time = std::time::Instant::now();
let mut tesselator = Tessellator::new( let mesh = Arc::make_mut(mesh);
mesh_context.pixels_per_point,
TESSELATION_OPTIONS,
Default::default(), // we don't tesselate fonts
vec![],
);
let mesh = Arc::make_mut(&mut self.mesh);
mesh.clear(); 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 self.strokes
.iter() .iter()
.chain([&self.current_stroke]) .chain([&*current_stroke])
.filter(|stroke| stroke.len() >= 2) .filter(|stroke| stroke.len() >= 2)
.map(|stroke| { .map(|stroke| {
//let points: Vec<Pos2> = stroke.iter().map(|&p| to_screen * p).collect(); //let points: Vec<Pos2> = stroke.iter().map(|&p| to_screen * p).collect();
egui::Shape::line(stroke.clone(), style.stroke) egui::Shape::line(stroke.clone(), style.stroke)
}) })
.for_each(|shape| { .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 // this can happen if the line contains duplicated consecutive positions
//for vertex in &mesh.vertices { //for vertex in &mesh.vertices {
// debug_assert!(vertex.pos.x.is_finite(), "{} must be finite", vertex.pos.x); // 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); // debug_assert!(vertex.pos.y.is_finite(), "{} must be finite", vertex.pos.y);
//} //}
let texture = texture!(self, ui, &mesh_context); let texture = texture!(self.e, ui, &mesh_context);
let triangles = mesh_triangles(&self.mesh); let triangles = mesh_triangles(&self.e.mesh);
let [px_x, px_y] = mesh_context.pixel_size(); let [px_x, px_y] = mesh_context.pixel_size();
let point_to_pixel = TSTransform::from_scaling(mesh_context.pixels_per_point); let point_to_pixel = TSTransform::from_scaling(mesh_context.pixels_per_point);
self.image = rasterize::<StrokeBlendMode>(px_x, px_y, point_to_pixel, triangles); self.e.image = rasterize::<StrokeBlendMode>(px_x, px_y, point_to_pixel, triangles);
texture.set(self.image.clone(), Default::default()); texture.set(self.e.image.clone(), Default::default());
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
{ {
@ -531,16 +545,21 @@ impl Handwriting {
response 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) { 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 { if last_canvas_pos == new_canvas_pos {
return; 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. /// Draw a single line onto the existing texture.
@ -551,16 +570,16 @@ impl Handwriting {
mesh_context: &MeshContext, mesh_context: &MeshContext,
ui: &mut Ui, ui: &mut Ui,
) { ) {
let mut tesselator = Tessellator::new( // INVARIANT: if this function was called, then pixels_per_point is the same as last frame,
mesh_context.pixels_per_point, // so there's no need to create a new tessellator.
TESSELATION_OPTIONS, let tessellator = self
Default::default(), // we don't tesselate fonts .e
vec![], .tessellator
); .get_or_insert_with(|| new_tessellator(mesh_context.pixels_per_point));
let mut mesh = Mesh::default(); let mut mesh = Mesh::default();
let line = egui::Shape::line_segment([from, to], mesh_context.stroke); 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); 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) { fn draw_mesh_to_texture(&mut self, mesh: &Mesh, mesh_context: &MeshContext, ui: &mut Ui) {
let triangles = mesh_triangles(mesh); let triangles = mesh_triangles(mesh);
let point_to_pixel = TSTransform::from_scaling(mesh_context.pixels_per_point); let point_to_pixel = TSTransform::from_scaling(mesh_context.pixels_per_point);
rasterize_onto::<StrokeBlendMode>(&mut self.image, point_to_pixel, triangles); rasterize_onto::<StrokeBlendMode>(&mut self.e.image, point_to_pixel, triangles);
texture!(self, ui, mesh_context).set(self.image.clone(), Default::default()); texture!(self.e, ui, mesh_context).set(self.e.image.clone(), Default::default());
} }
pub fn strokes(&self) -> &[Vec<Pos2>] { pub fn strokes(&self) -> &[Vec<Pos2>] {
@ -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 { impl Display for Handwriting {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let raw = self.encode_as_disk_format(); let raw = self.encode_as_disk_format();