diff --git a/crates/rnote-engine/src/pens/eraser.rs b/crates/rnote-engine/src/pens/eraser.rs index 821ef5e92c..f510e24367 100644 --- a/crates/rnote-engine/src/pens/eraser.rs +++ b/crates/rnote-engine/src/pens/eraser.rs @@ -20,15 +20,73 @@ pub enum EraserState { Down(Element), } +#[derive(Debug, Default, Clone, Copy)] +pub struct EraserMotion { + last_element: Option<(Element, Instant)>, + speed: f64, + pub added_width: f64, +} + +impl EraserMotion { + pub const SMOOTHING_FACTOR_ACCEL: f64 = 3.0; + pub const SMOOTHING_FACTOR_DECEL: f64 = 10.0; + pub const SPEED_LIMIT: f64 = 10000.0; + pub const MIN_SPEED: f64 = 100.0; + pub const SPEED_SCALING: f64 = 0.01; + pub const SPEED_SCALING_STEP_SIZE: f64 = 25.0; + + fn update(&mut self, element: Element, time: Instant) { + if let Some((last_element, last_element_time)) = self.last_element { + let delta = element.pos - last_element.pos; + let delta_time = time - last_element_time; + let new_speed = delta.norm() / delta_time.as_secs_f64(); + if new_speed > self.speed { + self.speed = Self::SPEED_LIMIT.min( + (self.speed * Self::SMOOTHING_FACTOR_ACCEL + new_speed) + / (Self::SMOOTHING_FACTOR_ACCEL + 1.0), + ); + } else { + self.speed = Self::SPEED_LIMIT.min( + (self.speed * Self::SMOOTHING_FACTOR_DECEL + new_speed) + / (Self::SMOOTHING_FACTOR_DECEL + 1.0), + ); + } + } + self.last_element = Some((element, time)); + + if self.speed > Self::MIN_SPEED { + let size_increase = Self::SPEED_SCALING * self.speed; + let lower_bound = self.added_width - 0.5 * Self::SPEED_SCALING_STEP_SIZE; + let upper_bound = self.added_width + 1.0 * Self::SPEED_SCALING_STEP_SIZE; + if size_increase < lower_bound { + self.added_width -= Self::SPEED_SCALING_STEP_SIZE; + } + if size_increase > upper_bound { + self.added_width += Self::SPEED_SCALING_STEP_SIZE; + } + } else { + self.added_width = 0.0; + } + } + + fn reset(&mut self) { + self.last_element = None; + self.speed = 0.0; + self.added_width = 0.0; + } +} + #[derive(Clone, Debug)] pub struct Eraser { pub(crate) state: EraserState, + pub(crate) motion: EraserMotion, } impl Default for Eraser { fn default() -> Self { Self { state: EraserState::Up, + motion: EraserMotion::default(), } } } @@ -53,14 +111,14 @@ impl PenBehaviour for Eraser { fn handle_event( &mut self, event: PenEvent, - _now: Instant, + now: Instant, engine_view: &mut EngineViewMut, ) -> (EventResult, WidgetFlags) { let mut widget_flags = WidgetFlags::default(); - let event_result = match (&mut self.state, event) { (EraserState::Up | EraserState::Proximity { .. }, PenEvent::Down { element, .. }) => { - widget_flags |= erase(element, engine_view); + self.motion.update(element, now); + widget_flags |= erase(element, self.motion.speed, engine_view); self.state = EraserState::Down(element); EventResult { handled: true, @@ -69,6 +127,7 @@ impl PenBehaviour for Eraser { } } (EraserState::Up | EraserState::Down { .. }, PenEvent::Proximity { element, .. }) => { + self.motion.update(element, now); self.state = EraserState::Proximity(element); EventResult { handled: false, @@ -85,7 +144,8 @@ impl PenBehaviour for Eraser { progress: PenProgress::Idle, }, (EraserState::Down(current_element), PenEvent::Down { element, .. }) => { - widget_flags |= erase(element, engine_view); + self.motion.update(element, now); + widget_flags |= erase(element, self.motion.added_width, engine_view); *current_element = element; EventResult { handled: true, @@ -94,8 +154,9 @@ impl PenBehaviour for Eraser { } } (EraserState::Down { .. }, PenEvent::Up { element, .. }) => { - widget_flags |= - erase(element, engine_view) | engine_view.store.record(Instant::now()); + self.motion.reset(); + widget_flags |= erase(element, self.motion.speed, engine_view) + | engine_view.store.record(Instant::now()); self.state = EraserState::Up; EventResult { handled: true, @@ -109,6 +170,7 @@ impl PenBehaviour for Eraser { progress: PenProgress::InProgress, }, (EraserState::Proximity(_), PenEvent::Up { .. }) => { + self.motion.reset(); self.state = EraserState::Up; EventResult { handled: false, @@ -117,6 +179,7 @@ impl PenBehaviour for Eraser { } } (EraserState::Proximity(current_element), PenEvent::Proximity { element, .. }) => { + self.motion.update(element, now); *current_element = element; EventResult { handled: false, @@ -167,7 +230,7 @@ impl DrawableOnDoc for Eraser { engine_view .pens_config .eraser_config - .eraser_bounds(*current_element), + .eraser_bounds(*current_element, self.motion.speed), ), } } @@ -190,7 +253,7 @@ impl DrawableOnDoc for Eraser { let bounds = engine_view .pens_config .eraser_config - .eraser_bounds(*current_element); + .eraser_bounds(*current_element, self.motion.added_width); let fill_rect = bounds.to_kurbo_rect(); let outline_rect = bounds.tightened(outline_width * 0.5).to_kurbo_rect(); @@ -202,7 +265,7 @@ impl DrawableOnDoc for Eraser { let bounds = engine_view .pens_config .eraser_config - .eraser_bounds(*current_element); + .eraser_bounds(*current_element, self.motion.added_width); let fill_rect = bounds.to_kurbo_rect(); let outline_rect = bounds.tightened(outline_width * 0.5).to_kurbo_rect(); @@ -217,20 +280,26 @@ impl DrawableOnDoc for Eraser { } } -fn erase(element: Element, engine_view: &mut EngineViewMut) -> WidgetFlags { +fn erase(element: Element, added_width: f64, engine_view: &mut EngineViewMut) -> WidgetFlags { // the widget_flags.store_modified flag is set in the `.trash_..()` methods let mut widget_flags = WidgetFlags::default(); match &engine_view.pens_config.eraser_config.style { EraserStyle::TrashCollidingStrokes => { widget_flags |= engine_view.store.trash_colliding_strokes( - engine_view.pens_config.eraser_config.eraser_bounds(element), + engine_view + .pens_config + .eraser_config + .eraser_bounds(element, added_width), engine_view.camera.viewport(), ); } EraserStyle::SplitCollidingStrokes => { let (modified_strokes, wf) = engine_view.store.split_colliding_strokes( - engine_view.pens_config.eraser_config.eraser_bounds(element), + engine_view + .pens_config + .eraser_config + .eraser_bounds(element, added_width), engine_view.camera.viewport(), ); widget_flags |= wf; diff --git a/crates/rnote-engine/src/pens/pensconfig/eraserconfig.rs b/crates/rnote-engine/src/pens/pensconfig/eraserconfig.rs index b0aa50ee35..245d160539 100644 --- a/crates/rnote-engine/src/pens/pensconfig/eraserconfig.rs +++ b/crates/rnote-engine/src/pens/pensconfig/eraserconfig.rs @@ -37,6 +37,8 @@ pub struct EraserConfig { pub width: f64, #[serde(rename = "style")] pub style: EraserStyle, + #[serde(rename = "speed_scale")] + pub speed_scaling: bool, } impl Default for EraserConfig { @@ -44,6 +46,7 @@ impl Default for EraserConfig { Self { width: Self::WIDTH_DEFAULT, style: EraserStyle::default(), + speed_scaling: true, } } } @@ -53,7 +56,14 @@ impl EraserConfig { pub const WIDTH_MAX: f64 = 500.0; pub const WIDTH_DEFAULT: f64 = 12.0; - pub(crate) fn eraser_bounds(&self, element: Element) -> Aabb { - Aabb::from_half_extents(element.pos.into(), na::Vector2::repeat(self.width * 0.5)) + pub(crate) fn eraser_bounds(&self, element: Element, added_width: f64) -> Aabb { + if self.speed_scaling { + Aabb::from_half_extents( + element.pos.into(), + na::Vector2::repeat((self.width + added_width) * 0.5), + ) + } else { + Aabb::from_half_extents(element.pos.into(), na::Vector2::repeat(self.width * 0.5)) + } } } diff --git a/crates/rnote-ui/data/ui/penssidebar/eraserpage.ui b/crates/rnote-ui/data/ui/penssidebar/eraserpage.ui index 443449386c..3238f85a05 100644 --- a/crates/rnote-ui/data/ui/penssidebar/eraserpage.ui +++ b/crates/rnote-ui/data/ui/penssidebar/eraserpage.ui @@ -47,5 +47,15 @@ rounded-rect + + + speed_scaling_toggle + Scale eraser size with speed + workspacelistentryicon-speedometer-symbolic + + + - \ No newline at end of file + diff --git a/crates/rnote-ui/src/penssidebar/eraserpage.rs b/crates/rnote-ui/src/penssidebar/eraserpage.rs index 8748d64540..c4968a6530 100644 --- a/crates/rnote-ui/src/penssidebar/eraserpage.rs +++ b/crates/rnote-ui/src/penssidebar/eraserpage.rs @@ -18,6 +18,8 @@ mod imp { pub(crate) eraserstyle_split_colliding_strokes_toggle: TemplateChild, #[template_child] pub(crate) stroke_width_picker: TemplateChild, + #[template_child] + pub(crate) speed_scaling_toggle: TemplateChild, } #[glib::object_subclass] @@ -165,6 +167,19 @@ impl RnEraserPage { } ), ); + + imp.speed_scaling_toggle.connect_toggled(clone!( + #[weak] + appwindow, + move |speed_scaling_toggle| { + let Some(canvas) = appwindow.active_tab_canvas() else { + return; + }; + + canvas.engine_mut().pens_config.eraser_config.speed_scaling = + speed_scaling_toggle.is_active(); + } + )); } pub(crate) fn refresh_ui(&self, active_tab: &RnCanvasWrapper) {