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)]
pub enum BufferItem {
Text(MdTextEdit),
Handwriting(Handwriting),
Handwriting(Box<Handwriting>),
}
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}");

View File

@ -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<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,
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<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>,
#[serde(skip)]
texture: Option<TextureHandle>,
#[serde(skip)]
image: ColorImage,
#[serde(skip)]
refresh_texture: bool,
/// Context of the last mesh render.
#[serde(skip)]
last_mesh_ctx: Option<MeshContext>,
}
@ -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<Pos2> = 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::<StrokeBlendMode>(px_x, px_y, point_to_pixel, triangles);
texture.set(self.image.clone(), Default::default());
self.e.image = rasterize::<StrokeBlendMode>(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::<StrokeBlendMode>(&mut self.image, point_to_pixel, triangles);
texture!(self, ui, mesh_context).set(self.image.clone(), Default::default());
rasterize_onto::<StrokeBlendMode>(&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<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 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let raw = self.encode_as_disk_format();