Re-use tessellator between frames
This commit is contained in:
@ -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}");
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user