Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improv: global modeler error type, finer-grained errors, using thiserror #5

Merged
merged 3 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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"))
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
Loading