diff --git a/.gitignore b/.gitignore index 0369a8b..e662361 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,6 @@ /Cargo.lock /.vscode /*.code-workspace -/ink-stroke-modeler/build +/docs/*.pdf /test /expand \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index a9bfba9..0c22efb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,3 +12,4 @@ svg = "0.16.0" tracing = "0.1.40" [dependencies] +thiserror = "1.0.61" diff --git a/README.md b/README.md index 014ac6e..eae4298 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ [![main docs](https://img.shields.io/badge/docs-main-informational)](https://flxzt.github.io/ink-stroke-modeler-rs/ink_stroke_modeler_rs/) [![CI](https://github.com/flxzt/ink-stroke-modeler-rs/actions/workflows/ci.yaml/badge.svg)](https://github.com/flxzt/ink-stroke-modeler-rs/actions/workflows/ci.yaml) -Partial rust rewrite of [https://github.com/google/ink-stroke-modeler](https://github.com/google/ink-stroke-modeler). Beware that not all functionalities are implemented (no kalman-based prediction) and the API is not identical either. +Partial rust rewrite of [https://github.com/google/ink-stroke-modeler](https://github.com/google/ink-stroke-modeler). +Beware that not all functionalities are implemented (no kalman-based prediction) and the API is not identical either. # Usage diff --git a/docs/position_modeling.html b/docs/position_modeling.html index 221782c..ff0ecc2 100644 --- a/docs/position_modeling.html +++ b/docs/position_modeling.html @@ -13,7 +13,7 @@

Position modeling

The position of the pen is modeled as a weight connected by a spring to an anchor.

The anchor moves along the resampled dewobbled inputs, -pulling the weight along with it accross a surface, with some amount of +pulling the weight along with it across a surface, with some amount of friction. Euler integration is used to solve for the position of the pen.

diff --git a/docs/position_modeling.typ b/docs/position_modeling.typ index 66248d9..e911012 100644 --- a/docs/position_modeling.typ +++ b/docs/position_modeling.typ @@ -17,7 +17,7 @@ #definition[ The position of the pen is modeled as a weight connected by a spring to an anchor. -The anchor moves along the _resampled dewobbled inputs_, pulling the weight along with it accross a surface, with some +The anchor moves along the _resampled dewobbled inputs_, pulling the weight along with it across a surface, with some amount of friction. Euler integration is used to solve for the position of the pen. #figure(image("position_model.svg")) diff --git a/justfile b/justfile index 4fd0f18..f646e7d 100644 --- a/justfile +++ b/justfile @@ -1,5 +1,9 @@ -# build docs -doc: +# justfile for ink-stroke-modeler-rs crate + +default: + just --list + +docs-build: pandoc docs/notations.typ -o docs/notations.html --mathml pandoc docs/position_modeling.typ -o docs/position_modeling.html --mathml pandoc docs/resampling.typ -o docs/resampling.html --mathml @@ -9,8 +13,7 @@ doc: cargo doc --open cp docs/position_model.svg target/doc/ink_stroke_modeler_rs/position_model.svg - -remove_html: +docs-remove-html: rm docs/notations.html rm docs/position_modeling.html rm docs/resampling.html diff --git a/src/engine.rs b/src/engine.rs index fae2adc..d06089a 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1,9 +1,11 @@ -use std::vec; - -use super::*; - +use crate::error::{ElementError, ElementOrderError}; +use crate::position_modeler::PositionModeler; +use crate::state_modeler::StateModeler; use crate::utils::interp; use crate::utils::normalize01_64; +use crate::{ModelerError, ModelerInput, ModelerInputEventType, ModelerParams, ModelerResult}; +use std::collections::VecDeque; +use std::vec; /// smooth out the input position from high frequency noise /// uses a moving average of position and interpolating between this @@ -58,23 +60,6 @@ pub struct StrokeModeler { pub(crate) state_modeler: StateModeler, } -/// errors -#[derive(Debug)] -pub enum Errors { - /// when a duplicate element is sent - DuplicateElement, - /// when a new event has a time that's less than the previous one - NegativeTimeDelta, - /// When order of element is not correct. - /// Either when - /// - down event is not the first event or a down event occured after another one - /// - no Down event occurred before a Move event - /// - No event occured before an up event - ElementOrderError, - /// When the time delta is too large between the input provided and the previous one - TooFarApart, -} - impl Default for StrokeModeler { fn default() -> Self { let params = ModelerParams::suggested(); @@ -157,11 +142,15 @@ impl StrokeModeler { /// /// If this does not return an error, results will contain at least one Result, and potentially /// more if the inputs are slower than the minimum output rate - pub fn update(&mut self, input: ModelerInput) -> Result, Errors> { + pub fn update(&mut self, input: ModelerInput) -> Result, ModelerError> { match input.event_type { ModelerInputEventType::Down => { if self.last_event.is_some() { - return Err(Errors::ElementOrderError); + return Err(ModelerError::Element { + src: ElementError::Order { + src: ElementOrderError::UnexpectedDown, + }, + }); } self.wobble_update(&input); // first event is "as is" @@ -183,7 +172,11 @@ impl StrokeModeler { ModelerInputEventType::Move => { // get the latest element if self.last_event.is_none() { - return Err(Errors::ElementOrderError); + return Err(ModelerError::Element { + src: ElementError::Order { + src: ElementOrderError::UnexpectedMove, + }, + }); } let latest_time = self.last_event.as_ref().unwrap().time; let new_time = input.time; @@ -191,10 +184,14 @@ impl StrokeModeler { // validate before doing anything // if the input is incorrect, return an error and leave the engine unmodified if new_time - latest_time < 0.0 { - return Err(Errors::NegativeTimeDelta); + return Err(ModelerError::Element { + src: ElementError::NegativeTimeDelta, + }); } if input == *self.last_event.as_ref().unwrap() { - return Err(Errors::DuplicateElement); + return Err(ModelerError::Element { + src: ElementError::Duplicate, + }); } self.state_modeler.update(input.clone()); @@ -207,7 +204,9 @@ impl StrokeModeler { // this errors if the number of steps is larger than // [ModelParams::sampling_max_outputs_per_call] if n_tsteps as usize > self.params.sampling_max_outputs_per_call { - return Err(Errors::TooFarApart); + return Err(ModelerError::Element { + src: ElementError::TooFarApart, + }); } let p_start = self.last_corrected_event.unwrap(); @@ -238,17 +237,25 @@ impl StrokeModeler { ModelerInputEventType::Up => { // get the latest element if self.last_event.is_none() { - return Err(Errors::ElementOrderError); + return Err(ModelerError::Element { + src: ElementError::Order { + src: ElementOrderError::UnexpectedUp, + }, + }); } let latest_time = self.last_event.as_ref().unwrap().time; let new_time = input.time; // validate before doing any changes to the modeler if new_time - latest_time < 0.0 { - return Err(Errors::NegativeTimeDelta); + return Err(ModelerError::Element { + src: ElementError::NegativeTimeDelta, + }); } if input == *self.last_event.as_ref().unwrap() { - return Err(Errors::DuplicateElement); + return Err(ModelerError::Element { + src: ElementError::Duplicate, + }); } self.state_modeler.update(input.clone()); @@ -261,7 +268,9 @@ impl StrokeModeler { // this errors if the number of steps is larger than // [ModelParams::sampling_max_outputs_per_call] if n_tsteps as usize > self.params.sampling_max_outputs_per_call { - return Err(Errors::TooFarApart); + return Err(ModelerError::Element { + src: ElementError::TooFarApart, + }); } let p_start = self.last_corrected_event.unwrap(); @@ -426,7 +435,7 @@ impl StrokeModeler { if self.wobble_duration_sum < 1e-12 { event.pos } else { - // calulate the average position + // calculate the average position let avg_position = ( self.wobble_weighted_pos_sum.0 / self.wobble_duration_sum, @@ -983,7 +992,7 @@ mod tests { ] )); - // we get more strokes as the model catches up to the anchor postion + // we get more strokes as the model catches up to the anchor position time += delta_time; let update = engine.update(ModelerInput { event_type: ModelerInputEventType::Up, @@ -1096,7 +1105,7 @@ mod tests { ] )); - // the stroke is finised, we get an error if we predict it + // the stroke is finished, we get an error if we predict it assert!(engine.predict().is_err()); } diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..5c8ca92 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,37 @@ +#[derive(Debug, Clone, thiserror::Error)] +#[non_exhaustive] +pub enum ElementError { + #[error("A duplicate element is sent to the modeler")] + Duplicate, + #[error("A sent element has a time earlier than the previous one")] + NegativeTimeDelta, + #[error("Sent element order is incorrect")] + Order { + #[from] + src: ElementOrderError, + }, + #[error("Sent element's time is too far apart from the previous one")] + TooFarApart, +} + +#[derive(Debug, Clone, thiserror::Error)] +#[non_exhaustive] +#[allow(clippy::enum_variant_names)] +pub enum ElementOrderError { + #[error("Down Event is not the first or occurred after a different event")] + UnexpectedDown, + #[error("Move event occurred before a initial down event")] + UnexpectedMove, + #[error("No other event occurred before an up event")] + UnexpectedUp, +} + +#[derive(Debug, Clone, thiserror::Error)] +#[non_exhaustive] +pub enum ModelerError { + #[error("Input element error")] + Element { + #[from] + src: ElementError, + }, +} diff --git a/src/lib.rs b/src/lib.rs index 80a8807..1e3c383 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,6 @@ -// imports -use std::collections::VecDeque; - -#[cfg(test)] -extern crate approx; - -// modules +// Modules mod engine; +pub mod error; mod input; mod params; mod position_modeler; @@ -13,12 +8,13 @@ mod results; mod state_modeler; mod utils; -pub use engine::Errors; +#[cfg(test)] +extern crate approx; + +// Re-Exports pub use engine::StrokeModeler; +pub use error::ModelerError; pub use input::ModelerInput; pub use input::ModelerInputEventType; pub use params::ModelerParams; -use position_modeler::PositionModeler; -use results::ModelerPartial; pub use results::ModelerResult; -use state_modeler::StateModeler; diff --git a/src/params.rs b/src/params.rs index 6b41099..3f992df 100644 --- a/src/params.rs +++ b/src/params.rs @@ -137,13 +137,10 @@ impl ModelerParams { Ok(self) } else { //Collect errors - let error_acc = parameter_tests - .iter() - .zip(errors) - .filter(|x| !*(x.0)) - .fold(String::from("the following errors occured : "), |acc, x| { - acc + x.1 - }); + let error_acc = parameter_tests.iter().zip(errors).filter(|x| !*(x.0)).fold( + String::from("the following errors occurred : "), + |acc, x| acc + x.1, + ); Err(error_acc) } diff --git a/src/position_modeler.rs b/src/position_modeler.rs index 09b8481..94922e6 100644 --- a/src/position_modeler.rs +++ b/src/position_modeler.rs @@ -1,5 +1,6 @@ +use crate::results::ModelerPartial; use crate::utils::{dist, nearest_point_on_segment}; -use crate::{ModelerInput, ModelerParams, ModelerPartial}; +use crate::{ModelerInput, ModelerParams}; /// This struct models the movement of the pen tip based on the laws of motion. /// The pen tip is represented as a mass, connected by a spring to a moving @@ -514,7 +515,7 @@ fn test_update_linear_path() { } #[test] -fn model_end_of_stroke_stationnary() { +fn model_end_of_stroke_stationary() { let mut model = PositionModeler::new( ModelerParams::suggested(), ModelerInput { diff --git a/src/state_modeler.rs b/src/state_modeler.rs index 1e8434c..b732940 100644 --- a/src/state_modeler.rs +++ b/src/state_modeler.rs @@ -1,14 +1,13 @@ use crate::utils::{dist, interp, interp2, nearest_point_on_segment}; use crate::ModelerInput; +use std::collections::VecDeque; // only imported for docstrings #[allow(unused)] -use crate::ModelerPartial; +use crate::results::ModelerPartial; #[allow(unused)] use crate::ModelerResult; -use std::collections::VecDeque; - /// Get the pressure for a position by querying /// information from the raw input strokes ///