Skip to content

Commit

Permalink
improv: global modeler error type, finer-grained errors, using `thise…
Browse files Browse the repository at this point in the history
…rror` (#5)

* improv: global modeler error type, finer-grained errors, using `thiserror`

* fix: public error module

* chore: fix typos
  • Loading branch information
flxzt authored Jun 7, 2024
1 parent 17ef7c0 commit 6e60b25
Show file tree
Hide file tree
Showing 12 changed files with 109 additions and 65 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
/Cargo.lock
/.vscode
/*.code-workspace
/ink-stroke-modeler/build
/docs/*.pdf
/test
/expand
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ svg = "0.16.0"
tracing = "0.1.40"

[dependencies]
thiserror = "1.0.61"
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/position_modeling.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ <h2>Position modeling</h2>
<p>The position of the pen is modeled as a weight connected by a spring
to an anchor.</p>
<p>The anchor moves along the <em>resampled dewobbled inputs</em>,
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.</p>
<figure>
Expand Down
2 changes: 1 addition & 1 deletion docs/position_modeling.typ
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,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"))
Expand Down
11 changes: 7 additions & 4 deletions justfile
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
77 changes: 43 additions & 34 deletions src/engine.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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<Vec<ModelerResult>, Errors> {
pub fn update(&mut self, input: ModelerInput) -> Result<Vec<ModelerResult>, 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"

Expand All @@ -183,18 +172,26 @@ 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;

// 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());
Expand All @@ -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();
Expand Down Expand Up @@ -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());
Expand All @@ -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();
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand 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());
}

Expand Down
37 changes: 37 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -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,
},
}
18 changes: 7 additions & 11 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,20 @@
// imports
use std::collections::VecDeque;

#[cfg(test)]
extern crate approx;

// modules
// Modules
mod engine;
pub mod error;
mod input;
mod params;
mod position_modeler;
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;
11 changes: 4 additions & 7 deletions src/params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
5 changes: 3 additions & 2 deletions src/position_modeler.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 {
Expand Down
5 changes: 2 additions & 3 deletions src/state_modeler.rs
Original file line number Diff line number Diff line change
@@ -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
///
Expand Down

0 comments on commit 6e60b25

Please sign in to comment.