ink-stroke-modeler-rs-0.1.0/.cargo_vcs_info.json0000644000000001360000000000100151670ustar { "git": { "sha1": "6155a0ae11848596ad0ae8b8419a8c84f039278f" }, "path_in_vcs": "" }ink-stroke-modeler-rs-0.1.0/.github/workflows/ci.yaml000064400000000000000000000023251046102023000206350ustar 00000000000000name: CI on: workflow_dispatch: push: branches: - main pull_request: branches: - main jobs: check: name: Check runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: submodules: recursive - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - run: cargo check test: name: Test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: submodules: recursive - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - run: cargo test fmt: name: Rustfmt runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: submodules: recursive - uses: dtolnay/rust-toolchain@stable with: components: rustfmt - uses: Swatinem/rust-cache@v2 - run: cargo fmt --all -- --check clippy: name: Clippy runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: submodules: recursive - uses: dtolnay/rust-toolchain@stable with: components: clippy - uses: Swatinem/rust-cache@v2 - run: cargo clippy --all -- -D warnings ink-stroke-modeler-rs-0.1.0/.github/workflows/docs.yaml000064400000000000000000000021401046102023000211650ustar 00000000000000on: workflow_dispatch: push: branches: - main name: Documentation jobs: deploy-docs: concurrency: deploy-docs runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 with: submodules: recursive - name: Toolchain uses: dtolnay/rust-toolchain@stable - name: Cache uses: Swatinem/rust-cache@v2 - name: Clean docs-gh dir run: rm -rf docs-gh shell: bash - name: Clean Rust docs dir run: cargo clean --manifest-path ./Cargo.toml --doc - name: Build Rust docs run: cargo doc --manifest-path ./Cargo.toml --all-features --no-deps - name: Move Rust docs run: mkdir -p docs-gh && mv target/doc/* docs-gh/. shell: bash - name: Configure root page run: echo '' > docs-gh/index.html - name: Deploy docs-gh uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./docs-gh ink-stroke-modeler-rs-0.1.0/.gitignore000064400000000000000000000000731046102023000157470ustar 00000000000000/target /Cargo.lock /.vscode /*.code-workspace /docs/*.pdf ink-stroke-modeler-rs-0.1.0/.gitmodules000064400000000000000000000000001046102023000161220ustar 00000000000000ink-stroke-modeler-rs-0.1.0/Cargo.lock0000644000000073350000000000100131520ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "anyhow" version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "approx" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" dependencies = [ "num-traits", ] [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "ink-stroke-modeler-rs" version = "0.1.0" dependencies = [ "anyhow", "approx", "svg", "thiserror", "tracing", ] [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "proc-macro2" version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] [[package]] name = "svg" version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94afda9cd163c04f6bee8b4bf2501c91548deae308373c436f36aeff3cf3c4a3" [[package]] name = "syn" version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "thiserror" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tracing" version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tracing-core" version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", ] [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" ink-stroke-modeler-rs-0.1.0/Cargo.toml0000644000000026720000000000100131740ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2024" rust-version = "1.85" name = "ink-stroke-modeler-rs" version = "0.1.0" build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = """ Unofficial and partial Rust rewrite of ink-stroke-modeler, a C++ library for beautifully smoothing freehand (touch/stylus/pointer) input""" homepage = "https://github.com/flxzt/ink-stroke-modeler-rs" readme = "README.md" keywords = [ "input", "handwriting", "smoothing", ] categories = [ "gui", "graphics", "algorithms", ] license = "MIT OR Apache-2.0" repository = "https://github.com/flxzt/ink-stroke-modeler-rs" [lib] name = "ink_stroke_modeler_rs" path = "src/lib.rs" [[example]] name = "stroke" path = "examples/stroke.rs" [dependencies.thiserror] version = "2.0.12" [dev-dependencies.anyhow] version = "1.0" [dev-dependencies.approx] version = "0.5.1" [dev-dependencies.svg] version = "0.18.0" [dev-dependencies.tracing] version = "0.1.41" ink-stroke-modeler-rs-0.1.0/Cargo.toml.orig000064400000000000000000000012131046102023000166430ustar 00000000000000[package] categories = ["gui", "graphics", "algorithms"] description = """\ Unofficial and partial Rust rewrite of ink-stroke-modeler, a C++ library for beautifully smoothing freehand (touch/stylus/pointer) input\ """ edition = "2024" homepage = "https://github.com/flxzt/ink-stroke-modeler-rs" keywords = ["input", "handwriting", "smoothing"] license = "MIT OR Apache-2.0" name = "ink-stroke-modeler-rs" readme = "README.md" repository = "https://github.com/flxzt/ink-stroke-modeler-rs" rust-version = "1.85" version = "0.1.0" [dev-dependencies] anyhow = "1.0" approx = "0.5.1" svg = "0.18.0" tracing = "0.1.41" [dependencies] thiserror = "2.0.12" ink-stroke-modeler-rs-0.1.0/LICENSES/Apache-2.0.txt000064400000000000000000000240501046102023000174040ustar 00000000000000Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ink-stroke-modeler-rs-0.1.0/LICENSES/MIT.txt000064400000000000000000000021021046102023000163510ustar 00000000000000MIT License Copyright (c) 2025 The ink-stroke-modeler-rs Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ink-stroke-modeler-rs-0.1.0/README.md000064400000000000000000000023001046102023000152310ustar 00000000000000# ink-stroke-modeler-rs [![docs.rs latest](https://docs.rs/ink-stroke-modeler-rs/badge.svg)](https://docs.rs/ink-stroke-modeler-rs/) [![docs main](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) Unofficial 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 (yet) and the API is not identical either. # Usage Run `cargo doc --open` to view the documentation or check `examples/stroke.rs` for a full example ### License Licensed under either of Apache License, Version 2.0 or MIT license at your option. ### Contributions Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. ink-stroke-modeler-rs-0.1.0/docs/math_part.typ000064400000000000000000000002471046102023000174270ustar 00000000000000#include "notations.typ" #include "wobble.typ" #include "resampling.typ" #include "position_modeling.typ" #include "stroke_end.typ" #include "stylus_state_modeler.typ"ink-stroke-modeler-rs-0.1.0/docs/notations.html000064400000000000000000000267521046102023000176270ustar 00000000000000

Notations

We denote :

An input stream is a sequence of raw inputs i = ( ν , t , x , y ) i = (\nu,t,x,y)

We addition define

With the vector shorthand v = ( v x , v y ) 2 v = \left( v_{x},v_{y} \right) \in {\mathbb{R}}^{2} and a = ( a x , a y ) 2 a = \left( a_{x},a_{y} \right) \in {\mathbb{R}}^{2}

ink-stroke-modeler-rs-0.1.0/docs/notations.typ000064400000000000000000000016541046102023000174710ustar 00000000000000#set page(width: 16cm, margin: 0.5em, height: auto) #let definition(content) = box(fill: luma(92%), width: 100%, inset: 0.5em, stroke: black)[#content] #let pr = $nu$ #let time = $t$ == Notations #definition[ We denote : - Pressure : $pr in [0,1]$ - time : $t >=0$ - point : defined by position $x$ and $y$ (or $p = (x,y) in RR^2$) - Raw inputs are denoted by a tuple $i[k] = (pr[k], t[k], x[k],y[k])$ with $k in NN$. ] #definition[ An _input stream_ is a sequence of raw inputs $i = (pr, t,x,y)$ - with time $t[k]$ $arrow.tr arrow.tr$ strictly increasing - starts with a #raw("Down") event - contains #raw("Move") for $k >=1$ - ends either with a #raw("Move") or a #raw("Up") . If it is a #raw("Up") we say the input stream is _complete_ ] #definition[ We addition define - $v_x, v_y, a_x, a_y$ as the velocity and acceleration. With the vector shorthand $v = (v_x,v_y) in RR^2$ and $a = (a_x, a_y) in RR^2$ ]ink-stroke-modeler-rs-0.1.0/docs/position_model.svg000064400000000000000000000177021046102023000204630ustar 00000000000000 image/svg+xml ink-stroke-modeler-rs-0.1.0/docs/position_modeling.html000064400000000000000000000630371046102023000213300ustar 00000000000000

Position modeling

The raw input is processed as follow

raw input \rightarrow wobble smoother \rightarrow upsampled \rightarrow 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 across a surface, with some amount of friction. Euler integration is used to solve for the position of the pen.

The physical model that is used to model the stroke is the following d 2 s d t 2 = Φ ( t ) s ( t ) k spring k drag d s d t \frac{d^{2}s}{dt^{2}} = \frac{\Phi(t) - s(t)}{k_{\text{spring }}} - k_{\text{drag }}\frac{ds}{dt} where

k spring k_{\text{spring}} is given by position_modeler_spring_mass_constant and k drag k_{\text{drag}} by position_modeler_drag_constant

We will thus have as input the upsampled dewobbled inputs taking the role of discretized Φ ( t ) \Phi(t) and s ( t ) s(t) will be the output

Modeling a stroke.

We define Φ [ k ] = p [ k ] \Phi\lbrack k\rbrack = p\lbrack k\rbrack . An euler scheme integration scheme is used with the initial conditions being v [ 0 ] = 0 v\lbrack 0\rbrack = 0 and p f [ 0 ] = p [ 0 ] p_{f}\lbrack 0\rbrack = p\lbrack 0\rbrack (same initial conditions)

Update rule is simply a f [ j ] = p [ j ] p f [ j 1 ] k spring k drag v f [ j 1 ] v f [ j ] = v f [ j 1 ] + ( t [ j ] t [ j 1 ] ) a f [ j ] p f [ j ] = p f [ j 1 ] + ( t [ j ] t [ j 1 ] ) v f [ j ] \begin{array}{r} a_{f}\lbrack j\rbrack = \frac{p\lbrack j\rbrack - p_{f}\lbrack j - 1\rbrack}{k_{\text{spring }}} - k_{\text{drag }}v_{f}\lbrack j - 1\rbrack \\ v_{f}\lbrack j\rbrack = v_{f}\lbrack j - 1\rbrack + \left( t\lbrack j\rbrack - t\lbrack j - 1\rbrack \right)a_{f}\lbrack j\rbrack \\ p_{f}\lbrack j\rbrack = p_{f}\lbrack j - 1\rbrack + \left( t\lbrack j\rbrack - t\lbrack j - 1\rbrack \right)v_{f}\lbrack j\rbrack \end{array} The position s [ j ] s\lbrack j\rbrack is the main thing to export but we can also export speed and acceleration if needed. We denote q [ j ] = ( p f [ j ] , v f [ j ] , a f [ j ] , t [ j ] ) q\lbrack j\rbrack = \left( p_{f}\lbrack j\rbrack,v_{f}\lbrack j\rbrack,a_{f}\lbrack j\rbrack,t\lbrack j\rbrack \right) and this will be our output with 0 j n 0 \leq j \leq n

ink-stroke-modeler-rs-0.1.0/docs/position_modeling.typ000064400000000000000000000037121046102023000211720ustar 00000000000000#set page(width: 16cm, margin: 0.5em, height: auto) #let definition(content) = box(fill: luma(92%), width: 100%, inset: 0.5em, stroke: black)[#content] #let pr = $nu$ #let time = $t$ == Position modeling #definition[The raw input is processed as follow raw input $->$ wobble smoother $->$ upsampled $->$ position modeling] #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 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")) The physical model that is used to model the stroke is the following $ (dif^2 s) / (dif t^2) = (Phi(t) - s(t)) / k_"spring" - k_"drag" (dif s) / (dif t) $ where - $t$ is time - $s(t)$ is the position of the pen - $Phi(t)$ is the position of the anchor - $k_"spring"$ and $k_"drag"$ are constants that sets how the spring and drag occurs $k_"spring"$ is given by `position_modeler_spring_mass_constant` and $k_"drag"$ by `position_modeler_drag_constant` We will thus have as input the _upsampled dewobbled inputs_ taking the role of discretized $Phi(t)$ and $s(t)$ will be the output ] #definition[ Modeling a stroke. - Input : input stream ${(p[k],t[k]), 0 <= k <=n}$ - Output : smoothed stream ${(p_f [k],v_f [k],a_f [k]), 0 <= k <=n}$ We define $Phi[k] = p[k]$. An euler scheme integration scheme is used with the initial conditions being $v[0] = 0$ and $p_f [0] = p[0]$ (same initial conditions) Update rule is simply $ a_f [j] = (p[j]- p_f [j-1]) / k_"spring" - k_"drag" v_f [j-1]\ v_f [j] = v_f [j-1] + (t[j]-t[j-1])a_f [j]\ p_f [j] = p_f [j-1] + (t[j]-t[j-1])v_f [j] $ The position $s[j]$ is the main thing to export but we can also export speed and acceleration if needed. We denote $ q[j] = (p_f [j],v_f [j],a_f [j],t[j]) $ and this will be our output with $0 <= j <= n$ ]ink-stroke-modeler-rs-0.1.0/docs/resampling.html000064400000000000000000000365531046102023000177520ustar 00000000000000

Resampling

Algorithm : Upsampling
Input : { v [ k ] = ( x [ k ] , y [ k ] , t [ k ] ) , 0 k n } \left\{ v\lbrack k\rbrack = \left( x\lbrack k\rbrack,y\lbrack k\rbrack,t\lbrack k\rbrack \right),0 \leq k \leq n \right\} and a target rate sampling_min_output_rate, Interpolate time and position between each k k by linearly adding interpolated values
This is done by adding linearly interpolated values so that the output stream is { v [ 0 ] , u 0 [ 1 ] , , u 0 [ n 0 1 ] interpolated , v [ 1 ] , u 1 [ 1 ] , , u 1 [ n 1 1 ] interpolated , v [ 2 ] , } \left\{ v\lbrack 0\rbrack,\underset{\text{ interpolated}}{\underbrace{u_{0}\lbrack 1\rbrack,\ldots,u_{0}\left\lbrack n_{0} - 1 \right\rbrack}},v\lbrack 1\rbrack,\underset{\text{ interpolated}}{\underbrace{u_{1}\lbrack 1\rbrack,\ldots,u_{1}\left\lbrack n_{1} - 1 \right\rbrack}},v\lbrack 2\rbrack,\ldots \right\} Each n i n_{i} is the minimum integer such that t [ i ] t [ i 1 ] n i < Δ target n i = t [ i + 1 ] t [ i ] Δ target \begin{aligned} & \frac{t\lbrack i\rbrack - t\lbrack i - 1\rbrack}{n_{i}} < \Delta_{\text{target}} \\ \Leftrightarrow & n_{i} = \left\lceil \frac{t\lbrack i + 1\rbrack - t\lbrack i\rbrack}{\Delta_{\text{target}}} \right\rceil \end{aligned} and the linear interpolation means u j [ k ] = ( 1 k n j ) v [ j ] + k n i v [ j + 1 ] u_{j}\lbrack k\rbrack = \left( 1 - \frac{k}{n_{j}} \right)v\lbrack j\rbrack + \frac{k}{n_{i}}v\lbrack j + 1\rbrack
Output : { ( x [ k ] , y [ k ] , t [ k ] ) , 0 k ; n } \left\{ \left( x\prime\lbrack k\prime\rbrack,y\prime\lbrack k\prime\rbrack,t\prime\lbrack k\prime\rbrack \right),0 \leq k; \leq n\prime \right\} the upsampled position and times. This verifies k , t [ k ] t [ k 1 ] < Δ target = 1 sampling_min_output_rate \forall k\prime,t\prime\lbrack k\prime\rbrack - t\prime\lbrack k\prime - 1\rbrack < \Delta_{\text{target }} = \frac{1}{\text{sampling_min_output_rate}}
Remark : As this is a streaming algorithm, we only calculate this interpolation with respect to the latest stroke position.

ink-stroke-modeler-rs-0.1.0/docs/resampling.typ000064400000000000000000000023121046102023000176040ustar 00000000000000#set page(width: 16cm, margin: 0.5em, height: auto) #let definition(content) = box(fill: luma(92%), width: 100%, inset: 0.5em, stroke: black)[#content] #let pr = $nu$ #let time = $t$ == Resampling #let inv = $v$ *Algorithm* : Upsampling\ *Input* : ${inv[k] = (x[k],y[k],t[k]), 0 <= k <= n}$ and a target rate `sampling_min_output_rate`\, Interpolate time and position between each $k$ by linearly adding interpolated values\ This is done by adding linearly interpolated values so that the output stream is $ { inv[0], underbrace(u_0[1]\, dots\, u_0[n_0-1], "interpolated"), inv[ 1 ], underbrace(u_1[1]\, dots\, u_1[n_1 - 1 ], "interpolated"),inv[2], dots } $ Each $n_i$ is the minimum integer such that $ & (t[i]-t[i-1]) / n_i < Delta_"target"\ <=> & n_i = ceil((t[i+1]-t[i])/Delta_"target") $ and the linear interpolation means $ u_j [k] = (1 - k / n_j) inv[j] + k / n_i inv[j+1] $\ *Output* : ${(x'[k'],y'[k'],t'[k']), 0 <= k; <= n'}$ the upsampled position and times. This verifies $ forall k', t'[k'] - t'[k'-1] < Delta_"target" = 1/#text[`sampling_min_output_rate`] $\ *Remark* : As this is a streaming algorithm, we only calculate this interpolation with respect to the latest stroke position.ink-stroke-modeler-rs-0.1.0/docs/stroke_end.html000064400000000000000000001000451046102023000177320ustar 00000000000000

Stroke end

The position modeling algorithm will lag behind the raw input by some distance. This algorithm iterates the previous dynamical system a few additional time using the raw input position as the anchor to allow a catch up of the stroke (though this prediction is only given by predict, so is not part of the results and becomes obsolete on the next input).

Algorithm :Stroke end
Input:

initialize the vector q o q_{o} with q 0 [ 0 ] = ( p f [ end ] p o [ 0 ] , v f [ end ] v o [ 0 ] , a f [ end ] a 0 [ 0 ] ) q_{0}\lbrack 0\rbrack = \left. \left( \underset{p_{o}\lbrack 0\rbrack}{\underbrace{p_{f}\left\lbrack \text{end} \right\rbrack}},\underset{v_{o}\lbrack 0\rbrack}{\underbrace{v_{f}\left\lbrack \text{end} \right\rbrack}},\underset{a_{0}\lbrack 0\rbrack}{\underbrace{a_{f}\left\lbrack \text{end} \right\rbrack}} \right) \right.
initialize Δ t = Δ target \Delta t = \Delta_{\text{target}}
for 1 k K max 1 \leq k \leq K_{\text{max}}
- calculate the next candidate a c = p [ end ] p o [ end ] k spring k drag v 0 [ end ] v c = v o [ 0 ] + Δ t a c p c = p o [ 0 ] + Δ v c \begin{aligned} a_{c} & = \frac{p\left\lbrack \text{end} \right\rbrack - p_{o}\left\lbrack \text{end} \right\rbrack}{k_{\text{spring}}} - k_{\text{drag }}v_{0}\left\lbrack \text{end} \right\rbrack \\ v_{c} & = v_{o}\lbrack 0\rbrack + \Delta ta_{c} \\ p_{c} & = p_{o}\lbrack 0\rbrack + \Delta v_{c} \end{aligned}


- if p c p [ end ] < d stop \left. \parallel{p_{c} - p\left\lbrack \text{end} \right\rbrack} \right.\parallel < d_{\text{stop}} (further iterations won’t be able to catch up and won’t move closer to the anchor, we stop here), - return q 0 q_{0}
- endif

- else
- q 0 [ e n d + 1 ] = ( p c , v c , q c ) q_{0}\lbrack end\ +1\rbrack = \left( p_{c},v_{c},q_{c} \right) (We append the result to the end of the q 0 q_{0} vector)
- endif

Output : { q o [ k ] = ( s o [ k ] , v o [ k ] , a o [ k ] ) , 0 k n ( K max 1 ) } \left\{ q_{o}\lbrack k\rbrack = \left( s_{o}\lbrack k\rbrack,v_{o}\lbrack k\rbrack,a_{o}\lbrack k\rbrack \right),0 \leq k \leq n\left( \leq K_{\text{max }} - 1 \right) \right\}

ink-stroke-modeler-rs-0.1.0/docs/stroke_end.typ000064400000000000000000000055041046102023000176060ustar 00000000000000#set page(width: 16cm, margin: 0.5em, height: auto) #let definition(content) = box(fill: luma(92%), width: 100%, inset: 0.5em, stroke: black)[#content] #let comment(body) = [#text(size: 0.8em)[(#body)]] #let pr = $nu$ #let time = $t$ == Stroke end The position modeling algorithm will lag behind the raw input by some distance. This algorithm iterates the previous dynamical system a few additional time using the raw input position as the anchor to allow a `catch up` of the stroke (though this prediction is only given by `predict`, so is not part of the `results` and becomes obsolete on the next input). *Algorithm* :Stroke end\ #[*Input*: - Final anchor position $p["end"] = (x["end"],y["end"])$ #comment(text(black)[From the original input stream]) - final tip state $q_f ["end"] = (p_f ["end"]= (x_f ["end"],y_f ["end"]),v_f ["end"], a_f ["end"])$ #comment[#text( fill: black, )[returned from the physical modeling from the last section, $dot_f$ signifies that we are looking at the filtered output]] - $K_"max"$ max number of iterations #comment[#text(fill: black)[`sampling_end_of_stroke_max_iterations`]] - $Delta_"target"$ the target time delay between stroke #comment[#text(fill: black)[1/`sampling_min_output_rate`]] - $d_"stop"$ stopping distance #comment[#text(fill: black)[`sampling_end_of_stroke_stopping_distance`]] - $k_"spring"$ and $k_"drag"$ the modeling coefficients ] #[*initialize* the vector $q_o$ with $q_0 [0] = lr((underbrace(p_f ["end"], p_o [0]), underbrace(v_f ["end"], v_o [0]), underbrace(a_f ["end"], a_0 [0])), size: #1em)$]\ #[*initialize* $Delta t = Delta_"target"$]\ #[*for* $1<= k <= K_"max"$]\ - #[*calculate* the next candidate $ a_c &= (p ["end"] - p_o ["end"])/(k_"spring") - k_"drag" v_0 ["end"]\ v_c &= v_o [0] + Delta t a_c\ p_c &= p_o [0] + Delta v_c $ ]\ - #[*if* $norm(p_c - p["end"])< d_"stop"$ #comment[#text(black)[further iterations won't be able to catch up and won't move closer to the anchor, we stop here]]], - #[*return* $q_0$]\ - #[*endif*]\ - #[*if* $angle.l p_c - p_o ["end"], p["end"] - p_o ["end"] angle.r < norm(p_c - p_o ["end"])$ #comment[#text(black)[we've overshot the anchor, we retry with a smaller step]] ]\ - #[$Delta t <- (Delta t)/2$], - #[*continue* #comment[this candidate will be discarded, try again with a smaller time step instead]], - #[*else*]\ - #[$q_0["end +1"] = (p_c, v_c, q_c)$ #comment[#text(black)[We append the result to the end of the $q_0$ vector]]]\ - #[*endif*]\ - #[*if* $norm(p_c - p["end"]) < d_"stop"$], - #[*return* #comment[We are within tolerance of the anchor, we stop iterating]], - #[*endif*], #[*Output* : ${q_o [k] = (s_o [k], v_o [k], a_o [k]), 0 <= k <= n (<= K_"max" - 1)}$]ink-stroke-modeler-rs-0.1.0/docs/stylus_state_modeler.html000064400000000000000000000333271046102023000220570ustar 00000000000000

Stylus state modeler

Up till now we have only used the raw input stream to create a new smoothed stream of positions, leaving behind the pressure attribute. This is what’s done here, to model the state of the stylus for these new position based on the pressure data of the raw input strokes.
Algorithm Stylus state modeler
Input :


initialize d = d = \infty , index = None \text{index} = \text{None} , interp = None \text{interp } = \text{ None}
for i = n n search i = n - n_{\text{search}} to n 1 n - 1 do
- Find q i q_{i} the position that’s closest to q q on the segment [ p [ i ] , p [ i + 1 ] ] \left\lbrack p\lbrack i\rbrack,p\lbrack i + 1\rbrack \right\rbrack and denote r [ 0 , 1 ] r \in \lbrack 0,1\rbrack the value such that q i = ( 1 r ) p [ i ] + r p [ i + 1 ] q_{i} = (1 - r)p\lbrack i\rbrack + rp\lbrack i + 1\rbrack
- if q q i < d \left. \parallel{q - q_{i}} \right.\parallel < d
- d q q i < d index = i interp = r \begin{array}{r} d \leftarrow \left. \parallel{q - q_{i}} \right.\parallel < d \\ \text{index } = i \\ \text{interp } = r \end{array}
- endif
endfor
calculate ν = ( 1 r ) ν [ index ] + r ν [ index + 1 ] \nu = (1 - r)\nu\left\lbrack \text{index} \right\rbrack + r\nu\left\lbrack \text{index } + 1 \right\rbrack
Output : interpolated pressure ν \nu

ink-stroke-modeler-rs-0.1.0/docs/stylus_state_modeler.typ000064400000000000000000000024231046102023000217200ustar 00000000000000#set page(width: 16cm, margin: 0.5em, height: auto) #let definition(content) = box(fill: luma(92%), width: 100%, inset: 0.5em, stroke: black)[#content] #let comment(body) = [#text(size: 0.8em)[(#body)]] #let pr = $nu$ #let time = $t$ == Stylus state modeler Up till now we have only used the raw input stream to create a new smoothed stream of positions, leaving behind the pressure attribute. This is what's done here, to model the state of the stylus for these new position based on the pressure data of the raw input strokes.\ *Algorithm* #[Stylus state modeler]\ #[*Input* : - input stream with pressure information ${(p[k]=(x[k],y[k]),pr[k]),0 <=k <= n}$ - query position $q = (x,y)$ - search window $n_"search"$ #comment[From `stylus_state_modeler_max_input_samples`], ]\ #[*initialize* $d = oo$, $"index"="None"$, $"interp" = "None"$]\ #[*for* $i=n-n_"search"$ to $n-1$ *do*]\ - #[*Find* $q_i$ the position that's closest to $q$ on the segment $[p[i],p[i+1]]$ and denote $r in [0,1]$ the value such that $ q_i = (1-r) p[i] + r p[i+1]$]\ - #[*if* $norm(q - q_i) < d$]\ - #[$d <- norm(q - q_i) < d\ "index" =i\ "interp" = r $]\ - #[*endif*]\ #[*endfor*]\ #[*calculate* $ pr = (1-r) pr["index"] + r pr["index" +1] $]\ #[*Output* : interpolated pressure $pr$] ink-stroke-modeler-rs-0.1.0/docs/wobble.html000064400000000000000000001103651046102023000170550ustar 00000000000000

Wobble smoothing

To reduce high frequency noise.

Algorithm: Wobble smoothing
Input : { ( x [ k ] , y [ k ] , t [ k ] ) 2 × + , 0 k n } \left\{ \left( x\lbrack k\rbrack,y\lbrack k\rbrack,t\lbrack k\rbrack \right) \in {\mathbb{R}}^{2} \times {\mathbb{R}}_{+},0 \leq k \leq n \right\} , Δ T > 0 \Delta T > 0 (from wobble_smoother_timeout), v min v_{\text{min}} (from wobble_smoother_speed_floor) and v max v_{\text{max}} ( from wobble_smoother_speed_ceiling)
Compute a weighted moving average of the positions p ¯ [ j ] = ( x ¯ [ j ] , y ¯ [ j ] ) \underset{¯}{p}\lbrack j\rbrack = \left( \underset{¯}{x}\lbrack j\rbrack,\underset{¯}{y}\lbrack j\rbrack \right) j 0 , n , p ¯ [ j ] = { k = 1 n p [ k ] ( t [ k ] t [ k 1 ] ) 𝟙 [ t [ j ] Δ T , t [ j ] ] ( t [ k ] ) k = 1 n 𝟙 [ t [ j ] Δ T , t [ j ] ] ( t [ k ] ) if the numerator 0 p [ j ] otherwise \forall j \in ⟦0,n⟧,\underset{¯}{p}\lbrack j\rbrack = \begin{cases} \frac{\sum_{k = 1}^{n}p\lbrack k\rbrack\left( t\lbrack k\rbrack - t\lbrack k - 1\rbrack \right)\mathbb{1}_{\left. \left\lbrack t\lbrack j\rbrack - \Delta T,t\lbrack j\rbrack \right\rbrack \right.}\left( t\lbrack k\rbrack \right)}{\sum_{k = 1}^{n}\mathbb{1}_{\left. \left\lbrack t\lbrack j\rbrack - \Delta T,t\lbrack j\rbrack \right\rbrack \right.}\left( t\lbrack k\rbrack \right)} & \text{if the numerator } \neq 0 \\ p\lbrack j\rbrack & \text{otherwise} \end{cases} Calculate a moving average velocity v ¯ [ j ] \underset{¯}{v}\lbrack j\rbrack j 0 , n , v ¯ [ j ] = { 0 j = 0 k = 1 n p [ k ] p [ k 1 ] 𝟙 [ t [ j ] Δ T , t [ j ] ] ( t [ k ] ) k = 1 n ( t [ k ] t [ k 1 ] ) 𝟙 [ t [ j ] Δ T , t [ j ] ] ( t [ k ] ) otherwise \forall j \in ⟦0,n⟧,\underset{¯}{v}\lbrack j\rbrack = \begin{cases} 0 & j = 0 \\ \frac{\sum_{k = 1}^{n}\left. \parallel{p\lbrack k\rbrack - p\lbrack k - 1\rbrack} \right.\parallel\mathbb{1}_{\left. \left\lbrack t\lbrack j\rbrack - \Delta T,t\lbrack j\rbrack \right\rbrack \right.}\left( t\lbrack k\rbrack \right)}{\sum_{k = 1}^{n}\left( t\lbrack k\rbrack - t\lbrack k - 1\rbrack \right)\mathbb{1}_{\left. \left\lbrack t\lbrack j\rbrack - \Delta T,t\lbrack j\rbrack \right\rbrack \right.}\left( t\lbrack k\rbrack \right)}\quad & \text{otherwise} \end{cases} Interpolate between the average position and the raw ones based on the average speed j 0 , n , p [ j ] = min ( v ¯ [ j ] v min v max v min 𝟙 [ v min , [ ( v ¯ [ j ] ) , 1 ) p ¯ [ j ] + ( 1 min ( v ¯ [ j ] v min v max v min 𝟙 [ v min , [ ( v ¯ [ j ] ) ) ) p [ j ] \begin{aligned} \forall j \in ⟦0,n⟧,p\prime\lbrack j\rbrack = & \min(\frac{\underset{¯}{v}\lbrack j\rbrack - v_{\text{min}}}{v_{\text{max }} - v_{\text{min}}}\mathbb{1}_{\lbrack v_{\text{min}},\infty\lbrack}\left( \underset{¯}{v}\lbrack j\rbrack \right),1)\underset{¯}{p}\lbrack j\rbrack \\ + & \left( 1 - \min(\frac{\underset{¯}{v}\lbrack j\rbrack - v_{\text{min}}}{v_{\text{max }} - v_{\text{min}}}\mathbb{1}_{\lbrack v_{\text{min}},\infty\lbrack}\left( \underset{¯}{v}\lbrack j\rbrack \right)) \right)p\lbrack j\rbrack \end{aligned} where p [ j ] = ( x [ j ] , y [ j ] ) p\prime\lbrack j\rbrack = \left( x\prime\lbrack j\rbrack,y\prime\lbrack j\rbrack \right)
Output: { ( x [ k ] , y [ k ] ) 2 , 0 k n } \left\{ \left( x\prime\lbrack k\rbrack,y\prime\lbrack k\rbrack \right) \in {\mathbb{R}}^{2},0 \leq k \leq n \right\} the filtered positions.
Hence for low local speeds, the smoothing is maximum (we take exactly the average position over the time Δ T \Delta T ) and for high speed there is no smoothing. We also note that the first position is thus never filtered.

ink-stroke-modeler-rs-0.1.0/docs/wobble.typ000064400000000000000000000037241046102023000167250ustar 00000000000000#set page(width: 16cm, margin: 0.5em, height: auto) #let definition(content) = box(fill: luma(92%), width: 100%, inset: 0.5em, stroke: black)[#content] #let pr = $nu$ #let time = $t$ = Wobble smoothing To reduce high frequency noise. *Algorithm*: Wobble smoothing\ *Input* : ${(x[k],y[k],t[k]) in RR^2 times RR_+, 0 <= k<=n}$, $Delta T>0$ (from `wobble_smoother_timeout`), $v_"min"$ (from `wobble_smoother_speed_floor`) and $v_"max"$ ( from `wobble_smoother_speed_ceiling`)\ Compute a weighted moving average of the positions $overline(p)[j] = (overline(x)[j],overline(y)[j])$ $ forall j in [|\0,n|], overline(p)[ j ] = cases( (display(sum_(k=1)^n p[k] (t[k] - t[k-1]) bb(1)_(lr([t[j] - Delta T, t[j]], size: #170%)) (t[k]))) /display(sum_(k=1)^n bb(1)_(lr([t[j] - Delta T, t[j]], size: #170%)) (t[k])) &"if the numerator" !=0, p[j]&"otherwise", ) $ Calculate a moving average velocity $overline(v)[j]$ $ forall j in [|0, n|], overline(v)[ j ] = cases(0 & j = 0, ( display(sum_(k=1)^n norm(p[k] - p[k-1]) bb(1)_( lr([t[j] - Delta T, t[j]], size: #170%)) (t[k])) )/( display(sum_(k=1)^n (t[k] - t[k-1]) bb(1)_( lr([t[j] - Delta T, t[j]], size: #170%)) (t[k])) )quad &"otherwise") $ Interpolate between the average position and the raw ones based on the average speed $ forall j in [|0,n|], p'[j] = & min((overline(v)[j] - v_"min")/(v_"max" - v_"min") bb(1)_(\[v_"min",oo\[) (overline(v)[j]), 1) overline(p)[ j ] \ + &(1 - min((overline(v)[j] - v_"min")/(v_"max" - v_"min") bb(1)_(\[v_"min",oo\[) (overline(v)[j]))) p[j] $ where $p'[j] = (x'[j],y'[j])$\ *Output*: ${(x'[k],y'[k]) in RR^2, 0<= k <=n}$ the filtered positions.\ Hence for low local speeds, the smoothing is maximum (we take exactly the average position over the time $Delta T$) and for high speed there is no smoothing. We also note that the first position is thus never filtered. ink-stroke-modeler-rs-0.1.0/examples/stroke/input.svg000064400000000000000000000025241046102023000207670ustar 00000000000000 ink-stroke-modeler-rs-0.1.0/examples/stroke/modeled.svg000064400000000000000000000243321046102023000212420ustar 00000000000000 ink-stroke-modeler-rs-0.1.0/examples/stroke.rs000064400000000000000000000130421046102023000174520ustar 00000000000000#![allow(unused)] use ink_stroke_modeler_rs::{ ModelerInput, ModelerInputEventType, ModelerParams, ModelerResult, StrokeModeler, }; use svg::Node; fn main() -> anyhow::Result<()> { let bounds = Aabb { mins: (0.0, 0.0), maxs: (300.0, 300.0), }; let input_stroke = vec![ ModelerInput { event_type: ModelerInputEventType::Down, pos: (90.0, 30.0), time: 0.0, pressure: 0.25, }, ModelerInput { event_type: ModelerInputEventType::Move, pos: (30.0, 45.0), time: 0.02, pressure: 0.3, }, ModelerInput { event_type: ModelerInputEventType::Move, pos: (60.0, 240.0), time: 0.04, pressure: 0.7, }, ModelerInput { event_type: ModelerInputEventType::Move, pos: (105.0, 270.0), time: 0.06, pressure: 1.0, }, ModelerInput { event_type: ModelerInputEventType::Move, pos: (120.0, 150.0), time: 0.10, pressure: 0.6, }, ModelerInput { event_type: ModelerInputEventType::Move, pos: (180.0, 30.0), time: 0.12, pressure: 0.3, }, ModelerInput { event_type: ModelerInputEventType::Move, pos: (240.0, 120.0), time: 0.16, pressure: 0.3, }, ModelerInput { event_type: ModelerInputEventType::Move, pos: (210.0, 150.0), time: 0.18, pressure: 0.9, }, ModelerInput { event_type: ModelerInputEventType::Move, pos: (150.0, 210.0), time: 0.20, pressure: 0.8, }, ModelerInput { event_type: ModelerInputEventType::Move, pos: (210.0, 240.0), time: 0.22, pressure: 0.8, }, ModelerInput { event_type: ModelerInputEventType::Move, pos: (255.0, 240.0), time: 0.24, pressure: 0.7, }, ModelerInput { event_type: ModelerInputEventType::Up, pos: (270.0, 270.0), time: 0.26, pressure: 0.5, }, ]; let input_elements = input_stroke .iter() .map(Element::from_modeler_input) .collect::>(); create_svg( &input_elements, bounds, std::path::PathBuf::from("./examples/stroke/input.svg"), )?; let mut modeler = StrokeModeler::default(); let result_stroke = input_stroke .into_iter() .filter_map(|i| { modeler .update(i) .map_err(|e| eprintln!("modeler updated, Err: {e:?}")) .ok() }) .flatten() .collect::>(); let result_elements = result_stroke .iter() .map(Element::from_modeler_result) .collect::>(); create_svg( &result_elements, bounds, std::path::PathBuf::from("./examples/stroke/modeled.svg"), )?; Ok(()) } #[derive(Debug, Clone)] struct Element { pos: (f64, f64), velocity: Option<(f64, f64)>, time: f64, pressure: f64, } impl Element { fn from_modeler_input(i: &ModelerInput) -> Self { Self { pos: i.pos, velocity: None, time: i.time, pressure: i.pressure, } } fn from_modeler_result(r: &ModelerResult) -> Self { Self { pos: r.pos, velocity: Some(r.velocity), time: r.time, pressure: r.pressure, } } } #[derive(Debug, Clone, Copy)] struct Aabb { mins: (f32, f32), maxs: (f32, f32), } impl Aabb { fn new_invalid() -> Self { Self { mins: (f32::MAX, f32::MAX), maxs: (f32::MIN, f32::MIN), } } fn width(&self) -> f32 { self.maxs.0 - self.mins.0 } fn height(&self) -> f32 { self.maxs.1 - self.mins.1 } fn extend(&mut self, coord: (f32, f32)) { self.mins.0 = self.mins.0.min(coord.0); self.mins.1 = self.mins.1.min(coord.1); self.maxs.0 = self.maxs.0.max(coord.0); self.maxs.1 = self.maxs.1.max(coord.1); } } fn create_svg( elements: &[Element], bounds: Aabb, file: impl AsRef, ) -> anyhow::Result<()> { let mut doc = svg::Document::new() .set("x", bounds.mins.0) .set("y", bounds.mins.1) .set("width", bounds.width()) .set("height", bounds.height()); doc.append( svg::node::element::Rectangle::new() .set("x", bounds.mins.0) .set("y", bounds.mins.1) .set("width", bounds.width()) .set("height", bounds.height()) .set("fill", "white"), ); for (start, end) in elements.iter().zip(elements.iter().skip(1)) { let brightness = 1.0 / (end.pressure + start.pressure) / 2.0; doc.append( svg::node::element::Line::new() .set("x1", start.pos.0) .set("y1", start.pos.1) .set("x2", end.pos.0) .set("y2", end.pos.1) .set( "stroke", format!("hsl(200, 100%, {}%", (brightness * 100.0).round()), ) .set("stroke-width", 2.0) .set("stroke-linecap", "round"), ); } Ok(svg::save(file, &doc)?) } ink-stroke-modeler-rs-0.1.0/justfile000064400000000000000000000014311046102023000155260ustar 00000000000000# 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 pandoc docs/stroke_end.typ -o docs/stroke_end.html --mathml pandoc docs/stylus_state_modeler.typ -o docs/stylus_state_modeler.html --mathml pandoc docs/wobble.typ -o docs/wobble.html --mathml cargo doc --open cp docs/position_model.svg target/doc/ink_stroke_modeler_rs/position_model.svg docs-remove-html: rm docs/notations.html rm docs/position_modeling.html rm docs/resampling.html rm docs/stroke_end.html rm docs/stylus_state_modeler.html rm docs/wobble.html ink-stroke-modeler-rs-0.1.0/src/engine.rs000064400000000000000000002776661046102023000164110ustar 00000000000000use 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 /// position and the raw position based on the speed. /// high speeds movements won't be smoothed but low speed will. /// /// wrapper time to include all needed information /// in the Deque #[derive(Debug)] pub(crate) struct WobbleSample { /// raw position pub position: (f64, f64), /// position weighted by the duration pub weighted_position: (f64, f64), /// distance to the previous element pub distance: f64, /// time distance to the previous element pub duration: f64, /// time of the event pub time: f64, } /// This class models a stroke from a raw input stream. The modeling is performed in /// several stages /// - Wobble smoothing : dampens high-frequency noise from quantization error /// - Position modeling : models the pen tip as a mass, connected by a spring, to a moving /// anchor /// - Stylus state modeling : constructs stylus states for modeled positions by interpolating /// over the raw input /// /// Additional, this class provides prediction of the modeled stroke /// /// StrokeModeler is unit-agnostic pub struct StrokeModeler { // all configuration parameters pub(crate) params: ModelerParams, /// wobble smoother structures /// deque to hold events that are recent /// to calculate a moving average pub(crate) wobble_deque: VecDeque, /// running weighted sum pub(crate) wobble_weighted_pos_sum: (f64, f64), /// running duration sum pub(crate) wobble_duration_sum: f64, /// running distance sum pub(crate) wobble_distance_sum: f64, // physical model for the stroke // only created on the first stroke pub(crate) position_modeler: Option, pub(crate) last_event: Option, pub(crate) last_corrected_event: Option<(f64, f64)>, pub(crate) state_modeler: StateModeler, } impl Default for StrokeModeler { fn default() -> Self { let params = ModelerParams::suggested(); Self { params, wobble_deque: VecDeque::with_capacity( (2.0 * params.sampling_min_output_rate * params.wobble_smoother_timeout) as usize, ), wobble_weighted_pos_sum: (0.0, 0.0), wobble_duration_sum: 0.0, wobble_distance_sum: 0.0, last_event: None, last_corrected_event: None, position_modeler: None, state_modeler: StateModeler::new(params.stylus_state_modeler_max_input_samples), } } } #[doc = include_str!("../docs/notations.html")] #[doc = include_str!("../docs/resampling.html")] #[doc = include_str!("../docs/position_modeling.html")] #[doc = include_str!("../docs/stylus_state_modeler.html")] #[doc = include_str!("../docs/stroke_end.html")] impl StrokeModeler { pub fn new(params: ModelerParams) -> Result { params.validate()?; Ok(Self { params, last_event: None, last_corrected_event: None, wobble_deque: VecDeque::with_capacity( (2.0 * params.sampling_min_output_rate * params.wobble_smoother_timeout) as usize, ), wobble_duration_sum: 0.0, wobble_weighted_pos_sum: (0.0, 0.0), wobble_distance_sum: 0.0, position_modeler: None, state_modeler: StateModeler::new(params.stylus_state_modeler_max_input_samples), }) } /// Clears any in-progress stroke, keeping the same model parameters pub fn reset(&mut self) { self.wobble_deque.clear(); self.wobble_weighted_pos_sum = (0.0, 0.0); self.wobble_duration_sum = 0.0; self.position_modeler = None; self.last_event = None; self.last_corrected_event = None; self.state_modeler .reset(self.params.stylus_state_modeler_max_input_samples); } /// Clears any in-progress stroke, and re initialize the model with /// the given parameters pub fn reset_w_params(&mut self, params: ModelerParams) -> Result<(), String> { params.validate()?; self.params = params; self.wobble_deque = VecDeque::with_capacity( (2.0 * params.sampling_min_output_rate * params.wobble_smoother_timeout) as usize, ); self.wobble_weighted_pos_sum = (0.0, 0.0); self.wobble_duration_sum = 0.0; self.wobble_distance_sum = 0.0; self.position_modeler = None; self.last_event = None; self.last_corrected_event = None; self.state_modeler .reset(params.stylus_state_modeler_max_input_samples); Ok(()) } /// Updates the model with a raw input, and appends newly generated Results to the results vector. /// Any previously generated Result values remain valid. /// (This does not require that any previous results returned remain in the results vector, as it is /// appended to without examining the existing contents) /// /// 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, ModelerError> { match input.event_type { ModelerInputEventType::Down => { if self.last_event.is_some() { return Err(ModelerError::Element { src: ElementError::Order { src: ElementOrderError::UnexpectedDown, }, }); } self.wobble_update(&input); // first event is "as is" self.position_modeler = Some(PositionModeler::new(self.params, input.clone())); self.last_event = Some(input.clone()); self.last_corrected_event = Some(input.pos); self.state_modeler .reset(self.params.stylus_state_modeler_max_input_samples); self.state_modeler.update(input.clone()); Ok(vec![ModelerResult { pos: input.pos, velocity: (0.0, 0.0), acceleration: (0.0, 0.0), time: input.time, pressure: input.pressure, }]) } ModelerInputEventType::Move => { // get the latest element if self.last_event.is_none() { 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(ModelerError::Element { src: ElementError::NegativeTimeDelta, }); } if input == *self.last_event.as_ref().unwrap() { return Err(ModelerError::Element { src: ElementError::Duplicate, }); } self.state_modeler.update(input.clone()); // calculate the number of element to predict let n_steps = ((new_time - latest_time) * self.params.sampling_min_output_rate).ceil() as i32; // this errors if the number of steps is larger than // [ModelParams::sampling_max_outputs_per_call] if n_steps as usize > self.params.sampling_max_outputs_per_call { return Err(ModelerError::Element { src: ElementError::TooFarApart, }); } let p_start = self.last_corrected_event.unwrap(); let p_end = self.wobble_update(&input); // seems like speeds are way higher than normal speed encountered so no smoothing occurs here let vec_out: Vec = self .position_modeler .as_mut() .unwrap() .update_along_linear_path(p_start, latest_time, p_end, new_time, n_steps) .into_iter() .map(|i| ModelerResult { pressure: self.state_modeler.query(i.pos), pos: i.pos, velocity: i.velocity, acceleration: i.acceleration, time: i.time, }) .collect(); // push the latest element (should we push everything we also interpolated as well ?) self.last_event = Some(input.clone()); self.last_corrected_event = Some(p_end); Ok(vec_out) } ModelerInputEventType::Up => { // get the latest element if self.last_event.is_none() { 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(ModelerError::Element { src: ElementError::NegativeTimeDelta, }); } if input == *self.last_event.as_ref().unwrap() { return Err(ModelerError::Element { src: ElementError::Duplicate, }); } self.state_modeler.update(input.clone()); // calculate the number of element to predict let n_tsteps = ((new_time - latest_time) * self.params.sampling_min_output_rate).ceil() as i32; // 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(ModelerError::Element { src: ElementError::TooFarApart, }); } let p_start = self.last_corrected_event.unwrap(); // the p_end is purposefully different from the original implementation // to match the Move part // the original takes the raw input here which means a different // behavior between the predict on a Move and a Up let p_end = self.wobble_update(&input); let mut vec_out = Vec::::with_capacity( (n_tsteps as usize) + self.params.sampling_end_of_stroke_max_iterations, ); vec_out.extend( self.position_modeler .as_mut() .unwrap() .update_along_linear_path(p_start, latest_time, p_end, new_time, n_tsteps) .into_iter() .map(|i| ModelerResult { pressure: self.state_modeler.query(i.pos), pos: i.pos, velocity: i.velocity, time: i.time, acceleration: i.acceleration, }), ); // model the end of stroke vec_out.extend( self.position_modeler .as_mut() .unwrap() .model_end_of_stroke( input.pos, 1. / self.params.sampling_min_output_rate, self.params.sampling_end_of_stroke_max_iterations, self.params.sampling_end_of_stroke_stopping_distance, ) .into_iter() .map(|i| ModelerResult { pressure: self.state_modeler.query(i.pos), pos: i.pos, velocity: i.velocity, acceleration: i.acceleration, time: i.time, }), ); if vec_out.is_empty() { let state_pos = self.position_modeler.as_ref().unwrap().state.clone(); vec_out.push(ModelerResult { pos: state_pos.pos, velocity: state_pos.velocity, acceleration: state_pos.acceleration, // this is so that the extra stroke added has a time that's larger than the previous one // when the Up happens at the same time as the Move // In the original implementation, this was always true because // the ModelEndOfStroke function did not restore the state of the modeler // so that even if a single candidate was tried and iterations stopped there // the status of the modeler changed, including the time by at least // `1. / self.params.sampling_min_output_rate` time: state_pos.time + 1. / self.params.sampling_min_output_rate, pressure: self.state_modeler.query(state_pos.pos), }); } // remove the last event self.last_event = None; Ok(vec_out) } } } /// Models the given input prediction without changing the internal model state /// /// Returns an error if the model has not yet been initialized, /// if there is no stroke in progress pub fn predict(&mut self) -> Result, String> { // for now return the latest element if it exists from the input if self.last_event.is_none() { // no data to predict from Err(String::from("empty input events")) } else { // construct the prediction (model_end_of_stroke does not modify the position modeler) let predict = self .position_modeler .as_mut() .unwrap() .model_end_of_stroke( self.last_event.as_ref().unwrap().pos, 1. / self.params.sampling_min_output_rate, self.params.sampling_end_of_stroke_max_iterations, self.params.sampling_end_of_stroke_stopping_distance, ) .into_iter() .map(|i| ModelerResult { pos: i.pos, velocity: i.velocity, acceleration: i.acceleration, time: i.time, pressure: self.state_modeler.query(i.pos), }) .collect(); Ok(predict) } } ///implements the wobble logic ///smoothes out the input position from high frequency noise ///uses a moving average of position and interpolating between this ///position and the raw position based on the speed. ///high speeds movements won't be smoothed but low speed will. #[doc = include_str!("../docs/wobble.html")] fn wobble_update(&mut self, event: &ModelerInput) -> (f64, f64) { match self.wobble_deque.len() { 0 => { self.wobble_deque.push_back(WobbleSample { position: event.pos, weighted_position: (0.0, 0.0), distance: 0.0, duration: 0.0, time: event.time, }); event.pos } _ => { let last_el = self.wobble_deque.back().unwrap(); let duration = event.time - last_el.time; let weighted_pos = (event.pos.0 * duration, event.pos.1 * duration); let distance = ((event.pos.0 - last_el.position.0).powi(2) + (event.pos.1 - last_el.position.1).powi(2)) .sqrt(); self.wobble_deque.push_back(WobbleSample { position: event.pos, weighted_position: weighted_pos, distance, duration, time: event.time, }); let last_pos = self.wobble_weighted_pos_sum; self.wobble_weighted_pos_sum = (last_pos.0 + weighted_pos.0, last_pos.1 + weighted_pos.1); self.wobble_distance_sum += distance; self.wobble_duration_sum += duration; while self.wobble_deque.front().unwrap().time < event.time - self.params.wobble_smoother_timeout { let front_el = self.wobble_deque.pop_front().unwrap(); let last_pos = self.wobble_weighted_pos_sum; self.wobble_weighted_pos_sum = ( last_pos.0 - front_el.weighted_position.0, last_pos.1 - front_el.weighted_position.1, ); self.wobble_distance_sum -= front_el.distance; self.wobble_duration_sum -= front_el.duration; } if self.wobble_duration_sum < 1e-12 { event.pos } else { // calculate the average position let avg_position = ( self.wobble_weighted_pos_sum.0 / self.wobble_duration_sum, self.wobble_weighted_pos_sum.1 / self.wobble_duration_sum, ); let avg_speed = self.wobble_distance_sum / self.wobble_duration_sum; let norm_value = normalize01_64( self.params.wobble_smoother_speed_floor, self.params.wobble_smoother_speed_ceiling, avg_speed, ); ( interp(avg_position.0, event.pos.0, norm_value), interp(avg_position.1, event.pos.1, norm_value), ) } } } } } #[cfg(test)] mod tests { use super::super::*; use crate::results::compare_results; /// compare (f64,f64) floats up to `0.0001` precision /// utility for testing only #[cfg(test)] fn util_compare_floats(a1: (f64, f64), a2: (f64, f64)) -> bool { return approx::abs_diff_eq!(a1.0, a2.0, epsilon = 0.0001) && approx::abs_diff_eq!(a1.1, a2.1, epsilon = 0.0001); } // wobble tests #[test] fn test_wobble_smoother_line() { // need to create a StrokeModeler let mut new_modeler = StrokeModeler::default(); new_modeler.wobble_update(&ModelerInput { event_type: ModelerInputEventType::Down, pos: (3., 4.), time: 1.0, pressure: 0.0, }); assert!(util_compare_floats( new_modeler.wobble_update(&ModelerInput { event_type: ModelerInputEventType::Move, pos: (3.016, 4.), time: 1.016, pressure: 0.0, }), (3.016, 4.) )); assert!(util_compare_floats( new_modeler.wobble_update(&ModelerInput { event_type: ModelerInputEventType::Move, pos: (3.032, 4.), time: 1.032, pressure: 0.0, }), (3.024, 4.) )); assert!(util_compare_floats( new_modeler.wobble_update(&ModelerInput { event_type: ModelerInputEventType::Move, pos: (3.048, 4.), time: 1.048, pressure: 0.0, }), (3.032, 4.) )); assert!(util_compare_floats( new_modeler.wobble_update(&ModelerInput { event_type: ModelerInputEventType::Move, pos: (3.064, 4.), time: 1.064, pressure: 0.0, }), (3.048, 4.) )); } #[test] fn test_wobble_zigzag_slow() { // need to create a StrokeModeler let mut new_modeler = StrokeModeler::default(); new_modeler.wobble_update(&ModelerInput { event_type: ModelerInputEventType::Down, pos: (1., 2.), time: 5.0, pressure: 0.0, }); assert!(util_compare_floats( new_modeler.wobble_update(&ModelerInput { event_type: ModelerInputEventType::Move, pos: (1.016, 2.), time: 5.016, pressure: 0.0, }), (1.016, 2.0) )); assert!(util_compare_floats( new_modeler.wobble_update(&ModelerInput { event_type: ModelerInputEventType::Move, pos: (1.016, 2.016), time: 5.032, pressure: 0.0, }), (1.016, 2.008) )); assert!(util_compare_floats( new_modeler.wobble_update(&ModelerInput { event_type: ModelerInputEventType::Move, pos: (1.032, 2.016), time: 5.048, pressure: 0.0, }), (1.02133, 2.01067) )); assert!(util_compare_floats( new_modeler.wobble_update(&ModelerInput { event_type: ModelerInputEventType::Move, pos: (1.032, 2.032), time: 5.064, pressure: 0.0, }), (1.0266667, 2.0213333) )); assert!(util_compare_floats( new_modeler.wobble_update(&ModelerInput { event_type: ModelerInputEventType::Move, pos: (1.048, 2.032), time: 5.080, pressure: 0.0, }), (1.0373333, 2.0266667) )); assert!(util_compare_floats( new_modeler.wobble_update(&ModelerInput { event_type: ModelerInputEventType::Move, pos: (1.048, 2.048), time: 5.096, pressure: 0.0, }), (1.0426667, 2.0373333) )); } #[test] fn fast_zigzag() { let mut new_modeler = StrokeModeler::default(); assert!(util_compare_floats( new_modeler.wobble_update(&ModelerInput { event_type: ModelerInputEventType::Move, pos: (7., 3.024), time: 8.016, pressure: 0.0, }), (7.0, 3.024) )); assert!(util_compare_floats( new_modeler.wobble_update(&ModelerInput { event_type: ModelerInputEventType::Move, pos: (7.024, 3.024), time: 8.032, pressure: 0.0, }), (7.024, 3.024) )); assert!(util_compare_floats( new_modeler.wobble_update(&ModelerInput { event_type: ModelerInputEventType::Move, pos: (7.024, 3.048), time: 8.048, pressure: 0.0, }), (7.024, 3.048) )); assert!(util_compare_floats( new_modeler.wobble_update(&ModelerInput { event_type: ModelerInputEventType::Move, pos: (7.048, 3.048), time: 8.064, pressure: 0.0, }), (7.048, 3.048) )); } #[test] fn input_test() { let mut modeler = StrokeModeler::new(ModelerParams::suggested()).unwrap(); let inputs = vec![ ModelerInput { event_type: ModelerInputEventType::Down, pos: (0.0, 0.0), time: 0.0, pressure: 0.1, }, ModelerInput { event_type: ModelerInputEventType::Move, pos: (1.0, 0.0), time: 0.02, pressure: 0.3, }, ModelerInput { event_type: ModelerInputEventType::Move, pos: (2.0, 0.0), time: 0.04, pressure: 0.5, }, ModelerInput { event_type: ModelerInputEventType::Move, pos: (2.5, 1.0), time: 0.06, pressure: 0.8, }, ModelerInput { event_type: ModelerInputEventType::Move, pos: (3.0, 1.5), time: 0.12, pressure: 0.9, }, ModelerInput { event_type: ModelerInputEventType::Move, pos: (4.0, 2.0), time: 0.13, pressure: 0.8, }, ModelerInput { event_type: ModelerInputEventType::Move, pos: (3.8, 2.1), time: 0.14, pressure: 0.7, }, ModelerInput { event_type: ModelerInputEventType::Up, pos: (3.5, 2.0), time: 0.14, pressure: 0.2, }, ]; for res in inputs.into_iter().flat_map(|i| modeler.update(i)) { println!("{res:?}"); } } //tests for the end of stroke prediction #[test] fn test_empty_prediction() { let mut engine = StrokeModeler::new(ModelerParams::suggested()).unwrap(); assert!(engine.predict().is_err()); } #[test] fn test_singleinput() { let mut engine = StrokeModeler::default(); engine .update(ModelerInput { pos: (4.0, 5.0), event_type: ModelerInputEventType::Down, time: 2.0, pressure: 1.0, }) .unwrap(); assert_eq!(engine.predict().unwrap().len(), 0); } // tests for the stroke modeler #[test] fn input_rate_slower() { // ceil is exactly on the limit making it different from the original test without this cast let delta_time = (1. / 30. as f32) as f64; let mut time = 0.0; let mut engine = StrokeModeler::new(ModelerParams { stylus_state_modeler_max_input_samples: 20, ..ModelerParams::suggested() }) .unwrap(); let first_iter = engine.update(ModelerInput { event_type: ModelerInputEventType::Down, pos: (3., 4.), time: time, pressure: 1.0, }); assert!(first_iter.is_ok()); assert!(compare_results( first_iter.unwrap(), vec![ModelerResult { pos: (3.0, 4.0), ..ModelerResult::default() }] )); assert!(engine.predict().is_ok()); assert!(engine.predict().unwrap().is_empty()); time += delta_time; assert!(compare_results( engine .update(ModelerInput { event_type: ModelerInputEventType::Move, pos: (3.2, 4.2), time: time, pressure: 1.0 }) .unwrap(), vec![ ModelerResult { pos: (3.0019, 4.0019), velocity: (0.4007, 0.4007), acceleration: (84.1557, 84.1564), time: 0.0048, pressure: 1.0 }, ModelerResult { pos: (3.0069, 4.0069), velocity: (1.0381, 1.0381), acceleration: (133.8378, 133.8369), time: 0.0095, pressure: 1.0 }, ModelerResult { pos: (3.0154, 4.0154), velocity: (1.7883, 1.7883), acceleration: (157.5465, 157.5459), time: 0.0143, pressure: 1.0 }, ModelerResult { pos: (3.0276, 4.0276), velocity: (2.5626, 2.5626), acceleration: (162.6039, 162.6021), time: 0.0190, pressure: 1.0 }, ModelerResult { pos: (3.0433, 4.0433), velocity: (3.3010, 3.3010), acceleration: (155.0670, 155.0666), time: 0.0238, pressure: 1.0 }, ModelerResult { pos: (3.0622, 4.0622), velocity: (3.9665, 3.9665), acceleration: (139.7575, 139.7564), time: 0.0286, pressure: 1.0 }, ModelerResult { pos: (3.0838, 4.0838), velocity: (4.5397, 4.5397), acceleration: (120.3618, 120.3625), time: 0.0333, pressure: 1.0 } ] )); assert!(engine.predict().is_ok()); assert!(compare_results( engine.predict().unwrap(), vec![ ModelerResult { pos: (3.1095, 4.1095), velocity: (4.6253, 4.6253), acceleration: (15.4218, 15.4223), time: 0.0389, pressure: 1.0 }, ModelerResult { pos: (3.1331, 4.1331), velocity: (4.2563, 4.2563), acceleration: (-66.4341, -66.4339), time: 0.0444, pressure: 1.0 }, ModelerResult { pos: (3.1534, 4.1534), velocity: (3.6479, 3.6479), acceleration: (-109.5083, -109.5081), time: 0.0500, pressure: 1.0 }, ModelerResult { pos: (3.1698, 4.1698), velocity: (2.9512, 2.9512), acceleration: (-125.3978, -125.3976), time: 0.0556, pressure: 1.0 }, ModelerResult { pos: (3.1824, 4.1824), velocity: (2.2649, 2.2649), acceleration: (-123.5318, -123.5310), time: 0.0611, pressure: 1.0 }, ModelerResult { pos: (3.1915, 4.1915), velocity: (1.6473, 1.6473), acceleration: (-111.1818, -111.1806), time: 0.0667, pressure: 1.0 }, ModelerResult { pos: (3.1978, 4.1978), velocity: (1.1269, 1.1269), acceleration: (-93.6643, -93.6636), time: 0.0722, pressure: 1.0 }, ModelerResult { pos: (3.1992, 4.1992), velocity: (1.0232, 1.0232), acceleration: (-74.6390, -74.6392), time: 0.0736, pressure: 1.0 } ] )); time += delta_time; let second_results = engine.update(ModelerInput { event_type: ModelerInputEventType::Move, pos: (3.5, 4.2), time: time, pressure: 1.0, }); assert!(second_results.is_ok()); assert!(compare_results( second_results.unwrap(), vec![ ModelerResult { pos: (3.1086, 4.1058), velocity: (5.2142, 4.6131), acceleration: (141.6557, 15.4223), time: 0.0381, pressure: 1.0 }, ModelerResult { pos: (3.1368, 4.1265), velocity: (5.9103, 4.3532), acceleration: (146.1873, -54.5680), time: 0.0429, pressure: 1.0 }, ModelerResult { pos: (3.1681, 4.1450), velocity: (6.5742, 3.8917), acceleration: (139.4012, -96.9169), time: 0.0476, pressure: 1.0 }, ModelerResult { pos: (3.2022, 4.1609), velocity: (7.1724, 3.3285), acceleration: (125.6306, -118.2742), time: 0.0524, pressure: 1.0 }, ModelerResult { pos: (3.2388, 4.1739), velocity: (7.6876, 2.7361), acceleration: (108.1908, -124.4087), time: 0.0571, pressure: 1.0 }, ModelerResult { pos: (3.2775, 4.1842), velocity: (8.1138, 2.1640), acceleration: (89.5049, -120.1309), time: 0.0619, pressure: 1.0 }, ModelerResult { pos: (3.3177, 4.1920), velocity: (8.4531, 1.6436), acceleration: (71.2473, -109.2959), time: 0.0667, pressure: 1.0 } ] )); assert!(engine.predict().is_ok()); assert!(compare_results( engine.predict().unwrap(), vec![ ModelerResult { pos: (3.3625, 4.1982), velocity: (8.0545, 1.1165), acceleration: (-71.7427, -94.8765), time: 0.0722, pressure: 1.0 }, ModelerResult { pos: (3.4018, 4.2021), velocity: (7.0831, 0.6987), acceleration: (-174.8469, -75.1957), time: 0.0778, pressure: 1.0 }, ModelerResult { pos: (3.4344, 4.2043), velocity: (5.8564, 0.3846), acceleration: (-220.8140, -56.5515), time: 0.0833, pressure: 1.0 }, ModelerResult { pos: (3.4598, 4.2052), velocity: (4.5880, 0.1611), acceleration: (-228.3204, -40.2244), time: 0.0889, pressure: 1.0 }, ModelerResult { pos: (3.4788, 4.2052), velocity: (3.4098, 0.0124), acceleration: (-212.0678, -26.7709), time: 0.0944, pressure: 1.0 }, ModelerResult { pos: (3.4921, 4.2048), velocity: (2.3929, -0.0780), acceleration: (-183.0373, -16.2648), time: 0.1000, pressure: 1.0 }, ModelerResult { pos: (3.4976, 4.2045), velocity: (1.9791, -0.1015), acceleration: (-148.9792, -8.4822), time: 0.1028, pressure: 1.0 }, ModelerResult { pos: (3.5001, 4.2044), velocity: (1.7911, -0.1098), acceleration: (-135.3759, -5.9543), time: 0.1042, pressure: 1.0 } ] )); // 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, pos: (3.7, 4.4), time: time, pressure: 1.0, }); assert!(update.is_ok()); assert!(compare_results( update.unwrap(), vec![ ModelerResult { pos: (3.3583, 4.1996), velocity: (8.5122, 1.5925), acceleration: (12.4129, -10.7201), time: 0.0714, pressure: 1.0 }, ModelerResult { pos: (3.3982, 4.2084), velocity: (8.3832, 1.8534), acceleration: (-27.0783, 54.7731), time: 0.0762, pressure: 1.0 }, ModelerResult { pos: (3.4369, 4.2194), velocity: (8.1393, 2.3017), acceleration: (-51.2222, 94.1542), time: 0.0810, pressure: 1.0 }, ModelerResult { pos: (3.4743, 4.2329), velocity: (7.8362, 2.8434), acceleration: (-63.6668, 113.7452), time: 0.0857, pressure: 1.0 }, ModelerResult { pos: (3.5100, 4.2492), velocity: (7.5143, 3.4101), acceleration: (-67.5926, 119.0224), time: 0.0905, pressure: 1.0 }, ModelerResult { pos: (3.5443, 4.2680), velocity: (7.2016, 3.9556), acceleration: (-65.6568, 114.5394), time: 0.0952, pressure: 1.0 }, ModelerResult { pos: (3.5773, 4.2892), velocity: (6.9159, 4.4505), acceleration: (-59.9999, 103.9444), time: 0.1000, pressure: 1.0 }, ModelerResult { pos: (3.6115, 4.3141), velocity: (6.1580, 4.4832), acceleration: (-136.4312, 5.8833), time: 0.1056, pressure: 1.0 }, ModelerResult { pos: (3.6400, 4.3369), velocity: (5.1434, 4.0953), acceleration: (-182.6254, -69.8314), time: 0.1111, pressure: 1.0 }, ModelerResult { pos: (3.6626, 4.3563), velocity: (4.0671, 3.4902), acceleration: (-193.7401, -108.9119), time: 0.1167, pressure: 1.0 }, ModelerResult { pos: (3.6796, 4.3719), velocity: (3.0515, 2.8099), acceleration: (-182.7957, -122.4598), time: 0.1222, pressure: 1.0 }, ModelerResult { pos: (3.6916, 4.3838), velocity: (2.1648, 2.1462), acceleration: (-159.6116, -119.4551), time: 0.1278, pressure: 1.0 }, ModelerResult { pos: (3.6996, 4.3924), velocity: (1.4360, 1.5529), acceleration: (-131.1906, -106.7926), time: 0.1333, pressure: 1.0 }, ModelerResult { pos: (3.7028, 4.3960), velocity: (1.1520, 1.3044), acceleration: (-102.2117, -89.4872), time: 0.1361, pressure: 1.0 } ] )); // the stroke is finished, we get an error if we predict it assert!(engine.predict().is_err()); } #[test] fn reset_keep_params() { let input = ModelerInput { event_type: ModelerInputEventType::Down, pos: (3.0, 4.0), time: 0.0, pressure: 1.0, }; let mut engine = StrokeModeler::default(); assert!(engine.update(input.clone()).is_ok()); assert!(engine.reset_w_params(ModelerParams::suggested()).is_ok()); assert!(engine.update(input.clone()).is_ok()); } /// InputRateFasterThanMinOutputRate #[test] fn input_rate_faster() { let delta_time = 1. / 300.; let mut engine = StrokeModeler::default(); let mut time = 2.0; let res1 = engine.update(ModelerInput { event_type: ModelerInputEventType::Down, pos: (5.0, -3.0), time: time, pressure: 1.0, }); assert!(res1.is_ok()); assert!(compare_results( res1.unwrap(), vec![ModelerResult { pos: (5.0, -3.0), time: time, velocity: (0.0, 0.0), acceleration: (0.0, 0.0), pressure: 1.0 }] )); assert!(engine.predict().is_ok()); assert!(engine.predict().unwrap().is_empty()); time += delta_time; let res2 = engine.update(ModelerInput { event_type: ModelerInputEventType::Move, pos: (5.0, -3.1), time: time, pressure: 1.0, }); assert!(res2.is_ok()); assert!(compare_results( res2.unwrap(), vec![ModelerResult { pos: (5.0, -3.0033), velocity: (0.0, -0.9818), acceleration: (0.0, -294.5452), time: 2.0033, pressure: 1.0 }] )); assert!(compare_results( engine.predict().unwrap(), vec![ ModelerResult { pos: (5.0, -3.0153), velocity: (0.0, -2.1719), acceleration: (0.0, -214.2145), time: 2.0089, pressure: 1.0 }, ModelerResult { pos: (5.0, -3.0303), velocity: (0.0, -2.6885), acceleration: (0.0, -92.9885), time: 2.0144, pressure: 1.0 }, ModelerResult { pos: (5.0, -3.0456), velocity: (0.0, -2.7541), acceleration: (0.0, -11.7992), time: 2.0200, pressure: 1.0 }, ModelerResult { pos: (5.0, -3.0597), velocity: (0.0, -2.5430), acceleration: (0.0, 37.9868), time: 2.0256, pressure: 1.0 }, ModelerResult { pos: (5.0, -3.0718), velocity: (0.0, -2.1852), acceleration: (0.0, 64.4053), time: 2.0311, pressure: 1.0 }, ModelerResult { pos: (5.0, -3.0817), velocity: (0.0, -1.7719), acceleration: (0.0, 74.4011), time: 2.0367, pressure: 1.0 }, ModelerResult { pos: (5.0, -3.0893), velocity: (0.0, -1.3628), acceleration: (0.0, 73.6345), time: 2.0422, pressure: 1.0 }, ModelerResult { pos: (5.0, -3.0948), velocity: (0.0, -0.9934), acceleration: (0.0, 66.4807), time: 2.0478, pressure: 1.0 }, ModelerResult { pos: (5.0, -3.0986), velocity: (0.0, -0.6815), acceleration: (0.0, 56.1448), time: 2.0533, pressure: 1.0 } ] )); assert!(engine.predict().is_ok()); time += delta_time; let res3 = engine.update(ModelerInput { event_type: ModelerInputEventType::Move, pos: (4.975, -3.175), time: time, pressure: 1.0, }); assert!(res3.is_ok()); assert!(compare_results( res3.unwrap(), vec![ModelerResult { pos: (4.9992, -3.0114), velocity: (-0.2455, -2.4322), acceleration: (-73.6366, -435.1238), time: 2.0067, pressure: 1.0 }] )); assert!(engine.predict().is_ok()); assert!(compare_results( engine.predict().unwrap(), vec![ ModelerResult { pos: (4.9962, -3.0344), velocity: (-0.5430, -4.1368), acceleration: (-53.5537, -306.8140), time: 2.0122, pressure: 1.0 }, ModelerResult { pos: (4.9924, -3.0609), velocity: (-0.6721, -4.7834), acceleration: (-23.2474, -116.3963), time: 2.0178, pressure: 1.0 }, ModelerResult { pos: (4.9886, -3.0873), velocity: (-0.6885, -4.7365), acceleration: (-2.9498, 8.4358), time: 2.0233, pressure: 1.0 }, ModelerResult { pos: (4.9851, -3.1110), velocity: (-0.6358, -4.2778), acceleration: (9.4971, 82.5682), time: 2.0289, pressure: 1.0 }, ModelerResult { pos: (4.9820, -3.1311), velocity: (-0.5463, -3.6137), acceleration: (16.1014, 119.5413), time: 2.0344, pressure: 1.0 }, ModelerResult { pos: (4.9796, -3.1471), velocity: (-0.4430, -2.8867), acceleration: (18.6005, 130.8578), time: 2.0400, pressure: 1.0 }, ModelerResult { pos: (4.9777, -3.1593), velocity: (-0.3407, -2.1881), acceleration: (18.4089, 125.7516), time: 2.0456, pressure: 1.0 }, ModelerResult { pos: (4.9763, -3.1680), velocity: (-0.2484, -1.5700), acceleration: (16.6198, 111.2560), time: 2.0511, pressure: 1.0 }, ModelerResult { pos: (4.9754, -3.1739), velocity: (-0.1704, -1.0564), acceleration: (14.0365, 92.4447), time: 2.0567, pressure: 1.0 } ] )); time += delta_time; let res4 = engine.update(ModelerInput { event_type: ModelerInputEventType::Move, pos: (4.9, -3.2), time: time, pressure: 1.0, }); assert!(res4.is_ok()); assert!(compare_results( res4.unwrap(), vec![ModelerResult { pos: (4.9953, -3.0237), velocity: (-1.1603, -3.7004), acceleration: (-274.4622, -380.4507), time: 2.0100, pressure: 1.0 }] )); assert!(engine.predict().is_ok()); assert!(compare_results( engine.predict().unwrap(), vec![ ModelerResult { pos: (4.9828, -3.0521), velocity: (-2.2559, -5.1049), acceleration: (-197.1994, -252.8115), time: 2.0156, pressure: 1.0 }, ModelerResult { pos: (4.9677, -3.0825), velocity: (-2.7081, -5.4835), acceleration: (-81.4051, -68.1520), time: 2.0211, pressure: 1.0 }, ModelerResult { pos: (4.9526, -3.1115), velocity: (-2.7333, -5.2122), acceleration: (-4.5282, 48.8396), time: 2.0267, pressure: 1.0 }, ModelerResult { pos: (4.9387, -3.1369), velocity: (-2.4999, -4.5756), acceleration: (42.0094, 114.5943), time: 2.0322, pressure: 1.0 }, ModelerResult { pos: (4.9268, -3.1579), velocity: (-2.1326, -3.7776), acceleration: (66.1132, 143.6292), time: 2.0378, pressure: 1.0 }, ModelerResult { pos: (4.9173, -3.1743), velocity: (-1.7184, -2.9554), acceleration: (74.5656, 147.9932), time: 2.0433, pressure: 1.0 }, ModelerResult { pos: (4.9100, -3.1865), velocity: (-1.3136, -2.1935), acceleration: (72.8575, 137.1578), time: 2.0489, pressure: 1.0 }, ModelerResult { pos: (4.9047, -3.1950), velocity: (-0.9513, -1.5369), acceleration: (65.2090, 118.1874), time: 2.0544, pressure: 1.0 }, ModelerResult { pos: (4.9011, -3.2006), velocity: (-0.6475, -1.0032), acceleration: (54.6929, 96.0608), time: 2.0600, pressure: 1.0 } ] )); time += delta_time; let res5 = engine.update(ModelerInput { event_type: ModelerInputEventType::Move, pos: (4.825, -3.2), time: time, pressure: 1.0, }); assert!(res5.is_ok()); assert!(compare_results( res5.unwrap(), vec![ModelerResult { pos: (4.9868, -3.0389), velocity: (-2.5540, -4.5431), acceleration: (-418.1093, -252.8115), time: 2.0133, pressure: 1.0, }] )); assert!(engine.predict().is_ok()); assert!(compare_results( engine.predict().unwrap(), vec![ ModelerResult { pos: (4.9636, -3.0687), velocity: (-4.1801, -5.3627), acceleration: (-292.6871, -147.5319), time: 2.0189, pressure: 1.0 }, ModelerResult { pos: (4.9370, -3.0985), velocity: (-4.7757, -5.3670), acceleration: (-107.2116, -0.7651), time: 2.0244, pressure: 1.0 }, ModelerResult { pos: (4.9109, -3.1256), velocity: (-4.6989, -4.8816), acceleration: (13.8210, 87.3644), time: 2.0300, pressure: 1.0 }, ModelerResult { pos: (4.8875, -3.1486), velocity: (-4.2257, -4.1466), acceleration: (85.1835, 132.2997), time: 2.0356, pressure: 1.0 }, ModelerResult { pos: (4.8677, -3.1671), velocity: (-3.5576, -3.3287), acceleration: (120.2579, 147.2335), time: 2.0411, pressure: 1.0 }, ModelerResult { pos: (4.8520, -3.1812), velocity: (-2.8333, -2.5353), acceleration: (130.3700, 142.8088), time: 2.0467, pressure: 1.0 }, ModelerResult { pos: (4.8401, -3.1914), velocity: (-2.1411, -1.8288), acceleration: (124.5846, 127.1714), time: 2.0522, pressure: 1.0 }, ModelerResult { pos: (4.8316, -3.1982), velocity: (-1.5312, -1.2386), acceleration: (109.7874, 106.2279), time: 2.0578, pressure: 1.0 }, ModelerResult { pos: (4.8280, -3.2010), velocity: (-1.2786, -1.0053), acceleration: (90.9288, 84.0051), time: 2.0606, pressure: 1.0 }, ModelerResult { pos: (4.8272, -3.2017), velocity: (-1.2209, -0.9529), acceleration: (83.2052, 75.4288), time: 2.0613, pressure: 1.0 } ] )); time += delta_time; let res6 = engine.update(ModelerInput { event_type: ModelerInputEventType::Move, pos: (4.75, -3.225), time: time, pressure: 1.0, }); assert!(res6.is_ok()); assert!(compare_results( res6.unwrap(), vec![ModelerResult { pos: (4.9726, -3.0565), velocity: (-4.2660, -5.2803), acceleration: (-513.5957, -221.1678), time: 2.0167, pressure: 1.0 }] )); assert!(engine.predict().is_ok()); assert!(compare_results( engine.predict().unwrap(), vec![ ModelerResult { pos: (4.9381, -3.0894), velocity: (-6.2018, -5.9261), acceleration: (-348.4476, -116.2445), time: 2.0222, pressure: 1.0 }, ModelerResult { pos: (4.9004, -3.1215), velocity: (-6.7995, -5.7749), acceleration: (-107.5834, 27.2264), time: 2.0278, pressure: 1.0 }, ModelerResult { pos: (4.8640, -3.1501), velocity: (-6.5400, -5.1591), acceleration: (46.7146, 110.8336), time: 2.0333, pressure: 1.0 }, ModelerResult { pos: (4.8319, -3.1741), velocity: (-5.7897, -4.3207), acceleration: (135.0462, 150.9226), time: 2.0389, pressure: 1.0 }, ModelerResult { pos: (4.8051, -3.1932), velocity: (-4.8132, -3.4248), acceleration: (175.7684, 161.2555), time: 2.0444, pressure: 1.0 }, ModelerResult { pos: (4.7841, -3.2075), velocity: (-3.7898, -2.5759), acceleration: (184.2227, 152.7958), time: 2.0500, pressure: 1.0 }, ModelerResult { pos: (4.7683, -3.2176), velocity: (-2.8312, -1.8324), acceleration: (172.5480, 133.8294), time: 2.0556, pressure: 1.0 }, ModelerResult { pos: (4.7572, -3.2244), velocity: (-1.9986, -1.2198), acceleration: (149.8577, 110.2830), time: 2.0611, pressure: 1.0 }, ModelerResult { pos: (4.7526, -3.2271), velocity: (-1.6580, -0.9805), acceleration: (122.6198, 86.1299), time: 2.0639, pressure: 1.0 } ] )); time += delta_time; let res7 = engine.update(ModelerInput { event_type: ModelerInputEventType::Move, pos: (4.7, -3.3), time: time, pressure: 1.0, }); assert!(res7.is_ok()); assert!(compare_results( res7.unwrap(), vec![ModelerResult { pos: (4.9529, -3.0778), velocity: (-5.9184, -6.4042), acceleration: (-495.7209, -337.1538), time: 2.0200, pressure: 1.0 }] )); assert!(engine.predict().is_ok()); assert!(compare_results( engine.predict().unwrap(), vec![ ModelerResult { pos: (4.9101, -3.1194), velocity: (-7.6886, -7.4784), acceleration: (-318.6394, -193.3594), time: 2.0256, pressure: 1.0 }, ModelerResult { pos: (4.8654, -3.1607), velocity: (-8.0518, -7.4431), acceleration: (-65.3698, 6.3579), time: 2.0311, pressure: 1.0 }, ModelerResult { pos: (4.8235, -3.1982), velocity: (-7.5377, -6.7452), acceleration: (92.5345, 125.6104), time: 2.0367, pressure: 1.0 }, ModelerResult { pos: (4.7872, -3.2299), velocity: (-6.5440, -5.7133), acceleration: (178.8654, 185.7426), time: 2.0422, pressure: 1.0 }, ModelerResult { pos: (4.7574, -3.2553), velocity: (-5.3529, -4.5748), acceleration: (214.4027, 204.9362), time: 2.0478, pressure: 1.0 }, ModelerResult { pos: (4.7344, -3.2746), velocity: (-4.1516, -3.4758), acceleration: (216.2348, 197.8224), time: 2.0533, pressure: 1.0 }, ModelerResult { pos: (4.7174, -3.2885), velocity: (-3.0534, -2.5004), acceleration: (197.6767, 175.5702), time: 2.0589, pressure: 1.0 }, ModelerResult { pos: (4.7056, -3.2979), velocity: (-2.1169, -1.6879), acceleration: (168.5711, 146.2573), time: 2.0644, pressure: 1.0 }, ModelerResult { pos: (4.7030, -3.3000), velocity: (-1.9283, -1.5276), acceleration: (135.7820, 115.3739), time: 2.0658, pressure: 1.0 }, ModelerResult { pos: (4.7017, -3.3010), velocity: (-1.8380, -1.4512), acceleration: (130.0928, 110.0859), time: 2.0665, pressure: 1.0 }, ] )); time += delta_time; let res8 = engine.update(ModelerInput { event_type: ModelerInputEventType::Move, pos: (4.675, -3.4), time: time, pressure: 1.0, }); assert!(res8.is_ok()); assert!(compare_results( res8.unwrap(), vec![ModelerResult { pos: (4.9288, -3.1046), velocity: (-7.2260, -8.0305), acceleration: (-392.2747, -487.9053), time: 2.0233, pressure: 1.0 },] )); assert!(engine.predict().is_ok()); assert!(compare_results( engine.predict().unwrap(), vec![ ModelerResult { pos: (4.8816, -3.1582), velocity: (-8.4881, -9.6525), acceleration: (-227.1831, -291.9628), time: 2.0289, pressure: 1.0 }, ModelerResult { pos: (4.8345, -3.2124), velocity: (-8.4738, -9.7482), acceleration: (2.5870, -17.2266), time: 2.0344, pressure: 1.0 }, ModelerResult { pos: (4.7918, -3.2619), velocity: (-7.6948, -8.9195), acceleration: (140.2131, 149.1810), time: 2.0400, pressure: 1.0 }, ModelerResult { pos: (4.7555, -3.3042), velocity: (-6.5279, -7.6113), acceleration: (210.0428, 235.4638), time: 2.0456, pressure: 1.0 }, ModelerResult { pos: (4.7264, -3.3383), velocity: (-5.2343, -6.1345), acceleration: (232.8451, 265.8274), time: 2.0511, pressure: 1.0 }, ModelerResult { pos: (4.7043, -3.3643), velocity: (-3.9823, -4.6907), acceleration: (225.3593, 259.8790), time: 2.0567, pressure: 1.0 }, ModelerResult { pos: (4.6884, -3.3832), velocity: (-2.8691, -3.3980), acceleration: (200.3802, 232.6849), time: 2.0622, pressure: 1.0 }, ModelerResult { pos: (4.6776, -3.3961), velocity: (-1.9403, -2.3135), acceleration: (167.1764, 195.2152), time: 2.0678, pressure: 1.0 }, ModelerResult { pos: (4.6752, -3.3990), velocity: (-1.7569, -2.0983), acceleration: (132.0560, 154.9868), time: 2.0692, pressure: 1.0 }, ] )); time += delta_time; let res9 = engine.update(ModelerInput { event_type: ModelerInputEventType::Move, pos: (4.675, -3.525), time: time, pressure: 1.0, }); assert!(res9.is_ok()); assert!(compare_results( res9.unwrap(), vec![ModelerResult { pos: (4.9022, -3.1387), velocity: (-7.9833, -10.2310), acceleration: (-227.1831, -660.1446), time: 2.0267, pressure: 1.0 },] )); assert!(engine.predict().is_ok()); assert!(compare_results( engine.predict().unwrap(), vec![ ModelerResult { pos: (4.8549, -3.2079), velocity: (-8.5070, -12.4602), acceleration: (-94.2781, -401.2599), time: 2.0322, pressure: 1.0 }, ModelerResult { pos: (4.8102, -3.2783), velocity: (-8.0479, -12.6650), acceleration: (82.6390, -36.8616), time: 2.0378, pressure: 1.0 }, ModelerResult { pos: (4.7711, -3.3429), velocity: (-7.0408, -11.6365), acceleration: (181.2765, 185.1286), time: 2.0433, pressure: 1.0 }, ModelerResult { pos: (4.7389, -3.3983), velocity: (-5.7965, -9.9616), acceleration: (223.9801, 301.4933), time: 2.0489, pressure: 1.0 }, ModelerResult { pos: (4.7137, -3.4430), velocity: (-4.5230, -8.0510), acceleration: (229.2397, 343.9032), time: 2.0544, pressure: 1.0 }, ModelerResult { pos: (4.6951, -3.4773), velocity: (-3.3477, -6.1727), acceleration: (211.5554, 338.0856), time: 2.0600, pressure: 1.0 }, ModelerResult { pos: (4.6821, -3.5022), velocity: (-2.3381, -4.4846), acceleration: (181.7131, 303.8597), time: 2.0656, pressure: 1.0 }, ModelerResult { pos: (4.6737, -3.5192), velocity: (-1.5199, -3.0641), acceleration: (147.2879, 255.7003), time: 2.0711, pressure: 1.0 }, ModelerResult { pos: (4.6718, -3.5231), velocity: (-1.3626, -2.7813), acceleration: (113.2437, 203.5595), time: 2.0725, pressure: 1.0 }, ] )); time += delta_time; // we get more results at the end of the stroke (stroke end catch up) let res10 = engine.update(ModelerInput { event_type: ModelerInputEventType::Up, pos: (4.7, -3.6), time: time, pressure: 1.0, }); assert!(res10.is_ok()); assert!(compare_results( res10.unwrap(), vec![ ModelerResult { pos: (4.8753, -3.1797), velocity: (-8.0521, -12.3049), acceleration: (-20.6429, -622.1685), time: 2.0300, pressure: 1.0 }, ModelerResult { pos: (4.8325, -3.2589), velocity: (-7.7000, -14.2607), acceleration: (63.3680, -352.0363), time: 2.0356, pressure: 1.0 }, ModelerResult { pos: (4.7948, -3.3375), velocity: (-6.7888, -14.1377), acceleration: (164.0215, 22.1350), time: 2.0411, pressure: 1.0 }, ModelerResult { pos: (4.7636, -3.4085), velocity: (-5.6249, -12.7787), acceleration: (209.5020, 244.6249), time: 2.0467, pressure: 1.0 }, ModelerResult { pos: (4.7390, -3.4685), velocity: (-4.4152, -10.8015), acceleration: (217.7452, 355.8801), time: 2.0522, pressure: 1.0 }, ModelerResult { pos: (4.7208, -3.5164), velocity: (-3.2880, -8.6333), acceleration: (202.8961, 390.2804), time: 2.0578, pressure: 1.0 }, ModelerResult { pos: (4.7079, -3.5528), velocity: (-2.3128, -6.5475), acceleration: (175.5414, 375.4407), time: 2.0633, pressure: 1.0 }, ModelerResult { pos: (4.6995, -3.5789), velocity: (-1.5174, -4.7008), acceleration: (143.1705, 332.4062), time: 2.0689, pressure: 1.0 }, ModelerResult { pos: (4.6945, -3.5965), velocity: (-0.9022, -3.1655), acceleration: (110.7325, 276.3669), time: 2.0744, pressure: 1.0 }, ModelerResult { pos: (4.6942, -3.5976), velocity: (-0.8740, -3.0899), acceleration: (81.2036, 217.6189), time: 2.0748, pressure: 1.0 }, ] )); // the stroke is finished so there's nothing left to predict assert!(engine.predict().is_err()); } #[test] fn wobble_smoothed() { let delta_time = 0.0167; let mut engine = StrokeModeler::default(); let mut time = 4.0; let res1 = engine.update(ModelerInput { event_type: ModelerInputEventType::Down, pos: (-6.0, -2.0), time: time, pressure: 1.0, }); assert!(res1.is_ok()); assert!(compare_results( res1.unwrap(), vec![ModelerResult { pos: (-6.0, -2.0), time: 4.0, ..ModelerResult::default() }] )); time += delta_time; let res2 = engine.update(ModelerInput { event_type: ModelerInputEventType::Move, pos: (-6.02, -2.0), time: time, ..ModelerInput::default() }); assert!(res2.is_ok()); assert!(compare_results( res2.unwrap(), vec![ ModelerResult { pos: (-6.0003, -2.0), velocity: (-0.0615, 0.0), acceleration: (-14.7276, 0.0), time: 4.0042, pressure: 1.0 }, ModelerResult { pos: (-6.0009, -2.0), velocity: (-0.1628, 0.0), acceleration: (-24.2725, 0.0), time: 4.0084, pressure: 1.0 }, ModelerResult { pos: (-6.0021, -2.0), velocity: (-0.2868, 0.0), acceleration: (-29.6996, 0.0), time: 4.0125, pressure: 1.0 }, ModelerResult { pos: (-6.0039, -2.0), velocity: (-0.4203, 0.0), acceleration: (-31.9728, 0.0), time: 4.0167, pressure: 1.0 }, ] )); time += delta_time; let res3 = engine.update(ModelerInput { event_type: ModelerInputEventType::Move, pos: (-6.02, -2.02), time: time, pressure: 1.0, }); assert!(res3.is_ok()); assert!(compare_results( res3.unwrap(), vec![ ModelerResult { pos: (-6.0059, -2.0001), velocity: (-0.4921, -0.0307), acceleration: (-17.1932, -7.3638), time: 4.0209, pressure: 1.0 }, ModelerResult { pos: (-6.0081, -2.0005), velocity: (-0.5170, -0.0814), acceleration: (-5.9729, -12.1355), time: 4.0251, pressure: 1.0 }, ModelerResult { pos: (-6.0102, -2.0010), velocity: (-0.5079, -0.1434), acceleration: (2.1807, -14.8493), time: 4.0292, pressure: 1.0 }, ModelerResult { pos: (-6.0122, -2.0019), velocity: (-0.4755, -0.2101), acceleration: (7.7710, -15.9860), time: 4.0334, pressure: 1.0 }, ] )); time += delta_time; let res4 = engine.update(ModelerInput { event_type: ModelerInputEventType::Move, pos: (-6.04, -2.02), time: time, pressure: 1.0, }); assert!(res4.is_ok()); assert!(compare_results( res4.unwrap(), vec![ ModelerResult { pos: (-6.0141, -2.0030), velocity: (-0.4489, -0.2563), acceleration: (6.3733, -11.0507), time: 4.0376, pressure: 1.0 }, ModelerResult { pos: (-6.0159, -2.0042), velocity: (-0.4277, -0.2856), acceleration: (5.0670, -7.0315), time: 4.0418, pressure: 1.0 }, ModelerResult { pos: (-6.0176, -2.0055), velocity: (-0.4115, -0.3018), acceleration: (3.8950, -3.8603), time: 4.0459, pressure: 1.0 }, ModelerResult { pos: (-6.0193, -2.0067), velocity: (-0.3994, -0.3078), acceleration: (2.8758, -1.4435), time: 4.0501, pressure: 1.0 }, ] )); time += delta_time; let res5 = engine.update(ModelerInput { event_type: ModelerInputEventType::Move, pos: (-6.04, -2.04), time: time, pressure: 1.0, }); assert!(res5.is_ok()); assert!( (compare_results( res5.unwrap(), vec![ ModelerResult { pos: (-6.0209, -2.0082), velocity: (-0.3910, -0.3372), acceleration: (2.0142, -7.0427), time: 4.0543, pressure: 1.0 }, ModelerResult { pos: (-6.0225, -2.0098), velocity: (-0.3856, -0.3814), acceleration: (1.3090, -10.5977), time: 4.0585, pressure: 1.0 }, ModelerResult { pos: (-6.0241, -2.0116), velocity: (-0.3825, -0.4338), acceleration: (0.7470, -12.5399), time: 4.0626, pressure: 1.0 }, ModelerResult { pos: (-6.0257, -2.0136), velocity: (-0.3811, -0.4891), acceleration: (0.3174, -13.2543), time: 4.0668, pressure: 1.0 }, ] )) ); } #[test] fn reset_stroke() { let mut engine = StrokeModeler::default(); let delta_time = 1. / 50.; let mut time = 0.0; let res = engine.update(ModelerInput { event_type: ModelerInputEventType::Down, pos: (-8.0, -10.0), time: time, pressure: 1.0, }); assert!(res.is_ok()); assert!(!res.unwrap().is_empty()); assert!(engine.predict().is_ok()); assert!(engine.predict().unwrap().is_empty()); time += delta_time; let res2 = engine.update(ModelerInput { event_type: ModelerInputEventType::Move, time: time, ..ModelerInput::default() }); assert!(res2.is_ok()); assert!(!res2.unwrap().is_empty()); assert!(engine.predict().is_ok()); assert!(!engine.predict().unwrap().is_empty()); time += delta_time; let res3 = engine.update(ModelerInput { event_type: ModelerInputEventType::Move, time: time, pos: (-11.0, -5.0), ..ModelerInput::default() }); assert!(res3.is_ok()); assert!(!res3.unwrap().is_empty()); assert!(engine.predict().is_ok()); assert!(!engine.predict().unwrap().is_empty()); engine.reset(); assert!(engine.predict().is_err()); } #[test] fn ignore_input_before_down() { let mut engine = StrokeModeler::default(); assert!( engine .update(ModelerInput { event_type: ModelerInputEventType::Move, ..ModelerInput::default() }) .is_err() ); assert!( engine .update(ModelerInput { event_type: ModelerInputEventType::Up, ..ModelerInput::default() }) .is_err() ); } #[test] fn tdown_in_progress_error() { let mut engine = StrokeModeler::default(); assert!( engine .update(ModelerInput { event_type: ModelerInputEventType::Down, ..ModelerInput::default() }) .is_ok() ); assert!( engine .update(ModelerInput { event_type: ModelerInputEventType::Down, ..ModelerInput::default() }) .is_err() ); } #[test] fn alternate_params() { let delta_time = 1. / 50.; let mut engine = StrokeModeler::new(ModelerParams { sampling_min_output_rate: 70.0, stylus_state_modeler_max_input_samples: 20, ..ModelerParams::suggested() }) .unwrap(); let mut time = 3.0; let res1 = engine.update(ModelerInput { event_type: ModelerInputEventType::Down, pos: (0.0, 0.0), time: time, pressure: 0.5, }); assert!(res1.is_ok()); assert!(compare_results( res1.unwrap(), vec![ModelerResult { time: 3.0, pressure: 0.5, ..ModelerResult::default() }] )); assert!(engine.predict().is_ok()); assert!(engine.predict().unwrap().is_empty()); time += delta_time; let res2 = engine.update(ModelerInput { event_type: ModelerInputEventType::Move, pos: (0.0, 0.5), time: time, pressure: 0.4, }); assert!(res2.is_ok()); assert!(compare_results( res2.unwrap(), vec![ ModelerResult { pos: (0.0, 0.0736), velocity: (0.0, 7.3636), acceleration: (0.0, 736.3636), time: 3.0100, pressure: 0.4853 }, ModelerResult { pos: (0.0, 0.2198), velocity: (0.0, 14.6202), acceleration: (0.0, 725.6529), time: 3.0200, pressure: 0.4560 }, ] )); assert!(engine.predict().is_ok()); assert!(compare_results( engine.predict().unwrap(), vec![ ModelerResult { pos: (0.0, 0.3823), velocity: (0.0, 11.3709), acceleration: (0.0, -227.4474), time: 3.0343, pressure: 0.4235 }, ModelerResult { pos: (0.0, 0.4484), velocity: (0.0, 4.6285), acceleration: (0.0, -471.9660), time: 3.0486, pressure: 0.4103 }, ModelerResult { pos: (0.0, 0.4775), velocity: (0.0, 2.0389), acceleration: (0.0, -181.2747), time: 3.0629, pressure: 0.4045 }, ModelerResult { pos: (0.0, 0.4902), velocity: (0.0, 0.8873), acceleration: (0.0, -80.6136), time: 3.0771, pressure: 0.4020 }, ModelerResult { pos: (0.0, 0.4957), velocity: (0.0, 0.3868), acceleration: (0.0, -35.0318), time: 3.0914, pressure: 0.4009 }, ModelerResult { pos: (0.0, 0.4981), velocity: (0.0, 0.1686), acceleration: (0.0, -15.2760), time: 3.1057, pressure: 0.4004 }, ModelerResult { pos: (0.0, 0.4992), velocity: (0.0, 0.0735), acceleration: (0.0, -6.6579), time: 3.1200, pressure: 0.4002 }, ] )); time += delta_time; let res3 = engine.update(ModelerInput { pos: (0.2, 1.0), time: time, pressure: 0.3, event_type: ModelerInputEventType::Move, }); assert!(res3.is_ok()); assert!(compare_results( res3.unwrap(), vec![ ModelerResult { pos: (0.0295, 0.4169), velocity: (2.9455, 19.7093), acceleration: (294.5455, 508.9161), time: 3.0300, pressure: 0.4166 }, ModelerResult { pos: (0.0879, 0.6439), velocity: (5.8481, 22.6926), acceleration: (290.2612, 298.3311), time: 3.0400, pressure: 0.3691 }, ] )); assert!(engine.predict().is_ok()); assert!(compare_results( engine.predict().unwrap(), vec![ ModelerResult { pos: (0.1529, 0.8487), velocity: (4.5484, 14.3374), acceleration: (-90.9790, -584.8687), time: 3.0543, pressure: 0.3293 }, ModelerResult { pos: (0.1794, 0.9338), velocity: (1.8514, 5.9577), acceleration: (-188.7864, -586.5760), time: 3.0686, pressure: 0.3128 }, ModelerResult { pos: (0.1910, 0.9712), velocity: (0.8156, 2.6159), acceleration: (-72.5099, -233.9289), time: 3.0829, pressure: 0.3056 }, ModelerResult { pos: (0.1961, 0.9874), velocity: (0.3549, 1.1389), acceleration: (-32.2455, -103.3868), time: 3.0971, pressure: 0.3024 }, ModelerResult { pos: (0.1983, 0.9945), velocity: (0.1547, 0.4965), acceleration: (-14.0127, -44.9693), time: 3.1114, pressure: 0.3011 }, ModelerResult { pos: (0.1993, 0.9976), velocity: (0.0674, 0.2164), acceleration: (-6.1104, -19.6068), time: 3.1257, pressure: 0.3005 }, ModelerResult { pos: (0.1997, 0.9990), velocity: (0.0294, 0.0943), acceleration: (-2.6631, -8.5455), time: 3.1400, pressure: 0.3002 }, ] )); time += delta_time; let res4 = engine.update(ModelerInput { event_type: ModelerInputEventType::Move, pos: (0.4, 1.4), time: time, pressure: 0.2, }); assert!(res4.is_ok()); assert!(compare_results( res4.unwrap(), vec![ ModelerResult { pos: (0.1668, 0.8712), velocity: (7.8837, 22.7349), acceleration: (203.5665, 4.2224), time: 3.0500, pressure: 0.3245 }, ModelerResult { pos: (0.2575, 1.0906), velocity: (9.0771, 21.9411), acceleration: (119.3324, -79.3721), time: 3.0600, pressure: 0.2761 }, ] )); assert!(engine.predict().is_ok()); assert!(compare_results( engine.predict().unwrap(), vec![ ModelerResult { pos: (0.3395, 1.2676), velocity: (5.7349, 12.3913), acceleration: (-233.9475, -668.4906), time: 3.0743, pressure: 0.2325 }, ModelerResult { pos: (0.3735, 1.3421), velocity: (2.3831, 5.2156), acceleration: (-234.6304, -502.2992), time: 3.0886, pressure: 0.2142 }, ModelerResult { pos: (0.3885, 1.3748), velocity: (1.0463, 2.2854), acceleration: (-93.5716, -205.1091), time: 3.1029, pressure: 0.2062 }, ModelerResult { pos: (0.3950, 1.3890), velocity: (0.4556, 0.9954), acceleration: (-41.3547, -90.3064), time: 3.1171, pressure: 0.2027 }, ModelerResult { pos: (0.3978, 1.3952), velocity: (0.1986, 0.4339), acceleration: (-17.9877, -39.3021), time: 3.1314, pressure: 0.2012 }, ModelerResult { pos: (0.3990, 1.3979), velocity: (0.0866, 0.1891), acceleration: (-7.8428, -17.1346), time: 3.1457, pressure: 0.2005 }, ModelerResult { pos: (0.3996, 1.3991), velocity: (0.0377, 0.0824), acceleration: (-3.4182, -7.4680), time: 3.1600, pressure: 0.2002 }, ] )); time += delta_time; let res5 = engine.update(ModelerInput { event_type: ModelerInputEventType::Up, pos: (0.7, 1.7), pressure: 0.1, time: time, }); assert!(res5.is_ok()); assert!(compare_results( res5.unwrap(), vec![ ModelerResult { pos: (0.3691, 1.2874), velocity: (11.1558, 19.6744), acceleration: (207.8707, -226.6725), time: 3.0700, pressure: 0.2256 }, ModelerResult { pos: (0.4978, 1.4640), velocity: (12.8701, 17.6629), acceleration: (171.4340, -201.1508), time: 3.0800, pressure: 0.1730 }, ModelerResult { pos: (0.6141, 1.5986), velocity: (8.1404, 9.4261), acceleration: (-331.0815, -576.5752), time: 3.0943, pressure: 0.1312 }, ModelerResult { pos: (0.6624, 1.6557), velocity: (3.3822, 3.9953), acceleration: (-333.0701, -380.1579), time: 3.1086, pressure: 0.1136 }, ModelerResult { pos: (0.6836, 1.6807), velocity: (1.4851, 1.7488), acceleration: (-132.8005, -157.2520), time: 3.1229, pressure: 0.1059 }, ModelerResult { pos: (0.6929, 1.6916), velocity: (0.6466, 0.7618), acceleration: (-58.6943, -69.0946), time: 3.1371, pressure: 0.1026 }, ModelerResult { pos: (0.6969, 1.6963), velocity: (0.2819, 0.3321), acceleration: (-25.5298, -30.0794), time: 3.1514, pressure: 0.1011 }, ModelerResult { pos: (0.6986, 1.6984), velocity: (0.1229, 0.1447), acceleration: (-11.1311, -13.1133), time: 3.1657, pressure: 0.1005 }, ModelerResult { pos: (0.6994, 1.6993), velocity: (0.0535, 0.0631), acceleration: (-4.8514, -5.7153), time: 3.1800, pressure: 0.1002 }, ] )); assert!(engine.predict().is_err()); } #[test] fn generate_output_up_nodelta() { let delta_time = 1. / 500.; let mut engine = StrokeModeler::default(); let mut time = 0.0; let res1 = engine.update(ModelerInput { event_type: ModelerInputEventType::Down, pos: (5.0, 5.0), time: time, pressure: 1.0, }); assert!(res1.is_ok()); assert!(compare_results( res1.unwrap(), vec![ModelerResult { pos: (5.0, 5.0), time: 0.0, ..ModelerResult::default() }] )); time += delta_time; let res2 = engine.update(ModelerInput { event_type: ModelerInputEventType::Move, pos: (5.0, 5.0), pressure: 1.0, time: time, }); assert!(res2.is_ok()); assert!(compare_results( res2.unwrap(), vec![ModelerResult { pos: (5.0, 5.0), time: 0.002, ..ModelerResult::default() }] )); let res3 = engine.update(ModelerInput { event_type: ModelerInputEventType::Up, pos: (5.0, 5.0), time: time, pressure: 1.0, }); assert!(res3.is_ok()); assert!(compare_results( res3.unwrap(), vec![ModelerResult { pos: (5.0, 5.0), time: 0.0076, pressure: 1.0, ..ModelerResult::default() }] )); } #[test] fn far_apart_times_move() { let mut engine = StrokeModeler::default(); let res1 = engine.update(ModelerInput { event_type: ModelerInputEventType::Down, pos: (0.0, 0.0), time: 0.0, pressure: 0.2, }); assert!(res1.is_ok()); assert!(!res1.unwrap().is_empty()); let res2 = engine.update(ModelerInput { event_type: ModelerInputEventType::Move, pos: (0.0, 0.0), pressure: 0.2, time: 2147483647.0, }); assert!(res2.is_err()); } #[test] fn far_apart_times_up() { let mut engine = StrokeModeler::default(); let res1 = engine.update(ModelerInput { event_type: ModelerInputEventType::Down, pos: (0.0, 0.0), time: 0.0, pressure: 0.2, }); assert!(res1.is_ok()); assert!(!res1.unwrap().is_empty()); let res2 = engine.update(ModelerInput { event_type: ModelerInputEventType::Up, pos: (0.0, 0.0), pressure: 0.2, time: 2147483647.0, }); assert!(res2.is_err()); } #[test] fn reject_negative_timedelta() { let mut engine = StrokeModeler::default(); let res1 = engine.update(ModelerInput { event_type: ModelerInputEventType::Down, pos: (0.0, 0.0), time: 0.0, pressure: 0.0, }); assert!(res1.is_ok()); assert!(!res1.unwrap().is_empty()); let res2 = engine.update(ModelerInput { event_type: ModelerInputEventType::Move, pos: (1.0, 1.0), time: -0.1, pressure: 0.0, }); assert!(res2.is_err()); let res3 = engine.update(ModelerInput { event_type: ModelerInputEventType::Move, pos: (1.0, 1.0), time: 0.1, pressure: 1.0, }); assert!(res3.is_ok()); assert!(!res3.unwrap().is_empty()); let res4 = engine.update(ModelerInput { event_type: ModelerInputEventType::Up, pos: (1.0, 1.0), time: 0.09, pressure: 1.0, }); assert!(res4.is_err()); } #[test] fn reject_duplicate_input() { let mut engine = StrokeModeler::default(); let res1 = engine.update(ModelerInput { event_type: ModelerInputEventType::Down, pos: (0.0, 0.0), time: 0.0, pressure: 0.2, }); assert!(res1.is_ok()); assert!(!res1.unwrap().is_empty()); let res2 = engine.update(ModelerInput { event_type: ModelerInputEventType::Down, pos: (0.0, 0.0), time: 0.0, pressure: 0.2, }); assert!(res2.is_err()); let res3 = engine.update(ModelerInput { event_type: ModelerInputEventType::Move, pos: (1.0, 2.0), time: 0.1, pressure: 0.1, }); assert!(res3.is_ok()); assert!(!res3.unwrap().is_empty()); let res4 = engine.update(ModelerInput { event_type: ModelerInputEventType::Move, pos: (1.0, 2.0), time: 0.1, pressure: 0.1, }); assert!(res4.is_err()); } } ink-stroke-modeler-rs-0.1.0/src/error.rs000064400000000000000000000020171046102023000162450ustar 00000000000000#[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, }, } ink-stroke-modeler-rs-0.1.0/src/input.rs000064400000000000000000000017051046102023000162560ustar 00000000000000/// modeler Input event Type #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[allow(non_camel_case_types)] #[allow(unused)] pub enum ModelerInputEventType { /// For the first pen down event (pen touches the screen) Down, /// For events between the first (`Down`) and last (`Up`) event (pen moving on the screen) Move, /// For the last event (pen going up) Up, } /// struct holding all information for input event #[derive(Clone, Debug, PartialEq)] pub struct ModelerInput { pub event_type: ModelerInputEventType, pub pos: (f64, f64), pub time: f64, pub pressure: f64, // tilt and orientation are optional parameters, so we remove them here to // make our lives easier } impl Default for ModelerInput { fn default() -> Self { Self { event_type: ModelerInputEventType::Down, pos: (0.0, 0.0), time: 0.0, pressure: 1.0, } } } ink-stroke-modeler-rs-0.1.0/src/lib.rs000064400000000000000000000005561046102023000156700ustar 00000000000000// Modules mod engine; pub mod error; mod input; mod params; mod position_modeler; mod results; mod state_modeler; mod utils; #[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; pub use results::ModelerResult; ink-stroke-modeler-rs-0.1.0/src/params.rs000064400000000000000000000167001046102023000164030ustar 00000000000000/// all parameters for the modeler #[derive(Debug, Clone, PartialEq, PartialOrd, Copy)] pub struct ModelerParams { /// these parameters are used to apply smoothing to the input to reduce /// wobble in the prediction /// /// The length of the window over which the moving average of speed and position is calculated /// /// Check if this can't be done with the rust time types as this probably comes from a /// conversion to float (DURATION) /// /// A good starting point is /// /// /// 2.5 /// / /// /// f /// input rate /// /// /// /// Should be positive pub wobble_smoother_timeout: f64, /// The range of speed considered for wobble smoothing. /// At [ModelerParams::wobble_smoother_speed_floor] the maximum /// amount of smoothing is applied. At [ModelerParams::wobble_smoother_speed_ceiling], /// no smoothing is applied /// /// Good starting points are 2 - 3 % of the expected speed of the inputs /// Should be positive and the speed floor smaller than the ceiling pub wobble_smoother_speed_floor: f64, pub wobble_smoother_speed_ceiling: f64, /// The mass of the "weight" being pulled along the path, multiplied by the spring constant. /// /// Should be positive pub position_modeler_spring_mass_constant: f64, /// The ratio of the pen's velocity that is subtracted from the pen's acceleration per unit time, to simulate drag. /// /// Should be positive pub position_modeler_drag_constant: f64, /// The minimum number of modeled inputs to output per unit time. If inputs are received at a lower rate, /// they will be upsampled to produce output of atleast [ModelerParams::sampling_min_output_rate]. /// If inputs are received at a higher rate, the output rate will match the input rate. /// /// Should be positive pub sampling_min_output_rate: f64, /// This determines the stop condition for the end-of-stroke modeling /// If the position is within this distance of the final raw input, or /// if the last update iteration moved less than this distance, /// it stops iterating. /// /// this should be a small distance, good heuristic is /// 2-3 orders of magnitude smaller than the expected distance /// between input points /// /// Should be positive pub sampling_end_of_stroke_stopping_distance: f64, /// The maximum number of iterations to perform at the end of the stroke, /// if it does not stop due to the constraint of the `sampling_end_of_stroke_stopping_distance` /// /// Should be positive and is capped at 1000 (to limit the memory requirements) pub sampling_end_of_stroke_max_iterations: usize, /// Maximum number of outputs to generate per call to Update or Predict. /// related to issues if input events are received with too long of a delay /// See what's done in the rnote call and on this end to limit things like this /// /// Should be strictly positive pub sampling_max_outputs_per_call: usize, /// the maximum number of raw inputs to look at when /// searching for the nearest states when interpolating /// /// Should be strictly positive pub stylus_state_modeler_max_input_samples: usize, } impl ModelerParams { /// [ModelerParams::wobble_smoother_timeout] : 0.04,\ /// [ModelerParams::wobble_smoother_speed_floor] : 1.31,\ /// [ModelerParams::wobble_smoother_speed_ceiling] : 1.44,\ /// [ModelerParams::position_modeler_spring_mass_constant] : 11.0 / 32400.0,\ /// [ModelerParams::position_modeler_drag_constant] : 72.0,\ /// [ModelerParams::sampling_min_output_rate] : 180.0,\ /// [ModelerParams::sampling_end_of_stroke_stopping_distance] : 0.001,\ /// [ModelerParams::sampling_end_of_stroke_max_iterations] : 20,\ /// [ModelerParams::sampling_max_outputs_per_call] : 20,\ /// [ModelerParams::stylus_state_modeler_max_input_samples] : 10, pub fn suggested() -> Self { Self { wobble_smoother_timeout: 0.04, wobble_smoother_speed_floor: 1.31, wobble_smoother_speed_ceiling: 1.44, position_modeler_spring_mass_constant: 11.0 / 32400.0, position_modeler_drag_constant: 72.0, sampling_min_output_rate: 180.0, sampling_end_of_stroke_stopping_distance: 0.001, sampling_end_of_stroke_max_iterations: 20, sampling_max_outputs_per_call: 20, stylus_state_modeler_max_input_samples: 10, } } /// validate the parameters as being correct, returns a error string with /// the reasons otherwise pub fn validate(self) -> Result { let parameter_tests = [ self.position_modeler_spring_mass_constant > 0.0, self.position_modeler_drag_constant > 0.0, self.sampling_min_output_rate > 0.0, self.sampling_end_of_stroke_stopping_distance > 0.0, self.sampling_end_of_stroke_max_iterations > 0, self.sampling_end_of_stroke_max_iterations < 1000, self.sampling_max_outputs_per_call > 0, self.wobble_smoother_timeout > 0.0, self.wobble_smoother_speed_floor > 0.0, self.wobble_smoother_speed_ceiling > 0.0, self.wobble_smoother_speed_floor < self.wobble_smoother_speed_ceiling, ]; let errors = vec![ "`position_modeler_spring_mass_constant` is not positive; ", "`position_modeler_drag_constant` is not positive; ", "`sampling_min_output_rate` is not positive; ", "`sampling_end_of_stroke_stopping_distance` is not positive; ", "`sampling_end_of_stroke_max_iterations` is not positive; ", "`sampling_end_of_stroke_max_iterations` is too large (>1000); ", "`sampling_max_outputs_per_call` is not positive; ", "`wobble_smoother_timeout` is not positive; ", "`wobble_smoother_speed_floor` is not positive; ", "`wobble_smoother_speed_ceiling` is not positive; ", "`wobble_smoother_speed_floor` should be strictly smaller than `wobble_smoother_speed_ceiling`", ]; let tests_passed = parameter_tests.iter().fold(true, |acc, x| acc & x); if tests_passed { Ok(self) } else { //Collect errors 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) } } } #[cfg(test)] mod test_params { // import parent use super::super::*; #[test] fn validation_modeler_params() { let s = (ModelerParams { wobble_smoother_timeout: -1.0, wobble_smoother_speed_floor: -1.0, wobble_smoother_speed_ceiling: -1.0, position_modeler_spring_mass_constant: -1.0, position_modeler_drag_constant: -1.0, sampling_min_output_rate: -1.0, sampling_end_of_stroke_stopping_distance: -1.0, sampling_end_of_stroke_max_iterations: 0, sampling_max_outputs_per_call: 0, stylus_state_modeler_max_input_samples: 0, }) .validate(); match s { Ok(_) => assert!(false), Err(_) => assert!(true), } } } ink-stroke-modeler-rs-0.1.0/src/position_modeler.rs000064400000000000000000000614101046102023000204710ustar 00000000000000use crate::results::ModelerPartial; use crate::utils::{dist, nearest_point_on_segment}; 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 /// anchor; as the anchor moves, it drags the pen tip along behind it. pub(crate) struct PositionModeler { //parameters for the model position_modeler_spring_mass_constant: f64, position_modeler_drag_constant: f64, // last state pub(crate) state: ModelerPartial, } impl PositionModeler { pub(crate) fn new(params: ModelerParams, first_input: ModelerInput) -> Self { Self { position_modeler_spring_mass_constant: params.position_modeler_spring_mass_constant, position_modeler_drag_constant: params.position_modeler_drag_constant, state: ModelerPartial { pos: first_input.pos, velocity: (0.0, 0.0), acceleration: (0.0, 0.0), time: first_input.time, }, } } // Given the position of the anchor and the time, updates the model and // returns the new state of the pen tip pub(crate) fn update(&mut self, anchor_pos: (f64, f64), time: f64) -> ModelerPartial { let delta_time = time - self.state.time; // self.state.acceleration = ( (anchor_pos.0 - self.state.pos.0) / (self.position_modeler_spring_mass_constant) - self.position_modeler_drag_constant * self.state.velocity.0, (anchor_pos.1 - self.state.pos.1) / (self.position_modeler_spring_mass_constant) - self.position_modeler_drag_constant * self.state.velocity.1, ); self.state.velocity = ( self.state.velocity.0 + (delta_time) * self.state.acceleration.0, self.state.velocity.1 + (delta_time) * self.state.acceleration.1, ); self.state.pos = ( self.state.pos.0 + delta_time * self.state.velocity.0, self.state.pos.1 + delta_time * self.state.velocity.1, ); self.state.time = time; self.state.clone() } /// update the model `n_steps` time between events /// this upsample between inputs linearly and applies /// these upstreamed events to the model pub(crate) fn update_along_linear_path( &mut self, start_pos: (f64, f64), start_time: f64, end_pos: (f64, f64), end_time: f64, n_steps: i32, ) -> Vec { (1..=n_steps) .map(|i| { let frac_adv = i as f64 / n_steps as f64; let anchor_pos = ( start_pos.0 + frac_adv * (end_pos.0 - start_pos.0), start_pos.1 + frac_adv * (end_pos.1 - start_pos.1), ); let time = start_time + frac_adv * (end_time - start_time); self.update(anchor_pos, time) }) .collect() } /// models the end of the stroke (catch-up) WITHOUT modifying the predictor /// (the state is saved then restored after calculations are done) /// /// This creates candidates solution using the latest event as an anchor /// but stops after `max_iterations`, if the distance between states is less /// than `stop_distance` or the candidate is close to the anchor (less than /// `stop_distance`) pub(crate) fn model_end_of_stroke( &mut self, anchor_pos: (f64, f64), delta_time: f64, max_iterations: usize, stop_distance: f64, ) -> Vec { let initial_state = self.state.clone(); let mut delta_time = delta_time; let mut out_events = Vec::::with_capacity(max_iterations); for _ in 0..max_iterations { let previous_state = self.state.clone(); let candidate = self.update(anchor_pos, previous_state.time + delta_time); if dist(previous_state.pos, candidate.pos) < stop_distance { // reset the state self.state = initial_state; // stop, we aren't making progress anymore return out_events; } if nearest_point_on_segment( (previous_state.pos.0, previous_state.pos.1), (candidate.pos.0, candidate.pos.1), (anchor_pos.0, anchor_pos.1), ) < 1.0 { // overshoot, try with a smaller delta t delta_time *= 0.5; self.state = previous_state; continue; } else { out_events.push(candidate.clone()); } if dist(candidate.pos, anchor_pos) < stop_distance { // very close to the anchor, stopping iterations // reset the state self.state = initial_state; return out_events; } } self.state = initial_state; out_events } } impl ModelerPartial { #[cfg(test)] fn near(self, compare: ModelerPartial) -> bool { let tol = 0.005; //tolerance increased for f64 approx::abs_diff_eq!(self.pos.0, compare.pos.0, epsilon = tol) && approx::abs_diff_eq!(self.pos.1, compare.pos.1, epsilon = tol) && approx::abs_diff_eq!(self.velocity.0, compare.velocity.0, epsilon = tol) && approx::abs_diff_eq!(self.velocity.1, compare.velocity.1, epsilon = tol) && approx::abs_diff_eq!(self.acceleration.0, compare.acceleration.0, epsilon = tol) && approx::abs_diff_eq!(self.acceleration.1, compare.acceleration.1, epsilon = tol) && approx::abs_diff_eq!(self.time, compare.time, epsilon = tol as f64) } } #[test] fn straight_line() { // init let mut modeler: PositionModeler = PositionModeler::new(ModelerParams::suggested(), ModelerInput::default()); let default_ts = 1. / 180 as f64; let mut current_time: f64 = 0.0; current_time += default_ts; assert!( modeler .update((1.0, 0.0), current_time) .near(ModelerPartial { pos: (0.0909, 0.0), velocity: (16.3636, 0.0), acceleration: (2945.4546, 0.0), time: current_time }) ); current_time += default_ts; assert!( modeler .update((2.0, 0.0), current_time) .near(ModelerPartial { pos: (0.319, 0.0), velocity: (41.0579, 0.0), acceleration: (4444.9590, 0.0), time: current_time }) ); current_time += default_ts; assert!( modeler .update((3.0, 0.0), current_time) .near(ModelerPartial { pos: (0.6996, 0.0), velocity: (68.5055, 0.0), acceleration: (4940.5737, 0.0), time: current_time }) ); current_time += default_ts; assert!( modeler .update((4.0, 0.0), current_time) .near(ModelerPartial { pos: (1.228, 0.0), velocity: (95.1099, 0.0), acceleration: (4788.8003, 0.0), time: current_time }) ); } #[test] fn zigzag() { // init let mut current_time: f64 = 3.0; let mut modeler = PositionModeler::new( ModelerParams::suggested(), ModelerInput { pos: (-1.0, -1.0), time: current_time, ..ModelerInput::default() }, ); let default_ts = 1. / 180 as f64; current_time += default_ts; assert!( modeler .update((-0.5, -1.0), current_time) .near(ModelerPartial { pos: (-0.9545, -1.0), velocity: (8.1818, 0.0), acceleration: (1472.7273, 0.0), time: current_time }) ); current_time += default_ts; assert!( modeler .update((-0.5, -0.5), current_time) .near(ModelerPartial { pos: (-0.886, -0.9545), velocity: (12.3471, 8.1818), acceleration: (749.7521, 1472.7273), time: current_time }) ); current_time += default_ts; assert!( modeler .update((-0.0, -0.5), current_time) .near(ModelerPartial { pos: (-0.7643, -0.886), velocity: (21.9056, 12.3471), acceleration: (1720.5348, 749.7521), time: current_time }) ); current_time += default_ts; assert!( modeler .update((0.0, 0.0), current_time) .near(ModelerPartial { pos: (-0.6218, -0.7643), velocity: (25.6493, 21.9056), acceleration: (673.8650, 1720.5348), time: current_time }) ); current_time += default_ts; assert!( modeler .update((0.5, 0.0), current_time) .near(ModelerPartial { pos: (-0.4343, -0.6218), velocity: (33.7456, 25.6493), acceleration: (1457.3298, 673.8650), time: current_time }) ) } #[test] fn sharp_turn() { // init let mut current_time: f64 = 1.6; let mut modeler = PositionModeler::new( ModelerParams::suggested(), ModelerInput { time: current_time, ..ModelerInput::default() }, ); let default_ts = 1. / 180 as f64; current_time += default_ts; assert!( modeler .update((0.25, 0.25), current_time) .near(ModelerPartial { pos: (0.0227, 0.0227), velocity: (4.0909, 4.0909), acceleration: (736.3636, 736.3636), time: current_time }) ); current_time += default_ts; assert!( modeler .update((0.5, 0.5), current_time) .near(ModelerPartial { pos: (0.0798, 0.0798), velocity: (10.2645, 10.2645), acceleration: (1111.2397, 1111.2397), time: current_time }) ); current_time += default_ts; assert!( modeler .update((0.75, 0.75), current_time) .near(ModelerPartial { pos: (0.1749, 0.1749), velocity: (17.1264, 17.1264), acceleration: (1235.1434, 1235.1434), time: current_time }) ); current_time += default_ts; assert!( modeler .update((1.0, 1.0), current_time) .near(ModelerPartial { pos: (0.307, 0.307), velocity: (23.7775, 23.7775), acceleration: (1197.2001, 1197.2001), time: current_time }) ); current_time += default_ts; assert!( modeler .update((1.25, 0.75), current_time) .near(ModelerPartial { pos: (0.472, 0.4265), velocity: (29.6975, 21.5157), acceleration: (1065.5977, -407.1296), time: current_time }) ); current_time += default_ts; assert!( modeler .update((1.5, 0.5), current_time) .near(ModelerPartial { pos: (0.6644, 0.5049), velocity: (34.6406, 14.1117), acceleration: (889.7637, -1332.7158), time: current_time }) ); current_time += default_ts; assert!( modeler .update((1.75, 0.25), current_time) .near(ModelerPartial { pos: (0.8786, 0.5288), velocity: (38.5482, 4.2955), acceleration: (703.3755, -1766.9114), time: current_time }) ); current_time += default_ts; assert!( modeler .update((2.0, 0.0), current_time) .near(ModelerPartial { pos: (1.109, 0.495), velocity: (41.4794, -6.0756), acceleration: (527.5996, -1866.8005), time: current_time }) ); } #[test] fn smooth_turn() { use std::f64::consts::PI; let point_on_circle = |x: f64| (x.cos(), x.sin()); // init let mut current_time: f64 = 1.0; let mut modeler = PositionModeler::new( ModelerParams::suggested(), ModelerInput { time: current_time, pos: point_on_circle(0.0), ..ModelerInput::default() }, ); let default_ts = 1. / 180 as f64; current_time += default_ts; assert!( modeler .update(point_on_circle(PI * 0.125), current_time) .near(ModelerPartial { pos: (0.9931, 0.0348), velocity: (-1.2456, 6.2621), acceleration: (-224.2095, 1127.1768), time: current_time }) ); current_time += default_ts; assert!( modeler .update(point_on_circle(PI * 0.25), current_time) .near(ModelerPartial { pos: (0.9629, 0.1168), velocity: (-5.4269, 14.7588), acceleration: (-752.6373, 1529.4097), time: current_time }) ); current_time += default_ts; assert!( modeler .update(point_on_circle(PI * 0.375), current_time) .near(ModelerPartial { pos: (0.8921, 0.2394), velocity: (-12.7511, 22.0623), acceleration: (-1318.3523, 1314.6320), time: current_time }) ); current_time += default_ts; assert!( modeler .update(point_on_circle(PI * 0.5), current_time) .near(ModelerPartial { pos: (0.7685, 0.3820), velocity: (-22.2485, 25.6844), acceleration: (-1709.5339, 651.9690), time: current_time }) ); current_time += default_ts; assert!( modeler .update(point_on_circle(PI * 0.625), current_time) .near(ModelerPartial { pos: (0.5897, 0.5169), velocity: (-32.1865, 24.2771), acceleration: (-1788.8300, -253.3177), time: current_time }) ); current_time += default_ts; assert!( modeler .update(point_on_circle(PI * 0.75), current_time) .near(ModelerPartial { pos: (0.3645, 0.6151), velocity: (-40.5319, 17.6785), acceleration: (-1502.1846, -1187.7462), time: current_time }) ); current_time += default_ts; assert!( modeler .update(point_on_circle(PI * 0.875), current_time) .near(ModelerPartial { pos: (0.1123, 0.6529), velocity: (-45.4017, 6.8034), acceleration: (-876.5552, -1957.5056), time: current_time }) ); current_time += default_ts; assert!( modeler .update(point_on_circle(PI), current_time) .near(ModelerPartial { pos: (-0.1402, 0.6162), velocity: (-45.4417, -6.6022), acceleration: (-7.2061, -2413.0093), time: current_time }) ); } #[test] fn test_update_linear_path() { let mut modeler = PositionModeler::new( ModelerParams::suggested(), ModelerInput { time: 3.0, pos: (5.0, 10.0), ..ModelerInput::default() }, ); let linear_path = modeler.update_along_linear_path((5.0, 10.0), 3.0, (15., 10.), 3.05, 5); let expected = vec![ ModelerPartial { pos: (5.5891, 10.0), velocity: (58.9091, 0.0), acceleration: (5890.9092, 0.0), time: 3.01, }, ModelerPartial { pos: (6.7587, 10.0), velocity: (116.9613, 0.0), acceleration: (5805.2231, 0.0), time: 3.02, }, ModelerPartial { pos: (8.3355, 10.0), velocity: (157.6746, 0.0), acceleration: (4071.3291, 0.0), time: 3.03, }, ModelerPartial { pos: (10.1509, 10.0), velocity: (181.5411, 0.0), acceleration: (2386.6475, 0.0), time: 3.04, }, ModelerPartial { pos: (12.0875, 10.0), velocity: (193.6607, 0.0), acceleration: (1211.9609, 0.0), time: 3.05, }, ]; assert!( linear_path .into_iter() .zip(expected) .fold(true, |acc, x| { acc && x.0.near(x.1) }) ); // second try let linear_path_2 = modeler.update_along_linear_path((15.0, 10.0), 3.05, (15.0, 16.0), 3.08, 3); let expected2 = vec![ ModelerPartial { pos: (13.4876, 10.5891), velocity: (140.0123, 58.9091), acceleration: (-5364.8398, 5890.9092), time: 3.06, }, ModelerPartial { pos: (14.3251, 11.7587), velocity: (83.7508, 116.9613), acceleration: (-5626.1528, 5805.2217), time: 3.07, }, ModelerPartial { pos: (14.7584, 13.3355), velocity: (43.3291, 157.6746), acceleration: (-4042.1616, 4071.3291), time: 3.08, }, ]; assert!( linear_path_2 .into_iter() .zip(expected2) .fold(true, |acc, x| { acc && x.0.near(x.1) }) ); } #[test] fn model_end_of_stroke_stationary() { let mut model = PositionModeler::new( ModelerParams::suggested(), ModelerInput { pos: (4.0, -2.0), ..ModelerInput::default() }, ); let result = model.model_end_of_stroke((3.0, -1.0), 1. / 180., 20, 0.01); let expected = vec![ ModelerPartial { pos: (3.9091, -1.9091), velocity: (-16.3636, 16.3636), acceleration: (-2945.4546, 2945.4546), time: 0.0056, }, ModelerPartial { pos: (3.7719, -1.7719), velocity: (-24.6942, 24.6942), acceleration: (-1499.5044, 1499.5042), time: 0.0111, }, ModelerPartial { pos: (3.6194, -1.6194), velocity: (-27.4476, 27.4476), acceleration: (-495.6155, 495.6150), time: 0.0167, }, ModelerPartial { pos: (3.4716, -1.4716), velocity: (-26.6045, 26.6044), acceleration: (151.7738, -151.7742), time: 0.0222, }, ModelerPartial { pos: (3.3401, -1.3401), velocity: (-23.6799, 23.6799), acceleration: (526.4102, -526.4102), time: 0.0278, }, ModelerPartial { pos: (3.2302, -1.2302), velocity: (-19.7725, 19.7725), acceleration: (703.3362, -703.3359), time: 0.0333, }, ModelerPartial { pos: (3.1434, -1.1434), velocity: (-15.6306, 15.6306), acceleration: (745.5521, -745.5518), time: 0.0389, }, ModelerPartial { pos: (3.0782, -1.0782), velocity: (-11.7244, 11.7244), acceleration: (703.1044, -703.1039), time: 0.0444, }, ModelerPartial { pos: (3.0320, -1.0320), velocity: (-8.3149, 8.3149), acceleration: (613.7169, -613.7166), time: 0.0500, }, ModelerPartial { pos: (3.0014, -1.0014), velocity: (-5.5133, 5.5133), acceleration: (504.2921, -504.2918), time: 0.0556, }, ]; assert!( result .into_iter() .zip(expected) .fold(true, |acc, x| { acc && x.0.near(x.1) }) ); } #[test] fn end_of_stroke_motion() { let mut model = PositionModeler { position_modeler_drag_constant: ModelerParams::suggested().position_modeler_drag_constant, position_modeler_spring_mass_constant: ModelerParams::suggested() .position_modeler_spring_mass_constant, state: ModelerPartial { pos: (-1.0, 2.0), velocity: (40.0, 10.0), acceleration: (0.0, 0.0), time: 1., }, }; let result = model.model_end_of_stroke((7.0, 2.0), 1. / 120., 20, 0.01); let expected = vec![ ModelerPartial { pos: (0.7697, 2.0333), velocity: (212.3636, 4.0000), acceleration: (20683.6367, -720.0000), time: 1.0083, }, ModelerPartial { pos: (2.7520, 2.0398), velocity: (237.8711, 0.7818), acceleration: (3060.8916, -386.1817), time: 1.0167, }, ModelerPartial { pos: (4.4138, 2.0343), velocity: (199.4186, -0.6654), acceleration: (-4614.2959, -173.6631), time: 1.0250, }, ModelerPartial { pos: (5.6075, 2.0251), velocity: (143.2474, -1.1081), acceleration: (-6740.5410, -53.1330), time: 1.0333, }, ModelerPartial { pos: (6.3698, 2.0162), velocity: (91.4784, -1.0586), acceleration: (-6212.2896, 5.9471), time: 1.0417, }, ModelerPartial { pos: (6.8037, 2.0094), velocity: (52.0592, -0.8222), acceleration: (-4730.2935, 28.3621), time: 1.0500, }, ModelerPartial { pos: (6.9655, 2.0065), velocity: (38.8512, -0.6909), acceleration: (-3169.9351, 31.5268), time: 1.0542, }, ModelerPartial { pos: (6.9850, 2.0062), velocity: (37.4471, -0.6750), acceleration: (-2695.7649, 30.5478), time: 1.0547, }, ]; assert!( result .into_iter() .zip(expected) .fold(true, |acc, x| { acc && x.0.near(x.1) }) ); } #[test] fn end_of_stroke_maxiters() { let mut model = PositionModeler { position_modeler_drag_constant: ModelerParams::suggested().position_modeler_drag_constant, position_modeler_spring_mass_constant: ModelerParams::suggested() .position_modeler_spring_mass_constant, state: ModelerPartial { pos: (8.0, -3.0), velocity: (-100.0, -150.0), acceleration: (0.0, 0.0), time: 1., }, }; let result = model.model_end_of_stroke((-9., -10.0), 0.0001, 10, 0.001); let expected = vec![ ModelerPartial { pos: (7.9896, -3.0151), velocity: (-104.2873, -150.9818), acceleration: (-42872.7266, -9818.1816), time: 1.0001, }, ModelerPartial { pos: (7.9787, -3.0303), velocity: (-108.5406, -151.9521), acceleration: (-42533.3242, -9703.0205), time: 1.0002, }, ModelerPartial { pos: (7.9674, -3.0456), velocity: (-112.7601, -152.9110), acceleration: (-42195.1211, -9588.4023), time: 1.0003, }, ModelerPartial { pos: (7.9557, -3.0610), velocity: (-116.9459, -153.8584), acceleration: (-41858.1016, -9474.3242), time: 1.0004, }, ModelerPartial { pos: (7.9436, -3.0764), velocity: (-121.0982, -154.7945), acceleration: (-41522.2734, -9360.7930), time: 1.0005, }, ModelerPartial { pos: (7.9311, -3.0920), velocity: (-125.2169, -155.7193), acceleration: (-41187.6445, -9247.7998), time: 1.0006, }, ModelerPartial { pos: (7.9182, -3.1077), velocity: (-129.3023, -156.6328), acceleration: (-40854.2109, -9135.3506), time: 1.0007, }, ModelerPartial { pos: (7.9048, -3.1234), velocity: (-133.3545, -157.5351), acceleration: (-40521.9727, -9023.4395), time: 1.0008, }, ModelerPartial { pos: (7.8911, -3.1393), velocity: (-137.3736, -158.4263), acceleration: (-40190.9414, -8912.0703), time: 1.0009, }, ModelerPartial { pos: (7.8770, -3.1552), velocity: (-141.3597, -159.3065), acceleration: (-39861.0977, -8801.2402), time: 1.0010, }, ]; assert!( result .into_iter() .zip(expected) .fold(true, |acc, x| { acc && x.0.near(x.1) }) ); } ink-stroke-modeler-rs-0.1.0/src/results.rs000064400000000000000000000050421046102023000166160ustar 00000000000000/// result struct /// contains the position, time, presusre as well as the velocity and acceleration data #[derive(Debug, PartialEq)] pub struct ModelerResult { pub pos: (f64, f64), pub velocity: (f64, f64), pub acceleration: (f64, f64), pub time: f64, pub pressure: f64, } /// A [ModelerResult] that does not have yet a pressure information #[derive(Clone, Debug)] pub(crate) struct ModelerPartial { pub pos: (f64, f64), pub velocity: (f64, f64), pub acceleration: (f64, f64), pub time: f64, } impl ModelerResult { #[cfg(test)] pub fn near(self, other: ModelerResult) -> bool { let tol = 3.0 * 1e-3; //tolerance increased for f64 approx::abs_diff_eq!(self.pos.0, other.pos.0, epsilon = tol) && approx::abs_diff_eq!(self.pos.1, other.pos.1, epsilon = tol) && approx::abs_diff_eq!(self.time, other.time, epsilon = tol as f64) && approx::abs_diff_eq!(self.acceleration.0, other.acceleration.0, epsilon = tol) && approx::abs_diff_eq!(self.acceleration.1, other.acceleration.1, epsilon = tol) && approx::abs_diff_eq!(self.velocity.0, other.velocity.0, epsilon = tol) && approx::abs_diff_eq!(self.velocity.1, other.velocity.1, epsilon = tol) && approx::abs_diff_eq!(self.pressure, other.pressure, epsilon = tol) } } impl Default for ModelerResult { fn default() -> Self { Self { pos: (0.0, 0.0), velocity: (0.0, 0.0), acceleration: (0.0, 0.0), pressure: 1.0, time: 0.0, } } } /// Utility to compare [ModelerResult] up to `tol =1e-4` (not settable for now) /// with printed output for debug purposes /// /// Only used for testing purposes #[allow(unused)] #[cfg(test)] pub(crate) fn compare_results(left: Vec, right: Vec) -> bool { if left.len() != right.len() { println!("\n\nleft : {:?} right {:?}", left.len(), right.len()); //iterate println!("left"); for el in left { println!("{:?}", el); } println!("right"); for el in right { println!("{:?}", el); } false } else { println!("\n\n\nleft : {:?} right {:?}", &left.len(), &right.len()); //iterate println!("left"); for el in &left { println!("{:?}", el); } println!("right"); for el in &right { println!("{:?}", el); } left.into_iter().zip(right).all(|x| x.0.near(x.1)) } } ink-stroke-modeler-rs-0.1.0/src/state_modeler.rs000064400000000000000000000215041046102023000177450ustar 00000000000000use crate::ModelerInput; use crate::utils::{dist, interp, interp2, nearest_point_on_segment}; use std::collections::VecDeque; // only imported for docstrings #[allow(unused)] use crate::ModelerResult; #[allow(unused)] use crate::results::ModelerPartial; /// Get the pressure for a position by querying /// information from the raw input strokes /// /// All raw input strokes are to be provided to this state modeler by calling `update` /// Then [ModelerPartial] structs can be converted to [ModelerResult] by querying the /// pressure data by calling this struct with the `query` function #[doc = include_str!("../docs/notations.html")] #[doc = include_str!("../docs/stylus_state_modeler.html")] pub(crate) struct StateModeler { /// max number of elements stylus_state_modeler_max_input_samples: usize, /// deque holding the data from strokes last_strokes: VecDeque, } impl Default for StateModeler { fn default() -> Self { Self { stylus_state_modeler_max_input_samples: 10, last_strokes: VecDeque::with_capacity(11), } } } impl StateModeler { /// initialize a new StateModeler pub(crate) fn new(param: usize) -> Self { // zero is not a valid parameter, we put 1 in that case // to prevent errors if param == 0 { return Self { stylus_state_modeler_max_input_samples: 1, last_strokes: VecDeque::with_capacity(2), }; } Self { stylus_state_modeler_max_input_samples: param, last_strokes: VecDeque::with_capacity(param + 1), } } /// add the most recent raw input to the StateModeler pub(crate) fn update(&mut self, input: ModelerInput) { // add the event to the strokes self.last_strokes.push_back(input); if self.last_strokes.len() > self.stylus_state_modeler_max_input_samples { self.last_strokes.pop_front(); } } /// reset the StateModeler pub(crate) fn reset(&mut self, max_input: usize) { self.last_strokes.clear(); self.stylus_state_modeler_max_input_samples = max_input; } /// query the pressure by interpolating it from raw input events pub(crate) fn query(&mut self, pos: (f64, f64)) -> f64 { // iterate over the deque match self.last_strokes.len() { 0 => 1.0, 1 => self.last_strokes.front().unwrap().pressure, _ => { let mut distance = f64::INFINITY; let mut r: f64 = 0.0; let mut start_pressure: f64 = 1.0; let mut end_pressure: f64 = 1.0; for index_it in 0..self.last_strokes.len() - 1 { let start_pos = self.last_strokes.get(index_it).unwrap().pos; let end_pos = self.last_strokes.get(index_it + 1).unwrap().pos; let r_c = nearest_point_on_segment(start_pos, end_pos, pos); let point_c = interp2(start_pos, end_pos, r_c); if dist(pos, point_c) < distance { distance = dist(pos, point_c); r = r_c; start_pressure = self.last_strokes.get(index_it).unwrap().pressure; end_pressure = self.last_strokes.get(index_it + 1).unwrap().pressure; } } interp(start_pressure, end_pressure, r) } } } } #[test] fn state_modeler_straight() { let mut state_mod = StateModeler::new(10); approx::assert_relative_eq!(state_mod.query((0.0, 0.0)), 1.0); // 1 is our "unknown" default value approx::assert_relative_eq!(state_mod.query((-5.0, 3.0)), 1.0); // 1 is our "unknown" default value } #[test] fn query_single_output() { let mut state_mod = StateModeler::new(10); state_mod.update(ModelerInput { pos: (0.0, 0.0), pressure: 0.75, ..ModelerInput::default() }); approx::assert_relative_eq!(state_mod.query((0.0, 0.0)), 0.75); approx::assert_relative_eq!(state_mod.query((1.0, 1.0)), 0.75); } #[test] fn query_multiple_output() { let mut state_mod = StateModeler::default(); state_mod.update(ModelerInput { pos: (0.5, 1.5), pressure: 0.3, ..Default::default() }); state_mod.update(ModelerInput { pos: (2.0, 1.5), pressure: 0.6, ..Default::default() }); state_mod.update(ModelerInput { pos: (3.0, 3.5), pressure: 0.8, ..Default::default() }); state_mod.update(ModelerInput { pos: (3.5, 4.0), pressure: 0.2, ..Default::default() }); let tol = 1e-5; approx::assert_abs_diff_eq!(state_mod.query((0.0, 2.0)), 0.3, epsilon = tol); approx::assert_abs_diff_eq!(state_mod.query((1.0, 2.0)), 0.4, epsilon = tol); approx::assert_abs_diff_eq!(state_mod.query((2.0, 1.5)), 0.6, epsilon = tol); approx::assert_abs_diff_eq!(state_mod.query((2.5, 1.875)), 0.65, epsilon = tol); approx::assert_abs_diff_eq!(state_mod.query((2.5, 3.125)), 0.75, epsilon = tol); approx::assert_abs_diff_eq!(state_mod.query((2.5, 4.0)), 0.8, epsilon = tol); approx::assert_abs_diff_eq!(state_mod.query((2.5, 4.0)), 0.8, epsilon = tol); approx::assert_abs_diff_eq!(state_mod.query((3.0, 4.0)), 0.5, epsilon = tol); approx::assert_abs_diff_eq!(state_mod.query((4.0, 4.0)), 0.2, epsilon = tol); } #[test] fn query_stale() { let mut state_mod = StateModeler::default(); state_mod.update(ModelerInput { pos: (1.0, 1.0), pressure: 0.6, ..Default::default() }); state_mod.update(ModelerInput { pos: (-1.0, 2.0), pressure: 0.3, ..Default::default() }); state_mod.update(ModelerInput { pos: (-4.0, 0.0), pressure: 0.9, ..Default::default() }); state_mod.update(ModelerInput { pos: (-6.0, -3.0), pressure: 0.4, ..Default::default() }); state_mod.update(ModelerInput { pos: (-5.0, -5.0), pressure: 0.3, ..Default::default() }); state_mod.update(ModelerInput { pos: (-3.0, -4.0), pressure: 0.6, ..Default::default() }); state_mod.update(ModelerInput { pos: (-6.0, -7.0), pressure: 0.9, ..Default::default() }); state_mod.update(ModelerInput { pos: (-9.0, -8.0), pressure: 0.8, ..Default::default() }); state_mod.update(ModelerInput { pos: (-11.0, -5.0), pressure: 0.2, ..Default::default() }); state_mod.update(ModelerInput { pos: (-10.0, -2.0), pressure: 0.7, ..Default::default() }); let tol = 1e-5; approx::assert_abs_diff_eq!(state_mod.query((2.0, 0.0)), 0.6, epsilon = tol); approx::assert_abs_diff_eq!(state_mod.query((1.0, 3.5)), 0.45, epsilon = tol); approx::assert_abs_diff_eq!(state_mod.query((-3.0, 17. / 6.)), 0.5, epsilon = tol); //adds a 11-th point so that the first point is discarded state_mod.update(ModelerInput { pos: (-8.0, 0.0), pressure: 0.6, ..Default::default() }); approx::assert_abs_diff_eq!(state_mod.query((2.0, 0.0)), 0.3, epsilon = tol); approx::assert_abs_diff_eq!(state_mod.query((1.0, 3.5)), 0.3, epsilon = tol); approx::assert_relative_eq!(state_mod.query((-3.0, 17. / 6.)), 0.5, epsilon = tol); state_mod.update(ModelerInput { pos: (-8.0, 0.0), pressure: 0.6, ..Default::default() }); approx::assert_abs_diff_eq!(state_mod.query((2.0, 0.0)), 0.9, epsilon = tol); approx::assert_abs_diff_eq!(state_mod.query((1.0, 3.5)), 0.9, epsilon = tol); approx::assert_abs_diff_eq!(state_mod.query((-3.0, 17. / 6.)), 0.9, epsilon = tol); } #[test] fn query_reset() { let mut state_mod = StateModeler::default(); state_mod.update(ModelerInput { pos: (4.0, 5.0), pressure: 0.4, ..Default::default() }); state_mod.update(ModelerInput { pos: (7.0, 8.0), pressure: 0.1, ..Default::default() }); let tol = 1e-5; approx::assert_abs_diff_eq!(state_mod.query((10.0, 12.0)), 0.1, epsilon = tol); state_mod.reset(10); approx::assert_relative_eq!(state_mod.query((10.0, 12.0)), 1.0); state_mod.update(ModelerInput { pos: (-1.0, 4.0), pressure: 0.4, ..Default::default() }); approx::assert_abs_diff_eq!(state_mod.query((6.0, 7.0)), 0.4, epsilon = tol); state_mod.update(ModelerInput { pos: (-3.0, 0.0), pressure: 0.7, ..Default::default() }); approx::assert_abs_diff_eq!(state_mod.query((-2.0, 2.0)), 0.55, epsilon = tol); approx::assert_abs_diff_eq!(state_mod.query((0.0, 5.0)), 0.4, epsilon = tol); } // remark : we suppose that pressure is always defined // and is set to 1 otherwise (both for input and outputs) ink-stroke-modeler-rs-0.1.0/src/utils.rs000064400000000000000000000077741046102023000162730ustar 00000000000000// utilities use std::ops::{Add, Mul, Sub}; // clamp : use clamp(self,min,max) with 0 and 1 for floats /// interpol value /// get the value to use for the interpolation /// 0 if value < start /// 1 if value > end /// and (value - start)/(end - start) otherwise pub(crate) fn normalize01_64(start: f64, end: f64, value: f64) -> f64 { if start == end { if value > start { 1.0 } else { 0.0 } } else { ((value - start) / (end - start)).clamp(0.0, 1.0) } } /// interpolate the value /// /// normal interpolation clamped to \[0,1\] for the `interp_amount` pub(crate) fn interp(start: T, end: T, interp_amount: f64) -> T where T: Sub, T: Add, T: Copy, T: Mul, { start + (end - start) * interp_amount.clamp(0.0, 1.0) } /// interpolation (with the `interp_amount` clamped between 0 and 1) for `(f64,f64)` types pub(crate) fn interp2(start: (f64, f64), end: (f64, f64), interp_amount: f64) -> (f64, f64) { ( start.0 + interp_amount.clamp(0.0, 1.0) * (end.0 - start.0), start.1 + interp_amount.clamp(0.0, 1.0) * (end.1 - start.1), ) } /// returns the point on the line segment from `segment_start` to `segment_end` /// that is closest to `point`, represented as the ratio of the length /// along the segment pub(crate) fn nearest_point_on_segment( start: (f64, f64), end: (f64, f64), point: (f64, f64), ) -> f64 { if start == end { 0.0 } else { let seg_vector = (end.0 - start.0, end.1 - start.1); let proj_vector = (point.0 - start.0, point.1 - start.1); (dot(proj_vector, seg_vector) / dot(seg_vector, seg_vector)).clamp(0.0, 1.0) } } /// dot product for `(f46,f64)` types pub(crate) fn dot(x: (f64, f64), y: (f64, f64)) -> f64 { x.0 * y.0 + x.1 * y.1 } /// distance calculation for `(f64,f64)` types pub fn dist(start: (f64, f64), end: (f64, f64)) -> f64 { ((start.0 - end.0).powi(2) + (start.1 - end.1).powi(2)).sqrt() } #[cfg(test)] mod test_utils { use crate::utils::{interp, interp2, nearest_point_on_segment, normalize01_64}; #[test] fn test_normalize_float() { approx::assert_relative_eq!(normalize01_64(1., 2., 1.5), 0.5); approx::assert_relative_eq!(normalize01_64(7., 3., 4.), 0.75); //should also work in reverse approx::assert_relative_eq!(normalize01_64(-1., 1., 2.), 1.); approx::assert_relative_eq!(normalize01_64(1., 1., 1.), 0.); approx::assert_relative_eq!(normalize01_64(1., 1., 0.), 0.); approx::assert_relative_eq!(normalize01_64(1., 1., 2.), 1.); } #[test] fn test_inter_float() { approx::assert_relative_eq!(interp(5.0, 10.0, 0.2), 6.0); approx::assert_relative_eq!(interp(10.0, -2.0, 0.75), 1.0); approx::assert_relative_eq!(interp(-1.0, 2.0, -3.0), -1.0); approx::assert_relative_eq!(interp(5.0, 7.0, 20.0), 7.0); } #[test] fn test_interp_vec2() { assert_eq!(interp2((1.0, 2.0), (3.0, 5.0), 0.5), (2.0, 3.5)); assert_eq!(interp2((-5.0, 5.0), (-15.0, 0.0), 0.4), (-9.0, 3.0)); assert_eq!(interp2((7.0, 9.0), (25.0, 30.0), -0.1), (7.0, 9.0)); assert_eq!(interp2((12.0, 5.0), (13.0, 14.0), 3.2), (13.0, 14.0)); } #[test] fn test_nearest_point() { assert_eq!( nearest_point_on_segment((0.0, 0.0), (1.0, 0.0), (0.25, 0.5),), 0.25 ); assert_eq!( nearest_point_on_segment((3.0, 4.0), (5.0, 6.0), (-1.0, -1.0),), 0.0 ); assert_eq!( nearest_point_on_segment((20.0, 10.0), (10.0, 5.0), (2.0, 2.0),), 1.0 ); assert_eq!( nearest_point_on_segment((0.0, 5.0), (5.0, 0.0), (3.0, 3.0),), 0.5 ); // degenerate cases assert_eq!( nearest_point_on_segment((0.0, 0.0), (0.0, 0.0), (5.0, 10.0),), 0.0 ); assert_eq!( nearest_point_on_segment((3.0, 7.0), (3.0, 7.0), (0.0, -20.0),), 0.0 ); } } ink-stroke-modeler-rs-0.1.0/todo.md000064400000000000000000000002611046102023000152450ustar 00000000000000- [ ] adapt the default settings (cm for distance and seconds for time != what's Rnote reporting) (to be done on the Rnote's code side : adapt the modeler parameters there)