|
|
|
|
@ -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();
|
|
|
|
|
|