2 Commits

Author SHA1 Message Date
fac896161e Remove dead comments 2025-10-25 17:02:41 +02:00
945bb4d9fe Add basic eraser 2025-10-25 17:02:27 +02:00
4 changed files with 127 additions and 52 deletions

View File

@ -1,5 +1,6 @@
mod canvas_rasterizer;
mod disk_format;
mod tool;
use std::{
fmt::{self, Display},
@ -21,12 +22,13 @@ use egui::{
use eyre::{Context, bail};
use eyre::{OptionExt, eyre};
use half::f16;
use serde::Serialize;
use zerocopy::{FromBytes, IntoBytes};
use crate::{custom_code_block::try_from_custom_code_block, rasterizer};
use crate::{custom_code_block::write_custom_code_block, util::random_id};
use self::tool::{ToolEvent, Tool};
const HANDWRITING_MIN_HEIGHT: f32 = 100.0;
const HANDWRITING_BOTTOM_PADDING: f32 = 80.0;
const HANDWRITING_MARGIN: f32 = 0.05;
@ -79,6 +81,14 @@ struct Ephemeral {
canvas_rasterizer: CanvasRasterizer,
tool: Tool,
/// Tool position in canvas space.
tool_position: Option<Pos2>,
/// Tool position last frame, in canvas space.
last_tool_position: Option<Pos2>,
/// The stroke that is currently being drawed.
current_stroke: Vec<Pos2>,
@ -129,6 +139,9 @@ impl Default for Ephemeral {
Self {
id: random_id(),
canvas_rasterizer: Default::default(),
tool: Tool::Eraser,
tool_position: None,
last_tool_position: None,
current_stroke: Default::default(),
tessellator: None,
mesh: Default::default(),
@ -173,6 +186,14 @@ impl Handwriting {
let _ = Clipboard::new().unwrap().set_text(text);
}
let (label, switch_to_tool) = match self.e.tool {
Tool::Pencil => ("eraser", Tool::Eraser),
Tool::Eraser => ("pencil", Tool::Pencil),
};
if ui.button(label).clicked() {
self.e.tool = switch_to_tool;
}
let vertex_count: usize = self.e.mesh.indices.len() / 3;
ui.label(format!("vertices: {vertex_count}"));
})
@ -212,6 +233,8 @@ impl Handwriting {
emath::RectTransform::from_to(Rect::from_min_size(Pos2::ZERO, size), response.rect);
let from_screen = to_screen.inverse();
self.e.last_tool_position = self.e.tool_position;
// Was the user in the process of drawing a stroke last frame?
let was_drawing = !self.e.current_stroke.is_empty();
@ -271,10 +294,21 @@ impl Handwriting {
// Process input events and turn them into strokes
for event in events {
let mut last_canvas_pos = self.e.current_stroke.last().copied();
process_event(&mut last_canvas_pos, from_screen, &event, |tool_event| {
if self.on_tool_event(tool_event) {
hw_response.changed = true; // FIXME: ugly
let mut last_tool_position = self.e.last_tool_position;
process_event(&mut last_tool_position, from_screen, &event, |tool_event| {
self.e.tool_position = tool_event.position();
match self.e.tool {
Tool::Pencil => {
hw_response.changed |=
tool::pencil::on_tool_event(self, tool_event);
}
Tool::Eraser => {
if tool::eraser::on_tool_event(self, tool_event) {
self.e.refresh_texture = true;
hw_response.changed = true;
}
}
}
});
}
@ -333,6 +367,12 @@ impl Handwriting {
// Draw the texture
self.e.canvas_rasterizer.show(ui.ctx(), &painter, mesh_rect);
if let Some(tool_position) = self.e.tool_position && let Tool::Eraser = self.e.tool {
let pos = to_screen * tool_position;
let shape = Shape::circle_stroke(pos, tool::eraser::RADIUS, style.stroke);
painter.add(shape);
}
response
}
@ -512,34 +552,6 @@ impl Handwriting {
bytes.into_boxed_slice()
}
/// Handle a [ToolEvent]. Returns true if a stroke was completed.
fn on_tool_event(&mut self, tool_event: ToolEvent) -> bool {
match tool_event {
ToolEvent::Press { at } => {
debug_assert!(self.e.current_stroke.is_empty());
self.push_to_stroke(at);
false
}
ToolEvent::Move { to } => {
self.push_to_stroke(to);
false
}
ToolEvent::Release => {
debug_assert!(!self.e.current_stroke.is_empty());
self.strokes.push(mem::take(&mut self.e.current_stroke));
true
}
}
}
}
/// A simple event that can defines how a tool (e.g. the pen) is used on a [Handwriting].
#[derive(Serialize)]
enum ToolEvent {
Press { at: Pos2 },
Move { to: Pos2 },
Release,
}
/// Convert [egui::Event]s to [ToolEvent]s.
@ -553,8 +565,6 @@ fn process_event(
&Event::PointerMoved(new_position) => {
let new_canvas_pos = from_screen * new_position;
if last_canvas_pos.is_some() && *last_canvas_pos != Some(new_canvas_pos) {
//self.push_to_stroke(new_canvas_pos);
//response.mark_changed();
*last_canvas_pos = Some(new_canvas_pos);
on_tool_event(ToolEvent::Move { to: new_canvas_pos });
}
@ -576,8 +586,6 @@ fn process_event(
if let Some(pos) = last_canvas_pos {
*pos += delta;
on_tool_event(ToolEvent::Move { to: *pos });
//self.push_to_stroke(last_canvas_pos + delta);
//response.mark_changed();
} else {
println!("Got `MouseMoved`, but have no previous pos");
}
@ -593,8 +601,6 @@ fn process_event(
if last_canvas_pos.is_none() {
let pos = from_screen * pos;
*last_canvas_pos = Some(pos);
//self.e.current_stroke.push(from_screen * pos);
on_tool_event(ToolEvent::Press { at: pos });
}
}
@ -603,28 +609,15 @@ fn process_event(
let pos = from_screen * pos;
on_tool_event(ToolEvent::Move { to: pos });
on_tool_event(ToolEvent::Release {});
//self.push_to_stroke(from_screen * pos);
//self.commit_current_line(hw_response);
//response.mark_changed();
}
// Stop reading events.
// TODO: In theory, we can get multiple press->draw->release series
// in the same frame. Should handle this.
//break;
}
(_, _) => {}
},
// Stop drawing after pointer disappears or the window is unfocused
// 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.e.current_stroke.is_empty() {
if last_canvas_pos.take().is_some() {
on_tool_event(ToolEvent::Release {});
//self.commit_current_line(hw_response);
//break;
}
}

View File

@ -0,0 +1,32 @@
use crate::handwriting::{Handwriting, ToolEvent};
use egui::Pos2;
pub const RADIUS: f32 = 12.0;
/// Handle a [ToolEvent]. Returns true if a stroke was completed.
pub fn on_tool_event(handwriting: &mut Handwriting, tool_event: ToolEvent) -> bool {
match tool_event {
ToolEvent::Press { at } => {
erase(handwriting, at, RADIUS)
}
ToolEvent::Move { to } => {
erase(handwriting, to, RADIUS)
}
ToolEvent::Release => {
false
}
}
}
fn erase(handwriting: &mut Handwriting, at: Pos2, radius: f32) -> bool {
let strokes = handwriting.strokes.len();
handwriting.strokes.retain(|stroke| {
stroke.iter().all(|&point| {
(point - at).length() > radius
})
});
handwriting.strokes.len() < strokes
}

View File

@ -0,0 +1,28 @@
pub mod pencil;
pub mod eraser;
use egui::Pos2;
use serde::Serialize;
pub enum Tool {
Pencil,
Eraser,
}
/// A simple event that can defines how a tool (e.g. the pen) is used on a [Handwriting].
#[derive(Debug, Serialize, Copy, Clone)]
pub enum ToolEvent {
Press { at: Pos2 },
Move { to: Pos2 },
Release,
}
impl ToolEvent {
pub const fn position(&self) -> Option<Pos2> {
match self {
&ToolEvent::Press { at } => Some(at),
&ToolEvent::Move { to } => Some(to),
ToolEvent::Release => None,
}
}
}

View File

@ -0,0 +1,22 @@
use crate::handwriting::{Handwriting, ToolEvent};
use std::mem;
/// Handle a [ToolEvent]. Returns true if a stroke was completed.
pub fn on_tool_event(handwriting: &mut Handwriting, tool_event: ToolEvent) -> bool {
match tool_event {
ToolEvent::Press { at } => {
debug_assert!(handwriting.e.current_stroke.is_empty());
handwriting.push_to_stroke(at);
false
}
ToolEvent::Move { to } => {
handwriting.push_to_stroke(to);
false
}
ToolEvent::Release => {
debug_assert!(!handwriting.e.current_stroke.is_empty());
handwriting.strokes.push(mem::take(&mut handwriting.e.current_stroke));
true
}
}
}